hare

[hare] The Hare programming language
git clone https://git.torresjrjr.com/hare.git
Log | Files | Refs | README | LICENSE

main.ha (8742B)


      1 // License: GPL-3.0
      2 // (c) 2021 Alexey Yerin <yyp@disroot.org>
      3 // (c) 2021 Drew DeVault <sir@cmpwn.com>
      4 // (c) 2021 Eyal Sawady <ecs@d2evs.net>
      5 // (c) 2022 Sebastian <sebastian@sebsite.pw>
      6 use bufio;
      7 use fmt;
      8 use fs;
      9 use getopt;
     10 use hare::ast;
     11 use hare::lex;
     12 use hare::module;
     13 use hare::parse;
     14 use hare::unparse;
     15 use io;
     16 use os;
     17 use os::exec;
     18 use path;
     19 use strings;
     20 use unix::tty;
     21 
     22 type format = enum {
     23 	HARE,
     24 	TTY,
     25 	HTML,
     26 	GEMTEXT,
     27 };
     28 
     29 type context = struct {
     30 	mctx: *module::context,
     31 	ident: ast::ident,
     32 	tags: []module::tag,
     33 	version: module::version,
     34 	summary: summary,
     35 	format: format,
     36 	template: bool,
     37 	show_undocumented: bool,
     38 	readme: (io::file | void),
     39 	out: io::handle,
     40 	pager: (exec::process | void),
     41 };
     42 
     43 export fn main() void = {
     44 	let fmt = if (tty::isatty(os::stdout_file)) {
     45 		yield format::TTY;
     46 	} else {
     47 		yield format::HARE;
     48 	};
     49 	let template = true;
     50 	let show_undocumented = false;
     51 	let tags = default_tags();
     52 	defer module::tags_free(tags);
     53 
     54 	const help: [_]getopt::help = [
     55 		"reads and formats Hare documentation",
     56 		('F', "format", "specify output format (hare, tty, html, or gemtext)"),
     57 		('T', "tags...", "set build tags"),
     58 		('X', "tags...", "unset build tags"),
     59 		('a', "show undocumented members (only applies to -Fhare and -Ftty)"),
     60 		('t', "disable HTML template (requires postprocessing)"),
     61 		"[identifiers...]",
     62 	];
     63 	const cmd = getopt::parse(os::args, help...);
     64 	defer getopt::finish(&cmd);
     65 
     66 	for (let i = 0z; i < len(cmd.opts); i += 1) {
     67 		let opt = cmd.opts[i];
     68 		switch (opt.0) {
     69 		case 'F' =>
     70 			switch (opt.1) {
     71 			case "hare" =>
     72 				fmt = format::HARE;
     73 			case "tty" =>
     74 				fmt = format::TTY;
     75 			case "html" =>
     76 				fmt = format::HTML;
     77 			case "gemtext" =>
     78 				fmt = format::GEMTEXT;
     79 			case =>
     80 				fmt::fatal("Invalid format", opt.1);
     81 			};
     82 		case 'T' =>
     83 			tags = match (addtags(tags, opt.1)) {
     84 			case void =>
     85 				fmt::fatal("Error parsing tags");
     86 			case let t: []module::tag =>
     87 				yield t;
     88 			};
     89 		case 'X' =>
     90 			tags = match (deltags(tags, opt.1)) {
     91 			case void =>
     92 				fmt::fatal("Error parsing tags");
     93 			case let t: []module::tag =>
     94 				yield t;
     95 			};
     96 		case 't' =>
     97 			template = false;
     98 		case 'a' =>
     99 			show_undocumented = true;
    100 		case => abort();
    101 		};
    102 	};
    103 
    104 	if (show_undocumented) switch (fmt) {
    105 	case format::HARE, format::TTY => void;
    106 	case =>
    107 		fmt::fatal("Option -a must be used only with -Fhare or -Ftty");
    108 	};
    109 
    110 	let decls: []ast::decl = [];
    111 	defer free(decls);
    112 
    113 	let ctx = module::context_init(tags, [], default_harepath());
    114 	defer module::context_finish(&ctx);
    115 
    116 	const id: ast::ident =
    117 		if (len(cmd.args) < 1) []
    118 		else match (parseident(cmd.args[0])) {
    119 		case let err: parse::error =>
    120 			fmt::fatal(parse::strerror(err));
    121 		case let id: ast::ident =>
    122 			yield id;
    123 		};
    124 
    125 	let decl = "";
    126 	let dirname = if (len(id) < 2) id else id[..len(id) - 1];
    127 	const version = match (module::lookup(&ctx, id)) {
    128 	case let ver: module::version =>
    129 		yield ver;
    130 	case let err: module::error =>
    131 		yield match (module::lookup(&ctx, dirname)) {
    132 		case let ver: module::version =>
    133 			assert(len(id) >= 2);
    134 			decl = id[len(id) - 1];
    135 			yield ver;
    136 		case let err: module::error =>
    137 			fmt::fatal("Error scanning input module:",
    138 				module::strerror(err));
    139 		};
    140 	};
    141 
    142 	for (let i = 0z; i < len(version.inputs); i += 1) {
    143 		const in = version.inputs[i];
    144 		const ext = path::extension(in.path);
    145 		if (ext.1 != ".ha") {
    146 			continue;
    147 		};
    148 		match (scan(in.path)) {
    149 		case let u: ast::subunit =>
    150 			for (let i = 0z; i < len(u.imports); i += 1) {
    151 				ast::import_finish(u.imports[i]);
    152 			};
    153 			append(decls, u.decls...);
    154 		case let err: error =>
    155 			fmt::fatal("Error:", strerror(err));
    156 		};
    157 	};
    158 
    159 	const rpath = path::join(version.basedir, "README");
    160 	defer free(rpath);
    161 	const readme: (io::file | void) = if (decl == "") {
    162 		yield match (os::open(rpath)) {
    163 		case let err: fs::error =>
    164 			yield void;
    165 		case let f: io::file =>
    166 			yield f;
    167 		};
    168 	} else void;
    169 
    170 	defer match (readme) {
    171 	case void => void;
    172 	case let f: io::file =>
    173 		io::close(f)!;
    174 	};
    175 
    176 	if (decl != "") {
    177 		let new: []ast::decl = [];
    178 		for (let i = 0z; i < len(decls); i += 1) {
    179 			if (has_decl(decls[i], decl)) {
    180 				append(new, decls[i]);
    181 			} else {
    182 				ast::decl_finish(decls[i]);
    183 			};
    184 		};
    185 		if (len(new) == 0) {
    186 			fmt::fatalf("Could not find {}::{}",
    187 				unparse::identstr(dirname), decl);
    188 		};
    189 		free(decls);
    190 		decls = new;
    191 
    192 		show_undocumented = true;
    193 	};
    194 
    195 	defer for (let i = 0z; i < len(decls); i += 1) {
    196 		ast::decl_finish(decls[i]);
    197 	};
    198 
    199 	const ctx = context {
    200 		mctx = &ctx,
    201 		ident = id,
    202 		tags = tags,
    203 		version = version,
    204 		summary = sort_decls(decls),
    205 		format = fmt,
    206 		template = template,
    207 		readme = readme,
    208 		show_undocumented = show_undocumented,
    209 		out = os::stdout,
    210 		pager = void,
    211 	};
    212 
    213 	if (fmt == format::TTY) {
    214 		ctx.out = init_tty(&ctx);
    215 	};
    216 
    217 	match (emit(&ctx)) {
    218 	case void => void;
    219 	case let err: error =>
    220 		fmt::fatal("Error:", strerror(err));
    221 	};
    222 
    223 	io::close(ctx.out)!;
    224 	match (ctx.pager) {
    225 	case void => void;
    226 	case let proc: exec::process =>
    227 		exec::wait(&proc)!;
    228 	};
    229 };
    230 
    231 // Nearly identical to parse::identstr, except alphanumeric lexical tokens are
    232 // converted to strings and there must be no trailing tokens that don't belong
    233 // to the ident in the string. For example, this function will parse `rt::abort`
    234 // as a valid identifier.
    235 fn parseident(in: str) (ast::ident | parse::error) = {
    236 	const buf = bufio::fixed(strings::toutf8(in), io::mode::READ);
    237 	const lexer = lex::init(&buf, "<string>");
    238 	let ident: []str = []; // TODO: errdefer
    239 	let z = 0z;
    240 	for (true) {
    241 		const tok = lex::lex(&lexer)?;
    242 		const name = if (tok.0 == lex::ltok::NAME) {
    243 			yield tok.1 as str;
    244 		} else if (tok.0 < lex::ltok::LAST_KEYWORD) {
    245 			yield lex::tokstr(tok);
    246 		} else {
    247 			lex::unlex(&lexer, tok);
    248 			const loc = lex::mkloc(&lexer);
    249 			const why = "Unexpected trailing :: in ident";
    250 			return (loc, why): lex::syntax: parse::error;
    251 		};
    252 		append(ident, name);
    253 		z += len(name);
    254 		const tok = lex::lex(&lexer)?;
    255 		switch (tok.0) {
    256 		case lex::ltok::EOF =>
    257 			break;
    258 		case lex::ltok::DOUBLE_COLON =>
    259 			z += 1;
    260 		case =>
    261 			lex::unlex(&lexer, tok);
    262 			const loc = lex::mkloc(&lexer);
    263 			const why = fmt::asprintf("Unexpected '{}' in ident",
    264 				lex::tokstr(tok));
    265 			return (loc, why): lex::syntax: parse::error;
    266 		};
    267 	};
    268 	if (z > ast::IDENT_MAX) {
    269 		const loc = lex::mkloc(&lexer);
    270 		const why = "Identifier exceeds maximum length";
    271 		return (loc, why): lex::syntax: parse::error;
    272 	};
    273 	return ident;
    274 };
    275 
    276 fn init_tty(ctx: *context) io::handle = {
    277 	const pager = match (os::getenv("PAGER")) {
    278 	case let name: str =>
    279 		yield match (exec::cmd(name)) {
    280 		case let cmd: exec::command =>
    281 			yield cmd;
    282 		case exec::error =>
    283 			return os::stdout;
    284 		};
    285 	case void =>
    286 		yield match (exec::cmd("less")) {
    287 		case let cmd: exec::command =>
    288 			yield cmd;
    289 		case exec::error =>
    290 			yield match (exec::cmd("more")) {
    291 			case let cmd: exec::command =>
    292 				yield cmd;
    293 			case exec::error =>
    294 				return os::stdout;
    295 			};
    296 		};
    297 	};
    298 
    299 	const pipe = exec::pipe();
    300 	exec::addfile(&pager, os::stdin_file, pipe.0);
    301 	exec::setenv(&pager, "LESS", "FRX")!;
    302 	ctx.pager = exec::start(&pager)!;
    303 	return pipe.1;
    304 };
    305 
    306 fn has_decl(decl: ast::decl, name: str) bool = {
    307 	if (!decl.exported) {
    308 		return false;
    309 	};
    310 
    311 	match (decl.decl) {
    312 	case let d: []ast::decl_const =>
    313 		for (let i = 0z; i < len(d); i += 1) {
    314 			if (len(d[i].ident) == 1 && d[i].ident[0] == name) {
    315 				return true;
    316 			};
    317 		};
    318 	case let d: ast::decl_func =>
    319 		if (len(d.ident) == 1 && d.ident[0] == name) {
    320 			return true;
    321 		};
    322 		const sym = strings::split(d.symbol, ".");
    323 		defer free(sym);
    324 		return len(sym) > 0 && sym[len(sym) - 1] == name;
    325 	case let d: []ast::decl_global =>
    326 		for (let i = 0z; i < len(d); i += 1) {
    327 			if (len(d[i].ident) == 1 && d[i].ident[0] == name) {
    328 				return true;
    329 			};
    330 		};
    331 	case let d: []ast::decl_type =>
    332 		for (let i = 0z; i < len(d); i += 1) {
    333 			if (len(d[i].ident) == 1 && d[i].ident[0] == name) {
    334 				return true;
    335 			};
    336 		};
    337 	};
    338 	return false;
    339 };
    340 
    341 fn scan(path: str) (ast::subunit | error) = {
    342 	const input = match (os::open(path)) {
    343 	case let f: io::file =>
    344 		yield f;
    345 	case let err: fs::error =>
    346 		fmt::fatalf("Error reading {}: {}", path, fs::strerror(err));
    347 	};
    348 	defer io::close(input)!;
    349 	const lexer = lex::init(input, path, lex::flags::COMMENTS);
    350 	return parse::subunit(&lexer)?;
    351 };
    352 
    353 fn emit(ctx: *context) (void | error) = {
    354 	switch (ctx.format) {
    355 	case format::HARE =>
    356 		emit_hare(ctx)?;
    357 	case format::TTY =>
    358 		emit_tty(ctx)?;
    359 	case format::HTML =>
    360 		emit_html(ctx)?;
    361 	case format::GEMTEXT => abort(); // TODO
    362 	};
    363 };
    364 
    365 @test fn parseident() void = {
    366 	assert(parseident("hare::lex") is ast::ident);
    367 	assert(parseident("strings::dup*{}&@") is parse::error);
    368 	assert(parseident("foo::bar::") is parse::error);
    369 	assert(parseident("rt::abort") is ast::ident);
    370 };