hare

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

main.ha (8860B)


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