hare

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

main.ha (11440B)


      1 // SPDX-License-Identifier: GPL-3.0-only
      2 // (c) Hare authors <https://harelang.org>
      3 
      4 use bufio;
      5 use cmd::haredoc::doc;
      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 strconv;
     20 use strings;
     21 
     22 const help: []getopt::help = [
     23 	"reads and formats Hare documentation",
     24 	('a', "show undocumented members (only applies to -Ftty)"),
     25 	('t', "disable HTML template (requires postprocessing)"),
     26 	('F', "format", "specify output format (tty or html)"),
     27 	('T', "tagset", "set/unset build tags"),
     28 	"[identifier|path]",
     29 ];
     30 
     31 export fn main() void = {
     32 	const cmd = getopt::parse(os::args, help...);
     33 	defer getopt::finish(&cmd);
     34 	match (doc(os::args[0], &cmd)) {
     35 	case void => void;
     36 	case let e: doc::error =>
     37 		fmt::fatal(doc::strerror(e));
     38 	case let e: exec::error =>
     39 		fmt::fatal(exec::strerror(e));
     40 	case let e: fs::error =>
     41 		fmt::fatal(fs::strerror(e));
     42 	case let e: io::error =>
     43 		fmt::fatal(io::strerror(e));
     44 	case let e: module::error =>
     45 		fmt::fatal(module::strerror(e));
     46 	case let e: path::error =>
     47 		fmt::fatal(path::strerror(e));
     48 	case let e: parse::error =>
     49 		fmt::fatal(parse::strerror(e));
     50 	case let e: strconv::error =>
     51 		fmt::fatal(strconv::strerror(e));
     52 	};
     53 };
     54 
     55 fn doc(name: str, cmd: *getopt::command) (void | error) = {
     56 	let html = false;
     57 	let template = true;
     58 	let show_undocumented = false;
     59 	let tags: []str = default_tags();
     60 	defer free(tags);
     61 
     62 	for (let (k, v) .. cmd.opts) {
     63 		switch (k) {
     64 		case 'F' =>
     65 			switch (v) {
     66 			case "tty" =>
     67 				html = false;
     68 			case "html" =>
     69 				html = true;
     70 			case =>
     71 				fmt::fatal("Invalid format", v);
     72 			};
     73 		case 'T' =>
     74 			merge_tags(&tags, v)?;
     75 		case 't' =>
     76 			template = false;
     77 		case 'a' =>
     78 			show_undocumented = true;
     79 		case => abort();
     80 		};
     81 	};
     82 
     83 	if (show_undocumented && html) {
     84 		fmt::fatal("Option -a must be used only with -Ftty");
     85 	};
     86 
     87 	if (len(cmd.args) > 1) {
     88 		getopt::printusage(os::stderr, os::args[0], help)!;
     89 		os::exit(os::status::FAILURE);
     90 	};
     91 
     92 	let ctx = module::context {
     93 		harepath = harepath(),
     94 		harecache = harecache(),
     95 		tags = tags,
     96 	};
     97 
     98 	let declpath = "";
     99 	defer free(declpath);
    100 	let declsrcs = module::srcset { ... };
    101 	defer module::finish_srcset(&declsrcs);
    102 	let modpath = "";
    103 	defer free(modpath);
    104 	let modsrcs = module::srcset { ... };
    105 	defer module::finish_srcset(&modsrcs);
    106 	let id: ast::ident = [];
    107 	defer free(id);
    108 
    109 	if (len(cmd.args) == 0) {
    110 		let (p, s) = module::find(&ctx, []: ast::ident)?;
    111 		modpath = strings::dup(p);
    112 		modsrcs = s;
    113 	} else match (parseident(cmd.args[0])) {
    114 	case let ident: (ast::ident, bool) =>
    115 		id = ident.0;
    116 		const trailing = ident.1;
    117 		if (!trailing) {
    118 			// check if it's an ident inside a module
    119 			match (module::find(&ctx, id[..len(id)-1])) {
    120 			case let s: (str, module::srcset) =>
    121 				declpath = strings::dup(s.0);
    122 				declsrcs = s.1;
    123 			case let e: module::error =>
    124 				module::finish_error(e);
    125 			};
    126 		};
    127 		// check if it's a module
    128 		match (module::find(&ctx, id)) {
    129 		case let s: (str, module::srcset) =>
    130 			modpath = strings::dup(s.0);
    131 			modsrcs = s.1;
    132 		case let e: module::error =>
    133 			module::finish_error(e);
    134 			if (declpath == "") {
    135 				const id = unparse::identstr(id);
    136 				fmt::fatalf("Could not find {}{}", id,
    137 					if (trailing) "::" else "");
    138 			};
    139 		};
    140 	case void =>
    141 		let buf = path::init(cmd.args[0])?;
    142 		let (p, s) = module::find(&ctx, &buf)?;
    143 		modpath = strings::dup(p);
    144 		modsrcs = s;
    145 	};
    146 
    147 	let decls: []ast::decl = [];
    148 	defer {
    149 		for (let decl .. decls) {
    150 			ast::decl_finish(decl);
    151 		};
    152 		free(decls);
    153 	};
    154 
    155 	if (declpath != "") {
    156 		for (let ha .. declsrcs.ha) {
    157 			let u = doc::scan(ha)?;
    158 			ast::imports_finish(u.imports);
    159 			append(decls, u.decls...);
    160 		};
    161 
    162 		let matching: []ast::decl = [];
    163 		let notmatching: []ast::decl = [];
    164 
    165 		for (let decl .. decls) {
    166 			if (has_decl(decl, id[len(id) - 1])) {
    167 				append(matching, decl);
    168 			} else {
    169 				append(notmatching, decl);
    170 			};
    171 		};
    172 		get_init_funcs(&matching, &notmatching);
    173 
    174 		for (let decl .. notmatching) {
    175 			ast::decl_finish(decl);
    176 		};
    177 		free(notmatching);
    178 		free(decls);
    179 		decls = matching;
    180 
    181 		if (len(matching) == 0) {
    182 			if (modpath == "") {
    183 				const id = unparse::identstr(id);
    184 				fmt::fatalf("Could not find {}", id);
    185 			};
    186 		} else {
    187 			show_undocumented = true;
    188 		};
    189 	};
    190 
    191 	let readme: (io::file | void) = void;
    192 	defer match (readme) {
    193 	case void => void;
    194 	case let f: io::file =>
    195 		io::close(f)!;
    196 	};
    197 
    198 	const ambiguous = modpath != "" && len(decls) > 0;
    199 
    200 	if (len(decls) == 0) {
    201 		for (let ha .. modsrcs.ha) {
    202 			let u = doc::scan(ha)?;
    203 			ast::imports_finish(u.imports);
    204 			append(decls, u.decls...);
    205 		};
    206 
    207 		const rpath = path::init(modpath, "README")!;
    208 		match (os::open(path::string(&rpath))) {
    209 		case let f: io::file =>
    210 			readme = f;
    211 		case fs::error => void;
    212 		};
    213 	};
    214 
    215 	const submods: []str = if (!ambiguous && modpath != "") {
    216 		yield match (doc::submodules(modpath, show_undocumented)) {
    217 		case let s: []str =>
    218 			yield s;
    219 		case doc::error =>
    220 			yield [];
    221 		};
    222 	} else [];
    223 	const srcs = if (!ambiguous && modpath != "") modsrcs else declsrcs;
    224 	const summary = doc::sort_decls(decls);
    225 	defer doc::finish_summary(summary);
    226 	const ctx = doc::context {
    227 		mctx = &ctx,
    228 		ident = id,
    229 		tags = tags,
    230 		ambiguous = ambiguous,
    231 		srcs = srcs,
    232 		submods = submods,
    233 		summary = summary,
    234 		template = template,
    235 		readme = readme,
    236 		show_undocumented = show_undocumented,
    237 		out = os::stdout,
    238 		pager = void,
    239 	};
    240 
    241 	const ret = if (html) {
    242 		yield doc::emit_html(&ctx);
    243 	} else {
    244 		ctx.out = init_tty(&ctx);
    245 		yield doc::emit_tty(&ctx);
    246 	};
    247 
    248 	io::close(ctx.out)!;
    249 	match (ctx.pager) {
    250 	case void => void;
    251 	case let proc: exec::process =>
    252 		exec::wait(&proc)!;
    253 	};
    254 
    255 	// TODO: remove ? (harec bug workaround)
    256 	return ret?;
    257 };
    258 
    259 // Nearly identical to parse::identstr, except alphanumeric lexical tokens are
    260 // converted to strings and there must be no trailing tokens that don't belong
    261 // to the ident in the string. For example, this function will parse `rt::abort`
    262 // as a valid identifier.
    263 fn parseident(in: str) ((ast::ident, bool) | void) = {
    264 	let buf = memio::fixed(strings::toutf8(in));
    265 	let sc = bufio::newscanner(&buf);
    266 	defer bufio::finish(&sc);
    267 	let lexer = lex::init(&sc, "<string>");
    268 	let success = false;
    269 	let ident: ast::ident = [];
    270 	defer if (!success) ast::ident_free(ident);
    271 	let trailing = false;
    272 	let z = 0z;
    273 	for (true) {
    274 		const tok = lex::lex(&lexer)!;
    275 		const name = if (tok.0 == lex::ltok::NAME) {
    276 			yield tok.1 as str;
    277 		} else if (tok.0 < lex::ltok::LAST_KEYWORD) {
    278 			yield strings::dup(lex::tokstr(tok));
    279 		} else if (tok.0 == lex::ltok::EOF && len(ident) > 0) {
    280 			trailing = true;
    281 			break;
    282 		} else {
    283 			lex::unlex(&lexer, tok);
    284 			return;
    285 		};
    286 		append(ident, name);
    287 		z += len(name);
    288 		const tok = lex::lex(&lexer)!;
    289 		switch (tok.0) {
    290 		case lex::ltok::EOF =>
    291 			break;
    292 		case lex::ltok::DOUBLE_COLON =>
    293 			z += 1;
    294 		case =>
    295 			lex::unlex(&lexer, tok);
    296 			return;
    297 		};
    298 	};
    299 	if (z > ast::IDENT_MAX) {
    300 		return;
    301 	};
    302 	success = true;
    303 	return (ident, trailing);
    304 };
    305 
    306 fn init_tty(ctx: *doc::context) io::handle = {
    307 	const pager = match (os::getenv("PAGER")) {
    308 	case let name: str =>
    309 		yield match (exec::cmd(name)) {
    310 		case let cmd: exec::command =>
    311 			yield cmd;
    312 		case exec::error =>
    313 			return os::stdout;
    314 		};
    315 	case void =>
    316 		yield match (exec::cmd("less", "-R")) {
    317 		case let cmd: exec::command =>
    318 			yield cmd;
    319 		case exec::error =>
    320 			yield match (exec::cmd("more", "-R")) {
    321 			case let cmd: exec::command =>
    322 				yield cmd;
    323 			case exec::error =>
    324 				return os::stdout;
    325 			};
    326 		};
    327 	};
    328 
    329 	const pipe = exec::pipe();
    330 	defer io::close(pipe.0)!;
    331 	exec::addfile(&pager, os::stdin_file, pipe.0);
    332 	// Get raw flag in if possible
    333 	exec::setenv(&pager, "LESS", os::tryenv("LESS", "FRX"))!;
    334 	exec::setenv(&pager, "MORE", os::tryenv("MORE", "R"))!;
    335 	ctx.pager = exec::start(&pager)!;
    336 	return pipe.1;
    337 };
    338 
    339 fn has_decl(decl: ast::decl, name: str) bool = {
    340 	if (!decl.exported) {
    341 		return false;
    342 	};
    343 
    344 	match (decl.decl) {
    345 	case let consts: []ast::decl_const =>
    346 		for (let d .. consts) {
    347 			if (len(d.ident) == 1 && d.ident[0] == name) {
    348 				return true;
    349 			};
    350 		};
    351 	case let d: ast::decl_func =>
    352 		if (len(d.ident) == 1 && d.ident[0] == name) {
    353 			return true;
    354 		};
    355 		let tok = strings::rtokenize(d.symbol, ".");
    356 		match (strings::next_token(&tok)) {
    357 		case done => void;
    358 		case let s: str =>
    359 			return s == name;
    360 		};
    361 	case let globals: []ast::decl_global =>
    362 		for (let d .. globals) {
    363 			if (len(d.ident) == 1 && d.ident[0] == name) {
    364 				return true;
    365 			};
    366 			let tok = strings::rtokenize(d.symbol, ".");
    367 			match (strings::next_token(&tok)) {
    368 			case done => void;
    369 			case let s: str =>
    370 				return s == name;
    371 			};
    372 		};
    373 	case let types: []ast::decl_type =>
    374 		for (let d .. types) {
    375 			if (len(d.ident) == 1 && d.ident[0] == name) {
    376 				return true;
    377 			};
    378 		};
    379 	case ast::assert_expr => void;
    380 	};
    381 	return false;
    382 };
    383 
    384 @test fn parseident() void = {
    385 	let (ident, trailing) = parseident("hare::lex") as (ast::ident, bool);
    386 	defer ast::ident_free(ident);
    387 	assert(ast::ident_eq(ident, ["hare", "lex"]));
    388 	assert(!trailing);
    389 
    390 	let (ident, trailing) = parseident("rt::abort") as (ast::ident, bool);
    391 	defer ast::ident_free(ident);
    392 	assert(ast::ident_eq(ident, ["rt", "abort"]));
    393 	assert(!trailing);
    394 
    395 	let (ident, trailing) = parseident("foo::bar::") as (ast::ident, bool);
    396 	defer ast::ident_free(ident);
    397 	assert(ast::ident_eq(ident, ["foo", "bar"]));
    398 	assert(trailing);
    399 	assert(parseident("strings::dup*{}&@") is void);
    400 	assert(parseident("") is void);
    401 	assert(parseident("::") is void);
    402 };
    403 
    404 fn get_init_funcs(matching: *[]ast::decl, notmatching: *[]ast::decl) void = {
    405 	if (len(matching) != 1) {
    406 		return;
    407 	};
    408 	let ident = match (matching[0].decl) {
    409 	case let d: []ast::decl_type =>
    410 		if (len(d) != 1 || len(d[0].ident) != 1) {
    411 			return;
    412 		};
    413 		if (d[0]._type.flags & ast::type_flag::ERROR != 0) {
    414 			return;
    415 		};
    416 		match (d[0]._type.repr) {
    417 		case let repr: ast::builtin_type =>
    418 			if (repr == ast::builtin_type::VOID) {
    419 				return;
    420 			};
    421 		case => void;
    422 		};
    423 		yield d[0].ident;
    424 	case =>
    425 		return;
    426 	};
    427 
    428 	for (let i = 0z; i < len(notmatching); i += 1) {
    429 		let _type = match (notmatching[i].decl) {
    430 		case let d: []ast::decl_const =>
    431 			yield match (d[0]._type) {
    432 			case let t: *ast::_type =>
    433 				yield t;
    434 			case null =>
    435 				continue;
    436 			};
    437 		case let d: []ast::decl_global =>
    438 			yield match (d[0]._type) {
    439 			case let t: *ast::_type =>
    440 				yield t;
    441 			case null =>
    442 				continue;
    443 			};
    444 		case let d: ast::decl_func =>
    445 			let _type = d.prototype.repr as ast::func_type;
    446 			yield _type.result;
    447 		case =>
    448 			continue;
    449 		};
    450 
    451 		if (is_init_type(ident, _type)) {
    452 			append(matching, notmatching[i]);
    453 			delete(notmatching[i]);
    454 			i -= 1;
    455 		};
    456 	};
    457 };
    458 
    459 fn is_init_type(ident: ast::ident, _type: *ast::_type) bool = {
    460 	let type_ident = match (_type.repr) {
    461 	case let repr: ast::alias_type =>
    462 		yield repr.ident;
    463 	case let repr: ast::list_type =>
    464 		if (!(repr.length is ast::len_slice)) {
    465 			return false;
    466 		};
    467 		yield match (repr.members.repr) {
    468 		case let repr: ast::alias_type =>
    469 			yield repr.ident;
    470 		case =>
    471 			return false;
    472 		};
    473 	case let repr: ast::pointer_type =>
    474 		yield match (repr.referent.repr) {
    475 		case let repr: ast::alias_type =>
    476 			yield repr.ident;
    477 		case =>
    478 			return false;
    479 		};
    480 	case let repr: ast::tagged_type =>
    481 		for (let t .. repr) {
    482 			if (is_init_type(ident, t)) {
    483 				return true;
    484 			};
    485 		};
    486 		return false;
    487 	case =>
    488 		return false;
    489 	};
    490 
    491 	return ast::ident_eq(ident, type_ident);
    492 };