main.ha (12323B)
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 let parse_errs: []lex::syntax = []; 156 defer { 157 for (const err .. parse_errs) { 158 free(err.1); 159 }; 160 free(parse_errs); 161 }; 162 163 if (declpath != "") { 164 for (let ha .. declsrcs.ha) { 165 let d = match (doc::scan(ha)) { 166 case let d: []ast::decl => 167 yield d; 168 case let err: parse::error => 169 if (html) { 170 return err; 171 }; 172 match (err) { 173 case let err: lex::syntax => 174 const msg = strings::dup(err.1); 175 append(parse_errs, (err.0, msg))!; 176 continue; 177 case => 178 return err; 179 }; 180 }; 181 defer free(d); 182 append(decls, d...)!; 183 }; 184 185 let matching: []ast::decl = []; 186 let notmatching: []ast::decl = []; 187 188 for (let decl .. decls) { 189 if (has_decl(decl, id[len(id) - 1])) { 190 append(matching, decl)!; 191 } else { 192 append(notmatching, decl)!; 193 }; 194 }; 195 get_init_funcs(&matching, ¬matching); 196 197 for (let decl .. notmatching) { 198 ast::decl_finish(decl); 199 }; 200 free(notmatching); 201 free(decls); 202 decls = matching; 203 204 if (len(matching) == 0) { 205 if (modpath == "") { 206 const id = unparse::identstr(id); 207 fmt::fatalf("Could not find {}", id); 208 }; 209 } else { 210 show_undocumented = true; 211 }; 212 }; 213 214 let readme: (io::file | void) = void; 215 defer match (readme) { 216 case void => void; 217 case let f: io::file => 218 io::close(f)!; 219 }; 220 221 const ambiguous = modpath != "" && len(decls) > 0; 222 223 if (len(decls) == 0) :nodecls { 224 for (let ha .. modsrcs.ha) { 225 let d = match (doc::scan(ha)) { 226 case let d: []ast::decl => 227 yield d; 228 case let err: parse::error => 229 if (html) { 230 return err; 231 }; 232 match (err) { 233 case let err: lex::syntax => 234 const msg = strings::dup(err.1); 235 append(parse_errs, (err.0, msg))!; 236 continue; 237 case => 238 return err; 239 }; 240 }; 241 defer free(d); 242 append(decls, d...)!; 243 }; 244 245 const rpath = match (path::init(modpath, "README")) { 246 case let buf: path::path => 247 yield buf; 248 case let err: path::error => 249 assert(err is path::too_long); 250 yield :nodecls; 251 }; 252 match (os::open(path::string(&rpath))) { 253 case let f: io::file => 254 readme = f; 255 case fs::error => void; 256 }; 257 }; 258 259 const submods: []str = if (!ambiguous && modpath != "") { 260 yield match (doc::submodules(modpath, show_undocumented)) { 261 case let s: []str => 262 yield s; 263 case doc::error => 264 yield []; 265 }; 266 } else []; 267 const srcs = if (!ambiguous && modpath != "") modsrcs else declsrcs; 268 const summary = doc::sort_decls(decls); 269 defer doc::finish_summary(summary); 270 const ctx = doc::context { 271 mctx = &ctx, 272 ident = id, 273 tags = tags, 274 ambiguous = ambiguous, 275 parse_errs = parse_errs, 276 srcs = srcs, 277 submods = submods, 278 summary = summary, 279 template = template, 280 readme = readme, 281 show_undocumented = show_undocumented, 282 out = os::stdout, 283 pager = void, 284 }; 285 286 const ret = if (html) { 287 yield doc::emit_html(&ctx); 288 } else { 289 ctx.out = init_tty(&ctx); 290 yield doc::emit_tty(&ctx); 291 }; 292 293 io::close(ctx.out)!; 294 match (ctx.pager) { 295 case void => void; 296 case let proc: exec::process => 297 exec::wait(&proc)!; 298 }; 299 300 // TODO: remove ? (harec bug workaround) 301 return ret?; 302 }; 303 304 // Nearly identical to parse::identstr, except alphanumeric lexical tokens are 305 // converted to strings and there must be no trailing tokens that don't belong 306 // to the ident in the string. For example, this function will parse `rt::abort` 307 // as a valid identifier. 308 fn parseident(in: str) ((ast::ident, bool) | void) = { 309 let buf = memio::fixed(strings::toutf8(in)); 310 let sc = bufio::newscanner(&buf); 311 defer bufio::finish(&sc); 312 let lexer = lex::init(&sc, "<string>"); 313 let success = false; 314 let ident: ast::ident = []; 315 defer if (!success) ast::ident_free(ident); 316 let trailing = false; 317 let z = 0z; 318 for (true) { 319 const tok = lex::lex(&lexer)!; 320 const name = if (tok.0 == lex::ltok::NAME) { 321 yield tok.1 as str; 322 } else if (tok.0 < lex::ltok::LAST_KEYWORD) { 323 yield strings::dup(lex::tokstr(tok)); 324 } else if (tok.0 == lex::ltok::EOF && len(ident) > 0) { 325 trailing = true; 326 break; 327 } else { 328 lex::unlex(&lexer, tok); 329 return; 330 }; 331 append(ident, name)!; 332 z += len(name); 333 const tok = lex::lex(&lexer)!; 334 switch (tok.0) { 335 case lex::ltok::EOF => 336 break; 337 case lex::ltok::DOUBLE_COLON => 338 z += 1; 339 case => 340 lex::unlex(&lexer, tok); 341 return; 342 }; 343 }; 344 if (z > ast::IDENT_MAX) { 345 return; 346 }; 347 success = true; 348 return (ident, trailing); 349 }; 350 351 fn init_tty(ctx: *doc::context) io::handle = { 352 const pager = match (os::getenv("PAGER")) { 353 case let name: str => 354 yield match (exec::cmd(name)) { 355 case let cmd: exec::command => 356 yield cmd; 357 case exec::error => 358 return os::stdout; 359 }; 360 case void => 361 yield match (exec::cmd("less", "-R")) { 362 case let cmd: exec::command => 363 yield cmd; 364 case exec::error => 365 yield match (exec::cmd("more", "-R")) { 366 case let cmd: exec::command => 367 yield cmd; 368 case exec::error => 369 return os::stdout; 370 }; 371 }; 372 }; 373 374 const pipe = exec::pipe(); 375 defer io::close(pipe.0)!; 376 exec::addfile(&pager, os::stdin_file, pipe.0); 377 // Get raw flag in if possible 378 exec::setenv(&pager, "LESS", os::tryenv("LESS", "FRX"))!; 379 exec::setenv(&pager, "MORE", os::tryenv("MORE", "R"))!; 380 ctx.pager = exec::start(&pager)!; 381 return pipe.1; 382 }; 383 384 fn has_decl(decl: ast::decl, name: str) bool = { 385 if (!decl.exported) { 386 return false; 387 }; 388 389 match (decl.decl) { 390 case let consts: []ast::decl_const => 391 for (let d .. consts) { 392 if (len(d.ident) == 1 && d.ident[0] == name) { 393 return true; 394 }; 395 }; 396 case let d: ast::decl_func => 397 if (len(d.ident) == 1 && d.ident[0] == name) { 398 return true; 399 }; 400 let tok = strings::rtokenize(d.symbol, "."); 401 match (strings::next_token(&tok)) { 402 case done => void; 403 case let s: str => 404 return s == name; 405 }; 406 case let globals: []ast::decl_global => 407 for (let d .. globals) { 408 if (len(d.ident) == 1 && d.ident[0] == name) { 409 return true; 410 }; 411 let tok = strings::rtokenize(d.symbol, "."); 412 match (strings::next_token(&tok)) { 413 case done => void; 414 case let s: str => 415 return s == name; 416 }; 417 }; 418 case let types: []ast::decl_type => 419 for (let d .. types) { 420 if (len(d.ident) == 1 && d.ident[0] == name) { 421 return true; 422 }; 423 }; 424 case ast::assert_expr => void; 425 }; 426 return false; 427 }; 428 429 @test fn parseident() void = { 430 let (ident, trailing) = parseident("hare::lex") as (ast::ident, bool); 431 defer ast::ident_free(ident); 432 assert(ast::ident_eq(ident, ["hare", "lex"])); 433 assert(!trailing); 434 435 let (ident, trailing) = parseident("rt::abort") as (ast::ident, bool); 436 defer ast::ident_free(ident); 437 assert(ast::ident_eq(ident, ["rt", "abort"])); 438 assert(!trailing); 439 440 let (ident, trailing) = parseident("foo::bar::") as (ast::ident, bool); 441 defer ast::ident_free(ident); 442 assert(ast::ident_eq(ident, ["foo", "bar"])); 443 assert(trailing); 444 assert(parseident("strings::dup*{}&@") is void); 445 assert(parseident("") is void); 446 assert(parseident("::") is void); 447 }; 448 449 fn get_init_funcs(matching: *[]ast::decl, notmatching: *[]ast::decl) void = { 450 if (len(matching) != 1) { 451 return; 452 }; 453 let ident = match (matching[0].decl) { 454 case let d: []ast::decl_type => 455 if (len(d) != 1 || len(d[0].ident) != 1) { 456 return; 457 }; 458 if (d[0]._type.flags & ast::type_flag::ERROR != 0) { 459 return; 460 }; 461 match (d[0]._type.repr) { 462 case let repr: ast::builtin_type => 463 if (repr == ast::builtin_type::VOID) { 464 return; 465 }; 466 case => void; 467 }; 468 yield d[0].ident; 469 case => 470 return; 471 }; 472 473 for (let i = 0z; i < len(notmatching); i += 1) { 474 let _type = match (notmatching[i].decl) { 475 case let d: []ast::decl_const => 476 yield match (d[0]._type) { 477 case let t: *ast::_type => 478 yield t; 479 case null => 480 continue; 481 }; 482 case let d: []ast::decl_global => 483 yield match (d[0]._type) { 484 case let t: *ast::_type => 485 yield t; 486 case null => 487 continue; 488 }; 489 case let d: ast::decl_func => 490 let _type = d.prototype.repr as ast::func_type; 491 yield _type.result; 492 case => 493 continue; 494 }; 495 496 if (is_init_type(ident, _type)) { 497 append(matching, notmatching[i])!; 498 delete(notmatching[i]); 499 i -= 1; 500 }; 501 }; 502 }; 503 504 fn is_init_type(ident: ast::ident, _type: *ast::_type) bool = { 505 let type_ident = match (_type.repr) { 506 case let repr: ast::alias_type => 507 yield repr.ident; 508 case let repr: ast::list_type => 509 if (!(repr.length is ast::len_slice)) { 510 return false; 511 }; 512 yield match (repr.members.repr) { 513 case let repr: ast::alias_type => 514 yield repr.ident; 515 case => 516 return false; 517 }; 518 case let repr: ast::pointer_type => 519 yield match (repr.referent.repr) { 520 case let repr: ast::alias_type => 521 yield repr.ident; 522 case => 523 return false; 524 }; 525 case let repr: ast::tagged_type => 526 for (let t .. repr) { 527 if (is_init_type(ident, t)) { 528 return true; 529 }; 530 }; 531 return false; 532 case => 533 return false; 534 }; 535 536 return ast::ident_eq(ident, type_ident); 537 };