hare

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

commit 7be34d683820fe98a757a0af0380d3112c407198
parent 0d248a7dd9869e68fa1847920d775d3f453f6384
Author: Ember Sawady <ecs@d2evs.net>
Date:   Wed,  6 Sep 2023 08:15:03 +0000

Rewrite build driver and hare::module

Closes: https://todo.sr.ht/~sircmpwn/hare/277
Closes: https://todo.sr.ht/~sircmpwn/hare/280
Closes: https://todo.sr.ht/~sircmpwn/hare/338
Closes: https://todo.sr.ht/~sircmpwn/hare/345
Closes: https://todo.sr.ht/~sircmpwn/hare/346
Closes: https://todo.sr.ht/~sircmpwn/hare/347
Closes: https://todo.sr.ht/~sircmpwn/hare/397
Closes: https://todo.sr.ht/~sircmpwn/hare/436
Closes: https://todo.sr.ht/~sircmpwn/hare/491
Closes: https://todo.sr.ht/~sircmpwn/hare/492
Closes: https://todo.sr.ht/~sircmpwn/hare/545
Closes: https://todo.sr.ht/~sircmpwn/hare/569
Closes: https://todo.sr.ht/~sircmpwn/hare/653
Closes: https://todo.sr.ht/~sircmpwn/hare/664
Closes: https://todo.sr.ht/~sircmpwn/hare/666
Closes: https://todo.sr.ht/~sircmpwn/hare/773
Closes: https://todo.sr.ht/~sircmpwn/hare/821
Co-authored-by: Autumn! <autumnull@posteo.net>
Co-authored-by: Sebastian <sebastian@sebsite.pw>
Signed-off-by: Ember Sawady <ecs@d2evs.net>

Diffstat:
M.gitignore | 1+
MMakefile | 61++++++++++++++++++++++++++++++++++---------------------------
Acmd/hare/arch.ha | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acmd/hare/build.ha | 200+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acmd/hare/build/gather.ha | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acmd/hare/build/queue.ha | 321+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acmd/hare/build/types.ha | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acmd/hare/build/util.ha | 281+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acmd/hare/cache.ha | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcmd/hare/deps.ha | 155++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Acmd/hare/design.txt | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acmd/hare/error.ha | 32++++++++++++++++++++++++++++++++
Mcmd/hare/main.ha | 92+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Dcmd/hare/plan.ha | 325-------------------------------------------------------------------------------
Dcmd/hare/progress.ha | 64----------------------------------------------------------------
Dcmd/hare/schedule.ha | 394-------------------------------------------------------------------------------
Dcmd/hare/subcmds.ha | 573-------------------------------------------------------------------------------
Dcmd/hare/target.ha | 81-------------------------------------------------------------------------------
Acmd/hare/util.ha | 48++++++++++++++++++++++++++++++++++++++++++++++++
Acmd/hare/version.ha | 45+++++++++++++++++++++++++++++++++++++++++++++
Mcmd/harec/gen.ha | 2+-
Acmd/haredoc/arch.ha | 8++++++++
Rcmd/haredoc/color.ha -> cmd/haredoc/doc/color.ha | 0
Acmd/haredoc/doc/hare.ha | 196+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acmd/haredoc/doc/html.ha | 1067+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acmd/haredoc/doc/resolve.ha | 191+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acmd/haredoc/doc/sort.ha | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acmd/haredoc/doc/tty.ha | 595+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acmd/haredoc/doc/types.ha | 55+++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acmd/haredoc/doc/util.ha | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Dcmd/haredoc/docstr.ha | 248-------------------------------------------------------------------------------
Dcmd/haredoc/env.ha | 104-------------------------------------------------------------------------------
Acmd/haredoc/error.ha | 19+++++++++++++++++++
Dcmd/haredoc/errors.ha | 26--------------------------
Dcmd/haredoc/hare.ha | 197-------------------------------------------------------------------------------
Dcmd/haredoc/html.ha | 1086-------------------------------------------------------------------------------
Mcmd/haredoc/main.ha | 227+++++++++++++++++++++++++++++++++++--------------------------------------------
Dcmd/haredoc/resolver.ha | 178-------------------------------------------------------------------------------
Dcmd/haredoc/sort.ha | 103-------------------------------------------------------------------------------
Dcmd/haredoc/tty.ha | 591-------------------------------------------------------------------------------
Mcmd/haredoc/util.ha | 96+++++++++++++++++++++++++++++++------------------------------------------------
Rcrypto/aes/+x86_64/ni_native.s -> crypto/aes/+x86_64/ni.s | 0
Adocs/hare-doc.5.scd | 29+++++++++++++++++++++++++++++
Adocs/hare.1.scd | 357+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ddocs/hare.scd | 310-------------------------------------------------------------------------------
Adocs/haredoc.1.scd | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ddocs/haredoc.scd | 116-------------------------------------------------------------------------------
Ddocs/modules.md | 161-------------------------------------------------------------------------------
Mhare/module/README | 13+++----------
Ahare/module/cache.ha | 10++++++++++
Dhare/module/context.ha | 129-------------------------------------------------------------------------------
Ahare/module/deps.ha | 204+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ahare/module/format.ha | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dhare/module/manifest.ha | 403-------------------------------------------------------------------------------
Dhare/module/scan.ha | 495-------------------------------------------------------------------------------
Ahare/module/srcs.ha | 363+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mhare/module/types.ha | 181++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Ahare/module/util.ha | 70++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dhare/module/walk.ha | 91-------------------------------------------------------------------------------
Mhare/parse/decl.ha | 32+++++++++++++++++---------------
Ahare/parse/doc/doc.ha | 255+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rrt/+aarch64/cpuid_native.s -> rt/+aarch64/cpuid.s | 0
Rrt/+riscv64/cpuid_native.s -> rt/+riscv64/cpuid.s | 0
Rrt/+x86_64/cpuid_native.s -> rt/+x86_64/cpuid.s | 0
Mscripts/gen-stdlib | 40++++++++++++++++++++++++++++++----------
Mstdlib.mk | 108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------
66 files changed, 5583 insertions(+), 6100 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -2,4 +2,5 @@ config.mk .cache .bin *.1 +*.5 docs/html diff --git a/Makefile b/Makefile @@ -9,7 +9,7 @@ testlib_env = env all: -.SUFFIXES: .ha .ssa .s .o .scd .1 +.SUFFIXES: .ha .ssa .s .o .scd .ssa.s: @printf 'QBE\t%s\n' "$@" @$(QBE) -o $@ $< @@ -18,20 +18,22 @@ all: @printf 'AS\t%s\n' "$@" @$(AS) -g -o $@ $< -.scd.1: +.scd: @printf 'SCDOC\t%s\n' "$@" @$(SCDOC) < $< > $@ + include stdlib.mk hare_srcs = \ + ./cmd/hare/arch.ha \ + ./cmd/hare/build.ha \ + ./cmd/hare/cache.ha \ ./cmd/hare/deps.ha \ + ./cmd/hare/error.ha \ ./cmd/hare/main.ha \ - ./cmd/hare/plan.ha \ - ./cmd/hare/progress.ha \ - ./cmd/hare/schedule.ha \ - ./cmd/hare/subcmds.ha \ - ./cmd/hare/target.ha + ./cmd/hare/util.ha \ + ./cmd/hare/version.ha harec_srcs = \ ./cmd/harec/main.ha \ @@ -39,12 +41,17 @@ harec_srcs = \ haredoc_srcs = \ ./cmd/haredoc/main.ha \ - ./cmd/haredoc/errors.ha \ - ./cmd/haredoc/env.ha \ - ./cmd/haredoc/hare.ha \ - ./cmd/haredoc/html.ha \ - ./cmd/haredoc/sort.ha \ - ./cmd/haredoc/resolver.ha + ./cmd/haredoc/arch.ha \ + ./cmd/haredoc/error.ha \ + ./cmd/haredoc/util.ha \ + ./cmd/haredoc/doc/color.ha \ + ./cmd/haredoc/doc/hare.ha \ + ./cmd/haredoc/doc/html.ha \ + ./cmd/haredoc/doc/resolve.ha \ + ./cmd/haredoc/doc/sort.ha \ + ./cmd/haredoc/doc/tty.ha \ + ./cmd/haredoc/doc/types.ha \ + ./cmd/haredoc/doc/util.ha include targets.mk @@ -76,11 +83,7 @@ $(BINOUT)/harec2: $(BINOUT)/hare $(harec_srcs) @env HAREPATH=. HAREC=$(HAREC) QBE=$(QBE) $(BINOUT)/hare build \ $(HARE_DEFINES) -o $(BINOUT)/harec2 cmd/harec -# Prevent $(BINOUT)/hare from running builds in parallel, workaround for build -# driver bugs -PARALLEL_HACK=$(BINOUT)/harec2 - -$(BINOUT)/haredoc: $(BINOUT)/hare $(haredoc_srcs) $(PARALLEL_HACK) +$(BINOUT)/haredoc: $(BINOUT)/hare $(haredoc_srcs) @mkdir -p $(BINOUT) @printf 'HARE\t%s\n' "$@" @env HAREPATH=. HAREC=$(HAREC) QBE=$(QBE) $(BINOUT)/hare build \ @@ -89,13 +92,15 @@ $(BINOUT)/haredoc: $(BINOUT)/hare $(haredoc_srcs) $(PARALLEL_HACK) docs/html: $(BINOUT)/haredoc scripts/gen-docs.sh BINOUT=$(BINOUT) $(SHELL) ./scripts/gen-docs.sh -docs/hare.1: docs/hare.scd -docs/haredoc.1: docs/haredoc.scd +docs/hare.1: docs/hare.1.scd +docs/haredoc.1: docs/haredoc.1.scd +docs/hare-doc.5: docs/hare-doc.5.scd -docs: docs/hare.1 docs/haredoc.1 +docs: docs/hare.1 docs/haredoc.1 docs/hare-doc.5 clean: - rm -rf $(HARECACHE) $(BINOUT) docs/hare.1 docs/haredoc.1 docs/html + rm -rf $(HARECACHE) $(BINOUT) docs/hare.1 docs/haredoc.1 docs/hare-doc.5 \ + docs/html check: $(BINOUT)/hare-tests @$(BINOUT)/hare-tests @@ -103,22 +108,24 @@ check: $(BINOUT)/hare-tests scripts/gen-docs.sh: scripts/gen-stdlib scripts/gen-stdlib: scripts/gen-stdlib.sh -all: $(BINOUT)/hare $(BINOUT)/harec2 $(BINOUT)/haredoc +all: $(BINOUT)/hare $(BINOUT)/harec2 docs install: docs scripts/install-mods - mkdir -p $(DESTDIR)$(BINDIR) $(DESTDIR)$(MANDIR)/man1 \ + mkdir -p \ + $(DESTDIR)$(BINDIR) $(DESTDIR)$(MANDIR)/man1 \ + $(DESTDIR)$(BINDIR) $(DESTDIR)$(MANDIR)/man5 \ $(DESTDIR)$(SRCDIR)/hare/stdlib install -m755 $(BINOUT)/hare $(DESTDIR)$(BINDIR)/hare - install -m755 $(BINOUT)/haredoc $(DESTDIR)$(BINDIR)/haredoc install -m644 docs/hare.1 $(DESTDIR)$(MANDIR)/man1/hare.1 install -m644 docs/haredoc.1 $(DESTDIR)$(MANDIR)/man1/haredoc.1 + install -m644 docs/hare-doc.5 $(DESTDIR)$(MANDIR)/man5/hare-doc.5 ./scripts/install-mods "$(DESTDIR)$(SRCDIR)/hare/stdlib" uninstall: $(RM) $(DESTDIR)$(BINDIR)/hare - $(RM) $(DESTDIR)$(BINDIR)/haredoc $(RM) $(DESTDIR)$(MANDIR)/man1/hare.1 $(RM) $(DESTDIR)$(MANDIR)/man1/haredoc.1 + $(RM) $(DESTDIR)$(MANDIR)/man5/hare-doc.5 $(RM) -r $(DESTDIR)$(SRCDIR)/hare/stdlib -.PHONY: all clean check docs install uninstall $(BINOUT)/harec2 $(BINOUT)/haredoc +.PHONY: all clean check docs install uninstall diff --git a/cmd/hare/arch.ha b/cmd/hare/arch.ha @@ -0,0 +1,61 @@ +use hare::module; +use os; +use strings; + +def AARCH64_AS = "as"; +def AARCH64_CC = "cc"; +def AARCH64_LD = "ld"; +def RISCV64_AS = "as"; +def RISCV64_CC = "cc"; +def RISCV64_LD = "ld"; +def X86_64_AS = "as"; +def X86_64_CC = "cc"; +def X86_64_LD = "ld"; + +type arch = struct { + name: str, + qbe_name: str, + as_cmd: str, + cc_cmd: str, + ld_cmd: str, +}; + +// TODO: implement cross compiling to other kernels (e.g. linux => freebsd) +// TODO: sysroots +const arches: [_]arch = [ + arch { + name = "aarch64", + qbe_name = "arm64", + as_cmd = AARCH64_AS, + cc_cmd = AARCH64_CC, + ld_cmd = AARCH64_LD, + }, + arch { + name = "riscv64", + qbe_name = "rv64", + as_cmd = RISCV64_AS, + cc_cmd = RISCV64_CC, + ld_cmd = RISCV64_LD, + }, + arch { + name = "x86_64", + qbe_name = "amd64_sysv", + as_cmd = X86_64_AS, + cc_cmd = X86_64_CC, + ld_cmd = X86_64_LD, + }, +]; + +fn set_arch_tags(tags: *[]str, a: *arch) void = { + merge_tags(tags, "-aarch64-riscv64-x86_64")!; + append(tags, strings::dup(a.name)); +}; + +fn get_arch(name: str) (*arch | unknown_arch) = { + for (let i = 0z; i < len(arches); i += 1) { + if (arches[i].name == name) { + return &arches[i]; + }; + }; + return name: unknown_arch; +}; diff --git a/cmd/hare/build.ha b/cmd/hare/build.ha @@ -0,0 +1,200 @@ +use cmd::hare::build; +use errors; +use fmt; +use fs; +use getopt; +use hare::ast; +use hare::lex; +use hare::module; +use hare::parse; +use io; +use memio; +use os; +use os::exec; +use path; +use strconv; +use strings; +use unix::tty; + +fn build(name: str, cmd: *getopt::command) (void | error) = { + let arch = get_arch(os::machine())?; + let output = ""; + let ctx = build::context { + ctx = module::context { + harepath = harepath(), + harecache = harecache(), + tags = default_tags()?, + }, + goal = build::stage::BIN, + jobs = match (os::cpucount()) { + case errors::error => + yield 1z; + case let ncpu: int => + yield ncpu: size; + }, + version = build::get_version(os::tryenv("HAREC", "harec"))?, + ... + }; + defer build::ctx_finish(&ctx); + + if (name == "test") { + ctx.test = true; + ctx.submods = len(cmd.args) == 0; + merge_tags(&ctx.ctx.tags, "+test")?; + }; + + if (!tty::isatty(os::stderr_file)) { + ctx.mode = build::output::SILENT; + }; + + for (let i = 0z; i < len(cmd.opts); i += 1) { + let opt = cmd.opts[i]; + switch (opt.0) { + case 'a' => + arch = get_arch(opt.1)?; + case 'D' => + const buf = memio::fixed(strings::toutf8(opt.1)); + const lexer = lex::init(&buf, "<-D argument>"); + defer lex::finish(&lexer); + append(ctx.defines, parse::define(&lexer)?); + case 'j' => + match (strconv::stoz(opt.1)) { + case let z: size => + ctx.jobs = z; + case strconv::invalid => + fmt::fatal("Number of jobs must be an integer"); + case strconv::overflow => + if (strings::hasprefix(opt.1, '-')) { + fmt::fatal("Number of jobs must be positive"); + } else { + fmt::fatal("Number of jobs is too large"); + }; + }; + if (ctx.jobs == 0) { + fmt::fatal("Number of jobs must be non-zero"); + }; + case 'L' => + append(ctx.libdirs, opt.1); + case 'l' => + append(ctx.libs, opt.1); + case 'N' => + ast::ident_free(ctx.ns); + ctx.ns = []; + match (parse::identstr(opt.1)) { + case let id: ast::ident => + ctx.ns = id; + case lex::syntax => + return opt.1: invalid_namespace; + case let e: parse::error => + return e; + }; + case 'o' => + output = opt.1; + case 'q' => + ctx.mode = build::output::SILENT; + case 'T' => + merge_tags(&ctx.ctx.tags, opt.1)?; + case 't' => + switch (opt.1) { + case "td" => + // intentionally undocumented + ctx.goal = build::stage::TD; + case "ssa" => + // intentionally undocumented + ctx.goal = build::stage::SSA; + case "s" => + ctx.goal = build::stage::S; + case "o" => + ctx.goal = build::stage::O; + case "bin" => + ctx.goal = build::stage::BIN; + case => + return opt.1: unknown_type; + }; + case 'v' => + if (ctx.mode == build::output::VERBOSE) { + ctx.mode = build::output::VVERBOSE; + } else { + ctx.mode = build::output::VERBOSE; + }; + case => + abort(); + }; + }; + if (len(cmd.args) > 1 && name == "build") { + getopt::printusage(os::stderr, name, cmd.help...)!; + os::exit(1); + }; + + ctx.arch = arch.qbe_name; + ctx.cmds = ["", + os::tryenv("HAREC", "harec"), + os::tryenv("QBE", "qbe"), + os::tryenv("AS", arch.as_cmd), + os::tryenv("LD", arch.ld_cmd), + ]; + set_arch_tags(&ctx.ctx.tags, arch); + if (len(ctx.libs) > 0) { + merge_tags(&ctx.ctx.tags, "+libc")?; + ctx.cmds[build::stage::BIN] = os::tryenv("CC", arch.cc_cmd); + }; + + const input = if (len(cmd.args) == 0) os::getcwd() else cmd.args[0]; + + ctx.mods = build::gather(&ctx, os::realpath(input)?)?; + append(ctx.hashes, [[void...]...], len(ctx.mods)); + + let built = build::execute(&ctx)?; + defer free(built); + + if (output == "") { + if (name != "build") { + return run(input, built, cmd.args); + }; + output = get_output(ctx.goal, input)?; + }; + + let dest = os::stdout_file; + if (output != "-") { + let mode: fs::mode = 0o644; + if (ctx.goal == build::stage::BIN) { + mode |= 0o111; + }; + os::remove(output): void; + dest = match (os::create(output, mode)) { + case let f: io::file => + yield f; + case let e: fs::error => + return (output, e): output_failed; + }; + }; + defer io::close(dest)!; + let src = os::open(built)?; + defer io::close(src)!; + io::copy(dest, src)?; +}; + +fn run(name: str, path: str, args: []str) error = { + const args: []str = if (len(args) != 0) args[1..] else []; + let cmd = exec::cmd(path, args...)?; + exec::setname(&cmd, name); + exec::exec(&cmd); +}; + +fn get_output(goal: build::stage, input: str) (str | error) = { + static let buf = path::buffer { ... }; + let stat = os::stat(input)?; + path::set(&buf, os::realpath(input)?)?; + if (!fs::isdir(stat.mode)) { + path::pop(&buf); + }; + if (goal != build::stage::BIN) { + path::push_ext(&buf, build::stage_ext[goal])?; + }; + match (path::peek(&buf)) { + case let s: str => + return s; + case void => + return unknown_output; + }; +}; diff --git a/cmd/hare/build/gather.ha b/cmd/hare/build/gather.ha @@ -0,0 +1,70 @@ +use fs; +use hare::ast; +use hare::module; +use os; +use path; +use strings; + +export fn gather(ctx: *context, input: str) ([]module::module | error) = { + let mods: []module::module = []; + path::set(&buf, input)!; + module::gather(&ctx.ctx, &mods, ["rt"])?; + if (ctx.test) { + module::gather(&ctx.ctx, &mods, ["test"])?; + }; + const nsubmods = if (ctx.submods) { + let id: ast::ident = []; + defer ast::ident_free(id); + yield gather_submodules(&ctx.ctx, &mods, &buf, &id)?; + } else 0z; + + ctx.top = match (module::gather(&ctx.ctx, &mods, &buf)) { + case let top: size => + yield top; + case let e: module::error => + if (!(unwrap_module_error(e) is module::not_found) + || nsubmods == 0) { + return e; + }; + // running `hare test` with no args in a directory which isn't a + // module + // add a dummy module so the driver knows where in the cache to + // put the test runner binary + append(mods, module::module { + path = strings::dup(input), + ... + }); + yield len(mods) - 1; + }; + return mods; +}; + +fn gather_submodules( + ctx: *module::context, + mods: *[]module::module, + buf: *path::buffer, + mod: *ast::ident, +) (size | error) = { + let n = 0z; + let it = os::iter(path::string(buf))?; + defer fs::finish(it); + for (true) match (module::next(it)) { + case void => + break; + case let dir: fs::dirent => + path::push(buf, dir.name)?; + defer path::pop(buf); + append(mod, dir.name); + defer delete(mod[len(mod) - 1]); + match (module::gather(ctx, mods, *mod)) { + case size => + n += 1; + case let e: module::error => + if (!(unwrap_module_error(e) is module::not_found)) { + return e; + }; + }; + n += gather_submodules(ctx, mods, buf, mod)?; + }; + return n; +}; diff --git a/cmd/hare/build/queue.ha b/cmd/hare/build/queue.ha @@ -0,0 +1,321 @@ +use crypto::sha256; +use encoding::hex; +use errors; +use fmt; +use fs; +use hare::module; +use hare::unparse; +use hash; +use io; +use memio; +use os; +use os::exec; +use path; +use shlex; +use sort; +use strings; +use unix::tty; + +export fn execute(ctx: *context) (str | error) = { + let q: []*task = []; + defer free(q); + defer for (let i = 0z; i < len(q); i += 1) { + free_task(q[i]); + }; + const goal = if (ctx.goal == stage::TD) stage::SSA else ctx.goal; + queue(ctx, &q, goal, ctx.top); + // sort by stage, harec then qbe then as then ld, and keep reverse + // topo sort within each stage + sort::sort(q, size(*task), &task_cmp); + ctx.total = len(q); + + let jobs: []job = alloc([], ctx.jobs); + defer free(jobs); + + if (len(os::tryenv("NO_COLOR", "")) == 0 + && os::getenv("HAREC_COLOR") is void + && tty::isatty(os::stderr_file)) { + os::setenv("HAREC_COLOR", "1")!; + }; + + for (let i = 0z; len(q) != 0; i += 1) { + if (i == len(q)) { + await_task(ctx, &jobs)?; + i = 0; + }; + if (run_task(ctx, &jobs, q[i])?) { + delete(q[i]); + i = -1; + }; + }; + for (await_task(ctx, &jobs) is size) void; + if (ctx.mode == output::DEFAULT && ctx.total != 0) { + fmt::errorln()?; + }; + + return get_cache(ctx, ctx.top, ctx.goal)?; +}; + +fn task_cmp(a: const *opaque, b: const *opaque) int = { + let a = a: const **task, b = b: const **task; + return a.kind - b.kind; +}; + +fn queue(ctx: *context, q: *[]*task, kind: stage, idx: size) *task = { + for (let i = 0z; i < len(q); i += 1) { + if (q[i].kind == kind && q[i].idx == idx) { + return q[i]; + }; + }; + let t = alloc(task { + kind = kind, + idx = idx, + ... + }); + switch (kind) { + case stage::BIN => + t.ndeps = len(ctx.mods); + for (let i = 0z; i < len(ctx.mods); i += 1) { + append(queue(ctx, q, stage::O, i).rdeps, t); + }; + case stage::O, stage::S => + t.ndeps = 1; + append(queue(ctx, q, kind - 1, idx).rdeps, t); + case stage::SSA => + t.ndeps = len(ctx.mods[idx].deps); + for (let i = 0z; i < len(ctx.mods[idx].deps); i += 1) { + let j = ctx.mods[idx].deps[i].0; + append(queue(ctx, q, stage::SSA, j).rdeps, t); + }; + case stage::TD => abort(); + }; + append(q, t); + return t; +}; + +fn run_task(ctx: *context, jobs: *[]job, t: *task) (bool | error) = { + if (len(jobs) == ctx.jobs) { + await_task(ctx, jobs)?; + }; + assert(len(jobs) < ctx.jobs); + if (t.ndeps != 0) { + return false; + }; + let mod = ctx.mods[t.idx]; + let deps = get_deps(ctx, t); + defer strings::freeall(deps); + let flags = get_flags(ctx, t)?; + defer strings::freeall(flags); + ctx.hashes[t.idx][t.kind] = get_hash(ctx, deps, flags, t); + + os::mkdirs(module::get_cache(ctx.ctx.harecache, mod.path)?, 0o755)!; + let out = get_cache(ctx, t.idx, t.kind)?; + defer free(out); + + path::set(&buf, out)?; + let tmp = path::push_ext(&buf, "tmp")?; + let lock = os::create(tmp, 0o644, fs::flag::WRONLY | fs::flag::CREATE)?; + if (!io::lock(lock, false, io::lockop::EXCLUSIVE)?) { + io::close(lock)?; + return false; + }; + io::trunc(lock, 0)?; + + let args = get_args(ctx, tmp, flags, t); + defer strings::freeall(args); + + path::set(&buf, out)?; + write_args(ctx, path::push_ext(&buf, "txt")?, args, t)?; + + let outdated = module::outdated(out, deps, mod.srcs.mtime); + let exec = t.kind != stage::SSA || len(mod.srcs.ha) != 0; + if (!exec || !outdated) { + io::close(lock)?; + if (outdated) { + cleanup_task(ctx, t)?; + } else if (t.kind == stage::SSA) { + get_td(ctx, t.idx)?; + }; + free_task(t); + ctx.total -= 1; + return true; + }; + + switch (ctx.mode) { + case output::DEFAULT, output::SILENT => void; + case output::VERBOSE => + if (tty::isatty(os::stderr_file)) { + fmt::errorfln("\x1b[1m{}\x1b[0m\t{}", + ctx.cmds[t.kind], mod.name)?; + } else { + fmt::errorfln("{}\t{}", ctx.cmds[t.kind], mod.name)?; + }; + case output::VVERBOSE => + fmt::error(ctx.cmds[t.kind])?; + for (let i = 0z; i < len(args); i += 1) { + fmt::error(" ")?; + shlex::quote(os::stderr, args[i])?; + }; + fmt::errorln()?; + }; + + let cmd = exec::cmd(ctx.cmds[t.kind], args...)?; + path::set(&buf, out)?; + let output = os::create(path::push_ext(&buf, "log")?, 0o644)?; + defer io::close(output)!; + exec::addfile(&cmd, os::stdout_file, output); + exec::addfile(&cmd, os::stderr_file, output); + static append(jobs, job { + pid = exec::start(&cmd)?, + task = t, + lock = lock, + }); + return true; +}; + +fn await_task(ctx: *context, jobs: *[]job) (size | void | error) = { + if (ctx.mode == output::DEFAULT && ctx.total != 0) { + let percent = 100z; + if (ctx.total != 0) { + percent = ctx.completed * 100 / ctx.total; + }; + fmt::errorf("\x1b[G\x1b[2K{}/{} tasks completed ({}%)", + ctx.completed, ctx.total, percent)?; + }; + if (len(jobs) == 0) { + return; + }; + + let (proc, status) = exec::waitany()?; + let i = 0z; + for (i < len(jobs) && jobs[i].pid != proc; i += 1) void; + assert(i < len(jobs), "Unknown PID returned from waitany"); + let j = jobs[i]; + let t = j.task; + static delete(jobs[i]); + + let out = get_cache(ctx, t.idx, t.kind)?; + defer free(out); + path::set(&buf, out)?; + + let output = os::open(path::push_ext(&buf, "log")?)?; + defer io::close(output)!; + let output = io::drain(output)?; + defer free(output); + if (len(output) > 0) { + fmt::errorln()?; + io::writeall(os::stderr, output)?; + }; + + match (exec::check(&status)) { + case void => void; + case let e: !exec::exit_status => + fmt::fatal(ctx.mods[t.idx].name, ctx.cmds[t.kind], + exec::exitstr(e)); + }; + + cleanup_task(ctx, t)?; + free_task(t); + io::close(j.lock)?; + ctx.completed += 1; + return i; +}; + +// update the cache after a task has been run +fn cleanup_task(ctx: *context, t: *task) (void | error) = { + let out = get_cache(ctx, t.idx, t.kind)?; + defer free(out); + let tmp = strings::concat(out, ".tmp"); + defer free(tmp); + os::move(tmp, out)?; + if (t.kind != stage::SSA) { + return; + }; + + // td file is hashed solely based on its contents. not worth doing this + // for other types of outputs, but it gets us better caching behavior + // for tds since we need to include the dependency tds in the ssa hash + // see design.txt for more details + let tmp = strings::concat(out, ".td.tmp"); + defer free(tmp); + + let f = match (os::open(tmp)) { + case let f: io::file => + yield f; + case errors::noentry => + return; + case let err: fs::error => + return err; + }; + defer io::close(f)!; + let h = sha256::sha256(); + io::copy(&h, f)!; + let prefix: [sha256::SZ]u8 = [0...]; + hash::sum(&h, prefix); + ctx.hashes[t.idx][stage::TD] = prefix; + + let ptr = strings::concat(out, ".td"); + defer free(ptr); + let ptr = os::create(ptr, 0o644)?; + defer io::close(ptr)!; + hex::encode(ptr, prefix)?; + + let td = update_env(ctx, t.idx)?; + defer free(td); + if (os::exists(td)) { + os::remove(tmp)?; + } else { + os::move(tmp, td)?; + }; +}; + +// get the td for a module whose harec has been skipped +fn get_td(ctx: *context, idx: size) (void | error) = { + let ssa = get_cache(ctx, idx, stage::SSA)?; + defer free(ssa); + let ptr = strings::concat(ssa, ".td"); + defer free(ptr); + let ptr = match (os::open(ptr)) { + case fs::error => + return; + case let ptr: io::file => + yield ptr; + }; + defer io::close(ptr)!; + + let ptr = hex::newdecoder(ptr); + let prefix: [sha256::SZ]u8 = [0...]; + io::readall(&ptr, prefix)?; + ctx.hashes[idx][stage::TD] = prefix; + + free(update_env(ctx, idx)?); +}; + +// set $HARE_TD_<module>, returning the path to the module's td +fn update_env(ctx: *context, idx: size) (str | error) = { + let path = get_cache(ctx, idx, stage::TD)?; + let ns = unparse::identstr(ctx.mods[idx].ns); + defer free(ns); + if (ctx.mode == output::VVERBOSE) { + fmt::errorfln("# HARE_TD_{}={}", ns, path)?; + }; + let var = strings::concat("HARE_TD_", ns); + defer free(var); + os::setenv(var, path)!; + return path; +}; + +fn get_cache(ctx: *context, idx: size, kind: stage) (str | error) = { + let prefix = match (ctx.hashes[idx][kind]) { + case void => abort("expected non-void prefix in get_cache()"); + case let prefix: [sha256::SZ]u8 => + yield prefix; + }; + let s = memio::dynamic(); + memio::concat(&s, module::get_cache(ctx.ctx.harecache, + ctx.mods[idx].path)?)!; + memio::concat(&s, "/")!; + hex::encode(&s, prefix)!; + memio::concat(&s, ".", stage_ext[kind])!; + return memio::string(&s)!; +}; diff --git a/cmd/hare/build/types.ha b/cmd/hare/build/types.ha @@ -0,0 +1,102 @@ +use crypto::sha256; +use fs; +use hare::ast; +use hare::module; +use io; +use os::exec; +use path; +use strings; + +export type error = !(exec::error | fs::error | io::error | module::error | path::error); + +// a kind of cache file +export type stage = enum { + TD = 0, + SSA, + S, + O, + BIN, +}; + +def NSTAGES = stage::BIN + 1; + +// file extensions corresponding to each [[stage]] +export const stage_ext = ["td", "ssa", "s", "o", "bin"]; + +// a command in the queue to be run +export type task = struct { + // number of unfinished dependencies + ndeps: size, + // tasks to update (by decrementing ndeps) when this task is finished + rdeps: []*task, + kind: stage, + idx: size, +}; + +export fn free_task(t: *task) void = { + for (let i = 0z; i < len(t.rdeps); i += 1) { + t.rdeps[i].ndeps -= 1; + }; + free(t.rdeps); + free(t); +}; + +// a command which is currently running +export type job = struct { + pid: exec::process, + task: *task, + // fd to be closed once the job has finished, in order to release the + // [[io::lock]] on it + lock: io::file, +}; + +export type output = enum { + DEFAULT, + SILENT, + VERBOSE, + VVERBOSE, +}; + +export type context = struct { + ctx: module::context, + arch: str, + goal: stage, + defines: []ast::decl_const, + libdirs: []str, + libs: []str, + jobs: size, + ns: ast::ident, + // index of the root module within the gathered module slice + top: size, + // output of harec -v + version: []u8, + // true if invoked as `hare test` + test: bool, + // whether submodules of the root module should have tests enabled + submods: bool, + + cmds: [NSTAGES]str, + + mode: output, + completed: size, + total: size, + + mods: []module::module, + hashes: [][NSTAGES]([sha256::SZ]u8 | void), +}; + +export fn ctx_finish(ctx: *context) void = { + strings::freeall(ctx.ctx.tags); + for (let i = 0z; i < len(ctx.defines); i += 1) { + ast::ident_free(ctx.defines[i].ident); + ast::type_finish(ctx.defines[i]._type); + ast::expr_finish(ctx.defines[i].init); + }; + free(ctx.defines); + free(ctx.libdirs); + free(ctx.libs); + ast::ident_free(ctx.ns); + free(ctx.version); + module::free_slice(ctx.mods); + free(ctx.hashes); +}; diff --git a/cmd/hare/build/util.ha b/cmd/hare/build/util.ha @@ -0,0 +1,281 @@ +use crypto::sha256; +use fmt; +use hare::ast; +use hare::module; +use hare::unparse; +use hash; +use io; +use memio; +use os; +use os::exec; +use path; +use shlex; +use strings; + +// for use as a scratch buffer +let buf = path::buffer { ... }; + +export fn get_version(harec: str) ([]u8 | error) = { + let cmd = exec::cmd(harec, "-v")?; + let pipe = exec::pipe(); + exec::addfile(&cmd, os::stdout_file, pipe.1); + let proc = exec::start(&cmd)?; + io::close(pipe.1)?; + const version = io::drain(pipe.0)?; + let status = exec::wait(&proc)?; + io::close(pipe.0)?; + match (exec::check(&status)) { + case void => + return version; + case let status: !exec::exit_status => + fmt::fatal(harec, "-v", exec::exitstr(status)); + }; +}; + +fn get_deps(ctx: *context, t: *task) []str = { + let mod = ctx.mods[t.idx]; + switch (t.kind) { + case stage::TD => abort(); + case stage::SSA => + let deps = strings::dupall(mod.srcs.ha); + for (let i = 0z; i < len(mod.deps); i += 1) { + append(deps, get_cache(ctx, mod.deps[i].0, stage::TD)!); + }; + return deps; + case stage::S => + return alloc([get_cache(ctx, t.idx, stage::SSA)!]...); + case stage::O => + let deps = strings::dupall(mod.srcs.s...); + append(deps, get_cache(ctx, t.idx, stage::S)!); + return deps; + case stage::BIN => + let deps: []str = []; + for (let i = 0z; i < len(ctx.mods); i += 1) { + let srcs = &ctx.mods[i].srcs; + for (let j = 0z; j < len(srcs.sc); j += 1) { + append(deps, strings::dup(srcs.sc[j])); + }; + append(deps, get_cache(ctx, i, stage::O)!); + for (let j = 0z; j < len(srcs.o); j += 1) { + append(deps, strings::dup(srcs.o[j])); + }; + }; + return deps; + }; +}; + +fn get_flags(ctx: *context, t: *task) ([]str | error) = { + let flags = switch (t.kind) { + case stage::TD => abort(); + case stage::SSA => + yield "HARECFLAGS"; + case stage::S => + yield "QBEFLAGS"; + case stage::O => + yield "ASFLAGS"; + case stage::BIN => + yield if (len(ctx.libs) > 0) "LDFLAGS" else "LDLINKFLAGS"; + }; + let flags: []str = match (shlex::split(os::tryenv(flags, ""))) { + case let s: []str => + yield s; + case shlex::syntaxerr => + fmt::errorfln("warning: invalid shell syntax in ${}; ignoring", + flags)?; + yield []; + }; + + switch (t.kind) { + case stage::TD => abort(); + case stage::SSA => void; // below + case stage::S => + append(flags, strings::dup("-t")); + append(flags, strings::dup(ctx.arch)); + return flags; + case stage::O => + return flags; + case stage::BIN => + for (let i = 0z; i < len(ctx.libdirs); i += 1) { + append(flags, strings::dup("-L")); + append(flags, strings::dup(ctx.libdirs[i])); + }; + if (len(ctx.libs) == 0) { + append(flags, strings::dup("--gc-sections")); + append(flags, strings::dup("-z")); + append(flags, strings::dup("noexecstack")); + } else { + append(flags, strings::dup("-Wl,--gc-sections")); + }; + return flags; + }; + + let mod = ctx.mods[t.idx]; + if (len(mod.ns) != 0 || len(ctx.libs) != 0) { + append(flags, strings::dup("-N")); + append(flags, unparse::identstr(mod.ns)); + }; + + path::set(&buf, mod.path)?; + let test = ctx.test && t.idx == ctx.top; + test ||= path::trimprefix(&buf, os::getcwd()) is str && ctx.submods; + if (test) { + append(flags, strings::dup("-T")); + }; + + for (let i = 0z; i < len(ctx.defines); i += 1) { + let ident = ctx.defines[i].ident; + let ns = ident[..len(ident) - 1]; + if (!ast::ident_eq(ns, mod.ns)) { + continue; + }; + let buf = memio::dynamic(); + memio::concat(&buf, "-D", ident[len(ident) - 1])!; + match (ctx.defines[i]._type) { + case null => void; + case let t: *ast::_type => + memio::concat(&buf, ":")!; + unparse::_type(&buf, 0, *t)!; + }; + memio::concat(&buf, "=")!; + unparse::expr(&buf, 0, *ctx.defines[i].init)!; + append(flags, memio::string(&buf)!); + }; + + return flags; +}; + +fn get_hash( + ctx: *context, + deps: []str, + flags: []str, + t: *task, +) [sha256::SZ]u8 = { + let h = sha256::sha256(); + + hash::write(&h, strings::toutf8(ctx.cmds[t.kind])); + for (let i = 0z; i < len(flags); i += 1) { + hash::write(&h, strings::toutf8(flags[i])); + }; + + switch (t.kind) { + case stage::TD => abort(); + case stage::SSA => + hash::write(&h, ctx.version); + hash::write(&h, [0]); + for (let i = 0z; i < len(ctx.mods[t.idx].deps); i += 1) { + let ns = unparse::identstr(ctx.mods[t.idx].deps[i].1); + defer free(ns); + let var = strings::concat("HARE_TD_", ns); + defer free(var); + let path = match (os::getenv(var)) { + case void => + continue; + case let path: str => + yield path; + }; + hash::write(&h, strings::toutf8(var)); + hash::write(&h, strings::toutf8("=")); + hash::write(&h, strings::toutf8(path)); + hash::write(&h, [0]); + }; + case stage::S => + hash::write(&h, strings::toutf8(ctx.arch)); + hash::write(&h, [0]); + case stage::O => void; + case stage::BIN => + for (let i = 0z; i < len(ctx.libs); i += 1) { + hash::write(&h, strings::toutf8(ctx.libs[i])); + hash::write(&h, [0]); + }; + }; + + for (let i = 0z; i < len(deps); i += 1) { + hash::write(&h, strings::toutf8(deps[i])); + hash::write(&h, [0]); + }; + + let prefix: [sha256::SZ]u8 = [0...]; + hash::sum(&h, prefix); + return prefix; +}; + +fn get_args(ctx: *context, tmp: str, flags: []str, t: *task) []str = { + let args = strings::dupall(flags); + append(args, strings::dup("-o")); + append(args, strings::dup(tmp)); + + // TODO: https://todo.sr.ht/~sircmpwn/hare/837 + let srcs: []str = switch (t.kind) { + case stage::TD => abort(); + case stage::SSA => + let td = get_cache(ctx, t.idx, stage::SSA)!; + defer free(td); + append(args, strings::dup("-t")); + append(args, strings::concat(td, ".td.tmp")); + yield ctx.mods[t.idx].srcs.ha; + case stage::S => + append(args, get_cache(ctx, t.idx, stage::SSA)!); + // TODO: https://todo.sr.ht/~sircmpwn/hare/875 + yield []: []str; + case stage::O => + append(args, get_cache(ctx, t.idx, stage::S)!); + yield ctx.mods[t.idx].srcs.s; + case stage::BIN => + for (let i = 0z; i < len(ctx.mods); i += 1) { + let srcs = ctx.mods[i].srcs; + for (let i = 0z; i < len(srcs.sc); i += 1) { + append(args, strings::dup("-T")); + append(args, strings::dup(srcs.sc[i])); + }; + append(args, get_cache(ctx, i, stage::O)!); + for (let i = 0z; i < len(srcs.o); i += 1) { + append(args, strings::dup(srcs.o[i])); + }; + }; + if (len(ctx.libs) > 0) { + append(args, strings::dup("-Wl,--no-gc-sections")); + }; + for (let i = 0z; i < len(ctx.libs); i += 1) { + append(args, strings::dup("-l")); + append(args, strings::dup(ctx.libs[i])); + }; + yield []: []str; + }; + for (let i = 0z; i < len(srcs); i += 1) { + append(args, strings::dup(srcs[i])); + }; + return args; +}; + +fn write_args(ctx: *context, out: str, args: []str, t: *task) (void | error) = { + let txt = os::create(out, 0o644)?; + defer io::close(txt)!; + if (t.kind == stage::SSA) { + for (let i = 0z; i < len(ctx.mods[t.idx].deps); i += 1) { + let ns = unparse::identstr(ctx.mods[t.idx].deps[i].1); + defer free(ns); + let var = strings::concat("HARE_TD_", ns); + defer free(var); + fmt::fprintfln(txt, "# {}={}", var, os::tryenv(var, ""))?; + }; + }; + fmt::fprint(txt, ctx.cmds[t.kind])?; + for (let i = 0z; i < len(args); i += 1) { + fmt::fprint(txt, " ")?; + shlex::quote(txt, args[i])?; + }; + fmt::fprintln(txt)?; +}; + +// XXX: somewhat questionable, related to the hare::module context hackery, can +// probably only be improved with language changes +fn unwrap_module_error(err: module::error) module::error = { + let unwrapped = err; + for (true) match (unwrapped) { + case let e: module::errcontext => + unwrapped = *e.1; + case => + break; + }; + return unwrapped; +}; diff --git a/cmd/hare/cache.ha b/cmd/hare/cache.ha @@ -0,0 +1,66 @@ +use fmt; +use fs; +use getopt; +use os; +use path; + +// TODO: flesh this out some more. we probably want to have some sort of +// per-module statistics (how much space it's taking up in the cache and whether +// it's up to date for each tagset) and maybe also some sort of auto-pruner +// (only prune things that can no longer ever be considered up-to-date?) so that +// people don't need to periodically run hare cache -c n order to avoid the +// cache growing indefinitely + +fn cache(name: str, cmd: *getopt::command) (void | error) = { + let clear = false; + for (let i = 0z; i < len(cmd.opts); i += 1) { + let opt = cmd.opts[i]; + switch (opt.0) { + case 'c' => + clear = true; + case => abort(); + }; + }; + if (len(cmd.args) != 0) { + getopt::printusage(os::stderr, name, cmd.help)?; + }; + let cachedir = harecache(); + + if (clear) { + os::rmdirall(cachedir)?; + fmt::println(cachedir, "(0 B)")?; + return; + }; + + os::mkdirs(cachedir, 0o755)!; + let buf = path::init(cachedir)?; + let sz = dirsize(&buf)?; + const suffix = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"]; + let i = 0z; + for (i < len(suffix) - 1 && sz >= 1024; i += 1) { + sz /= 1024; + }; + fmt::printfln("{} ({} {})", cachedir, sz, suffix[i])?; +}; + +fn dirsize(buf: *path::buffer) (size | error) = { + let s = 0z; + let it = os::iter(path::string(buf))?; + defer os::finish(it); + for (true) match (fs::next(it)) { + case void => + break; + case let d: fs::dirent => + if (d.name == "." || d.name == "..") { + continue; + }; + path::push(buf, d.name)?; + let stat = os::stat(path::string(buf))?; + s += stat.sz; + if (fs::isdir(stat.mode)) { + s += dirsize(buf)?; + }; + path::pop(buf); + }; + return s; +}; diff --git a/cmd/hare/deps.ha b/cmd/hare/deps.ha @@ -1,104 +1,138 @@ use fmt; +use getopt; +use hare::ast; use hare::module; use hare::parse; use io; use os; +use path; use sort; use strings; -type depnode = struct { - ident: str, - depends: []size, +type deps_fmt = enum { + DOT, + TERM, +}; + +type link = struct { depth: uint, + child: size, + final: bool, }; -// the start of the cycle in the stack -type dep_cycle = !size; +fn deps(name: str, cmd: *getopt::command) (void | error) = { + let tags = default_tags()?; + defer free(tags); -// depth-first initial exploration, cycle-detection, reverse topological sort -fn explore_deps(ctx: *module::context, stack: *[]str, visited: *[]depnode, ident: str) (size | dep_cycle) = { - // check for cycles - for (let i = 0z; i < len(stack); i += 1) { - if (ident == stack[i]) { - append(stack, ident); - return i: dep_cycle; + let build_dir: str = ""; + let goal = deps_fmt::TERM; + for (let i = 0z; i < len(cmd.opts); i += 1) { + let opt = cmd.opts[i]; + switch (opt.0) { + case 'd' => + goal = deps_fmt::DOT; + case 'T' => + merge_tags(&tags, opt.1)?; + case => + abort(); }; }; - // return existing depnode if visited already - for (let i = 0z; i < len(visited); i += 1) { - if (ident == visited[i].ident) return i; + if (len(cmd.args) > 1) { + getopt::printusage(os::stderr, name, cmd.help...)!; + os::exit(1); }; - append(stack, ident); - let this = depnode{ident = strings::dup(ident), depends = [], depth = 0}; - let ver = match (module::lookup(ctx, parse::identstr(ident)!)) { - case let e: module::error => - fmt::fatal(module::strerror(e)); - case let ver: module::version => - yield ver; - }; - for (let i = 0z; i < len(ver.depends); i += 1) { - const name = strings::join("::", ver.depends[i]...); - defer free(name); - const child = explore_deps(ctx, stack, visited, name)?; - append(this.depends, child); - }; - // reverse-sort depends so that we know the last in the list is the - // "final" child during show_deps - sort::sort(this.depends, size(size), &cmpsz); + const input = if (len(cmd.args) == 0) os::getcwd() else cmd.args[0]; - static delete(stack[len(stack)-1]); - append(visited, this); - return len(visited) - 1; -}; + let ctx = module::context { + harepath = harepath(), + harecache = harecache(), + tags = tags, + }; + let mods: []module::module = []; -// sorts in reverse -fn cmpsz(a: const *opaque, b: const *opaque) int = (*(b: *size) - *(a: *size)): int; + let mod = match (parse::identstr(input)) { + case let id: ast::ident => + yield id; + case parse::error => + static let buf = path::buffer { ... }; + path::set(&buf, os::realpath(input)?)?; + yield &buf; + }; + module::gather(&ctx, &mods, mod)?; + defer module::free_slice(mods); -type link = struct { - depth: uint, - child: size, - final: bool, + switch (goal) { + case deps_fmt::TERM => + deps_graph(&mods); + case deps_fmt::DOT => + fmt::println("strict digraph deps {")!; + for (let i = 0z; i < len(mods); i += 1) { + for (let j = 0z; j < len(mods[i].deps); j += 1) { + const child = mods[mods[i].deps[j].0]; + fmt::printfln("\t\"{}\" -> \"{}\";", + mods[i].name, child.name)!; + }; + }; + fmt::println("}")!; + }; }; -fn show_deps(depnodes: *[]depnode) void = { +fn deps_graph(mods: *[]module::module) void = { let links: []link = []; defer free(links); + let depth: []uint = alloc([0...], len(mods)); // traverse in reverse because reverse-topo-sort - for (let i = len(depnodes) - 1; 0 <= i && i < len(depnodes); i -= 1) { + for (let i = len(mods) - 1; 0 <= i && i < len(mods); i -= 1) { + // reverse-sort deps so that we know the last in the list is the + // "final" child during show_deps + sort::sort(mods[i].deps, size((size, ast::ident)), &revsort); + for (let j = 0z; j < len(links); j += 1) { - if (i < links[j].child) continue; - if (depnodes[i].depth < links[j].depth + 1) depnodes[i].depth = links[j].depth + 1; + if (i < links[j].child) { + continue; + }; + if (depth[i] <= links[j].depth) { + depth[i] = links[j].depth + 1; + }; }; // print in-between row - for (let d = 0u; d < depnodes[i].depth; d += 1) { + for (let d = 0u; d < depth[i]; d += 1) { let passing = false; for (let j = 0z; j < len(links); j += 1) { - if (i < links[j].child) continue; + if (i < links[j].child) { + continue; + }; if (d == links[j].depth) { passing = true; }; }; fmt::print(if (passing) "│ " else " ")!; }; - if (i < len(depnodes) - 1) fmt::println()!; + if (i < len(mods) - 1) { + fmt::println()!; + }; // print row itself let on_path = false; - for (let d = 0u; d < depnodes[i].depth; d += 1) { + for (let d = 0u; d < depth[i]; d += 1) { let connected = false; let passing = false; let final = false; for (let j = 0z; j < len(links); j += 1) { - if (i < links[j].child) continue; + if (i < links[j].child) { + continue; + }; if (d == links[j].depth) { passing = true; if (i == links[j].child) { connected = true; on_path = true; - if (links[j].final) final = true; + if (links[j].final) { + final = true; + }; }; }; }; @@ -110,13 +144,20 @@ fn show_deps(depnodes: *[]depnode) void = { else " " )!; }; - fmt::println(depnodes[i].ident)!; - for (let j = 0z; j < len(depnodes[i].depends); j += 1) { + fmt::println(mods[i].name)!; + for (let j = 0z; j < len(mods[i].deps); j += 1) { append(links, link{ - depth = depnodes[i].depth, - child = depnodes[i].depends[j], - final = len(depnodes[i].depends) == j + 1, + depth = depth[i], + child = mods[i].deps[j].0, + final = len(mods[i].deps) == j + 1, }); }; }; }; + +// sorts in reverse +fn revsort(a: const *opaque, b: const *opaque) int = { + let a = *(a: *(size, str)); + let b = *(b: *(size, str)); + return (b.0 - a.0): int; +}; diff --git a/cmd/hare/design.txt b/cmd/hare/design.txt @@ -0,0 +1,67 @@ +# caching + +the cached Stuff for a module is stored under $HARECACHE/path/to/module. under +this path, the outputs of various commands (harec, qbe, as, and ld) are stored, +in <hash>.<ext>, where <ext> is td/ssa for harec, s for qbe, o for as, and bin +for ld + +the way the hash is computed varies slightly between extension: for everything +but .td, the hash contains the full argument list for the command used to +generate the file. for .ssa, the version of harec (the output of harec -v) and +the various HARE_TD_* environment variables are hashed as well + +.td is hashed solely based on its contents, in order to get better caching +behavior. this causes some trickiness which we'll get to later, so it's not +worth doing for everything, but doing this for .tds allows us to only recompile +a dependency of a module when its api changes, since the way that dependency +rebuilds are triggered is via $HARE_TD_depended::on::module changing. this is +particularly important for working on eg. rt::, since you don't actually need to +recompile most things most of the time despite the fact that rt:: is in the +dependency tree for most of the stdlib + +in order to check if the cache is already up to date, we do the following: +- find the sources for the module, including the latest time at which it was + modified. this gives us enough information to... +- figure out what command we would run to compile it, and generate the hash at + the same time +- find the mtime of $XDG_CACHE_HOME/path/to/module/<hash>.<ext>. if it isn't + earlier than the mtime from step 1, exit early +- run the command + +however, there's a bit of a problem here: how do we figure out the hash for the +.td if we don't end up rebuilding the module? we need it in order to set +$HARE_TD_module::ident, but since it's hashed based on its contents, there's no +way to figure it out without running harec. in order to get around this, we +store the td hash in <ssa_hash>.ssa.td, and read it from that file whenever we +skip running harec + +in order to avoid problems when running multiple hare builds in parallel, we +take an exclusive flock on <hash>.<ext>.tmp and have the command output to +there, then rename that to <hash>.<ext> once the command is done. if taking the +lock fails, we defer running that command as though it had unfinished +dependencies + +# queuing and running jobs + +the first step when running hare build is to gather all of the dependencies of a +given module and queue up all of the commands that will need to be run in order +to compile them. we keep track of each command in a task struct, which contains +a module::module, the compilation stage it's running, and the command's +prerequisites. the prerequisites for a harec are all of the harecs of the +modules it depends on[0], for qbe/as it's the harec/qbe for that module, and for +ld it's the ases for all of the modules that have been queued. we insert these +into an array of tasks, sorted with all of the harecs first, then qbes, then +ases, then ld, with a topological sort within each of these (such that each +command comes before all of the commands that depend on it). in order to run a +command, we scan from the start of this array until we find a job which doesn't +have any unfinished prerequisites and run that + +the reason for this sort order is to try to improve parallelism: in order to +make better use of available job slots, we want to prioritize jobs that will +unblock as many other jobs as possible. running a harec will always unblock more +jobs than a qbe or as, so we want to try to run them as early as possible. in my +tests, this roughly halved most compilation times at -j4 + +[0]: note that we only need the typedef file, one future improvement which would +improve parallelism would be to somehow have harec signal to hare build that +it's done with the typedefs so that we can unblock other harecs diff --git a/cmd/hare/error.ha b/cmd/hare/error.ha @@ -0,0 +1,32 @@ +use fs; +use hare::module; +use hare::parse; +use io; +use os::exec; +use path; +use strconv; + +type error = !( + exec::error | + fs::error | + io::error | + module::error | + path::error | + parse::error | + strconv::error | + unknown_arch | + unknown_output | + unknown_type | + output_failed | + invalid_namespace | +); + +type unknown_arch = !str; + +type unknown_output = !void; + +type unknown_type = !str; + +type output_failed = !(str, fs::error); + +type invalid_namespace = !str; diff --git a/cmd/hare/main.ha b/cmd/hare/main.ha @@ -1,12 +1,18 @@ // License: GPL-3.0 // (c) 2021 Drew DeVault <sir@cmpwn.com> // (c) 2021 Ember Sawady <ecs@d2evs.net> +use fmt; +use fs; use getopt; +use hare::module; +use hare::parse; +use io; use os; -use fmt; +use os::exec; +use path; +use strconv; def VERSION: str = "unknown"; -def PLATFORM: str = "unknown"; def HAREPATH: str = "."; const help: []getopt::help = [ @@ -15,54 +21,53 @@ const help: []getopt::help = [ "args...", ("build", [ "compiles the Hare program at <path>", - ('c', "build object instead of executable"), - ('v', "print executed commands"), + ('q', "build silently"), + ('v', "print executed commands (specify twice to print arguments)"), + ('a', "arch", "set target architecture"), ('D', "ident[:type]=value", "define a constant"), ('j', "jobs", "set parallelism for build"), ('L', "libdir", "add directory to linker library search path"), - ('l', "name", "link with a system library"), + ('l', "libname", "link with a system library"), ('N', "namespace", "override namespace for module"), ('o', "path", "set output file name"), - ('t', "arch", "set target architecture"), - ('T', "tags...", "set build tags"), - ('X', "tags...", "unset build tags"), - "<path>" + ('T', "tagset", "set/unset build tags"), + ('t', "type", "build type (s/o/bin)"), + "[path]" ]: []getopt::help), ("cache", [ "manages the build cache", - ('c', "cleans the specified modules"), - "modules...", + ('c', "clears the cache"), ]: []getopt::help), ("deps", [ "prints dependency information for a Hare program", ('d', "print dot syntax for use with graphviz"), - ('M', "build-dir", "print rules for POSIX make"), - ('T', "tags...", "set build tags"), - ('X', "tags...", "unset build tags"), - "<path|module>", + ('T', "tagset", "set/unset build tags"), + "[path|module]", ]: []getopt::help), ("run", [ "compiles and runs the Hare program at <path>", - ('v', "print executed commands"), + ('q', "build silently"), + ('v', "print executed commands (specify twice to print arguments)"), + ('a', "arch", "set target architecture"), ('D', "ident[:type]=value", "define a constant"), ('j', "jobs", "set parallelism for build"), ('L', "libdir", "add directory to linker library search path"), - ('l', "name", "link with a system library"), - ('T', "tags...", "set build tags"), - ('X', "tags...", "unset build tags"), - "<path>", "<args...>", + ('l', "libname", "link with a system library"), + ('T', "tagset", "set/unset build tags"), + "[path [args...]]", ]: []getopt::help), ("test", [ "compiles and runs tests for Hare programs", - ('v', "print executed commands"), + ('q', "build silently"), + ('v', "print executed commands (specify twice to print arguments)"), + ('a', "arch", "set target architecture"), ('D', "ident[:type]=value", "define a constant"), ('j', "jobs", "set parallelism for build"), ('L', "libdir", "add directory to linker library search path"), - ('l', "name", "link with a system library"), + ('l', "libname", "link with a system library"), ('o', "path", "set output file name"), - ('T', "tags...", "set build tags"), - ('X', "tags...", "unset build tags"), - "[tests...]" + ('T', "tagset", "set/unset build tags"), + "[path]" ]: []getopt::help), ("version", [ "provides version information for the Hare environment", @@ -79,20 +84,43 @@ export fn main() void = { os::exit(1); case let subcmd: (str, *getopt::command) => const task = switch (subcmd.0) { - case "build" => + case "build", "run", "test" => yield &build; case "cache" => yield &cache; case "deps" => yield &deps; - case "run" => - yield &run; - case "test" => - yield &test; case "version" => yield &version; - case => abort(); // unreachable + case => abort(); + }; + match (task(subcmd.0, subcmd.1)) { + case void => void; + case let e: exec::error => + fmt::fatal("Error:", exec::strerror(e)); + case let e: fs::error => + fmt::fatal("Error:", fs::strerror(e)); + case let e: io::error => + fmt::fatal("Error:", io::strerror(e)); + case let e: module::error => + fmt::fatal("Error:", module::strerror(e)); + case let e: path::error => + fmt::fatal("Error:", path::strerror(e)); + case let e: parse::error => + fmt::fatal("Error:", parse::strerror(e)); + case let e: strconv::error => + fmt::fatal("Error:", strconv::strerror(e)); + case let e: unknown_arch => + fmt::fatalf("Error: Unknown arch: {}", e); + case unknown_output => + fmt::fatal("Error: Can't guess output in root directory"); + case let e: unknown_type => + fmt::fatalf("Error: Unknown build type: {}", e); + case let e: output_failed => + fmt::fatalf("Error: Could not open output '{}': {}", + e.0, fs::strerror(e.1)); + case let e: invalid_namespace => + fmt::fatalf("Error: Invalid namespace: {}", e); }; - task(subcmd.1); }; }; diff --git a/cmd/hare/plan.ha b/cmd/hare/plan.ha @@ -1,325 +0,0 @@ -// License: GPL-3.0 -// (c) 2021-2022 Alexey Yerin <yyp@disroot.org> -// (c) 2021-2022 Drew DeVault <sir@cmpwn.com> -// (c) 2021 Ember Sawady <ecs@d2evs.net> -use fmt; -use fs; -use hare::ast; -use hare::module; -use io; -use os::exec; -use os; -use path; -use shlex; -use strings; -use temp; -use unix::tty; - -type status = enum { - SCHEDULED, - COMPLETE, - SKIP, -}; - -type task = struct { - status: status, - depend: []*task, - output: str, - cmd: []str, - module: (str | void), -}; - -fn task_free(task: *task) void = { - free(task.depend); - free(task.output); - free(task.cmd); - match (task.module) { - case let s: str => - free(s); - case => void; - }; - free(task); -}; - -type modcache = struct { - hash: u32, - task: *task, - ident: ast::ident, - version: module::version, -}; - -type plan = struct { - context: *module::context, - target: *target, - workdir: str, - counter: uint, - scheduled: []*task, - complete: []*task, - script: str, - libdir: []str, - libs: []str, - environ: [](str, str), - modmap: [64][]modcache, - progress: plan_progress, -}; - -type plan_progress = struct { - tty: (io::file | void), - complete: size, - total: size, - current_module: str, - maxwidth: size, -}; - -fn mkplan( - ctx: *module::context, - libdir: []str, - libs: []str, - target: *target, -) plan = { - const rtdir = match (module::lookup(ctx, ["rt"])) { - case let err: module::error => - fmt::fatal("Error resolving rt:", module::strerror(err)); - case let ver: module::version => - yield ver.basedir; - }; - - // Look up the most appropriate hare.sc file - let ntag = 0z; - const buf = path::init()!; - const iter = os::iter(rtdir)!; - defer os::finish(iter); - for (true) match (fs::next(iter)) { - case let d: fs::dirent => - const p = module::parsename(d.name); - const name = p.0, ext = p.1, tags = p.2; - defer module::tags_free(tags); - - if (len(tags) >= ntag && name == "hare" && ext == "sc" - && module::tagcompat(ctx.tags, tags)) { - ntag = len(tags); - path::set(&buf, rtdir, d.name)!; - }; - case void => - break; - }; - - ar_tool.0 = target.ar_cmd; - as_tool.0 = target.as_cmd; - cc_tool.0 = target.cc_cmd; - ld_tool.0 = target.ld_cmd; - - let environ: [](str, str) = alloc([ - (strings::dup("HARECACHE"), strings::dup(ctx.cache)), - ]); - - if (len(os::tryenv("NO_COLOR", "")) == 0 - && os::getenv("HAREC_COLOR") is void - && tty::isatty(os::stderr_file)) { - append(environ, - (strings::dup("HAREC_COLOR"), strings::dup("1")) - ); - }; - - return plan { - context = ctx, - target = target, - workdir = os::tryenv("HARE_DEBUG_WORKDIR", temp::dir()), - script = strings::dup(path::string(&buf)), - environ = environ, - libdir = libdir, - libs = libs, - progress = plan_progress { - tty = if (tty::isatty(os::stderr_file)) os::stderr_file - else void, - ... - }, - ... - }; -}; - -fn plan_finish(plan: *plan) void = { - if (os::getenv("HARE_DEBUG_WORKDIR") is void) { - os::rmdirall(plan.workdir)!; - }; - - for (let i = 0z; i < len(plan.complete); i += 1) { - let task = plan.complete[i]; - task_free(task); - }; - free(plan.complete); - - for (let i = 0z; i < len(plan.scheduled); i += 1) { - let task = plan.scheduled[i]; - task_free(task); - }; - free(plan.scheduled); - - for (let i = 0z; i < len(plan.environ); i += 1) { - free(plan.environ[i].0); - free(plan.environ[i].1); - }; - free(plan.environ); - - free(plan.script); - - for (let i = 0z; i < len(plan.modmap); i += 1) { - free(plan.modmap[i]); - }; -}; - -fn plan_execute(plan: *plan, verbose: bool) (void | !exec::exit_status) = { - plan.progress.total = len(plan.scheduled); - - if (verbose) { - plan.progress.tty = void; - for (let i = 0z; i < len(plan.environ); i += 1) { - let item = plan.environ[i]; - fmt::errorf("# {}=", item.0)!; - shlex::quote(os::stderr, item.1)!; - fmt::errorln()!; - }; - }; - - for (len(plan.scheduled) != 0) { - let next: nullable *task = null; - let i = 0z; - for (i < len(plan.scheduled); i += 1) { - let task = plan.scheduled[i]; - let eligible = true; - for (let j = 0z; j < len(task.depend); j += 1) { - if (task.depend[j].status == status::SCHEDULED) { - eligible = false; - break; - }; - }; - if (eligible) { - next = task; - break; - }; - }; - - let task = next as *task; - match (task.module) { - case let s: str => - plan.progress.current_module = s; - case => void; - }; - - progress_increment(plan); - - match (execute(plan, task, verbose)) { - case let err: exec::error => - progress_clear(plan); - fmt::fatalf("Error: {}: {}", task.cmd[0], - exec::strerror(err)); - case let err: !exec::exit_status => - progress_clear(plan); - fmt::errorfln("Error: {}: {}", task.cmd[0], - exec::exitstr(err))!; - return err; - case void => void; - }; - - task.status = status::COMPLETE; - - delete(plan.scheduled[i]); - append(plan.complete, task); - }; - - progress_clear(plan); - update_modcache(plan); -}; - -fn update_cache(plan: *plan, mod: modcache) void = { - let manifest = module::manifest { - ident = mod.ident, - inputs = mod.version.inputs, - versions = [mod.version], - }; - match (module::manifest_write(plan.context, &manifest)) { - case let err: module::error => - fmt::fatal("Error updating module cache:", - module::strerror(err)); - case void => void; - }; -}; - -fn update_modcache(plan: *plan) void = { - for (let i = 0z; i < len(plan.modmap); i += 1) { - let mods = plan.modmap[i]; - if (len(mods) == 0) { - continue; - }; - for (let j = 0z; j < len(mods); j += 1) { - if (mods[j].task.status == status::COMPLETE) { - update_cache(plan, mods[j]); - }; - }; - }; -}; - -fn execute( - plan: *plan, - task: *task, - verbose: bool, -) (void | exec::error | !exec::exit_status) = { - if (verbose) { - for (let i = 0z; i < len(task.cmd); i += 1) { - fmt::errorf("{} ", task.cmd[i])?; - }; - fmt::errorln()?; - }; - - let cmd = match (exec::cmd(task.cmd[0], task.cmd[1..]...)) { - case let cmd: exec::command => - yield cmd; - case let err: exec::error => - progress_clear(plan); - fmt::fatalf("Error resolving {}: {}", task.cmd[0], - exec::strerror(err)); - }; - for (let i = 0z; i < len(plan.environ); i += 1) { - let e = plan.environ[i]; - exec::setenv(&cmd, e.0, e.1)!; - }; - - const pipe = if (plan.progress.tty is io::file) { - const pipe = exec::pipe(); - exec::addfile(&cmd, os::stderr_file, pipe.1); - yield pipe; - } else (0: io::file, 0: io::file); - - let proc = exec::start(&cmd)?; - if (pipe.0 != 0) { - io::close(pipe.1)?; - }; - - let cleared = false; - if (pipe.0 != 0) { - for (true) { - let buf: [os::BUFSZ]u8 = [0...]; - match (io::read(pipe.0, buf)?) { - case let n: size => - if (!cleared) { - progress_clear(plan); - cleared = true; - }; - io::writeall(os::stderr, buf[..n])?; - case io::EOF => - break; - }; - }; - }; - let st = exec::wait(&proc)?; - return exec::check(&st); -}; - -fn mkfile(plan: *plan, input: str, ext: str) str = { - static let namebuf: [32]u8 = [0...]; - const name = fmt::bsprintf(namebuf, "temp.{}.{}.{}", - input, plan.counter, ext); - plan.counter += 1; - const buf = path::init(plan.workdir, name)!; - return strings::dup(path::string(&buf)); -}; diff --git a/cmd/hare/progress.ha b/cmd/hare/progress.ha @@ -1,64 +0,0 @@ -use fmt; -use io; -use math; -use unix::tty; - -fn progress_update(plan: *plan) void = { - const tty = match (plan.progress.tty) { - case let f: io::file => - yield f; - case => - return; - }; - - const width = match (tty::winsize(tty)) { - case let ts: tty::ttysize => - yield if (ts.columns > 80 || ts.columns == 0) 80 else ts.columns; - case => - yield 64; - }: size; - - const complete = plan.progress.complete, - total = plan.progress.total, - current_module = plan.progress.current_module; - - const total_width = math::ceilf64(math::log10f64(total: f64)): size; - const counter_width = 1 + total_width + 1 + total_width + 3; - const progress_width = width - counter_width - 2 - plan.progress.maxwidth; - - fmt::fprintf(tty, "\x1b[G\x1b[K[{%}/{}] [", - complete, &fmt::modifiers { - width = total_width: uint, - ... - }, - total)!; - const stop = (complete: f64 / total: f64 * progress_width: f64): size; - for (let i = 0z; i < progress_width; i += 1) { - if (i > stop) { - fmt::fprint(tty, ".")!; - } else { - fmt::fprint(tty, "#")!; - }; - }; - if (len(current_module) > 0) { - fmt::fprintf(tty, "] {}", current_module)!; - } else { - // Don't print a leading space - fmt::fprint(tty, "]")!; - }; -}; - -fn progress_clear(plan: *plan) void = { - const tty = match (plan.progress.tty) { - case let f: io::file => - yield f; - case => - return; - }; - fmt::fprint(tty, "\x1b[G\x1b[K")!; -}; - -fn progress_increment(plan: *plan) void = { - plan.progress.complete += 1; - progress_update(plan); -}; diff --git a/cmd/hare/schedule.ha b/cmd/hare/schedule.ha @@ -1,394 +0,0 @@ -// License: GPL-3.0 -// (c) 2021-2022 Alexey Yerin <yyp@disroot.org> -// (c) 2021-2022 Drew DeVault <sir@cmpwn.com> -// (c) 2021 Ember Sawady <ecs@d2evs.net> -// (c) 2021 Thomas Bracht Laumann Jespersen <t@laumann.xyz> -// (c) 2022 Jon Eskin <eskinjp@gmail.com> -use encoding::hex; -use fmt; -use fs; -use hare::ast; -use hare::module; -use hare::unparse; -use hash::fnv; -use hash; -use os; -use path; -use shlex; -use strings; - -fn getenv(var: str) []str = { - match (os::getenv(var)) { - case let val: str => - match (shlex::split(val)) { - case let fields: []str => - return fields; - case => void; - }; - case => void; - }; - - return []; -}; - -// (executable name, executable variable, flags variable) -type tool = (str, str, str); - -let cc_tool: tool = ("", "CC", "LDFLAGS"); -let ld_tool: tool = ("", "LD", "LDLINKFLAGS"); -let as_tool: tool = ("", "AS", "ASFLAGS"); -let ar_tool: tool = ("", "AR", "ARFLAGS"); -let qbe_tool: tool = ("qbe", "QBE", "QBEFLAGS"); - -fn getcmd(tool: *tool, args: str...) []str = { - let execargs: []str = []; - - let vals = getenv(tool.1); - defer free(vals); - if (len(vals) == 0) { - append(execargs, tool.0); - } else { - append(execargs, vals...); - }; - - let vals = getenv(tool.2); - defer free(vals); - append(execargs, vals...); - - append(execargs, args...); - - return execargs; -}; - -fn ident_hash(ident: ast::ident) u32 = { - let hash = fnv::fnv32(); - for (let i = 0z; i < len(ident); i += 1) { - hash::write(&hash, strings::toutf8(ident[i])); - hash::write(&hash, [0]); - }; - return fnv::sum32(&hash); -}; - -fn sched_module(plan: *plan, ident: ast::ident, link: *[]*task) *task = { - let hash = ident_hash(ident); - let bucket = &plan.modmap[hash % len(plan.modmap)]; - for (let i = 0z; i < len(bucket); i += 1) { - if (bucket[i].hash == hash - && ast::ident_eq(bucket[i].ident, ident)) { - return bucket[i].task; - }; - }; - - let ver = match (module::lookup(plan.context, ident)) { - case let err: module::error => - let ident = unparse::identstr(ident); - progress_clear(plan); - fmt::fatalf("Error resolving {}: {}", ident, - module::strerror(err)); - case let ver: module::version => - yield ver; - }; - - let depends: []*task = []; - defer free(depends); - for (let i = 0z; i < len(ver.depends); i += 1) { - const dep = ver.depends[i]; - let obj = sched_module(plan, dep, link); - append(depends, obj); - }; - - let obj = sched_hare_object(plan, ver, ident, void, depends...); - append(bucket, modcache { - hash = hash, - task = obj, - ident = ident, - version = ver, - }); - append(link, obj); - return obj; -}; - -// Schedules a task which compiles objects into an executable. -fn sched_ld(plan: *plan, output: str, depend: *task...) *task = { - let linker = if (len(plan.libs) > 0) { - yield &cc_tool; - } else { - yield &ld_tool; - }; - let task = alloc(task { - status = status::SCHEDULED, - output = output, - depend = alloc(depend...), - cmd = getcmd(linker, - "-T", plan.script, - "-o", output), - module = void, - }); - - if (len(plan.libdir) != 0) { - for (let i = 0z; i < len(plan.libdir); i += 1) { - append(task.cmd, strings::concat("-L", plan.libdir[i])); - }; - }; - - // Using --gc-sections and -z noexecstack will not work when using cc as - // the linker - if (linker == &ld_tool) { - append(task.cmd, ["--gc-sections", "-z", "noexecstack"]...); - }; - - let archives: []str = []; - defer free(archives); - - for (let i = 0z; i < len(depend); i += 1) { - if (strings::hassuffix(depend[i].output, ".a")) { - append(archives, depend[i].output); - } else { - append(task.cmd, depend[i].output); - }; - }; - append(task.cmd, archives...); - for (let i = 0z; i < len(plan.libs); i += 1) { - append(task.cmd, strings::concat("-l", plan.libs[i])); - }; - append(plan.scheduled, task); - return task; -}; - -// Schedules a task which merges objects into an archive. -fn sched_ar(plan: *plan, output: str, depend: *task...) *task = { - let task = alloc(task { - status = status::SCHEDULED, - output = output, - depend = alloc(depend...), - cmd = getcmd(&ar_tool, "-c", output), - module = void, - }); - - // POSIX specifies `ar -r [-cuv] <archive> <file>` - // Add -r here so it is always before any ARFLAGS - insert(task.cmd[1], "-r"); - - for (let i = 0z; i < len(depend); i += 1) { - assert(strings::hassuffix(depend[i].output, ".o")); - append(task.cmd, depend[i].output); - }; - append(plan.scheduled, task); - return task; -}; - -// Schedules a task which compiles assembly into an object. -fn sched_as(plan: *plan, output: str, input: str, depend: *task...) *task = { - let task = alloc(task { - status = status::SCHEDULED, - output = output, - depend = alloc(depend...), - cmd = getcmd(&as_tool, "-g", "-o", output), - module = void, - }); - - append(task.cmd, input); - - append(plan.scheduled, task); - return task; -}; - -// Schedules a task which compiles an SSA file into assembly. -fn sched_qbe(plan: *plan, output: str, depend: *task) *task = { - let task = alloc(task { - status = status::SCHEDULED, - output = output, - depend = alloc([depend]), - cmd = getcmd(&qbe_tool, - "-t", plan.target.qbe_target, - "-o", output, - depend.output), - module = void, - }); - append(plan.scheduled, task); - return task; -}; - -// Schedules tasks which compiles a Hare module into an object or archive. -fn sched_hare_object( - plan: *plan, - ver: module::version, - namespace: ast::ident, - output: (void | str), - depend: *task... -) *task = { - // XXX: Do we care to support assembly-only modules? - let mixed = false; - for (let i = 0z; i < len(ver.inputs); i += 1) { - if (strings::hassuffix(ver.inputs[i].path, ".s")) { - mixed = true; - break; - }; - }; - - const ns = unparse::identstr(namespace); - const displayed_ns = if (len(ns) == 0) "(root)" else ns; - if (len(ns) > plan.progress.maxwidth) - plan.progress.maxwidth = len(ns); - - let ssa = mkfile(plan, ns, "ssa"); - let harec = alloc(task { - status = status::SCHEDULED, - output = ssa, - depend = alloc(depend...), - cmd = alloc([ - os::tryenv("HAREC", "harec"), "-o", ssa, - ]), - module = strings::dup(ns), - }); - - let libc = false; - for (let i = 0z; i < len(plan.context.tags); i += 1) { - if (plan.context.tags[i].mode == module::tag_mode::INCLUSIVE - && plan.context.tags[i].name == "test") { - const opaths = plan.context.paths; - plan.context.paths = ["."]; - const ver = module::lookup(plan.context, namespace); - if (ver is module::version) { - append(harec.cmd, "-T"); - }; - plan.context.paths = opaths; - } else if (plan.context.tags[i].mode == module::tag_mode::INCLUSIVE - && plan.context.tags[i].name == "libc") { - libc = true; - }; - }; - - if (len(ns) != 0 || libc) { - append(harec.cmd, ["-N", ns]...); - }; - - let current = false; - let output = if (output is str) { - static let buf = path::buffer{...}; - path::set(&buf, output as str)!; - // TODO: Should we use the cache here? - const ext = match (path::peek_ext(&buf)) { - case let s: str => yield s; - case void => yield ""; - }; - const expected = if (mixed) "a" else "o"; - if (ext != expected) { - fmt::errorfln("Warning: Expected output file extension {}, found {}", - expected, output)!; - }; - yield strings::dup(output as str); - } else if (len(namespace) != 0) { - let buf = path::init(plan.context.cache)!; - path::push(&buf, namespace...)!; - const path = path::string(&buf); - match (os::mkdirs(path, 0o755)) { - case void => void; - case let err: fs::error => - progress_clear(plan); - fmt::fatalf("Error: mkdirs {}: {}", path, - fs::strerror(err)); - }; - - let version = hex::encodestr(ver.hash); - let td = fmt::asprintf("{}.td", version); - defer free(td); - let name = fmt::asprintf("{}.{}", version, - if (mixed) "a" else "o"); - defer free(name); - path::push(&buf, td)!; - - append(plan.environ, ( - fmt::asprintf("HARE_TD_{}", ns), - strings::dup(path::string(&buf)), - )); - - // TODO: Keep this around and append new versions, rather than - // overwriting with just the latest - let manifest = match (module::manifest_load( - plan.context, namespace)) { - case let err: module::error => - progress_clear(plan); - fmt::fatalf("Error reading cache entry for {}: {}", - displayed_ns, module::strerror(err)); - case let m: module::manifest => - yield m; - }; - defer module::manifest_finish(&manifest); - current = module::current(&manifest, &ver); - - append(harec.cmd, ["-t", strings::dup(path::string(&buf))]...); - yield strings::dup(path::push(&buf, "..", name)!); - } else { - // XXX: This is probably kind of dumb - // It would be better to apply any defines which affect this - // namespace instead - for (let i = 0z; i < len(plan.context.defines); i += 1) { - append(harec.cmd, ["-D", plan.context.defines[i]]...); - }; - - yield mkfile(plan, ns, "o"); // TODO: Should exes go in the cache? - }; - - let hare_inputs = 0z; - for (let i = 0z; i < len(ver.inputs); i += 1) { - let path = ver.inputs[i].path; - if (strings::hassuffix(path, ".ha")) { - append(harec.cmd, path); - hare_inputs += 1; - }; - }; - if (hare_inputs == 0) { - progress_clear(plan); - fmt::fatalf("Error: Module {} has no Hare input files", - displayed_ns); - }; - - if (current) { - harec.status = status::COMPLETE; - harec.output = output; - append(plan.complete, harec); - return harec; - } else { - append(plan.scheduled, harec); - }; - - let s = mkfile(plan, ns, "s"); - let qbe = sched_qbe(plan, s, harec); - let hare_obj = sched_as(plan, - if (mixed) mkfile(plan, ns, "o") else output, - s, qbe); - if (!mixed) { - return hare_obj; - }; - - let objs: []*task = alloc([hare_obj]); - defer free(objs); - for (let i = 0z; i < len(ver.inputs); i += 1) { - // XXX: All of our assembly files don't depend on anything else, - // but that may not be generally true. We may have to address - // this at some point. - let path = ver.inputs[i].path; - if (!strings::hassuffix(path, ".s")) { - continue; - }; - append(objs, sched_as(plan, mkfile(plan, ns, "o"), path)); - }; - return sched_ar(plan, output, objs...); -}; - -// Schedules tasks which compiles hare sources into an executable. -fn sched_hare_exe( - plan: *plan, - ver: module::version, - output: str, - depend: *task... -) *task = { - let obj = sched_hare_object(plan, ver, [], void, depend...); - // TODO: We should be able to use partial variadic application - let link: []*task = alloc([], len(depend)); - defer free(link); - append(link, obj); - append(link, depend...); - return sched_ld(plan, strings::dup(output), link...); -}; diff --git a/cmd/hare/subcmds.ha b/cmd/hare/subcmds.ha @@ -1,573 +0,0 @@ -// License: GPL-3.0 -// (c) 2021-2022 Alexey Yerin <yyp@disroot.org> -// (c) 2021 Drew DeVault <sir@cmpwn.com> -// (c) 2021 Ember Sawady <ecs@d2evs.net> -use ascii; -use bufio; -use encoding::utf8; -use errors; -use fmt; -use fs; -use getopt; -use hare::ast; -use hare::module; -use hare::parse; -use io; -use os::exec; -use os; -use path; -use sort; -use strings; -use unix::tty; - -fn addtags(tags: []module::tag, in: str) ([]module::tag | void) = { - let in = match (module::parsetags(in)) { - case void => - return void; - case let t: []module::tag => - yield t; - }; - defer free(in); - append(tags, in...); - return tags; -}; - -fn deltags(tags: []module::tag, in: str) ([]module::tag | void) = { - if (in == "^") { - module::tags_free(tags); - return []; - }; - let in = match (module::parsetags(in)) { - case void => - return void; - case let t: []module::tag => - yield t; - }; - defer free(in); - for (let i = 0z; i < len(tags); i += 1) { - for (let j = 0z; j < len(in); j += 1) { - if (tags[i].name == in[j].name - && tags[i].mode == in[j].mode) { - free(tags[i].name); - delete(tags[i]); - i -= 1; - }; - }; - }; - return tags; -}; - -type goal = enum { - OBJ, - EXE, -}; - -fn build(cmd: *getopt::command) void = { - let build_target = default_target(); - let tags = module::tags_dup(build_target.tags); - defer module::tags_free(tags); - - let verbose = false; - let output = ""; - let goal = goal::EXE; - let defines: []str = []; - defer free(defines); - let libdir: []str = []; - defer free(libdir); - let libs: []str = []; - defer free(libs); - let namespace: ast::ident = []; - for (let i = 0z; i < len(cmd.opts); i += 1) { - let opt = cmd.opts[i]; - switch (opt.0) { - case 'c' => - goal = goal::OBJ; - case 'v' => - verbose = true; - case 'D' => - append(defines, opt.1); - case 'j' => - abort("-j option not implemented yet."); // TODO - case 'L' => - append(libdir, opt.1); - case 'l' => - append(libs, opt.1); - case 'N' => - namespace = match (parse::identstr(opt.1)) { - case let id: ast::ident => - yield id; - case let err: parse::error => - fmt::fatalf("Error parsing namespace {}: {}", - opt.1, parse::strerror(err)); - }; - case 'o' => - output = opt.1; - case 't' => - match (get_target(opt.1)) { - case void => - fmt::fatalf("Unsupported target '{}'", opt.1); - case let t: *target => - build_target = t; - module::tags_free(tags); - tags = module::tags_dup(t.tags); - }; - case 'T' => - tags = match (addtags(tags, opt.1)) { - case void => - fmt::fatal("Error parsing tags"); - case let t: []module::tag => - yield t; - }; - case 'X' => - tags = match (deltags(tags, opt.1)) { - case void => - fmt::fatal("Error parsing tags"); - case let t: []module::tag => - yield t; - }; - case => - abort(); - }; - }; - - const input = - if (len(cmd.args) == 0) os::getcwd() - else if (len(cmd.args) == 1) cmd.args[0] - else { - getopt::printusage(os::stderr, "build", cmd.help...)!; - os::exit(1); - }; - - if (len(libs) > 0) { - append(tags, module::tag { - mode = module::tag_mode::INCLUSIVE, - name = strings::dup("libc"), - }); - }; - - const ctx = module::context_init(tags, defines, HAREPATH); - defer module::context_finish(&ctx); - - const plan = mkplan(&ctx, libdir, libs, build_target); - defer plan_finish(&plan); - - const ver = match (module::scan(&ctx, input)) { - case let ver: module::version => - yield ver; - case let err: module::error => - fmt::fatal("Error scanning input module:", - module::strerror(err)); - }; - - const depends: []*task = []; - sched_module(&plan, ["rt"], &depends); - - for (let i = 0z; i < len(ver.depends); i += 1z) { - const dep = ver.depends[i]; - sched_module(&plan, dep, &depends); - }; - - // TODO: Choose this more intelligently - if (output == "") { - output = path::basename(ver.basedir); - }; - switch (goal) { - case goal::EXE => - sched_hare_exe(&plan, ver, output, depends...); - case goal::OBJ => - sched_hare_object(&plan, ver, - namespace, output, depends...); - }; - match (plan_execute(&plan, verbose)) { - case void => void; - case !exec::exit_status => - fmt::fatalf("{} build: build failed", os::args[0]); - }; -}; - -fn cache(cmd: *getopt::command) void = { - abort("cache subcommand not implemented yet."); // TODO -}; - -type deps_goal = enum { - DOT, - MAKE, - TERM, -}; - -fn deps(cmd: *getopt::command) void = { - let build_target = default_target(); - let tags = module::tags_dup(build_target.tags); - defer module::tags_free(tags); - - let build_dir: str = ""; - let goal = deps_goal::TERM; - for (let i = 0z; i < len(cmd.opts); i += 1) { - let opt = cmd.opts[i]; - switch (opt.0) { - case 'd' => - goal = deps_goal::DOT; - case 'M' => - goal = deps_goal::MAKE; - build_dir = opt.1; - case 'T' => - tags = match (addtags(tags, opt.1)) { - case void => - fmt::fatal("Error parsing tags"); - case let t: []module::tag => - yield t; - }; - case 'X' => - tags = match (deltags(tags, opt.1)) { - case void => - fmt::fatal("Error parsing tags"); - case let t: []module::tag => - yield t; - }; - case => - abort(); - }; - }; - - const input = - if (len(cmd.args) == 0) os::getcwd() - else if (len(cmd.args) == 1) cmd.args[0] - else { - getopt::printusage(os::stderr, "deps", cmd.help...)!; - os::exit(1); - }; - - const ctx = module::context_init(tags, [], HAREPATH); - defer module::context_finish(&ctx); - - const ver = match (parse::identstr(input)) { - case let ident: ast::ident => - yield match (module::lookup(&ctx, ident)) { - case let ver: module::version => - yield ver; - case let err: module::error => - fmt::fatal("Error scanning input module:", - module::strerror(err)); - }; - case parse::error => - yield match (module::scan(&ctx, input)) { - case let ver: module::version => - yield ver; - case let err: module::error => - fmt::fatal("Error scanning input path:", - module::strerror(err)); - }; - }; - - let visited: []depnode = []; - let stack: []str = []; - defer free(stack); - const ctx = module::context_init([], [], HAREPATH); - - let toplevel = depnode{ident = strings::dup(path::basename(input)), depends = [], depth = 0}; - - for (let i = 0z; i < len(ver.depends); i += 1) { - const name = strings::join("::", ver.depends[i]...); - defer free(name); - const child = match (explore_deps(&ctx, &stack, &visited, name)) { - case let index: size => yield index; - case let start: dep_cycle => - const chain = strings::join(" -> ", stack[start..]...); - defer free(chain); - fmt::errorln("Dependency cycle detected:", chain)!; - os::exit(1); - }; - append(toplevel.depends, child); - }; - - sort::sort(toplevel.depends, size(size), &cmpsz); - append(visited, toplevel); - defer for (let i = 0z; i < len(visited); i += 1) { - free(visited[i].ident); - free(visited[i].depends); - }; - - switch (goal) { - case deps_goal::TERM => - show_deps(&visited); - case deps_goal::DOT => - fmt::println("strict digraph deps {")!; - for (let i = 0z; i < len(visited); i += 1) { - for (let j = 0z; j < len(visited[i].depends); j += 1) { - const child = visited[visited[i].depends[j]]; - fmt::printfln("\t\"{}\" -> \"{}\";", visited[i].ident, child.ident)!; - }; - }; - fmt::println("}")!; - case deps_goal::MAKE => - abort("-M option not implemented yet"); - }; -}; - -fn run(cmd: *getopt::command) void = { - const build_target = default_target(); - let tags = module::tags_dup(build_target.tags); - defer module::tags_free(tags); - - let verbose = false; - let defines: []str = []; - defer free(defines); - let libdir: []str = []; - defer free(libdir); - let libs: []str = []; - defer free(libs); - for (let i = 0z; i < len(cmd.opts); i += 1) { - let opt = cmd.opts[i]; - switch (opt.0) { - case 'v' => - verbose = true; - case 'D' => - append(defines, opt.1); - case 'j' => - abort("-j option not implemented yet."); // TODO - case 'L' => - append(libdir, opt.1); - case 'l' => - append(libs, opt.1); - case 't' => - abort("-t option not implemented yet."); // TODO - case 'T' => - tags = match (addtags(tags, opt.1)) { - case void => - fmt::fatal("Error parsing tags"); - case let t: []module::tag => - yield t; - }; - case 'X' => - tags = match (deltags(tags, opt.1)) { - case void => - fmt::fatal("Error parsing tags"); - case let t: []module::tag => - yield t; - }; - case => - abort(); - }; - }; - - let input = ""; - let runargs: []str = []; - if (len(cmd.args) == 0) { - input = os::getcwd(); - } else { - input = cmd.args[0]; - runargs = cmd.args[1..]; - }; - - if (len(libs) > 0) { - append(tags, module::tag { - mode = module::tag_mode::INCLUSIVE, - name = strings::dup("libc"), - }); - }; - - const ctx = module::context_init(tags, defines, HAREPATH); - defer module::context_finish(&ctx); - - const plan = mkplan(&ctx, libdir, libs, build_target); - defer plan_finish(&plan); - - const ver = match (module::scan(&ctx, input)) { - case let ver: module::version => - yield ver; - case let err: module::error => - fmt::fatal("Error scanning input module:", - module::strerror(err)); - }; - - let depends: []*task = []; - sched_module(&plan, ["rt"], &depends); - - for (let i = 0z; i < len(ver.depends); i += 1z) { - const dep = ver.depends[i]; - sched_module(&plan, dep, &depends); - }; - - const output = mkfile(&plan, "", "out"); - sched_hare_exe(&plan, ver, output, depends...); - match (plan_execute(&plan, verbose)) { - case void => void; - case !exec::exit_status => - fmt::fatalf("{} run: build failed", os::args[0]); - }; - const cmd = match (exec::cmd(output, runargs...)) { - case let err: exec::error => - fmt::fatal("exec:", exec::strerror(err)); - case let cmd: exec::command => - yield cmd; - }; - exec::setname(&cmd, input); - exec::exec(&cmd); -}; - -fn test(cmd: *getopt::command) void = { - const build_target = default_target(); - let tags = module::tags_dup(build_target.tags); - append(tags, module::tag { - name = strings::dup("test"), - mode = module::tag_mode::INCLUSIVE, - }); - - let output = ""; - let verbose = false; - let defines: []str = []; - defer free(defines); - let libdir: []str = []; - defer free(libdir); - let libs: []str = []; - defer free(libs); - for (let i = 0z; i < len(cmd.opts); i += 1) { - const opt = cmd.opts[i]; - switch (opt.0) { - case 'v' => - verbose = true; - case 'D' => - append(defines, opt.1); - case 'j' => - abort("-j option not implemented yet."); // TODO - case 'L' => - append(libdir, opt.1); - case 'l' => - append(libs, opt.1); - case 't' => - abort("-t option not implemented yet."); // TODO - case 'o' => - output = opt.1; - case 'T' => - tags = match (addtags(tags, opt.1)) { - case void => - fmt::fatal("Error parsing tags"); - case let t: []module::tag => - yield t; - }; - case 'X' => - tags = match (deltags(tags, opt.1)) { - case void => - fmt::fatal("Error parsing tags"); - case let t: []module::tag => - yield t; - }; - case => - abort(); - }; - }; - - if (len(libs) > 0) { - append(tags, module::tag { - mode = module::tag_mode::INCLUSIVE, - name = strings::dup("libc"), - }); - }; - - const ctx = module::context_init(tags, defines, HAREPATH); - defer module::context_finish(&ctx); - - const plan = mkplan(&ctx, libdir, libs, build_target); - defer plan_finish(&plan); - - let depends: []*task = []; - sched_module(&plan, ["test"], &depends); - - let items = match (module::walk(&ctx, ".")) { - case let items: []ast::ident => - yield items; - case let err: module::error => - fmt::fatal("Error scanning source root:", - module::strerror(err)); - }; - - defer module::walk_free(items); - for (let i = 0z; i < len(items); i += 1) { - if (len(items[i]) > 0 && items[i][0] == "cmd") { - continue; - }; - match (module::lookup(plan.context, items[i])) { - case let ver: module::version => - if (len(ver.inputs) == 0) continue; - case module::error => - continue; - }; - sched_module(&plan, items[i], &depends); - }; - - const have_output = len(output) != 0; - if (!have_output) { - output = mkfile(&plan, "", "out"); - }; - sched_ld(&plan, strings::dup(output), depends...); - match (plan_execute(&plan, verbose)) { - case void => void; - case !exec::exit_status => - fmt::fatalf("{} test: build failed", os::args[0]); - }; - - if (have_output) { - return; - }; - - const cmd = match (exec::cmd(output, cmd.args...)) { - case let err: exec::error => - fmt::fatal("exec:", exec::strerror(err)); - case let cmd: exec::command => - yield cmd; - }; - exec::setname(&cmd, os::getcwd()); - exec::exec(&cmd); -}; - -fn version(cmd: *getopt::command) void = { - let verbose = false; - for (let i = 0z; i < len(cmd.opts); i += 1) { - // The only option is verbose - verbose = true; - }; - - fmt::printfln("Hare {}", VERSION)!; - - if (verbose) { - fmt::printf("Build tags\t")!; - const build_target = default_target(); - const tags = build_target.tags; - for (let i = 0z; i < len(tags); i += 1) { - const tag = tags[i]; - const inclusive = (tag.mode & module::tag_mode::INCLUSIVE) == 0; - fmt::printf("{}{}", if (inclusive) '+' else '-', tag.name)!; - }; - fmt::println()!; - - if (tty::isatty(os::stdout_file)) { - // Pretty print - match (os::getenv("HAREPATH")) { - case void => - const items = strings::split(HAREPATH, ":"); - defer free(items); - const items = strings::join("\n\t\t", items...); - defer free(items); - fmt::printfln("HAREPATH\t{}", items)!; - case let env: str => - fmt::printf("HAREPATH\t")!; - bufio::flush(os::stdout)!; - fmt::errorf("(from environment)")!; - const items = strings::split(env, ":"); - defer free(items); - const items = strings::join("\n\t\t", items...); - defer free(items); - fmt::printfln("\n\t\t{}", items)!; - }; - } else { - // Print for ease of machine parsing - const val = match (os::getenv("HAREPATH")) { - case void => - yield HAREPATH; - case let env: str => - yield env; - }; - fmt::printfln("HAREPATH\t{}", val)!; - }; - }; -}; diff --git a/cmd/hare/target.ha b/cmd/hare/target.ha @@ -1,81 +0,0 @@ -use hare::module; -use hare::module::{tag_mode}; - -type target = struct { - name: str, - ar_cmd: str, - as_cmd: str, - cc_cmd: str, - ld_cmd: str, - qbe_target: str, - tags: []module::tag, -}; - -fn default_target() *target = { - let default = get_target(ARCH); - match (default) { - case void => - abort("Build configuration error - unknown default target"); - case let t: *target => - return t; - }; -}; - -fn get_target(name: str) (*target | void) = { - for (let i = 0z; i < len(targets); i += 1) { - if (targets[i].name == name) { - return &targets[i]; - }; - }; -}; - -// TODO: -// - Implement cross compiling to other kernels (e.g. Linux => FreeBSD) -// - sysroots -const targets: [_]target = [ - target { - name = "aarch64", - ar_cmd = AARCH64_AR, - as_cmd = AARCH64_AS, - cc_cmd = AARCH64_CC, - ld_cmd = AARCH64_LD, - qbe_target = "arm64", - tags = [module::tag { - name = "aarch64", - mode = tag_mode::INCLUSIVE, - }, module::tag { - name = PLATFORM, - mode = module::tag_mode::INCLUSIVE, - }], - }, - target { - name = "riscv64", - ar_cmd = RISCV64_AR, - as_cmd = RISCV64_AS, - cc_cmd = RISCV64_CC, - ld_cmd = RISCV64_LD, - qbe_target = "rv64", - tags = [module::tag { - name = "riscv64", - mode = tag_mode::INCLUSIVE, - }, module::tag { - name = PLATFORM, - mode = module::tag_mode::INCLUSIVE, - }], - }, - target { - name = "x86_64", - ar_cmd = X86_64_AR, - as_cmd = X86_64_AS, - cc_cmd = X86_64_CC, - ld_cmd = X86_64_LD, - qbe_target = "amd64_sysv", - tags = [module::tag { - name = "x86_64", - mode = tag_mode::INCLUSIVE, - }, module::tag { - name = PLATFORM, - mode = module::tag_mode::INCLUSIVE, - }], - }, -]; diff --git a/cmd/hare/util.ha b/cmd/hare/util.ha @@ -0,0 +1,48 @@ +use ascii; +use dirs; +use errors; +use hare::module; +use os; +use strings; + +fn merge_tags(current: *[]str, new: str) (void | module::error) = { + let trimmed = strings::ltrim(new, '^'); + if (trimmed != new) { + strings::freeall(*current); + *current = []; + }; + let newtags = module::parse_tags(trimmed)?; + for (let i = 0z; i < len(newtags); i += 1) :new { + for (let j = 0z; j < len(current); j += 1) { + if (newtags[i].name == current[j]) { + if (!newtags[i].include) { + free(current[j]); + static delete(current[j]); + }; + continue :new; + }; + }; + if (newtags[i].include) { + append(current, strings::dup(newtags[i].name)); + }; + }; +}; + +fn harepath() str = os::tryenv("HAREPATH", HAREPATH); + +fn harecache() str = { + match (os::getenv("HARECACHE")) { + case let s: str => + return s; + case void => + return dirs::cache("hare"); + }; +}; + +// result must be freed with strings::freeall +fn default_tags() ([]str | error) = { + let arch = get_arch(os::machine())?; + let platform = ascii::strlower(os::sysname()); + let tags: []str = alloc([strings::dup(arch.name), platform]); + return tags; +}; diff --git a/cmd/hare/version.ha b/cmd/hare/version.ha @@ -0,0 +1,45 @@ +use ascii; +use bufio; +use fmt; +use getopt; +use os; +use strings; +use unix::tty; + +fn version(name: str, cmd: *getopt::command) (void | error) = { + let verbose = false; + for (let i = 0z; i < len(cmd.opts); i += 1) { + const opt = cmd.opts[i]; + switch (opt.0) { + case 'v' => + verbose = true; + case => abort(); + }; + }; + + fmt::printfln("hare {}", VERSION)!; + if (!verbose) { + return; + }; + + let build_arch = get_arch(os::machine())?; + let build_platform = ascii::strlower(os::sysname()); + + if (!tty::isatty(os::stdout_file)) { + fmt::printfln("build tags\t+{}+{}\nHAREPATH\t{}", + build_arch.name, build_platform, harepath())?; + return; + }; + + fmt::printfln("build tags:\n\t+{}\n\t+{}\nHAREPATH{}:", + build_arch.name, build_platform, + if (os::getenv("HAREPATH") is str) " (from environment)" else "")?; + + let tok = strings::tokenize(harepath(), ":"); + for (true) match (strings::next_token(&tok)) { + case void => + break; + case let s: str => + fmt::printfln("\t{}", s)?; + }; +}; diff --git a/cmd/harec/gen.ha b/cmd/harec/gen.ha @@ -51,7 +51,7 @@ fn gen_func(ctx: *context, decl: *unit::decl) void = { const fntype = fndecl.prototype.repr as types::func; ctx.serial = 0; - const ident = module::identuscore(fndecl.ident); + const ident = strings::join("_", fndecl.ident...); defer free(ident); fmt::fprintf(ctx.out, "{}section \".text.{}\" \"ax\" function", if (decl.exported) "export " else "", ident)!; diff --git a/cmd/haredoc/arch.ha b/cmd/haredoc/arch.ha @@ -0,0 +1,8 @@ +use hare::module; +use os; +use strings; + +fn set_arch_tags(tags: *[]str, a: str) void = { + merge_tags(tags, "-aarch64-riscv64-x86_64")!; + append(tags, strings::dup(a)); +}; diff --git a/cmd/haredoc/color.ha b/cmd/haredoc/doc/color.ha diff --git a/cmd/haredoc/doc/hare.ha b/cmd/haredoc/doc/hare.ha @@ -0,0 +1,196 @@ +// License: GPL-3.0 +// (c) 2021 Alexey Yerin <yyp@disroot.org> +// (c) 2021 Drew DeVault <sir@cmpwn.com> +// (c) 2021 Ember Sawady <ecs@d2evs.net> +use bufio; +use fmt; +use hare::ast; +use hare::lex; +use hare::module; +use hare::unparse; +use io; +use os; +use strings; + +// Formats output as Hare source code (prototypes) +export fn emit_hare(ctx: *context) (void | error) = { + const summary = ctx.summary; + + let first = true; + match (ctx.readme) { + case let readme: io::file => + first = false; + for (true) { + match (bufio::scanline(readme)?) { + case io::EOF => + break; + case let b: []u8 => + fmt::fprintfln(ctx.out, + "// {}", strings::fromutf8(b)!)?; + free(b); + }; + }; + case void => void; + }; + + emit_submodules_hare(ctx)?; + + // XXX: Should we emit the dependencies, too? + for (let i = 0z; i < len(summary.types); i += 1) { + if (!first) { + fmt::fprintln(ctx.out)?; + }; + first = false; + details_hare(ctx, summary.types[i])?; + }; + for (let i = 0z; i < len(summary.constants); i += 1) { + if (!first) { + fmt::fprintln(ctx.out)?; + }; + first = false; + details_hare(ctx, summary.constants[i])?; + }; + for (let i = 0z; i < len(summary.errors); i += 1) { + if (!first) { + fmt::fprintln(ctx.out)?; + }; + first = false; + details_hare(ctx, summary.errors[i])?; + }; + for (let i = 0z; i < len(summary.globals); i += 1) { + if (!first) { + fmt::fprintln(ctx.out)?; + }; + first = false; + details_hare(ctx, summary.globals[i])?; + }; + for (let i = 0z; i < len(summary.funcs); i += 1) { + if (!first) { + fmt::fprintln(ctx.out)?; + }; + first = false; + details_hare(ctx, summary.funcs[i])?; + }; +}; + +fn emit_submodules_hare(ctx: *context) (void | error) = { + if (len(ctx.submods) != 0) { + fmt::fprintln(ctx.out)?; + if (len(ctx.ident) == 0) { + fmt::fprintln(ctx.out, "// Modules")?; + } else { + fmt::fprintln(ctx.out, "// Submodules")?; + }; + for (let i = 0z; i < len(ctx.submods); i += 1) { + let submodule = if (len(ctx.ident) != 0) { + const s = unparse::identstr(ctx.ident); + defer free(s); + yield strings::concat(s, "::", ctx.submods[i]); + } else { + yield strings::dup(ctx.submods[i]); + }; + defer free(submodule); + + fmt::fprintf(ctx.out, "// - [[")?; + fmt::fprintf(ctx.out, submodule)?; + fmt::fprintfln(ctx.out, "]]")?; + }; + }; +}; + +fn details_hare(ctx: *context, decl: ast::decl) (void | error) = { + if (len(decl.docs) == 0 && !ctx.show_undocumented) { + return; + }; + + const iter = strings::tokenize(decl.docs, "\n"); + for (true) { + match (strings::next_token(&iter)) { + case void => + break; + case let s: str => + if (len(s) != 0) { + fmt::fprintfln(ctx.out, "//{}", s)?; + }; + }; + }; + + unparse_hare(ctx.out, decl)?; + fmt::fprintln(ctx.out)?; + return; +}; + +// Forked from [[hare::unparse]] +fn unparse_hare(out: io::handle, d: ast::decl) (size | io::error) = { + let n = 0z; + match (d.decl) { + case let g: []ast::decl_global => + n += fmt::fprint(out, + if (g[0].is_const) "const " else "let ")?; + for (let i = 0z; i < len(g); i += 1) { + if (len(g[i].symbol) != 0) { + n += fmt::fprintf(out, + "@symbol(\"{}\") ", g[i].symbol)?; + }; + n += unparse::ident(out, g[i].ident)?; + match (g[i]._type) { + case null => + yield; + case let ty: *ast::_type => + n += fmt::fprint(out, ": ")?; + n += unparse::_type(out, 0, *ty)?; + }; + if (i + 1 < len(g)) { + n += fmt::fprint(out, ", ")?; + }; + }; + case let t: []ast::decl_type => + n += fmt::fprint(out, "type ")?; + for (let i = 0z; i < len(t); i += 1) { + n += unparse::ident(out, t[i].ident)?; + n += fmt::fprint(out, " = ")?; + n += unparse::_type(out, 0, t[i]._type)?; + if (i + 1 < len(t)) { + n += fmt::fprint(out, ", ")?; + }; + }; + case let c: []ast::decl_const => + n += fmt::fprint(out, "def ")?; + for (let i = 0z; i < len(c); i += 1) { + n += unparse::ident(out, c[i].ident)?; + n += fmt::fprint(out, ": ")?; + match (c[i]._type) { + case null => + yield; + case let ty: *ast::_type => + n += fmt::fprint(out, ": ")?; + n += unparse::_type(out, 0, *ty)?; + }; + if (i + 1 < len(c)) { + n += fmt::fprint(out, ", ")?; + }; + }; + case let f: ast::decl_func => + n += fmt::fprint(out, switch (f.attrs) { + case ast::fndecl_attrs::NONE => + yield ""; + case ast::fndecl_attrs::FINI => + yield "@fini "; + case ast::fndecl_attrs::INIT => + yield "@init "; + case ast::fndecl_attrs::TEST => + yield "@test "; + })?; + let p = f.prototype.repr as ast::func_type; + if (len(f.symbol) != 0) { + n += fmt::fprintf(out, "@symbol(\"{}\") ", + f.symbol)?; + }; + n += fmt::fprint(out, "fn ")?; + n += unparse::ident(out, f.ident)?; + n += unparse::prototype(out, 0, + f.prototype.repr as ast::func_type)?; + }; + n += fmt::fprint(out, ";")?; + return n; +}; diff --git a/cmd/haredoc/doc/html.ha b/cmd/haredoc/doc/html.ha @@ -0,0 +1,1067 @@ +// License: GPL-3.0 +// (c) 2021-2022 Alexey Yerin <yyp@disroot.org> +// (c) 2022 Byron Torres <b@torresjrjr.com> +// (c) 2021-2022 Drew DeVault <sir@cmpwn.com> +// (c) 2021 Ember Sawady <ecs@d2evs.net> +// (c) 2021 Thomas Bracht Laumann Jespersen <t@laumann.xyz> +// (c) 2022 Umar Getagazov <umar@handlerug.me> + +// Note: ast::ident should never have to be escaped +use encoding::utf8; +use fmt; +use hare::ast; +use hare::ast::{variadism}; +use hare::lex; +use hare::module; +use hare::parse::doc; +use hare::unparse; +use io; +use memio; +use net::ip; +use net::uri; +use os; +use path; +use strings; + +// Prints a string to an output handle, escaping any of HTML's reserved +// characters. +fn html_escape(out: io::handle, in: str) (size | io::error) = { + let z = 0z; + let iter = strings::iter(in); + for (true) { + match (strings::next(&iter)) { + case void => + break; + case let rn: rune => + z += fmt::fprint(out, switch (rn) { + case '&' => + yield "&amp;"; + case '<' => + yield "&lt;"; + case '>' => + yield "&gt;"; + case '"' => + yield "&quot;"; + case '\'' => + yield "&apos;"; + case => + yield strings::fromutf8(utf8::encoderune(rn))!; + })?; + }; + }; + return z; +}; + +@test fn html_escape() void = { + let sink = memio::dynamic(); + defer io::close(&sink)!; + html_escape(&sink, "hello world!")!; + assert(memio::string(&sink)! == "hello world!"); + + let sink = memio::dynamic(); + defer io::close(&sink)!; + html_escape(&sink, "\"hello world!\"")!; + assert(memio::string(&sink)! == "&quot;hello world!&quot;"); + + let sink = memio::dynamic(); + defer io::close(&sink)!; + html_escape(&sink, "<hello & 'world'!>")!; + assert(memio::string(&sink)! == "&lt;hello &amp; &apos;world&apos;!&gt;"); +}; + +// Formats output as HTML +export fn emit_html(ctx: *context) (void | error) = { + const decls = ctx.summary; + const ident = unparse::identstr(ctx.ident); + defer free(ident); + + if (ctx.template) { + head(ctx.ident)?; + }; + + if (len(ident) == 0) { + fmt::fprintf(ctx.out, "<h2>The Hare standard library <span class='heading-extra'>")?; + } else { + fmt::fprintf(ctx.out, "<h2><span class='heading-body'>{}</span><span class='heading-extra'>", ident)?; + }; + for (let i = 0z; i < len(ctx.tags); i += 1) { + fmt::fprintf(ctx.out, "+{} ", ctx.tags[i])?; + }; + fmt::fprintln(ctx.out, "</span></h2>")?; + + match (ctx.readme) { + case void => void; + case let f: io::file => + fmt::fprintln(ctx.out, "<div class='readme'>")?; + markup_html(ctx, f)?; + fmt::fprintln(ctx.out, "</div>")?; + }; + + let identpath = strings::join("/", ctx.ident...); + defer free(identpath); + + if (len(ctx.submods) != 0) { + if (len(ctx.ident) == 0) { + fmt::fprintln(ctx.out, "<h3>Modules</h3>")?; + } else { + fmt::fprintln(ctx.out, "<h3>Submodules</h3>")?; + }; + fmt::fprintln(ctx.out, "<ul class='submodules'>")?; + for (let i = 0z; i < len(ctx.submods); i += 1) { + let submodule = ctx.submods[i]; + let path = path::init("/", identpath, submodule)!; + + fmt::fprintf(ctx.out, "<li><a href='")?; + html_escape(ctx.out, path::string(&path))?; + fmt::fprintf(ctx.out, "'>")?; + html_escape(ctx.out, submodule)?; + fmt::fprintfln(ctx.out, "</a></li>")?; + }; + fmt::fprintln(ctx.out, "</ul>")?; + }; + + if (len(decls.types) == 0 + && len(decls.errors) == 0 + && len(decls.constants) == 0 + && len(decls.globals) == 0 + && len(decls.funcs) == 0) { + return; + }; + + fmt::fprintln(ctx.out, "<h3>Index</h3>")?; + tocentries(ctx.out, decls.types, "Types", "types")?; + tocentries(ctx.out, decls.errors, "Errors", "Errors")?; + tocentries(ctx.out, decls.constants, "Constants", "constants")?; + tocentries(ctx.out, decls.globals, "Globals", "globals")?; + tocentries(ctx.out, decls.funcs, "Functions", "functions")?; + + if (len(decls.types) != 0) { + fmt::fprintln(ctx.out, "<h3>Types</h3>")?; + for (let i = 0z; i < len(decls.types); i += 1) { + details(ctx, decls.types[i])?; + }; + }; + + if (len(decls.errors) != 0) { + fmt::fprintln(ctx.out, "<h3>Errors</h3>")?; + for (let i = 0z; i < len(decls.errors); i += 1) { + details(ctx, decls.errors[i])?; + }; + }; + + if (len(decls.constants) != 0) { + fmt::fprintln(ctx.out, "<h3>Constants</h3>")?; + for (let i = 0z; i < len(decls.constants); i += 1) { + details(ctx, decls.constants[i])?; + }; + }; + + if (len(decls.globals) != 0) { + fmt::fprintln(ctx.out, "<h3>Globals</h3>")?; + for (let i = 0z; i < len(decls.globals); i += 1) { + details(ctx, decls.globals[i])?; + }; + }; + + if (len(decls.funcs) != 0) { + fmt::fprintln(ctx.out, "<h3>Functions</h3>")?; + for (let i = 0z; i < len(decls.funcs); i += 1) { + details(ctx, decls.funcs[i])?; + }; + }; +}; + +fn comment_html(out: io::handle, s: str) (size | io::error) = { + // TODO: handle [[references]] + let z = fmt::fprint(out, "<span class='comment'>//")?; + z += html_escape(out, s)?; + z += fmt::fprint(out, "</span><br>")?; + return z; +}; + +fn docs_html(out: io::handle, s: str, indent: size) (size | io::error) = { + const iter = strings::tokenize(s, "\n"); + let z = 0z; + for (true) match (strings::next_token(&iter)) { + case let s: str => + if (!(strings::peek_token(&iter) is void)) { + z += comment_html(out, s)?; + for (let i = 0z; i < indent; i += 1) { + z += fmt::fprint(out, "\t")?; + }; + }; + case void => + break; + }; + + return z; +}; + +fn tocentries( + out: io::handle, + decls: []ast::decl, + name: str, + lname: str, +) (void | error) = { + if (len(decls) == 0) { + return; + }; + fmt::fprintfln(out, "<h4>{}</h4>", name)?; + fmt::fprintln(out, "<pre>")?; + let undoc = false; + for (let i = 0z; i < len(decls); i += 1) { + if (!undoc && decls[i].docs == "") { + fmt::fprintfln( + out, + "{}<span class='comment'>// Undocumented {}:</span>", + if (i == 0) "" else "\n", + lname)?; + undoc = true; + }; + tocentry(out, decls[i])?; + }; + fmt::fprint(out, "</pre>")?; + return; +}; + +fn tocentry(out: io::handle, decl: ast::decl) (void | error) = { + fmt::fprintf(out, "{} ", + match (decl.decl) { + case ast::decl_func => + yield "fn"; + case []ast::decl_type => + yield "type"; + case []ast::decl_const => + yield "const"; + case []ast::decl_global => + yield "let"; + })?; + fmt::fprintf(out, "<a href='#")?; + unparse::ident(out, decl_ident(decl))?; + fmt::fprintf(out, "'>")?; + unparse::ident(out, decl_ident(decl))?; + fmt::fprint(out, "</a>")?; + + match (decl.decl) { + case let t: []ast::decl_type => void; + case let g: []ast::decl_global => + let g = g[0]; + match (g._type) { + case null => + yield; + case let ty: *ast::_type => + fmt::fprint(out, ": ")?; + type_html(out, 0, *ty, true)?; + }; + case let c: []ast::decl_const => + let c = c[0]; + match (c._type) { + case null => + yield; + case let ty: *ast::_type => + fmt::fprint(out, ": ")?; + type_html(out, 0, *ty, true)?; + }; + case let f: ast::decl_func => + prototype_html(out, 0, + f.prototype.repr as ast::func_type, + true)?; + }; + fmt::fprintln(out, ";")?; + return; +}; + +fn details(ctx: *context, decl: ast::decl) (void | error) = { + fmt::fprintln(ctx.out, "<section class='member'>")?; + fmt::fprint(ctx.out, "<h4 id='")?; + unparse::ident(ctx.out, decl_ident(decl))?; + fmt::fprint(ctx.out, "'><span class='heading-body'>")?; + fmt::fprintf(ctx.out, "{} ", match (decl.decl) { + case ast::decl_func => + yield "fn"; + case []ast::decl_type => + yield "type"; + case []ast::decl_const => + yield "def"; + case []ast::decl_global => + yield "let"; + })?; + unparse::ident(ctx.out, decl_ident(decl))?; + // TODO: Add source URL + fmt::fprint(ctx.out, "</span><span class='heading-extra'><a href='#")?; + unparse::ident(ctx.out, decl_ident(decl))?; + fmt::fprint(ctx.out, "'>[link]</a> + </span>")?; + fmt::fprintln(ctx.out, "</h4>")?; + + if (len(decl.docs) == 0) { + fmt::fprintln(ctx.out, "<details>")?; + fmt::fprintln(ctx.out, "<summary>Show undocumented member</summary>")?; + }; + + fmt::fprintln(ctx.out, "<pre class='decl'>")?; + unparse_html(ctx.out, decl)?; + fmt::fprintln(ctx.out, "</pre>")?; + + if (len(decl.docs) != 0) { + const trimmed = trim_comment(decl.docs); + defer free(trimmed); + const buf = strings::toutf8(trimmed); + markup_html(ctx, &memio::fixed(buf))?; + } else { + fmt::fprintln(ctx.out, "</details>")?; + }; + + fmt::fprintln(ctx.out, "</section>")?; + return; +}; + +fn htmlref(ctx: *context, ref: ast::ident) (void | error) = { + const ik = + match (resolve(ctx, ref)?) { + case let ik: (ast::ident, symkind) => + yield ik; + case void => + const ident = unparse::identstr(ref); + fmt::errorfln("Warning: Unresolved reference: {}", ident)?; + fmt::fprintf(ctx.out, "<a href='#' " + "class='ref invalid' " + "title='This reference could not be found'>{}</a>", + ident)?; + free(ident); + return; + }; + + // TODO: The reference is not necessarily in the stdlib + const kind = ik.1, id = ik.0; + const ident = unparse::identstr(id); + switch (kind) { + case symkind::LOCAL => + fmt::fprintf(ctx.out, "<a href='#{0}' class='ref'>{0}</a>", ident)?; + case symkind::MODULE => + let ipath = strings::join("/", id...); + defer free(ipath); + fmt::fprintf(ctx.out, "<a href='/{}' class='ref'>{}</a>", + ipath, ident)?; + case symkind::SYMBOL => + let ipath = strings::join("/", id[..len(id) - 1]...); + defer free(ipath); + fmt::fprintf(ctx.out, "<a href='/{}#{}' class='ref'>{}</a>", + ipath, id[len(id) - 1], ident)?; + case symkind::ENUM_LOCAL => + fmt::fprintf(ctx.out, "<a href='#{}' class='ref'>{}</a>", + id[len(id) - 2], ident)?; + case symkind::ENUM_REMOTE => + let ipath = strings::join("/", id[..len(id) - 2]...); + defer free(ipath); + fmt::fprintf(ctx.out, "<a href='/{}#{}' class='ref'>{}</a>", + ipath, id[len(id) - 2], ident)?; + }; + free(ident); +}; + +fn markup_html(ctx: *context, in: io::handle) (void | error) = { + let parser = doc::parse(in); + let waslist = false; + for (true) { + const tok = match (doc::scan(&parser)) { + case void => + if (waslist) { + fmt::fprintln(ctx.out, "</ul>")?; + }; + break; + case let tok: doc::token => + yield tok; + }; + match (tok) { + case doc::paragraph => + if (waslist) { + fmt::fprintln(ctx.out, "</ul>")?; + waslist = false; + }; + fmt::fprintln(ctx.out)?; + fmt::fprint(ctx.out, "<p>")?; + case let tx: doc::text => + defer free(tx); + match (uri::parse(strings::trim(tx))) { + case let uri: uri::uri => + defer uri::finish(&uri); + if (uri.host is net::ip::addr || len(uri.host as str) > 0) { + fmt::fprint(ctx.out, "<a rel='nofollow noopener' href='")?; + uri::fmt(ctx.out, &uri)?; + fmt::fprint(ctx.out, "'>")?; + html_escape(ctx.out, tx)?; + fmt::fprint(ctx.out, "</a>")?; + } else { + html_escape(ctx.out, tx)?; + }; + case uri::invalid => + html_escape(ctx.out, tx)?; + }; + case let re: doc::reference => + htmlref(ctx, re)?; + case let sa: doc::sample => + if (waslist) { + fmt::fprintln(ctx.out, "</ul>")?; + waslist = false; + }; + fmt::fprint(ctx.out, "<pre class='sample'>")?; + html_escape(ctx.out, sa)?; + fmt::fprint(ctx.out, "</pre>")?; + free(sa); + case doc::listitem => + if (!waslist) { + fmt::fprintln(ctx.out, "<ul>")?; + waslist = true; + }; + fmt::fprint(ctx.out, "<li>")?; + }; + }; + fmt::fprintln(ctx.out)?; + return; +}; + +// Forked from [[hare::unparse]] +fn unparse_html(out: io::handle, d: ast::decl) (size | io::error) = { + let n = 0z; + match (d.decl) { + case let c: []ast::decl_const => + n += fmt::fprintf(out, "<span class='keyword'>def</span> ")?; + for (let i = 0z; i < len(c); i += 1) { + n += unparse::ident(out, c[i].ident)?; + match (c[i]._type) { + case null => + yield; + case let ty: *ast::_type => + n += fmt::fprint(out, ": ")?; + n += type_html(out, 0, *ty, false)?; + }; + if (i + 1 < len(c)) { + n += fmt::fprint(out, ", ")?; + }; + }; + case let g: []ast::decl_global => + n += fmt::fprintf(out, "<span class='keyword'>{}</span>", + if (g[0].is_const) "const " else "let ")?; + for (let i = 0z; i < len(g); i += 1) { + n += unparse::ident(out, g[i].ident)?; + match (g[i]._type) { + case null => + yield; + case let ty: *ast::_type => + n += fmt::fprint(out, ": ")?; + n += type_html(out, 0, *ty, false)?; + }; + if (i + 1 < len(g)) { + n += fmt::fprint(out, ", ")?; + }; + }; + case let t: []ast::decl_type => + n += fmt::fprint(out, "<span class='keyword'>type</span> ")?; + for (let i = 0z; i < len(t); i += 1) { + n += unparse::ident(out, t[i].ident)?; + n += fmt::fprint(out, " = ")?; + n += type_html(out, 0, t[i]._type, false)?; + if (i + 1 < len(t)) { + n += fmt::fprint(out, ", ")?; + }; + }; + case let f: ast::decl_func => + n += fmt::fprint(out, switch (f.attrs) { + case ast::fndecl_attrs::NONE => + yield ""; + case ast::fndecl_attrs::FINI => + yield "@fini "; + case ast::fndecl_attrs::INIT => + yield "@init "; + case ast::fndecl_attrs::TEST => + yield "@test "; + })?; + let p = f.prototype.repr as ast::func_type; + n += fmt::fprint(out, "<span class='keyword'>fn</span> ")?; + n += unparse::ident(out, f.ident)?; + n += prototype_html(out, 0, + f.prototype.repr as ast::func_type, + false)?; + }; + n += fmt::fprint(out, ";")?; + return n; +}; + +fn enum_html( + out: io::handle, + indent: size, + t: ast::enum_type +) (size | io::error) = { + let z = 0z; + + z += fmt::fprint(out, "<span class='type'>enum</span> ")?; + if (t.storage != ast::builtin_type::INT) { + z += fmt::fprintf(out, "<span class='type'>{}</span> ", + unparse::builtin_type(t.storage))?; + }; + z += fmt::fprintln(out, "{")?; + indent += 1; + for (let i = 0z; i < len(t.values); i += 1) { + for (let i = 0z; i < indent; i += 1) { + z += fmt::fprint(out, "\t")?; + }; + const val = t.values[i]; + let wrotedocs = false; + if (val.docs != "") { + // Check if comment should go above or next to field + if (multiline_comment(val.docs)) { + z += docs_html(out, val.docs, indent)?; + wrotedocs = true; + }; + }; + + z += fmt::fprint(out, val.name)?; + + match (val.value) { + case null => void; + case let expr: *ast::expr => + z += fmt::fprint(out, " = ")?; + z += unparse::expr(out, indent, *expr)?; + }; + + z += fmt::fprint(out, ",")?; + + if (val.docs != "" && !wrotedocs) { + z += fmt::fprint(out, " ")?; + z += docs_html(out, val.docs, 0)?; + } else { + z += fmt::fprintln(out)?; + }; + }; + indent -= 1; + for (let i = 0z; i < indent; i += 1) { + z += fmt::fprint(out, "\t")?; + }; + z += newline(out, indent)?; + z += fmt::fprint(out, "}")?; + return z; +}; + +fn struct_union_html( + out: io::handle, + indent: size, + t: ast::_type, + brief: bool, +) (size | io::error) = { + let z = 0z; + let members = match (t.repr) { + case let t: ast::struct_type => + z += fmt::fprint(out, "<span class='keyword'>struct</span>")?; + if (t.packed) { + z += fmt::fprint(out, " @packed")?; + }; + z += fmt::fprint(out, " {")?; + yield t.members: []ast::struct_member; + case let t: ast::union_type => + z += fmt::fprint(out, "<span class='keyword'>union</span> {")?; + yield t: []ast::struct_member; + }; + + indent += 1; + for (let i = 0z; i < len(members); i += 1) { + const member = members[i]; + + z += newline(out, indent)?; + if (member.docs != "" && !brief) { + z += docs_html(out, member.docs, indent)?; + }; + match (member._offset) { + case null => void; + case let expr: *ast::expr => + z += fmt::fprint(out, "@offset(")?; + z += unparse::expr(out, indent, *expr)?; + z += fmt::fprint(out, ") ")?; + }; + + match (member.member) { + case let f: ast::struct_field => + z += fmt::fprintf(out, "{}: ", f.name)?; + z += type_html(out, indent, *f._type, brief)?; + case let embed: ast::struct_embedded => + z += type_html(out, indent, *embed, brief)?; + case let indent: ast::struct_alias => + z += unparse::ident(out, indent)?; + }; + z += fmt::fprint(out, ",")?; + }; + + indent -= 1; + z += newline(out, indent)?; + z += fmt::fprint(out, "}")?; + + return z; +}; + +fn type_html( + out: io::handle, + indent: size, + _type: ast::_type, + brief: bool, +) (size | io::error) = { + if (brief) { + let buf = memio::dynamic(); + defer io::close(&buf)!; + unparse::_type(&buf, indent, _type)?; + return html_escape(out, memio::string(&buf)!)?; + }; + + // TODO: More detailed formatter which can find aliases nested deeper in + // other types and highlight more keywords, like const + let z = 0z; + + if (_type.flags & ast::type_flag::CONST != 0 + && !(_type.repr is ast::func_type)) { + z += fmt::fprint(out, "<span class='keyword'>const</span> ")?; + }; + + if (_type.flags & ast::type_flag::ERROR != 0) { + if (_type.repr is ast::builtin_type) { + z += fmt::fprint(out, "<span class='type'>!</span>")?; + } else { + z += fmt::fprint(out, "!")?; + }; + }; + + match (_type.repr) { + case let a: ast::alias_type => + if (a.unwrap) { + z += fmt::fprint(out, "...")?; + }; + z += unparse::ident(out, a.ident)?; + case let t: ast::builtin_type => + z += fmt::fprintf(out, "<span class='type'>{}</span>", + unparse::builtin_type(t))?; + case let t: ast::tagged_type => + // rough estimate of current line length + let linelen: size = z + (indent + 1) * 8; + z = 0; + linelen += fmt::fprint(out, "(")?; + for (let i = 0z; i < len(t); i += 1) { + linelen += type_html(out, indent, *t[i], brief)?; + if (i + 1 == len(t)) { + break; + }; + linelen += fmt::fprint(out, " |")?; + // use 72 instead of 80 to give a bit of leeway for long + // type names + if (linelen > 72) { + z += linelen; + linelen = (indent + 1) * 8; + z += fmt::fprintln(out)?; + for (let i = 0z; i < indent; i += 1) { + z += fmt::fprint(out, "\t")?; + }; + } else { + linelen += fmt::fprint(out, " ")?; + }; + }; + z += linelen; + z += fmt::fprint(out, ")")?; + case let t: ast::tuple_type => + // rough estimate of current line length + let linelen: size = z + (indent + 1) * 8; + z = 0; + linelen += fmt::fprint(out, "(")?; + for (let i = 0z; i < len(t); i += 1) { + linelen += type_html(out, indent, *t[i], brief)?; + if (i + 1 == len(t)) { + break; + }; + linelen += fmt::fprint(out, ",")?; + // use 72 instead of 80 to give a bit of leeway for long + // type names + if (linelen > 72) { + z += linelen; + linelen = (indent + 1) * 8; + z += fmt::fprintln(out)?; + for (let i = 0z; i < indent; i += 1) { + z += fmt::fprint(out, "\t")?; + }; + } else { + linelen += fmt::fprint(out, " ")?; + }; + }; + z += linelen; + z += fmt::fprint(out, ")")?; + case let t: ast::pointer_type => + if (t.flags & ast::pointer_flag::NULLABLE != 0) { + z += fmt::fprint(out, "<span class='type'>nullable</span> ")?; + }; + z += fmt::fprint(out, "*")?; + z += type_html(out, indent, *t.referent, brief)?; + case let t: ast::func_type => + z += fmt::fprint(out, "<span class='keyword'>fn</span>(")?; + for (let i = 0z; i < len(t.params); i += 1) { + const param = t.params[i]; + z += fmt::fprintf(out, "{}: ", + if (len(param.name) == 0) "_" else param.name)?; + z += type_html(out, indent, *param._type, brief)?; + + if (i + 1 == len(t.params) + && t.variadism == ast::variadism::HARE) { + // TODO: Highlight that as well + z += fmt::fprint(out, "...")?; + }; + if (i + 1 < len(t.params)) { + z += fmt::fprint(out, ", ")?; + }; + }; + if (t.variadism == ast::variadism::C) { + z += fmt::fprint(out, ", ...")?; + }; + z += fmt::fprint(out, ") ")?; + z += type_html(out, indent, *t.result, brief)?; + case let t: ast::enum_type => + z += enum_html(out, indent, t)?; + case let t: ast::list_type => + z += fmt::fprint(out, "[")?; + match (t.length) { + case let expr: *ast::expr => + z += unparse::expr(out, indent, *expr)?; + case ast::len_slice => + z += 0; + case ast::len_unbounded => + z += fmt::fprintf(out, "*")?; + case ast::len_contextual => + z += fmt::fprintf(out, "_")?; + }; + z += fmt::fprint(out, "]")?; + + z += type_html(out, indent, *t.members, brief)?; + case let t: ast::struct_type => + z += struct_union_html(out, indent, _type, brief)?; + case let t: ast::union_type => + z += struct_union_html(out, indent, _type, brief)?; + }; + + return z; +}; + +fn prototype_html( + out: io::handle, + indent: size, + t: ast::func_type, + brief: bool, +) (size | io::error) = { + let n = 0z; + n += fmt::fprint(out, "(")?; + + // estimate length of prototype to determine if it should span multiple + // lines + const linelen = if (len(t.params) == 0 || brief) { + yield 0z; // If no parameters or brief, only use one line. + } else { + let linelen = indent * 8 + 5; + linelen += if (len(t.params) != 0) len(t.params) * 3 - 1 else 0; + for (let i = 0z; i < len(t.params); i += 1) { + const param = t.params[i]; + linelen += unparse::_type(io::empty, indent, + *param._type)?; + linelen += if (param.name == "") 1 else len(param.name); + }; + switch (t.variadism) { + case variadism::NONE => void; + case variadism::HARE => + linelen += 3; + case variadism::C => + linelen += 5; + }; + linelen += unparse::_type(io::empty, indent, *t.result)?; + yield linelen; + }; + + // use 72 instead of 80 to give a bit of leeway for preceding text + if (linelen > 72) { + indent += 1; + for (let i = 0z; i < len(t.params); i += 1) { + const param = t.params[i]; + n += newline(out, indent)?; + n += fmt::fprintf(out, "{}: ", + if (param.name == "") "_" else param.name)?; + n += type_html(out, indent, *param._type, brief)?; + if (i + 1 == len(t.params) + && t.variadism == variadism::HARE) { + n += fmt::fprint(out, "...")?; + } else { + n += fmt::fprint(out, ",")?; + }; + }; + if (t.variadism == variadism::C) { + n += newline(out, indent)?; + n += fmt::fprint(out, "...")?; + }; + indent -= 1; + n += newline(out, indent)?; + } else for (let i = 0z; i < len(t.params); i += 1) { + const param = t.params[i]; + if (!brief) { + n += fmt::fprintf(out, "{}: ", + if (param.name == "") "_" else param.name)?; + }; + n += type_html(out, indent, *param._type, brief)?; + if (i + 1 == len(t.params)) { + switch (t.variadism) { + case variadism::NONE => void; + case variadism::HARE => + n += fmt::fprint(out, "...")?; + case variadism::C => + n += fmt::fprint(out, ", ...")?; + }; + } else { + n += fmt::fprint(out, ", ")?; + }; + }; + + n += fmt::fprint(out, ") ")?; + n += type_html(out, indent, *t.result, brief)?; + return n; +}; + +fn breadcrumb(ident: ast::ident) str = { + if (len(ident) == 0) { + return ""; + }; + let buf = memio::dynamic(); + fmt::fprintf(&buf, "<a href='/'>stdlib</a> » ")!; + for (let i = 0z; i < len(ident) - 1; i += 1) { + let ipath = strings::join("/", ident[..i+1]...); + defer free(ipath); + fmt::fprintf(&buf, "<a href='/{}'>{}</a>::", ipath, ident[i])!; + }; + fmt::fprint(&buf, ident[len(ident) - 1])!; + return memio::string(&buf)!; +}; + +const harriet_b64 = "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEAAQMAAABmvDolAAAABlBMVEUAAAD///+l2Z/dAAAK40lEQVRo3u3ZX2xb1R0H8O/NzWIXXGw0xILa1QE6Wk0gMspIESU3WSf2sD/wODFtpFC1Q1Ob0AJpacm5pYVUAxHENK2IUiONaQ/TBIjRFKXNvSHbijSDeaGja5vr/ovHlmIHQ66de+/57iF27Gv7um8TD/glUvzROb9z7jnnnp9/4GU++Ap8iYEeJ6EFA9k9SSlGgkFRFiizs8HgPKWQ33ZFIEgZjiYNSwsECTpxaViJQKDRSUnDSgUBKcjN0mAmEJAclAbtIOCRhiMNOkHAIVl0DRaDQJ6k5xr0gkCGpOuRbhDIkvzUWwi2IbBI8smF4TYEr5C0nzTIIGCQ5N1NgEbaPGaUZD2QgvKw0QxYzviJkSbAZXH8RPQVozSceuDROzw3ciYYFOkdPhE9YxhBwOGlwydGThtkqjHIk/98fOT06wtz3hBMnfh85HTWCAI2p6a+ME7zWCCQU3MfaUkRDBzL/mg0Sa8JcE4Mz/DY4rKui+HTY/cPz9AIBHJm6onhGVbWfS2Yn7F+uXfGYBD4wnGtGXVmLBjwsf5jTYHzpHdUvTDmBYGMw0tT6ucMBLZjfPoLpRnwjLmtvV+UNmlj8Piu3lwzQHu0N5cNBpLj+d5cfxOQH8/3FrYGgrx0lrX3Ok3BA2sVZyttJ2hVe8faFSdqB4F5/vxgu+JodnALYupfitMVDJytcgeKg8HAE3NCKTIQFN1B3tLrBc+k5261blG814OBXOFs6PX+3AREt3T0en8IBC6fvXSkpwmQ3P+1I/DeDgbyvbaP4R02AsFQsu09eIezweCvLWl41wZ2QbFR7YOL/mAwrXYoLoQVBLRzSidcPHkmCBj58Atw9WYA+hVyYksgSMzq5hXy4mNeICjqPbfKt78VAKy0dQQ9Qj59q5dvCEw9dQTKqNy7rL/h7i704d6j92FU/vpUAFASWbcdo+5Tp37VECRDzLirO+ha0tncALjZEWYkbqZNOr0NwPMik7MlHpMqKU+JepDRisxLXcuuIjnfANAaYp77jPxxkvP1XbjMWymHfzOOkqTM1gE5tDszeZKTTqpyD/ABzU7EeZI/c/OlC1Ut0Heet5hkf+nqkKkFxYnu3eQFitIrM1ULXHXEIrtZvsX9o66LUJ7kIWGUl1YtONS2m6RVvnn018XwaUgzFq4gJMl7a+fBLWzXFi8xpKx7+7vKzkTV8Pm7uqm23Or5YflaWwGmRkpt8WKRzdUAZ2+CVTEwNVcDCshmSBbKozhlCz+QLYP+N4et+UEiGr8MqAyAJHnRNmrmYeFPjo7hhkh6dqImhoWYCnSttEKymI/7QenZHBC2MCFIJ+cH7vWh0hulaOjQyHyhBnA2J0qPCUiQLERrpnrhmnsjbQGkGgFOkuQGOoSSqQcFU3guKQfpEWq+UQvqYlcLYHe0wRF0Xi63KKA69eB8QewhKc/atKAWSTkV8oHptigpzjJDsiHI2iRlnHGSUM6SHPWDUCFO0hWuQwJnSXK4QZAhFklCyZHMTtQsOS1TTkAAk+R/0z7wXKE9SroicxepK30knVkfWJfTSA5TdgvqAEk+EphnLYC5og8sbJOikAnSRIcgDbfhkpvuFjQBksd8QGrnF9bDlCDTCzF4vhbS0btJyqhkGVg1XZiCLh1mk2QOSiOgCZK0EinmECI55wOumCApGKVGuojXpdXF82nBAj/jXJykSZIc93WRSpPZImfnKhn3UX8MWZKajEoxXJVyVc3D1bl1dEnK7ZWLgC+G4lmNGdKtJLsUogpkmNNIg5PFFP0HwuKSm3U1Kcj8Sbsq/a2AwkAhcjxPSnGS5AdDlSjL4KGCUGjxrPy6IA++X3m+JZDrWtGmUmPc0wW5653Kdi+B9+QTK65ySTomKe3Buqn+GH1sd0hy4pAopWludQyzs89SJWWeE4mEb42VgwzFB6OC71BLrvEfayWQTu+IjguSorCqvIonq8Fes88qkJTiXLQExNPVIIdn4ueNcSbsd5eX/qP5DpBcy4pdz4id7LIPvVSKasVSXwybhrpyMs+u7FgpSDeyonqYE+qOyKRhc0vq/KrSeYru6mHGQvqy5zWXD2eT58pXD9+CGVCe6Sp0F+mIk/tLQLd9jxvron13k/Pisx2bSQ6Se3y7G+jsTgtSWnO59eT0JsG9ftDy6t05Usoxt0+1eCaZ5/BMFZDX5/Zft50Guf1IUknQGctyOFsNHppc3k5q5ODR0xtesmgbHPY9rLASW8LufjLjHei7K0GSz6+qbgFQVVd+YGezfCO55i2SfP4bVcDtiUVDnzCZGSuy80N1jSD53APVLehYHprUilk6o30vYns/OWreWh2Drq4N/Z351Jzd/8lhbN9iFV80Vf9ErR/RN9uJS/Lk2ZVQt1jFF+F7Lb6GNjUseNcu74WdK6EsPbmhBuiIqLGhoW27jNc6f4QYPn5Yb/G9L0yoz9y+Q5um6OgMAzjQgw5fC0/hytbIfSJJ66ftMewDwi1+cAhAGKnTjpErgxt94ICC5P1IFB0ndxuwD51hfMe3qtMK0vcpY/mxvHsH8BpiUGK+Fs6hZf/tapfdPchHASAGxHwtJDG8dvW1m4aG7uWjVwKIdaDFdwwWwti+ujU5ZU9l3CvQis4OoLoFcwB9Pwg/95KVOTPtXnFtK2JA9UxaPAdErx75zcvZ7PuFZS9CeQFQfCfMtBJbtmd4zctZeebUZh2qDiylf3cPqOqPeVf/7lOntqQBYKleHaQZ7klfhYfHh7bSeXkBRNZXgJzk7B59+bYfjouZFOc/eVAHYuH1vi7yKmLusrHBS2c4/5/vmUA7enyb92ALsFvt9C6+YnXMf9iDcASoasHFughwce+A4DtjFz42gchN1UCSbjuU48MDXXTeenyFiWtaWxTf+WBe1Qn1gz8ORBXnjjvu+FAHdGWv/5XUgfg+uTEykX+8bTSnA1AmfaO4qgdxTF1QzOOb2kZzaQAIVQNTAlAOXlInRnY/txJpAFCrQI4EoPxll/ryN9cl0ToBILykugVXjQHKd3/zoLZ07brV6AEQifsv3jrQsnlV34qlHdcsQw+A1hpgAh33bOu7xnsVoRvuaQDSQF9ywOwUb6DtBgDlFbe4HtJAZP/GyevFm0BLKwD4Uhg9WgCWHvj++o7Nb4aBlXWAhQFgyXVt2LRV+RMQ2wfAly2avx8A2te0tGzdqBLAPsRUzR/kNHD1bcAHSdhHAACqUQ3+jVbgxptiiCTx26M9PQCW1CRBLvBgayewBPvWnTYbAJq4R9GBPdBv9kwsbovF7a+aiAA9APSbb+kB4E+rcypNlD+RJX2PhDFY04UEAHQCQCT8RC68WKAozaQOFwAGVCAGbBtoDWk1LZh7dQA/ARCLoBPoqgEXoOrlGJZMdgJd9T+qL4Lw5FqgvjyR6yx9H8O7nQtJTPX7oh2YXRynuXi8+LrIl/sIm8CVhXjtPOjKCwCANvQAWBatbcEk3ygBLJ5w/nv1qy2ofKxa4CLqjFS+v7Nxqait/L268/N4I7Cp9H1L4s7F3NgHZjoA4KbtaqXM41tyiAMApgejlV+Ka/KLtLq8e9806ZlqQLFJ04xsk4IXECIzx11EgytiBUCp/OofWFMbaQ4KVRW1WpCGIuaDg6waXLYBSFdin2v0uCcqOyhqNAkSomllMK01Lx2evUxt8enLFB8roeXizae6Os2qBwXEm9U302heANUvUyEd/n9Vac3mwFW+qlZ/WcH/ADT9vVqjZ2RdAAAAAElFTkSuQmCC"; + +fn head(ident: ast::ident) (void | error) = { + const id = unparse::identstr(ident); + defer free(id); + + let breadcrumb = breadcrumb(ident); + defer free(breadcrumb); + + const title = + if (len(id) == 0) + fmt::asprintf("Hare documentation") + else + fmt::asprintf("{} — Hare documentation", id); + defer free(title); + + // TODO: Move bits to +embed? + fmt::printfln("<!doctype html> +<html lang='en'> +<meta charset='utf-8' /> +<meta name='viewport' content='width=device-width, initial-scale=1' /> +<title>{}</title> +<link rel='icon' type='image/png' href='data:image/png;base64,{}'>", title, harriet_b64)?; + fmt::println("<style> +body { + font-family: sans-serif; + line-height: 1.3; + margin: 0 auto; + padding: 0 1rem; +} + +nav:not(#TableOfContents) { + max-width: calc(800px + 128px + 128px); + margin: 1rem auto 0; + display: grid; + grid-template-rows: auto auto 1fr; + grid-template-columns: auto 1fr; + grid-template-areas: + 'logo header' + 'logo nav' + 'logo none'; +} + +nav:not(#TableOfContents) img { + grid-area: logo; +} + +nav:not(#TableOfContents) h1 { + grid-area: header; + margin: 0; + padding: 0; +} + +nav:not(#TableOfContents) ul { + grid-area: nav; + margin: 0.5rem 0 0 0; + padding: 0; + list-style: none; + display: flex; + flex-direction: row; + justify-content: left; + flex-wrap: wrap; +} + +nav:not(#TableOfContents) li:not(:first-child) { + margin-left: 2rem; +} + +#TableOfContents { + font-size: 1.1rem; +} + +main { + padding: 0 128px; + max-width: 800px; + margin: 0 auto; + +} + +pre { + background-color: #eee; + padding: 0.25rem 1rem; + margin: 0 -1rem 1rem; + font-size: 1.2rem; + max-width: calc(100% + 1rem); + overflow-x: auto; +} + +pre .keyword { + color: #008; +} + +pre .type { + color: #44F; +} + +ol { + padding-left: 0; + list-style: none; +} + +ol li { + padding-left: 0; +} + +h2, h3, h4 { + display: flex; +} + +h3 { + border-bottom: 1px solid #ccc; + padding-bottom: 0.25rem; +} + +.invalid { + color: red; +} + +.heading-body { + word-wrap: anywhere; +} + +.heading-extra { + align-self: flex-end; + flex-grow: 1; + padding-left: 0.5rem; + text-align: right; + font-size: 0.8rem; + color: #444; +} + +h4:target + pre { + background: #ddf; +} + +details { + background: #eee; + margin: 1rem -1rem 1rem; +} + +summary { + cursor: pointer; + padding: 0.5rem 1rem; +} + +details pre { + margin: 0; +} + +.comment { + color: #000; + font-weight: bold; +} + +@media(max-width: 1000px) { + main { + padding: 0; + } +} + +@media(prefers-color-scheme: dark) { + body { + background: #121415; + color: #e1dfdc; + } + + img.mascot { + filter: invert(.92); + } + + a { + color: #78bef8; + } + + a:visited { + color: #48a7f5; + } + + summary { + background: #16191c; + } + + h3 { + border-bottom: solid #16191c; + } + + h4:target + pre { + background: #162329; + } + + pre { + background-color: #16191c; + } + + pre .keyword { + color: #69f; + } + + pre .type { + color: #3cf; + } + + .comment { + color: #fff; + } + + .heading-extra { + color: #9b9997; + } +} +</style>")?; + fmt::printfln("<nav> + <img src='data:image/png;base64,{}' + class='mascot' + alt='An inked drawing of the Hare mascot, a fuzzy rabbit' + width='128' height='128' /> + <h1>Hare documentation</h1> + <ul> + <li> + <a href='https://harelang.org'>Home</a> + </li>", harriet_b64)?; + fmt::printf("<li>{}</li>", breadcrumb)?; + fmt::print("</ul> +</nav> +<main>")?; + return; +}; diff --git a/cmd/haredoc/doc/resolve.ha b/cmd/haredoc/doc/resolve.ha @@ -0,0 +1,191 @@ +// License: GPL-3.0 +// (c) 2021 Drew DeVault <sir@cmpwn.com> +// (c) 2021 Ember Sawady <ecs@d2evs.net> +// (c) 2022 Alexey Yerin <yyp@disroot.org> +use fmt; +use fs; +use hare::ast; +use hare::lex; +use hare::module; +use hare::parse; +use io; +use os; +use path; + +type symkind = enum { + LOCAL, + MODULE, + SYMBOL, + ENUM_LOCAL, + ENUM_REMOTE, +}; + +// Resolves a reference. Given an identifier, determines if it refers to a local +// symbol, a module, or a symbol in a remote module, then returns this +// information combined with a corrected ident if necessary. +fn resolve(ctx: *context, what: ast::ident) ((ast::ident, symkind) | void | error) = { + if (is_local(ctx, what)) { + return (what, symkind::LOCAL); + }; + + if (len(what) > 1) { + // Look for symbol in remote module + let partial = what[..len(what) - 1]; + + match (module::find(ctx.mctx, partial)) { + case let r: (str, module::srcset) => + module::finish_srcset(&r.1); + return (what, symkind::SYMBOL); + case module::error => void; + }; + }; + if (len(what) == 2) { + match (lookup_local_enum(ctx, what)) { + case let id: ast::ident => + return (id, symkind::ENUM_LOCAL); + case => void; + }; + }; + if (len(what) > 2) { + match (lookup_remote_enum(ctx, what)?) { + case let id: ast::ident => + return (id, symkind::ENUM_REMOTE); + case => void; + }; + }; + + match (module::find(ctx.mctx, what)) { + case let r: (str, module::srcset) => + module::finish_srcset(&r.1); + return (what, symkind::MODULE); + case module::error => void; + }; + + return; +}; + +fn is_local(ctx: *context, what: ast::ident) bool = { + if (len(what) != 1) { + return false; + }; + + const summary = ctx.summary; + for (let i = 0z; i < len(summary.constants); i += 1) { + const name = decl_ident(summary.constants[i])[0]; + if (name == what[0]) { + return true; + }; + }; + for (let i = 0z; i < len(summary.errors); i += 1) { + const name = decl_ident(summary.errors[i])[0]; + if (name == what[0]) { + return true; + }; + }; + for (let i = 0z; i < len(summary.types); i += 1) { + const name = decl_ident(summary.types[i])[0]; + if (name == what[0]) { + return true; + }; + }; + for (let i = 0z; i < len(summary.globals); i += 1) { + const name = decl_ident(summary.globals[i])[0]; + if (name == what[0]) { + return true; + }; + }; + for (let i = 0z; i < len(summary.funcs); i += 1) { + const name = decl_ident(summary.funcs[i])[0]; + if (name == what[0]) { + return true; + }; + }; + + return false; +}; + +fn lookup_local_enum(ctx: *context, what: ast::ident) (ast::ident | void) = { + for (let i = 0z; i < len(ctx.summary.types); i += 1) { + const decl = ctx.summary.types[i]; + const name = decl_ident(decl)[0]; + if (name == what[0]) { + const t = (decl.decl as []ast::decl_type)[0]; + const e = match (t._type.repr) { + case let e: ast::enum_type => + yield e; + case => + return; + }; + for (let i = 0z; i < len(e.values); i += 1) { + if (e.values[i].name == what[1]) { + return what; + }; + }; + }; + }; +}; + +fn lookup_remote_enum(ctx: *context, what: ast::ident) (ast::ident | void | error) = { + // mod::decl_name::member + const mod = what[..len(what) - 2]; + const decl_name = what[len(what) - 2]; + const member = what[len(what) - 1]; + + const srcs = match (module::find(ctx.mctx, mod)) { + case let s: (str, module::srcset) => + yield s.1; + case let e: module::error => + module::finish_error(e); + return void; + }; + + // This would take a lot of memory to load + let decls: []ast::decl = []; + defer { + for (let i = 0z; i < len(decls); i += 1) { + ast::decl_finish(decls[i]); + }; + free(decls); + }; + for (let i = 0z; i < len(srcs.ha); i += 1) { + const in = srcs.ha[i]; + let u = scan(in)?; + append(decls, u.decls...); + }; + + for (let i = 0z; i < len(decls); i += 1) { + const decl = match (decls[i].decl) { + case let t: []ast::decl_type => + yield t; + case => + continue; + }; + for (let i = 0z; i < len(decl); i += 1) { + if (decl[i].ident[0] == decl_name) { + const e = match (decl[i]._type.repr) { + case let e: ast::enum_type => + yield e; + case => + abort(); + }; + for (let i = 0z; i < len(e.values); i += 1) { + if (e.values[i].name == member) { + return what; + }; + }; + }; + }; + }; +}; + +export fn scan(path: str) (ast::subunit | error) = { + const input = match (os::open(path)) { + case let f: io::file => + yield f; + case let err: fs::error => + fmt::fatalf("Error reading {}: {}", path, fs::strerror(err)); + }; + defer io::close(input)!; + const lexer = lex::init(input, path, lex::flag::COMMENTS); + return parse::subunit(&lexer)?; +}; diff --git a/cmd/haredoc/doc/sort.ha b/cmd/haredoc/doc/sort.ha @@ -0,0 +1,95 @@ +// License: GPL-3.0 +// (c) 2021 Drew DeVault <sir@cmpwn.com> +// (c) 2021 Ember Sawady <ecs@d2evs.net> +use hare::ast; +use sort; +use strings; + +// Sorts declarations by removing unexported declarations, moving undocumented +// declarations to the end, sorting by identifier, and ensuring that only one +// member is present in each declaration (so that "let x: int = 10, y: int = 20" +// becomes two declarations: "let x: int = 10; let y: int = 20;"). +export fn sort_decls(decls: []ast::decl) summary = { + let sorted = summary { ... }; + + for (let i = 0z; i < len(decls); i += 1) { + let decl = decls[i]; + if (!decl.exported) { + continue; + }; + + match (decl.decl) { + case let f: ast::decl_func => + append(sorted.funcs, decl); + case let t: []ast::decl_type => + for (let j = 0z; j < len(t); j += 1) { + let bucket = &sorted.types; + if (t[j]._type.flags & ast::type_flag::ERROR == ast::type_flag::ERROR) { + bucket = &sorted.errors; + }; + append(bucket, ast::decl { + exported = true, + start = decl.start, + end = decl.end, + decl = alloc([t[j]]), + docs = decl.docs, + }); + }; + case let c: []ast::decl_const => + for (let j = 0z; j < len(c); j += 1) { + append(sorted.constants, ast::decl { + exported = true, + start = decl.start, + end = decl.end, + decl = alloc([c[j]]), + docs = decl.docs, + }); + }; + case let g: []ast::decl_global => + for (let j = 0z; j < len(g); j += 1) { + append(sorted.globals, ast::decl { + exported = true, + start = decl.start, + end = decl.end, + decl = alloc([g[j]]), + docs = decl.docs, + }); + }; + }; + }; + + sort::sort(sorted.constants, size(ast::decl), &decl_cmp); + sort::sort(sorted.errors, size(ast::decl), &decl_cmp); + sort::sort(sorted.types, size(ast::decl), &decl_cmp); + sort::sort(sorted.globals, size(ast::decl), &decl_cmp); + sort::sort(sorted.funcs, size(ast::decl), &decl_cmp); + return sorted; +}; + +fn decl_cmp(a: const *opaque, b: const *opaque) int = { + const a = *(a: const *ast::decl); + const b = *(b: const *ast::decl); + if (a.docs == "" && b.docs != "") { + return 1; + } else if (a.docs != "" && b.docs == "") { + return -1; + }; + const id_a = decl_ident(a), id_b = decl_ident(b); + return strings::compare(id_a[len(id_a) - 1], id_b[len(id_b) - 1]); +}; + +fn decl_ident(decl: ast::decl) ast::ident = { + match (decl.decl) { + case let f: ast::decl_func => + return f.ident; + case let t: []ast::decl_type => + assert(len(t) == 1); + return t[0].ident; + case let c: []ast::decl_const => + assert(len(c) == 1); + return c[0].ident; + case let g: []ast::decl_global => + assert(len(g) == 1); + return g[0].ident; + }; +}; diff --git a/cmd/haredoc/doc/tty.ha b/cmd/haredoc/doc/tty.ha @@ -0,0 +1,595 @@ +// License: GPL-3.0 +// (c) 2021 Alexey Yerin <yyp@disroot.org> +// (c) 2021 Drew DeVault <sir@cmpwn.com> +// (c) 2021 Ember Sawady <ecs@d2evs.net> +use ascii; +use bufio; +use fmt; +use hare::ast; +use hare::ast::{variadism}; +use hare::lex; +use hare::unparse; +use io; +use memio; +use os; +use strings; + +let firstline: bool = true; + +// Formats output as Hare source code (prototypes) with syntax highlighting +export fn emit_tty(ctx: *context) (void | error) = { + init_colors(); + const summary = ctx.summary; + + match (ctx.readme) { + case let readme: io::file => + for (true) match (bufio::scanline(readme)?) { + case io::EOF => + break; + case let b: []u8 => + defer free(b); + firstline = false; + insert(b[0], ' '); + comment_tty(ctx.out, strings::fromutf8(b)!)?; + }; + case void => void; + }; + + emit_submodules_tty(ctx)?; + + // XXX: Should we emit the dependencies, too? + for (let i = 0z; i < len(summary.types); i += 1) { + details_tty(ctx, summary.types[i])?; + }; + for (let i = 0z; i < len(summary.constants); i += 1) { + details_tty(ctx, summary.constants[i])?; + }; + for (let i = 0z; i < len(summary.errors); i += 1) { + details_tty(ctx, summary.errors[i])?; + }; + for (let i = 0z; i < len(summary.globals); i += 1) { + details_tty(ctx, summary.globals[i])?; + }; + for (let i = 0z; i < len(summary.funcs); i += 1) { + details_tty(ctx, summary.funcs[i])?; + }; +}; + +fn emit_submodules_tty(ctx: *context) (void | error) = { + if (len(ctx.submods) != 0) { + fmt::fprintln(ctx.out)?; + if (len(ctx.ident) == 0) { + render(ctx.out, syn::COMMENT)?; + fmt::fprintln(ctx.out, "// Modules")?; + render(ctx.out, syn::NORMAL)?; + } else { + render(ctx.out, syn::COMMENT)?; + fmt::fprintln(ctx.out, "// Submodules")?; + render(ctx.out, syn::NORMAL)?; + }; + for (let i = 0z; i < len(ctx.submods); i += 1) { + let submodule = if (len(ctx.ident) != 0) { + const s = unparse::identstr(ctx.ident); + defer free(s); + yield strings::concat(s, "::", ctx.submods[i]); + } else { + yield strings::dup(ctx.submods[i]); + }; + defer free(submodule); + + render(ctx.out, syn::COMMENT)?; + fmt::fprintfln(ctx.out, "// - [[{}]]", submodule)?; + render(ctx.out, syn::NORMAL)?; + }; + }; +}; + +fn comment_tty(out: io::handle, s: str) (size | io::error) = { + let n = 0z; + n += render(out, syn::COMMENT)?; + n += fmt::fprintfln(out, "//{}", s)?; + n += render(out, syn::NORMAL)?; + return n; +}; + +fn docs_tty(out: io::handle, s: str, indent: size) (size | io::error) = { + const iter = strings::tokenize(s, "\n"); + let z = 0z; + for (true) match (strings::next_token(&iter)) { + case let s: str => + if (!(strings::peek_token(&iter) is void)) { + z += comment_tty(out, s)?; + for (let i = 0z; i < indent; i += 1) { + z += fmt::fprint(out, "\t")?; + }; + }; + case void => + break; + }; + + return z; +}; + +fn isws(s: str) bool = { + const iter = strings::iter(s); + for (true) { + match (strings::next(&iter)) { + case let r: rune => + if (!ascii::isspace(r)) { + return false; + }; + case void => + break; + }; + }; + return true; +}; + +fn details_tty(ctx: *context, decl: ast::decl) (void | error) = { + if (len(decl.docs) == 0 && !ctx.show_undocumented) { + return; + }; + + if (!firstline) { + fmt::fprintln(ctx.out)?; + }; + firstline = false; + + docs_tty(ctx.out, decl.docs, 0)?; + unparse_tty(ctx.out, decl)?; + fmt::fprintln(ctx.out)?; +}; + +// Forked from [[hare::unparse]] +fn unparse_tty(out: io::handle, d: ast::decl) (size | io::error) = { + let n = 0z; + match (d.decl) { + case let g: []ast::decl_global => + n += render(out, syn::KEYWORD)?; + n += fmt::fprint(out, if (g[0].is_const) "const " else "let ")?; + for (let i = 0z; i < len(g); i += 1) { + if (len(g[i].symbol) != 0) { + n += render(out, syn::ATTRIBUTE)?; + n += fmt::fprintf(out, "@symbol(")?; + n += render(out, syn::STRING)?; + n += fmt::fprintf(out, `"{}"`, g[i].symbol)?; + n += render(out, syn::ATTRIBUTE)?; + n += fmt::fprintf(out, ") ")?; + n += render(out, syn::NORMAL)?; + }; + n += render(out, syn::GLOBAL)?; + n += unparse::ident(out, g[i].ident)?; + match (g[i]._type) { + case null => + yield; + case let ty: *ast::_type => + n += render(out, syn::PUNCTUATION)?; + n += fmt::fprint(out, ": ")?; + n += type_tty(out, 0, *ty)?; + }; + if (i + 1 < len(g)) { + n += render(out, syn::PUNCTUATION)?; + n += fmt::fprint(out, ", ")?; + }; + n += render(out, syn::NORMAL)?; + }; + case let c: []ast::decl_const => + n += render(out, syn::KEYWORD)?; + n += fmt::fprintf(out, "def ")?; + for (let i = 0z; i < len(c); i += 1) { + n += render(out, syn::CONSTANT)?; + n += unparse::ident(out, c[i].ident)?; + n += render(out, syn::PUNCTUATION)?; + match (c[i]._type) { + case null => + yield; + case let ty: *ast::_type => + n += render(out, syn::PUNCTUATION)?; + n += fmt::fprint(out, ": ")?; + n += type_tty(out, 0, *ty)?; + }; + if (i + 1 < len(c)) { + n += render(out, syn::PUNCTUATION)?; + n += fmt::fprint(out, ", ")?; + }; + }; + case let t: []ast::decl_type => + n += render(out, syn::KEYWORD)?; + n += fmt::fprint(out, "type ")?; + for (let i = 0z; i < len(t); i += 1) { + n += render(out, syn::TYPEDEF)?; + n += unparse::ident(out, t[i].ident)?; + n += render(out, syn::PUNCTUATION)?; + n += fmt::fprint(out, " = ")?; + n += type_tty(out, 0, t[i]._type)?; + if (i + 1 < len(t)) { + n += render(out, syn::PUNCTUATION)?; + n += fmt::fprint(out, ", ")?; + }; + }; + case let f: ast::decl_func => + n += render(out, syn::ATTRIBUTE)?; + n += fmt::fprint(out, switch (f.attrs) { + case ast::fndecl_attrs::NONE => + yield ""; + case ast::fndecl_attrs::FINI => + yield "@fini "; + case ast::fndecl_attrs::INIT => + yield "@init "; + case ast::fndecl_attrs::TEST => + yield "@test "; + })?; + n += render(out, syn::NORMAL)?; + + let p = f.prototype.repr as ast::func_type; + if (len(f.symbol) != 0) { + n += render(out, syn::ATTRIBUTE)?; + n += fmt::fprintf(out, "@symbol(")?; + n += render(out, syn::STRING)?; + n += fmt::fprintf(out, `"{}"`, f.symbol)?; + n += render(out, syn::ATTRIBUTE)?; + n += fmt::fprintf(out, ") ")?; + n += render(out, syn::NORMAL)?; + }; + n += render(out, syn::KEYWORD)?; + n += fmt::fprint(out, "fn ")?; + n += render(out, syn::FUNCTION)?; + n += unparse::ident(out, f.ident)?; + n += fmt::fprint(out, "\x1b[0m")?; + n += prototype_tty(out, 0, + f.prototype.repr as ast::func_type)?; + }; + n += render(out, syn::PUNCTUATION)?; + n += fmt::fprint(out, ";")?; + return n; +}; + +fn prototype_tty( + out: io::handle, + indent: size, + t: ast::func_type, +) (size | io::error) = { + let n = 0z; + n += render(out, syn::PUNCTUATION)?; + n += fmt::fprint(out, "(")?; + + let typenames: []str = []; + // TODO: https://todo.sr.ht/~sircmpwn/hare/581 + if (len(t.params) > 0) { + typenames = alloc([""...], len(t.params)); + }; + defer strings::freeall(typenames); + let retname = ""; + defer free(retname); + + // estimate length of prototype to determine if it should span multiple + // lines + const linelen = if (len(t.params) == 0) { + let strm = memio::dynamic(); + defer io::close(&strm)!; + type_tty(&strm, indent, *t.result)?; + retname = strings::dup(memio::string(&strm)!); + yield 0z; // only use one line if there's no parameters + } else { + let strm = memio::dynamic(); + defer io::close(&strm)!; + let linelen = indent * 8 + 5; + linelen += if (len(t.params) != 0) len(t.params) * 3 - 1 else 0; + for (let i = 0z; i < len(t.params); i += 1) { + const param = t.params[i]; + linelen += unparse::_type(&strm, indent, *param._type)?; + typenames[i] = strings::dup(memio::string(&strm)!); + linelen += if (param.name == "") 1 else len(param.name); + memio::reset(&strm); + }; + switch (t.variadism) { + case variadism::NONE => void; + case variadism::HARE => + linelen += 3; + case variadism::C => + linelen += 5; + }; + linelen += type_tty(&strm, indent, *t.result)?; + retname = strings::dup(memio::string(&strm)!); + yield linelen; + }; + + // use 72 instead of 80 to give a bit of leeway for preceding text + if (linelen > 72) { + indent += 1; + for (let i = 0z; i < len(t.params); i += 1) { + const param = t.params[i]; + n += newline(out, indent)?; + n += render(out, syn::SECONDARY)?; + n += fmt::fprint(out, + if (param.name == "") "_" else param.name)?; + n += render(out, syn::PUNCTUATION)?; + n += fmt::fprint(out, ": ")?; + n += render(out, syn::TYPE)?; + n += fmt::fprint(out, typenames[i])?; + if (i + 1 == len(t.params) + && t.variadism == variadism::HARE) { + n += render(out, syn::OPERATOR)?; + n += fmt::fprint(out, "...")?; + } else { + n += render(out, syn::PUNCTUATION)?; + n += fmt::fprint(out, ",")?; + }; + }; + if (t.variadism == variadism::C) { + n += newline(out, indent)?; + n += render(out, syn::OPERATOR)?; + n += fmt::fprint(out, "...")?; + }; + indent -= 1; + n += newline(out, indent)?; + } else for (let i = 0z; i < len(t.params); i += 1) { + const param = t.params[i]; + n += render(out, syn::SECONDARY)?; + n += fmt::fprint(out, + if (param.name == "") "_" else param.name)?; + n += render(out, syn::PUNCTUATION)?; + n += fmt::fprint(out, ": ")?; + n += render(out, syn::TYPE)?; + n += fmt::fprint(out, typenames[i])?; + if (i + 1 == len(t.params)) { + switch (t.variadism) { + case variadism::NONE => void; + case variadism::HARE => + n += render(out, syn::OPERATOR)?; + n += fmt::fprint(out, "...")?; + case variadism::C => + n += render(out, syn::PUNCTUATION)?; + n += fmt::fprint(out, ", ")?; + n += render(out, syn::OPERATOR)?; + n += fmt::fprint(out, "...")?; + }; + } else { + n += render(out, syn::PUNCTUATION)?; + n += fmt::fprint(out, ", ")?; + }; + }; + + n += render(out, syn::PUNCTUATION)?; + n += fmt::fprint(out, ")", retname)?; + return n; +}; + +// Forked from [[hare::unparse]] +fn struct_union_type_tty( + out: io::handle, + indent: size, + t: ast::_type, +) (size | io::error) = { + let n = 0z; + let membs = match (t.repr) { + case let st: ast::struct_type => + n += render(out, syn::TYPE)?; + n += fmt::fprint(out, "struct")?; + if (st.packed) { + n += render(out, syn::ATTRIBUTE)?; + n += fmt::fprint(out, " @packed")?; + }; + n += render(out, syn::PUNCTUATION)?; + n += fmt::fprint(out, " {")?; + yield st.members: []ast::struct_member; + case let ut: ast::union_type => + n += render(out, syn::TYPE)?; + n += fmt::fprint(out, "union")?; + n += render(out, syn::PUNCTUATION)?; + n += fmt::fprint(out, " {")?; + yield ut: []ast::struct_member; + }; + + indent += 1z; + for (let i = 0z; i < len(membs); i += 1) { + n += newline(out, indent)?; + if (membs[i].docs != "") { + n += docs_tty(out, membs[i].docs, indent)?; + }; + + match (membs[i]._offset) { + case null => void; + case let ex: *ast::expr => + n += render(out, syn::ATTRIBUTE)?; + n += fmt::fprint(out, "@offset(")?; + n += render(out, syn::NUMBER)?; + n += unparse::expr(out, indent, *ex)?; + n += render(out, syn::ATTRIBUTE)?; + n += fmt::fprint(out, ")")?; + n += render(out, syn::NORMAL)?; + }; + + match (membs[i].member) { + case let se: ast::struct_embedded => + n += type_tty(out, indent, *se)?; + case let sa: ast::struct_alias => + n += unparse::ident(out, sa)?; + case let sf: ast::struct_field => + n += render(out, syn::SECONDARY)?; + n += fmt::fprint(out, sf.name)?; + n += render(out, syn::PUNCTUATION)?; + n += fmt::fprint(out, ": ")?; + n += type_tty(out, indent, *sf._type)?; + }; + + n += render(out, syn::PUNCTUATION)?; + n += fmt::fprint(out, ",")?; + }; + + indent -= 1; + n += newline(out, indent)?; + n += render(out, syn::PUNCTUATION)?; + n += fmt::fprint(out, "}")?; + return n; +}; + +// Forked from [[hare::unparse]] +fn type_tty( + out: io::handle, + indent: size, + t: ast::_type, +) (size | io::error) = { + let n = 0z; + if (t.flags & ast::type_flag::CONST != 0 + && !(t.repr is ast::func_type)) { + n += render(out, syn::TYPE)?; + n += fmt::fprint(out, "const ")?; + }; + if (t.flags & ast::type_flag::ERROR != 0) { + n += render(out, syn::OPERATOR)?; + n += fmt::fprint(out, "!")?; + }; + + match (t.repr) { + case let a: ast::alias_type => + if (a.unwrap) { + n += render(out, syn::OPERATOR)?; + n += fmt::fprint(out, "...")?; + }; + n += render(out, syn::TYPE)?; + n += unparse::ident(out, a.ident)?; + case let b: ast::builtin_type => + n += render(out, syn::TYPE)?; + n += fmt::fprintf(out, "{}", unparse::builtin_type(b))?; + case let e: ast::enum_type => + n += render(out, syn::TYPE)?; + n += fmt::fprint(out, "enum ")?; + if (e.storage != ast::builtin_type::INT) { + n += fmt::fprintf(out, + "{} ", unparse::builtin_type(e.storage))?; + }; + n += render(out, syn::PUNCTUATION)?; + n += fmt::fprintln(out, "{")?; + indent += 1; + for (let i = 0z; i < len(e.values); i += 1) { + for (let i = 0z; i < indent; i += 1) { + n += fmt::fprint(out, "\t")?; + }; + let value = e.values[i]; + let wrotedocs = false; + if (value.docs != "") { + // Check if comment should go above or next to + // field + if (multiline_comment(value.docs)) { + n += docs_tty(out, value.docs, indent)?; + wrotedocs = true; + }; + }; + n += render(out, syn::SECONDARY)?; + n += fmt::fprint(out, value.name)?; + match (value.value) { + case null => void; + case let e: *ast::expr => + n += render(out, syn::OPERATOR)?; + n += fmt::fprint(out, " = ")?; + n += render(out, syn::NORMAL)?; + n += unparse::expr(out, indent, *e)?; + }; + n += render(out, syn::PUNCTUATION)?; + n += fmt::fprint(out, ",")?; + if (value.docs != "" && !wrotedocs) { + n += fmt::fprint(out, " ")?; + n += docs_tty(out, value.docs, 0)?; + } else { + n += fmt::fprintln(out)?; + }; + }; + indent -= 1; + for (let i = 0z; i < indent; i += 1) { + n += fmt::fprint(out, "\t")?; + }; + n += render(out, syn::PUNCTUATION)?; + n += fmt::fprint(out, "}")?; + case let f: ast::func_type => + n += render(out, syn::TYPE)?; + n += fmt::fprint(out, "fn")?; + n += prototype_tty(out, indent, f)?; + case let l: ast::list_type => + n += render(out, syn::OPERATOR)?; + n += fmt::fprint(out, "[")?; + match (l.length) { + case ast::len_slice => void; + case ast::len_unbounded => + n += fmt::fprint(out, "*")?; + case ast::len_contextual => + n += fmt::fprint(out, "_")?; + case let e: *ast::expr => + n += unparse::expr(out, indent, *e)?; + }; + n += render(out, syn::OPERATOR)?; + n += fmt::fprint(out, "]")?; + n += type_tty(out, indent, *l.members)?; + case let p: ast::pointer_type => + if (p.flags & ast::pointer_flag::NULLABLE != 0) { + n += render(out, syn::TYPE)?; + n += fmt::fprint(out, "nullable ")?; + }; + n += render(out, syn::OPERATOR)?; + n += fmt::fprint(out, "*")?; + n += type_tty(out, indent, *p.referent)?; + case ast::struct_type => + n += struct_union_type_tty(out, indent, t)?; + case ast::union_type => + n += struct_union_type_tty(out, indent, t)?; + case let t: ast::tagged_type => + // rough estimate of current line length + let linelen: size = n + (indent + 1) * 8; + n = 0; + n += render(out, syn::PUNCTUATION)?; + linelen += fmt::fprint(out, "(")?; + for (let i = 0z; i < len(t); i += 1) { + linelen += type_tty(out, indent, *t[i])?; + if (i + 1 == len(t)) { + break; + }; + n += render(out, syn::PUNCTUATION)?; + linelen += fmt::fprint(out, " |")?; + // use 72 instead of 80 to give a bit of leeway for long + // type names + if (linelen > 72) { + n += linelen; + linelen = (indent + 1) * 8; + n += fmt::fprintln(out)?; + for (let i = 0z; i <= indent; i += 1) { + n += fmt::fprint(out, "\t")?; + }; + } else { + linelen += fmt::fprint(out, " ")?; + }; + }; + n += linelen; + n += render(out, syn::PUNCTUATION)?; + n += fmt::fprint(out, ")")?; + case let t: ast::tuple_type => + // rough estimate of current line length + let linelen: size = n + (indent + 1) * 8; + n = 0; + n += render(out, syn::PUNCTUATION)?; + linelen += fmt::fprint(out, "(")?; + for (let i = 0z; i < len(t); i += 1) { + linelen += type_tty(out, indent, *t[i])?; + if (i + 1 == len(t)) { + break; + }; + n += render(out, syn::PUNCTUATION)?; + linelen += fmt::fprint(out, ",")?; + // use 72 instead of 80 to give a bit of leeway for long + // type names + if (linelen > 72) { + n += linelen; + linelen = (indent + 1) * 8; + n += fmt::fprintln(out)?; + for (let i = 0z; i <= indent; i += 1) { + n += fmt::fprint(out, "\t")?; + }; + } else { + linelen += fmt::fprint(out, " ")?; + }; + }; + n += linelen; + n += render(out, syn::PUNCTUATION)?; + n += fmt::fprint(out, ")")?; + }; + return n; +}; diff --git a/cmd/haredoc/doc/types.ha b/cmd/haredoc/doc/types.ha @@ -0,0 +1,55 @@ +// License: GPL-3.0 +// (c) 2021 Drew DeVault <sir@cmpwn.com> +// (c) 2021 Ember Sawady <ecs@d2evs.net> +use fs; +use hare::ast; +use hare::lex; +use hare::module; +use hare::parse; +use io; +use os::exec; + +export type error = !(lex::error | parse::error | io::error | module::error | exec::error | fs::error); + +export fn strerror(err: error) str = { + match (err) { + case let err: lex::error => + return lex::strerror(err); + case let err: parse::error => + return parse::strerror(err); + case let err: io::error => + return io::strerror(err); + case let err: module::error => + return module::strerror(err); + }; +}; + +export type format = enum { + HARE, + TTY, + HTML, +}; + +export type context = struct { + mctx: *module::context, + ident: ast::ident, + tags: []str, + modpath: str, + srcs: module::srcset, + submods: []str, + summary: summary, + format: format, + template: bool, + show_undocumented: bool, + readme: (io::file | void), + out: io::handle, + pager: (exec::process | void), +}; + +export type summary = struct { + constants: []ast::decl, + errors: []ast::decl, + types: []ast::decl, + globals: []ast::decl, + funcs: []ast::decl, +}; diff --git a/cmd/haredoc/doc/util.ha b/cmd/haredoc/doc/util.ha @@ -0,0 +1,53 @@ +// License: GPL-3.0 +// (c) 2022 Byron Torres <b@torresjrjr.com> +// (c) 2022 Sebastian <sebastian@sebsite.pw> +use fmt; +use fs; +use hare::module; +use io; +use memio; +use os; +use sort; +use strings; + +// Forked from [[hare::unparse]]. +fn newline(out: io::handle, indent: size) (size | io::error) = { + let n = 0z; + n += fmt::fprint(out, "\n")?; + for (let i = 0z; i < indent; i += 1) { + n += fmt::fprint(out, "\t")?; + }; + return n; +}; + +fn multiline_comment(s: str) bool = + strings::byteindex(s, '\n') as size != len(s) - 1; + +fn trim_comment(s: str) str = { + let trimmed = memio::dynamic(); + let tok = strings::tokenize(s, "\n"); + for (true) { + const line = match (strings::next_token(&tok)) { + case void => + break; + case let line: str => + yield line; + }; + memio::concat(&trimmed, strings::trimprefix(line, " "), "\n")!; + }; + return strings::dup(memio::string(&trimmed)!); +}; + +export fn submodules(path: str) ([]str | error) = { + let submodules: []str = []; + let it = os::iter(path)?; + defer fs::finish(it); + for (true) match (module::next(it)) { + case void => + break; + case let d: fs::dirent => + append(submodules, strings::dup(d.name)); + }; + sort::strings(submodules); + return submodules; +}; diff --git a/cmd/haredoc/docstr.ha b/cmd/haredoc/docstr.ha @@ -1,248 +0,0 @@ -// License: GPL-3.0 -// (c) 2022 Alexey Yerin <yyp@disroot.org> -// (c) 2021 Drew DeVault <sir@cmpwn.com> -// (c) 2021 Ember Sawady <ecs@d2evs.net> -// (c) 2021 Thomas Bracht Laumann Jespersen <t@laumann.xyz> -// (c) 2022 Umar Getagazov <umar@handlerug.me> -use ascii; -use bufio; -use encoding::utf8; -use fmt; -use hare::ast; -use hare::parse; -use io; -use memio; -use strings; - -type paragraph = void; -type text = str; -type reference = ast::ident; -type sample = str; -type listitem = void; -type token = (paragraph | text | reference | sample | listitem); - -type docstate = enum { - PARAGRAPH, - TEXT, - LIST, -}; - -type parser = struct { - src: bufio::stream, - state: docstate, -}; - -fn parsedoc(in: io::handle) parser = { - static let buf: [4096]u8 = [0...]; - return parser { - src = bufio::init(in, buf[..], []), - state = docstate::PARAGRAPH, - }; -}; - -fn scandoc(par: *parser) (token | void) = { - const rn = match (bufio::scanrune(&par.src)!) { - case let rn: rune => - yield rn; - case io::EOF => - return; - }; - - bufio::unreadrune(&par.src, rn); - switch (par.state) { - case docstate::TEXT => - switch (rn) { - case '[' => - return scanref(par); - case => - return scantext(par); - }; - case docstate::LIST => - switch (rn) { - case '[' => - return scanref(par); - case '-' => - return scanlist(par); - case => - return scantext(par); - }; - case docstate::PARAGRAPH => - switch (rn) { - case ' ', '\t' => - return scansample(par); - case '-' => - return scanlist(par); - case => - return scantext(par); - }; - }; -}; - -fn scantext(par: *parser) (token | void) = { - if (par.state == docstate::PARAGRAPH) { - par.state = docstate::TEXT; - return paragraph; - }; - // TODO: Collapse whitespace - const buf = memio::dynamic(); - for (true) { - const rn = match (bufio::scanrune(&par.src)!) { - case io::EOF => break; - case let rn: rune => - yield rn; - }; - switch (rn) { - case '[' => - bufio::unreadrune(&par.src, rn); - break; - case '\n' => - memio::appendrune(&buf, rn)!; - const rn = match (bufio::scanrune(&par.src)!) { - case io::EOF => break; - case let rn: rune => - yield rn; - }; - if (rn == '\n') { - par.state = docstate::PARAGRAPH; - break; - }; - bufio::unreadrune(&par.src, rn); - if (rn == '-' && par.state == docstate::LIST) { - break; - }; - case => - memio::appendrune(&buf, rn)!; - }; - }; - let result = memio::string(&buf)!; - if (len(result) == 0) { - return; - }; - return result: text; -}; - -fn scanref(par: *parser) (token | void) = { - match (bufio::scanrune(&par.src)!) { - case io::EOF => - return; - case let rn: rune => - if (rn != '[') { - abort(); - }; - }; - match (bufio::scanrune(&par.src)!) { - case io::EOF => - return; - case let rn: rune => - if (rn != '[') { - bufio::unreadrune(&par.src, rn); - return strings::dup("["): text; - }; - }; - - const buf = memio::dynamic(); - defer io::close(&buf)!; - // TODO: Handle invalid syntax here - for (true) { - match (bufio::scanrune(&par.src)!) { - case let rn: rune => - switch (rn) { - case ']' => - bufio::scanrune(&par.src) as rune; // ] - break; - case => - memio::appendrune(&buf, rn)!; - }; - case io::EOF => break; - }; - }; - let id = parse::identstr(memio::string(&buf)!) as ast::ident; - return id: reference; -}; - -fn scansample(par: *parser) (token | void) = { - let nws = 0z; - for (true) { - match (bufio::scanrune(&par.src)!) { - case io::EOF => - return; - case let rn: rune => - switch (rn) { - case ' ' => - nws += 1; - case '\t' => - nws += 8; - case => - bufio::unreadrune(&par.src, rn); - break; - }; - }; - }; - if (nws <= 1) { - return scantext(par); - }; - - let cont = true; - let buf = memio::dynamic(); - for (cont) { - const rn = match (bufio::scanrune(&par.src)!) { - case io::EOF => break; - case let rn: rune => - yield rn; - }; - switch (rn) { - case '\n' => - memio::appendrune(&buf, rn)!; - case => - memio::appendrune(&buf, rn)!; - continue; - }; - - // Consume whitespace - for (let i = 0z; i < nws) { - match (bufio::scanrune(&par.src)!) { - case io::EOF => break; - case let rn: rune => - switch (rn) { - case ' ' => - i += 1; - case '\t' => - i += 8; - case '\n' => - memio::appendrune(&buf, rn)!; - i = 0; - case => - bufio::unreadrune(&par.src, rn); - cont = false; - break; - }; - }; - }; - }; - - let buf = memio::string(&buf)!; - // Trim trailing newlines - buf = strings::rtrim(buf, '\n'); - return buf: sample; -}; - -fn scanlist(par: *parser) (token | void) = { - match (bufio::scanrune(&par.src)!) { - case io::EOF => return void; - case let rn: rune => - if (rn != '-') { - abort(); - }; - }; - const rn = match (bufio::scanrune(&par.src)!) { - case io::EOF => return void; - case let rn: rune => - yield rn; - }; - if (rn != ' ') { - bufio::unreadrune(&par.src, rn); - return strings::dup("-"): text; - }; - par.state = docstate::LIST; - return listitem; -}; diff --git a/cmd/haredoc/env.ha b/cmd/haredoc/env.ha @@ -1,104 +0,0 @@ -// License: GPL-3.0 -// (c) 2021 Drew DeVault <sir@cmpwn.com> -// (c) 2022 Haelwenn (lanodan) Monnier <contact@hacktivis.me> -use bufio; -use fmt; -use hare::module; -use io; -use os::exec; -use os; -use strings; - -def PLATFORM: str = "unknown"; - -fn default_tags() ([]module::tag | error) = { - let cmd = match (exec::cmd("hare", "version", "-v")) { - case let cmd: exec::command => - yield cmd; - case exec::nocmd => - let platform = strings::dup(PLATFORM); - let machine = strings::dup(os::machine()); - fmt::errorln("Couldn't find hare binary in PATH")?; - fmt::errorfln("Build tags defaulting to +{}+{}", - platform, machine)?; - - return alloc([module::tag { - name = platform, - mode = module::tag_mode::INCLUSIVE, - }, module::tag { - name = machine, - mode = module::tag_mode::INCLUSIVE, - }]); - case let err: exec::error => - return err; - }; - - let pipe = exec::pipe(); - defer io::close(pipe.0)!; - exec::addfile(&cmd, os::stdout_file, pipe.1); - let proc = exec::start(&cmd)?; - io::close(pipe.1)?; - - let tags: []module::tag = []; - for (true) match (bufio::scanline(pipe.0)?) { - case let b: []u8 => - defer free(b); - const (k, v) = strings::cut(strings::fromutf8(b)!, "\t"); - if (k == "Build tags") { - tags = module::parsetags(v) as []module::tag; - break; - }; - case io::EOF => - // process exited with failure; handled below - break; - }; - - let status = exec::wait(&proc)?; - match (exec::check(&status)) { - case void => - assert(len(tags) > 0); - case let status: !exec::exit_status => - fmt::fatal("Error: hare:", exec::exitstr(status)); - }; - return tags; -}; - -fn addtags(tags: []module::tag, in: str) ([]module::tag | void) = { - let in = match (module::parsetags(in)) { - case void => - return void; - case let t: []module::tag => - yield t; - }; - defer free(in); - append(tags, in...); - return tags; -}; - -fn deltags(tags: []module::tag, in: str) ([]module::tag | void) = { - if (in == "^") { - module::tags_free(tags); - return []; - }; - let in = match (module::parsetags(in)) { - case void => - return void; - case let t: []module::tag => - yield t; - }; - defer free(in); - for (let i = 0z; i < len(tags); i += 1) { - for (let j = 0z; j < len(in); j += 1) { - if (tags[i].name == in[j].name - && tags[i].mode == in[j].mode) { - free(tags[i].name); - i -= 1; - }; - }; - }; - return tags; -}; - -fn default_harepath() str = { - return HAREPATH; -}; diff --git a/cmd/haredoc/error.ha b/cmd/haredoc/error.ha @@ -0,0 +1,19 @@ +use cmd::haredoc::doc; +use fs; +use hare::module; +use hare::parse; +use io; +use os::exec; +use path; +use strconv; + +type error = !( + exec::error | + fs::error | + io::error | + module::error | + path::error | + parse::error | + strconv::error | + doc::error | +); diff --git a/cmd/haredoc/errors.ha b/cmd/haredoc/errors.ha @@ -1,26 +0,0 @@ -// License: GPL-3.0 -// (c) 2021 Drew DeVault <sir@cmpwn.com> -// (c) 2021 Ember Sawady <ecs@d2evs.net> -use hare::lex; -use hare::module; -use hare::parse; -use io; -use os::exec; - -type error = !(lex::error | parse::error | io::error | module::error | - exec::error); - -fn strerror(err: error) str = { - match (err) { - case let err: lex::error => - return lex::strerror(err); - case let err: parse::error => - return parse::strerror(err); - case let err: io::error => - return io::strerror(err); - case let err: module::error => - return module::strerror(err); - case let err: exec::error => - return exec::strerror(err); - }; -}; diff --git a/cmd/haredoc/hare.ha b/cmd/haredoc/hare.ha @@ -1,197 +0,0 @@ -// License: GPL-3.0 -// (c) 2021 Alexey Yerin <yyp@disroot.org> -// (c) 2021 Drew DeVault <sir@cmpwn.com> -// (c) 2021 Ember Sawady <ecs@d2evs.net> -use bufio; -use fmt; -use hare::ast; -use hare::lex; -use hare::module; -use hare::unparse; -use io; -use os; -use strings; - -// Formats output as Hare source code (prototypes) -fn emit_hare(ctx: *context) (void | error) = { - const summary = ctx.summary; - - let first = true; - match (ctx.readme) { - case let readme: io::file => - first = false; - for (true) { - match (bufio::scanline(readme)?) { - case io::EOF => break; - case let b: []u8 => - fmt::fprintfln(ctx.out, - "// {}", strings::fromutf8(b)!)?; - free(b); - }; - }; - case void => void; - }; - - emit_submodules_hare(ctx)?; - - // XXX: Should we emit the dependencies, too? - for (let i = 0z; i < len(summary.types); i += 1) { - if (!first) { - fmt::fprintln(ctx.out)?; - }; - first = false; - details_hare(ctx, summary.types[i])?; - }; - for (let i = 0z; i < len(summary.constants); i += 1) { - if (!first) { - fmt::fprintln(ctx.out)?; - }; - first = false; - details_hare(ctx, summary.constants[i])?; - }; - for (let i = 0z; i < len(summary.errors); i += 1) { - if (!first) { - fmt::fprintln(ctx.out)?; - }; - first = false; - details_hare(ctx, summary.errors[i])?; - }; - for (let i = 0z; i < len(summary.globals); i += 1) { - if (!first) { - fmt::fprintln(ctx.out)?; - }; - first = false; - details_hare(ctx, summary.globals[i])?; - }; - for (let i = 0z; i < len(summary.funcs); i += 1) { - if (!first) { - fmt::fprintln(ctx.out)?; - }; - first = false; - details_hare(ctx, summary.funcs[i])?; - }; -}; - -fn emit_submodules_hare(ctx: *context) (void | error) = { - const submodules = submodules(ctx)?; - defer strings::freeall(submodules); - - if (len(submodules) != 0) { - fmt::fprintln(ctx.out)?; - if (len(ctx.ident) == 0) { - fmt::fprintln(ctx.out, "// Modules")?; - } else { - fmt::fprintln(ctx.out, "// Submodules")?; - }; - for (let i = 0z; i < len(submodules); i += 1) { - let submodule = if (len(ctx.ident) != 0) { - const s = unparse::identstr(ctx.ident); - defer free(s); - yield strings::concat(s, "::", submodules[i]); - } else { - yield strings::dup(submodules[i]); - }; - defer free(submodule); - - fmt::fprintf(ctx.out, "// - [[")?; - fmt::fprintf(ctx.out, submodule)?; - fmt::fprintfln(ctx.out, "]]")?; - }; - }; -}; - -fn details_hare(ctx: *context, decl: ast::decl) (void | error) = { - if (len(decl.docs) == 0 && !ctx.show_undocumented) { - return; - }; - - const iter = strings::tokenize(decl.docs, "\n"); - for (true) { - match (strings::next_token(&iter)) { - case void => break; - case let s: str => - if (len(s) != 0) { - fmt::fprintfln(ctx.out, "//{}", s)?; - }; - }; - }; - - unparse_hare(ctx.out, decl)?; - fmt::fprintln(ctx.out)?; - return; -}; - -// Forked from [[hare::unparse]] -fn unparse_hare(out: io::handle, d: ast::decl) (size | io::error) = { - let n = 0z; - match (d.decl) { - case let g: []ast::decl_global => - n += fmt::fprint(out, - if (g[0].is_const) "const " else "let ")?; - for (let i = 0z; i < len(g); i += 1) { - if (len(g[i].symbol) != 0) { - n += fmt::fprintf(out, - "@symbol(\"{}\") ", g[i].symbol)?; - }; - n += unparse::ident(out, g[i].ident)?; - match (g[i]._type) { - case null => - yield; - case let ty: *ast::_type => - n += fmt::fprint(out, ": ")?; - n += unparse::_type(out, 0, *ty)?; - }; - if (i + 1 < len(g)) { - n += fmt::fprint(out, ", ")?; - }; - }; - case let t: []ast::decl_type => - n += fmt::fprint(out, "type ")?; - for (let i = 0z; i < len(t); i += 1) { - n += unparse::ident(out, t[i].ident)?; - n += fmt::fprint(out, " = ")?; - n += unparse::_type(out, 0, t[i]._type)?; - if (i + 1 < len(t)) { - n += fmt::fprint(out, ", ")?; - }; - }; - case let c: []ast::decl_const => - n += fmt::fprint(out, "def ")?; - for (let i = 0z; i < len(c); i += 1) { - n += unparse::ident(out, c[i].ident)?; - n += fmt::fprint(out, ": ")?; - match (c[i]._type) { - case null => - yield; - case let ty: *ast::_type => - n += fmt::fprint(out, ": ")?; - n += unparse::_type(out, 0, *ty)?; - }; - if (i + 1 < len(c)) { - n += fmt::fprint(out, ", ")?; - }; - }; - case let f: ast::decl_func => - n += fmt::fprint(out, switch (f.attrs) { - case ast::fndecl_attrs::NONE => - yield ""; - case ast::fndecl_attrs::FINI => - yield "@fini "; - case ast::fndecl_attrs::INIT => - yield "@init "; - case ast::fndecl_attrs::TEST => - yield "@test "; - })?; - let p = f.prototype.repr as ast::func_type; - if (len(f.symbol) != 0) { - n += fmt::fprintf(out, "@symbol(\"{}\") ", - f.symbol)?; - }; - n += fmt::fprint(out, "fn ")?; - n += unparse::ident(out, f.ident)?; - n += unparse::prototype(out, 0, - f.prototype.repr as ast::func_type)?; - }; - n += fmt::fprint(out, ";")?; - return n; -}; diff --git a/cmd/haredoc/html.ha b/cmd/haredoc/html.ha @@ -1,1086 +0,0 @@ -// License: GPL-3.0 -// (c) 2021-2022 Alexey Yerin <yyp@disroot.org> -// (c) 2022 Byron Torres <b@torresjrjr.com> -// (c) 2021-2022 Drew DeVault <sir@cmpwn.com> -// (c) 2021 Ember Sawady <ecs@d2evs.net> -// (c) 2021 Thomas Bracht Laumann Jespersen <t@laumann.xyz> -// (c) 2022 Umar Getagazov <umar@handlerug.me> - -// Note: ast::ident should never have to be escaped -use encoding::utf8; -use fmt; -use hare::ast; -use hare::ast::{variadism}; -use hare::lex; -use hare::module; -use hare::unparse; -use io; -use memio; -use net::ip; -use net::uri; -use os; -use path; -use strings; - -// Prints a string to an output handle, escaping any of HTML's reserved -// characters. -fn html_escape(out: io::handle, in: str) (size | io::error) = { - let z = 0z; - let iter = strings::iter(in); - for (true) { - match (strings::next(&iter)) { - case void => break; - case let rn: rune => - z += fmt::fprint(out, switch (rn) { - case '&' => - yield "&amp;"; - case '<' => - yield "&lt;"; - case '>' => - yield "&gt;"; - case '"' => - yield "&quot;"; - case '\'' => - yield "&apos;"; - case => - yield strings::fromutf8(utf8::encoderune(rn))!; - })?; - }; - }; - return z; -}; - -@test fn html_escape() void = { - let sink = memio::dynamic(); - defer io::close(&sink)!; - html_escape(&sink, "hello world!")!; - assert(memio::string(&sink)! == "hello world!"); - - let sink = memio::dynamic(); - defer io::close(&sink)!; - html_escape(&sink, "\"hello world!\"")!; - assert(memio::string(&sink)! == "&quot;hello world!&quot;"); - - let sink = memio::dynamic(); - defer io::close(&sink)!; - html_escape(&sink, "<hello & 'world'!>")!; - assert(memio::string(&sink)! == "&lt;hello &amp; &apos;world&apos;!&gt;"); -}; - -// Formats output as HTML -fn emit_html(ctx: *context) (void | error) = { - const decls = ctx.summary; - const ident = unparse::identstr(ctx.ident); - defer free(ident); - - if (ctx.template) head(ctx.ident)?; - - if (len(ident) == 0) { - fmt::fprintf(ctx.out, "<h2>The Hare standard library <span class='heading-extra'>")?; - } else { - fmt::fprintf(ctx.out, "<h2><span class='heading-body'>{}</span><span class='heading-extra'>", ident)?; - }; - for (let i = 0z; i < len(ctx.tags); i += 1) { - const mode = switch (ctx.tags[i].mode) { - case module::tag_mode::INCLUSIVE => - yield '+'; - case module::tag_mode::EXCLUSIVE => - yield '-'; - }; - fmt::fprintf(ctx.out, "{}{} ", mode, ctx.tags[i].name)?; - }; - fmt::fprintln(ctx.out, "</span></h2>")?; - - match (ctx.readme) { - case void => void; - case let f: io::file => - fmt::fprintln(ctx.out, "<div class='readme'>")?; - markup_html(ctx, f)?; - fmt::fprintln(ctx.out, "</div>")?; - }; - - let identpath = module::identpath(ctx.ident); - defer free(identpath); - - let submodules: []str = []; - defer free(submodules); - - for (let i = 0z; i < len(ctx.version.subdirs); i += 1) { - let dir = ctx.version.subdirs[i]; - // XXX: the list of reserved directory names is not yet - // finalized. See https://todo.sr.ht/~sircmpwn/hare/516 - if (dir == "contrib") continue; - if (dir == "cmd") continue; - if (dir == "docs") continue; - if (dir == "ext") continue; - if (dir == "vendor") continue; - if (dir == "scripts") continue; - - let submod = [identpath, dir]: ast::ident; - if (module::lookup(ctx.mctx, submod) is module::error) { - continue; - }; - - append(submodules, dir); - }; - - if (len(submodules) != 0) { - if (len(ctx.ident) == 0) { - fmt::fprintln(ctx.out, "<h3>Modules</h3>")?; - } else { - fmt::fprintln(ctx.out, "<h3>Submodules</h3>")?; - }; - fmt::fprintln(ctx.out, "<ul class='submodules'>")?; - for (let i = 0z; i < len(submodules); i += 1) { - let submodule = submodules[i]; - let path = path::init("/", identpath, submodule)!; - - fmt::fprintf(ctx.out, "<li><a href='")?; - html_escape(ctx.out, path::string(&path))?; - fmt::fprintf(ctx.out, "'>")?; - html_escape(ctx.out, submodule)?; - fmt::fprintfln(ctx.out, "</a></li>")?; - }; - fmt::fprintln(ctx.out, "</ul>")?; - }; - - if (len(decls.types) == 0 - && len(decls.errors) == 0 - && len(decls.constants) == 0 - && len(decls.globals) == 0 - && len(decls.funcs) == 0) { - return; - }; - - fmt::fprintln(ctx.out, "<h3>Index</h3>")?; - tocentries(ctx.out, decls.types, "Types", "types")?; - tocentries(ctx.out, decls.errors, "Errors", "Errors")?; - tocentries(ctx.out, decls.constants, "Constants", "constants")?; - tocentries(ctx.out, decls.globals, "Globals", "globals")?; - tocentries(ctx.out, decls.funcs, "Functions", "functions")?; - - if (len(decls.types) != 0) { - fmt::fprintln(ctx.out, "<h3>Types</h3>")?; - for (let i = 0z; i < len(decls.types); i += 1) { - details(ctx, decls.types[i])?; - }; - }; - - if (len(decls.errors) != 0) { - fmt::fprintln(ctx.out, "<h3>Errors</h3>")?; - for (let i = 0z; i < len(decls.errors); i += 1) { - details(ctx, decls.errors[i])?; - }; - }; - - if (len(decls.constants) != 0) { - fmt::fprintln(ctx.out, "<h3>Constants</h3>")?; - for (let i = 0z; i < len(decls.constants); i += 1) { - details(ctx, decls.constants[i])?; - }; - }; - - if (len(decls.globals) != 0) { - fmt::fprintln(ctx.out, "<h3>Globals</h3>")?; - for (let i = 0z; i < len(decls.globals); i += 1) { - details(ctx, decls.globals[i])?; - }; - }; - - if (len(decls.funcs) != 0) { - fmt::fprintln(ctx.out, "<h3>Functions</h3>")?; - for (let i = 0z; i < len(decls.funcs); i += 1) { - details(ctx, decls.funcs[i])?; - }; - }; -}; - -fn comment_html(out: io::handle, s: str) (size | io::error) = { - // TODO: handle [[references]] - let z = fmt::fprint(out, "<span class='comment'>//")?; - z += html_escape(out, s)?; - z += fmt::fprint(out, "</span><br>")?; - return z; -}; - -fn docs_html(out: io::handle, s: str, indent: size) (size | io::error) = { - const iter = strings::tokenize(s, "\n"); - let z = 0z; - for (true) match (strings::next_token(&iter)) { - case let s: str => - if (!(strings::peek_token(&iter) is void)) { - z += comment_html(out, s)?; - for (let i = 0z; i < indent; i += 1) { - z += fmt::fprint(out, "\t")?; - }; - }; - case void => break; - }; - - return z; -}; - -fn tocentries( - out: io::handle, - decls: []ast::decl, - name: str, - lname: str, -) (void | error) = { - if (len(decls) == 0) { - return; - }; - fmt::fprintfln(out, "<h4>{}</h4>", name)?; - fmt::fprintln(out, "<pre>")?; - let undoc = false; - for (let i = 0z; i < len(decls); i += 1) { - if (!undoc && decls[i].docs == "") { - fmt::fprintfln( - out, - "{}<span class='comment'>// Undocumented {}:</span>", - if (i == 0) "" else "\n", - lname)?; - undoc = true; - }; - tocentry(out, decls[i])?; - }; - fmt::fprint(out, "</pre>")?; - return; -}; - -fn tocentry(out: io::handle, decl: ast::decl) (void | error) = { - fmt::fprintf(out, "{} ", - match (decl.decl) { - case ast::decl_func => - yield "fn"; - case []ast::decl_type => - yield "type"; - case []ast::decl_const => - yield "const"; - case []ast::decl_global => - yield "let"; - })?; - fmt::fprintf(out, "<a href='#")?; - unparse::ident(out, decl_ident(decl))?; - fmt::fprintf(out, "'>")?; - unparse::ident(out, decl_ident(decl))?; - fmt::fprint(out, "</a>")?; - - match (decl.decl) { - case let t: []ast::decl_type => void; - case let g: []ast::decl_global => - let g = g[0]; - match (g._type) { - case null => - yield; - case let ty: *ast::_type => - fmt::fprint(out, ": ")?; - type_html(out, 0, *ty, true)?; - }; - case let c: []ast::decl_const => - let c = c[0]; - match (c._type) { - case null => - yield; - case let ty: *ast::_type => - fmt::fprint(out, ": ")?; - type_html(out, 0, *ty, true)?; - }; - case let f: ast::decl_func => - prototype_html(out, 0, - f.prototype.repr as ast::func_type, - true)?; - }; - fmt::fprintln(out, ";")?; - return; -}; - -fn details(ctx: *context, decl: ast::decl) (void | error) = { - fmt::fprintln(ctx.out, "<section class='member'>")?; - fmt::fprint(ctx.out, "<h4 id='")?; - unparse::ident(ctx.out, decl_ident(decl))?; - fmt::fprint(ctx.out, "'><span class='heading-body'>")?; - fmt::fprintf(ctx.out, "{} ", match (decl.decl) { - case ast::decl_func => - yield "fn"; - case []ast::decl_type => - yield "type"; - case []ast::decl_const => - yield "def"; - case []ast::decl_global => - yield "let"; - })?; - unparse::ident(ctx.out, decl_ident(decl))?; - // TODO: Add source URL - fmt::fprint(ctx.out, "</span><span class='heading-extra'><a href='#")?; - unparse::ident(ctx.out, decl_ident(decl))?; - fmt::fprint(ctx.out, "'>[link]</a> - </span>")?; - fmt::fprintln(ctx.out, "</h4>")?; - - if (len(decl.docs) == 0) { - fmt::fprintln(ctx.out, "<details>")?; - fmt::fprintln(ctx.out, "<summary>Show undocumented member</summary>")?; - }; - - fmt::fprintln(ctx.out, "<pre class='decl'>")?; - unparse_html(ctx.out, decl)?; - fmt::fprintln(ctx.out, "</pre>")?; - - if (len(decl.docs) != 0) { - const trimmed = trim_comment(decl.docs); - defer free(trimmed); - const buf = strings::toutf8(trimmed); - markup_html(ctx, &memio::fixed(buf))?; - } else { - fmt::fprintln(ctx.out, "</details>")?; - }; - - fmt::fprintln(ctx.out, "</section>")?; - return; -}; - -fn htmlref(ctx: *context, ref: ast::ident) (void | io::error) = { - const ik = - match (resolve(ctx, ref)) { - case let ik: (ast::ident, symkind) => - yield ik; - case void => - const ident = unparse::identstr(ref); - fmt::errorfln("Warning: Unresolved reference: {}", ident)?; - fmt::fprintf(ctx.out, "<a href='#' " - "class='ref invalid' " - "title='This reference could not be found'>{}</a>", - ident)?; - free(ident); - return; - }; - - // TODO: The reference is not necessarily in the stdlib - const kind = ik.1, id = ik.0; - const ident = unparse::identstr(id); - switch (kind) { - case symkind::LOCAL => - fmt::fprintf(ctx.out, "<a href='#{0}' class='ref'>{0}</a>", ident)?; - case symkind::MODULE => - let ipath = module::identpath(id); - defer free(ipath); - fmt::fprintf(ctx.out, "<a href='/{}' class='ref'>{}</a>", - ipath, ident)?; - case symkind::SYMBOL => - let ipath = module::identpath(id[..len(id) - 1]); - defer free(ipath); - fmt::fprintf(ctx.out, "<a href='/{}#{}' class='ref'>{}</a>", - ipath, id[len(id) - 1], ident)?; - case symkind::ENUM_LOCAL => - fmt::fprintf(ctx.out, "<a href='#{}' class='ref'>{}</a>", - id[len(id) - 2], ident)?; - case symkind::ENUM_REMOTE => - let ipath = module::identpath(id[..len(id) - 2]); - defer free(ipath); - fmt::fprintf(ctx.out, "<a href='/{}#{}' class='ref'>{}</a>", - ipath, id[len(id) - 2], ident)?; - }; - free(ident); -}; - -fn markup_html(ctx: *context, in: io::handle) (void | io::error) = { - let parser = parsedoc(in); - let waslist = false; - for (true) { - const tok = match (scandoc(&parser)) { - case void => - if (waslist) { - fmt::fprintln(ctx.out, "</ul>")?; - }; - break; - case let tok: token => - yield tok; - }; - match (tok) { - case paragraph => - if (waslist) { - fmt::fprintln(ctx.out, "</ul>")?; - waslist = false; - }; - fmt::fprintln(ctx.out)?; - fmt::fprint(ctx.out, "<p>")?; - case let tx: text => - defer free(tx); - match (uri::parse(strings::trim(tx))) { - case let uri: uri::uri => - defer uri::finish(&uri); - if (uri.host is net::ip::addr || len(uri.host as str) > 0) { - fmt::fprint(ctx.out, "<a rel='nofollow noopener' href='")?; - uri::fmt(ctx.out, &uri)?; - fmt::fprint(ctx.out, "'>")?; - html_escape(ctx.out, tx)?; - fmt::fprint(ctx.out, "</a>")?; - } else { - html_escape(ctx.out, tx)?; - }; - case uri::invalid => - html_escape(ctx.out, tx)?; - }; - case let re: reference => - htmlref(ctx, re)?; - case let sa: sample => - if (waslist) { - fmt::fprintln(ctx.out, "</ul>")?; - waslist = false; - }; - fmt::fprint(ctx.out, "<pre class='sample'>")?; - html_escape(ctx.out, sa)?; - fmt::fprint(ctx.out, "</pre>")?; - free(sa); - case listitem => - if (!waslist) { - fmt::fprintln(ctx.out, "<ul>")?; - waslist = true; - }; - fmt::fprint(ctx.out, "<li>")?; - }; - }; - fmt::fprintln(ctx.out)?; - return; -}; - -// Forked from [[hare::unparse]] -fn unparse_html(out: io::handle, d: ast::decl) (size | io::error) = { - let n = 0z; - match (d.decl) { - case let c: []ast::decl_const => - n += fmt::fprintf(out, "<span class='keyword'>def</span> ")?; - for (let i = 0z; i < len(c); i += 1) { - n += unparse::ident(out, c[i].ident)?; - match (c[i]._type) { - case null => - yield; - case let ty: *ast::_type => - n += fmt::fprint(out, ": ")?; - n += type_html(out, 0, *ty, false)?; - }; - if (i + 1 < len(c)) { - n += fmt::fprint(out, ", ")?; - }; - }; - case let g: []ast::decl_global => - n += fmt::fprintf(out, "<span class='keyword'>{}</span>", - if (g[0].is_const) "const " else "let ")?; - for (let i = 0z; i < len(g); i += 1) { - n += unparse::ident(out, g[i].ident)?; - match (g[i]._type) { - case null => - yield; - case let ty: *ast::_type => - n += fmt::fprint(out, ": ")?; - n += type_html(out, 0, *ty, false)?; - }; - if (i + 1 < len(g)) { - n += fmt::fprint(out, ", ")?; - }; - }; - case let t: []ast::decl_type => - n += fmt::fprint(out, "<span class='keyword'>type</span> ")?; - for (let i = 0z; i < len(t); i += 1) { - n += unparse::ident(out, t[i].ident)?; - n += fmt::fprint(out, " = ")?; - n += type_html(out, 0, t[i]._type, false)?; - if (i + 1 < len(t)) { - n += fmt::fprint(out, ", ")?; - }; - }; - case let f: ast::decl_func => - n += fmt::fprint(out, switch (f.attrs) { - case ast::fndecl_attrs::NONE => - yield ""; - case ast::fndecl_attrs::FINI => - yield "@fini "; - case ast::fndecl_attrs::INIT => - yield "@init "; - case ast::fndecl_attrs::TEST => - yield "@test "; - })?; - let p = f.prototype.repr as ast::func_type; - n += fmt::fprint(out, "<span class='keyword'>fn</span> ")?; - n += unparse::ident(out, f.ident)?; - n += prototype_html(out, 0, - f.prototype.repr as ast::func_type, - false)?; - }; - n += fmt::fprint(out, ";")?; - return n; -}; - -fn enum_html( - out: io::handle, - indent: size, - t: ast::enum_type -) (size | io::error) = { - let z = 0z; - - z += fmt::fprint(out, "<span class='type'>enum</span> ")?; - if (t.storage != ast::builtin_type::INT) { - z += fmt::fprintf(out, "<span class='type'>{}</span> ", - unparse::builtin_type(t.storage))?; - }; - z += fmt::fprintln(out, "{")?; - indent += 1; - for (let i = 0z; i < len(t.values); i += 1) { - for (let i = 0z; i < indent; i += 1) { - z += fmt::fprint(out, "\t")?; - }; - const val = t.values[i]; - let wrotedocs = false; - if (val.docs != "") { - // Check if comment should go above or next to field - if (multiline_comment(val.docs)) { - z += docs_html(out, val.docs, indent)?; - wrotedocs = true; - }; - }; - - z += fmt::fprint(out, val.name)?; - - match (val.value) { - case null => void; - case let expr: *ast::expr => - z += fmt::fprint(out, " = ")?; - z += unparse::expr(out, indent, *expr)?; - }; - - z += fmt::fprint(out, ",")?; - - if (val.docs != "" && !wrotedocs) { - z += fmt::fprint(out, " ")?; - z += docs_html(out, val.docs, 0)?; - } else { - z += fmt::fprintln(out)?; - }; - }; - indent -= 1; - for (let i = 0z; i < indent; i += 1) { - z += fmt::fprint(out, "\t")?; - }; - z += newline(out, indent)?; - z += fmt::fprint(out, "}")?; - return z; -}; - -fn struct_union_html( - out: io::handle, - indent: size, - t: ast::_type, - brief: bool, -) (size | io::error) = { - let z = 0z; - let members = match (t.repr) { - case let t: ast::struct_type => - z += fmt::fprint(out, "<span class='keyword'>struct</span>")?; - if (t.packed) { - z += fmt::fprint(out, " @packed")?; - }; - z += fmt::fprint(out, " {")?; - yield t.members: []ast::struct_member; - case let t: ast::union_type => - z += fmt::fprint(out, "<span class='keyword'>union</span> {")?; - yield t: []ast::struct_member; - }; - - indent += 1; - for (let i = 0z; i < len(members); i += 1) { - const member = members[i]; - - z += newline(out, indent)?; - if (member.docs != "" && !brief) { - z += docs_html(out, member.docs, indent)?; - }; - match (member._offset) { - case null => void; - case let expr: *ast::expr => - z += fmt::fprint(out, "@offset(")?; - z += unparse::expr(out, indent, *expr)?; - z += fmt::fprint(out, ") ")?; - }; - - match (member.member) { - case let f: ast::struct_field => - z += fmt::fprintf(out, "{}: ", f.name)?; - z += type_html(out, indent, *f._type, brief)?; - case let embed: ast::struct_embedded => - z += type_html(out, indent, *embed, brief)?; - case let indent: ast::struct_alias => - z += unparse::ident(out, indent)?; - }; - z += fmt::fprint(out, ",")?; - }; - - indent -= 1; - z += newline(out, indent)?; - z += fmt::fprint(out, "}")?; - - return z; -}; - -fn type_html( - out: io::handle, - indent: size, - _type: ast::_type, - brief: bool, -) (size | io::error) = { - if (brief) { - let buf = memio::dynamic(); - defer io::close(&buf)!; - unparse::_type(&buf, indent, _type)?; - return html_escape(out, memio::string(&buf)!)?; - }; - - // TODO: More detailed formatter which can find aliases nested deeper in - // other types and highlight more keywords, like const - let z = 0z; - - if (_type.flags & ast::type_flag::CONST != 0 - && !(_type.repr is ast::func_type)) { - z += fmt::fprint(out, "<span class='keyword'>const</span> ")?; - }; - - if (_type.flags & ast::type_flag::ERROR != 0) { - if (_type.repr is ast::builtin_type) { - z += fmt::fprint(out, "<span class='type'>!</span>")?; - } else { - z += fmt::fprint(out, "!")?; - }; - }; - - match (_type.repr) { - case let a: ast::alias_type => - if (a.unwrap) { - z += fmt::fprint(out, "...")?; - }; - z += unparse::ident(out, a.ident)?; - case let t: ast::builtin_type => - z += fmt::fprintf(out, "<span class='type'>{}</span>", - unparse::builtin_type(t))?; - case let t: ast::tagged_type => - // rough estimate of current line length - let linelen: size = z + (indent + 1) * 8; - z = 0; - linelen += fmt::fprint(out, "(")?; - for (let i = 0z; i < len(t); i += 1) { - linelen += type_html(out, indent, *t[i], brief)?; - if (i + 1 == len(t)) break; - linelen += fmt::fprint(out, " |")?; - // use 72 instead of 80 to give a bit of leeway for long - // type names - if (linelen > 72) { - z += linelen; - linelen = (indent + 1) * 8; - z += fmt::fprintln(out)?; - for (let i = 0z; i < indent; i += 1) { - z += fmt::fprint(out, "\t")?; - }; - } else { - linelen += fmt::fprint(out, " ")?; - }; - }; - z += linelen; - z += fmt::fprint(out, ")")?; - case let t: ast::tuple_type => - // rough estimate of current line length - let linelen: size = z + (indent + 1) * 8; - z = 0; - linelen += fmt::fprint(out, "(")?; - for (let i = 0z; i < len(t); i += 1) { - linelen += type_html(out, indent, *t[i], brief)?; - if (i + 1 == len(t)) break; - linelen += fmt::fprint(out, ",")?; - // use 72 instead of 80 to give a bit of leeway for long - // type names - if (linelen > 72) { - z += linelen; - linelen = (indent + 1) * 8; - z += fmt::fprintln(out)?; - for (let i = 0z; i < indent; i += 1) { - z += fmt::fprint(out, "\t")?; - }; - } else { - linelen += fmt::fprint(out, " ")?; - }; - }; - z += linelen; - z += fmt::fprint(out, ")")?; - case let t: ast::pointer_type => - if (t.flags & ast::pointer_flag::NULLABLE != 0) { - z += fmt::fprint(out, "<span class='type'>nullable</span> ")?; - }; - z += fmt::fprint(out, "*")?; - z += type_html(out, indent, *t.referent, brief)?; - case let t: ast::func_type => - z += fmt::fprint(out, "<span class='keyword'>fn</span>(")?; - for (let i = 0z; i < len(t.params); i += 1) { - const param = t.params[i]; - z += fmt::fprintf(out, "{}: ", - if (len(param.name) == 0) "_" else param.name)?; - z += type_html(out, indent, *param._type, brief)?; - - if (i + 1 == len(t.params) - && t.variadism == ast::variadism::HARE) { - // TODO: Highlight that as well - z += fmt::fprint(out, "...")?; - }; - if (i + 1 < len(t.params)) { - z += fmt::fprint(out, ", ")?; - }; - }; - if (t.variadism == ast::variadism::C) { - z += fmt::fprint(out, ", ...")?; - }; - z += fmt::fprint(out, ") ")?; - z += type_html(out, indent, *t.result, brief)?; - case let t: ast::enum_type => - z += enum_html(out, indent, t)?; - case let t: ast::list_type => - z += fmt::fprint(out, "[")?; - match (t.length) { - case let expr: *ast::expr => - z += unparse::expr(out, indent, *expr)?; - case ast::len_slice => - z += 0; - case ast::len_unbounded => - z += fmt::fprintf(out, "*")?; - case ast::len_contextual => - z += fmt::fprintf(out, "_")?; - }; - z += fmt::fprint(out, "]")?; - - z += type_html(out, indent, *t.members, brief)?; - case let t: ast::struct_type => - z += struct_union_html(out, indent, _type, brief)?; - case let t: ast::union_type => - z += struct_union_html(out, indent, _type, brief)?; - }; - - return z; -}; - -fn prototype_html( - out: io::handle, - indent: size, - t: ast::func_type, - brief: bool, -) (size | io::error) = { - let n = 0z; - n += fmt::fprint(out, "(")?; - - // estimate length of prototype to determine if it should span multiple - // lines - const linelen = if (len(t.params) == 0 || brief) { - yield 0z; // If no parameters or brief, only use one line. - } else { - let linelen = indent * 8 + 5; - linelen += if (len(t.params) != 0) len(t.params) * 3 - 1 else 0; - for (let i = 0z; i < len(t.params); i += 1) { - const param = t.params[i]; - linelen += unparse::_type(io::empty, indent, - *param._type)?; - linelen += if (param.name == "") 1 else len(param.name); - }; - switch (t.variadism) { - case variadism::NONE => void; - case variadism::HARE => - linelen += 3; - case variadism::C => - linelen += 5; - }; - linelen += unparse::_type(io::empty, indent, *t.result)?; - yield linelen; - }; - - // use 72 instead of 80 to give a bit of leeway for preceding text - if (linelen > 72) { - indent += 1; - for (let i = 0z; i < len(t.params); i += 1) { - const param = t.params[i]; - n += newline(out, indent)?; - n += fmt::fprintf(out, "{}: ", - if (param.name == "") "_" else param.name)?; - n += type_html(out, indent, *param._type, brief)?; - if (i + 1 == len(t.params) - && t.variadism == variadism::HARE) { - n += fmt::fprint(out, "...")?; - } else { - n += fmt::fprint(out, ",")?; - }; - }; - if (t.variadism == variadism::C) { - n += newline(out, indent)?; - n += fmt::fprint(out, "...")?; - }; - indent -= 1; - n += newline(out, indent)?; - } else for (let i = 0z; i < len(t.params); i += 1) { - const param = t.params[i]; - if (!brief) { - n += fmt::fprintf(out, "{}: ", - if (param.name == "") "_" else param.name)?; - }; - n += type_html(out, indent, *param._type, brief)?; - if (i + 1 == len(t.params)) { - switch (t.variadism) { - case variadism::NONE => void; - case variadism::HARE => - n += fmt::fprint(out, "...")?; - case variadism::C => - n += fmt::fprint(out, ", ...")?; - }; - } else { - n += fmt::fprint(out, ", ")?; - }; - }; - - n += fmt::fprint(out, ") ")?; - n += type_html(out, indent, *t.result, brief)?; - return n; -}; - -fn breadcrumb(ident: ast::ident) str = { - if (len(ident) == 0) { - return ""; - }; - let buf = memio::dynamic(); - fmt::fprintf(&buf, "<a href='/'>stdlib</a> » ")!; - for (let i = 0z; i < len(ident) - 1; i += 1) { - let ipath = module::identpath(ident[..i+1]); - defer free(ipath); - fmt::fprintf(&buf, "<a href='/{}'>{}</a>::", ipath, ident[i])!; - }; - fmt::fprint(&buf, ident[len(ident) - 1])!; - return memio::string(&buf)!; -}; - -const harriet_b64 = "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEAAQMAAABmvDolAAAABlBMVEUAAAD///+l2Z/dAAAK40lEQVRo3u3ZX2xb1R0H8O/NzWIXXGw0xILa1QE6Wk0gMspIESU3WSf2sD/wODFtpFC1Q1Ob0AJpacm5pYVUAxHENK2IUiONaQ/TBIjRFKXNvSHbijSDeaGja5vr/ovHlmIHQ66de+/57iF27Gv7um8TD/glUvzROb9z7jnnnp9/4GU++Ap8iYEeJ6EFA9k9SSlGgkFRFiizs8HgPKWQ33ZFIEgZjiYNSwsECTpxaViJQKDRSUnDSgUBKcjN0mAmEJAclAbtIOCRhiMNOkHAIVl0DRaDQJ6k5xr0gkCGpOuRbhDIkvzUWwi2IbBI8smF4TYEr5C0nzTIIGCQ5N1NgEbaPGaUZD2QgvKw0QxYzviJkSbAZXH8RPQVozSceuDROzw3ciYYFOkdPhE9YxhBwOGlwydGThtkqjHIk/98fOT06wtz3hBMnfh85HTWCAI2p6a+ME7zWCCQU3MfaUkRDBzL/mg0Sa8JcE4Mz/DY4rKui+HTY/cPz9AIBHJm6onhGVbWfS2Yn7F+uXfGYBD4wnGtGXVmLBjwsf5jTYHzpHdUvTDmBYGMw0tT6ucMBLZjfPoLpRnwjLmtvV+UNmlj8Piu3lwzQHu0N5cNBpLj+d5cfxOQH8/3FrYGgrx0lrX3Ok3BA2sVZyttJ2hVe8faFSdqB4F5/vxgu+JodnALYupfitMVDJytcgeKg8HAE3NCKTIQFN1B3tLrBc+k5261blG814OBXOFs6PX+3AREt3T0en8IBC6fvXSkpwmQ3P+1I/DeDgbyvbaP4R02AsFQsu09eIezweCvLWl41wZ2QbFR7YOL/mAwrXYoLoQVBLRzSidcPHkmCBj58Atw9WYA+hVyYksgSMzq5hXy4mNeICjqPbfKt78VAKy0dQQ9Qj59q5dvCEw9dQTKqNy7rL/h7i704d6j92FU/vpUAFASWbcdo+5Tp37VECRDzLirO+ha0tncALjZEWYkbqZNOr0NwPMik7MlHpMqKU+JepDRisxLXcuuIjnfANAaYp77jPxxkvP1XbjMWymHfzOOkqTM1gE5tDszeZKTTqpyD/ABzU7EeZI/c/OlC1Ut0Heet5hkf+nqkKkFxYnu3eQFitIrM1ULXHXEIrtZvsX9o66LUJ7kIWGUl1YtONS2m6RVvnn018XwaUgzFq4gJMl7a+fBLWzXFi8xpKx7+7vKzkTV8Pm7uqm23Or5YflaWwGmRkpt8WKRzdUAZ2+CVTEwNVcDCshmSBbKozhlCz+QLYP+N4et+UEiGr8MqAyAJHnRNmrmYeFPjo7hhkh6dqImhoWYCnSttEKymI/7QenZHBC2MCFIJ+cH7vWh0hulaOjQyHyhBnA2J0qPCUiQLERrpnrhmnsjbQGkGgFOkuQGOoSSqQcFU3guKQfpEWq+UQvqYlcLYHe0wRF0Xi63KKA69eB8QewhKc/atKAWSTkV8oHptigpzjJDsiHI2iRlnHGSUM6SHPWDUCFO0hWuQwJnSXK4QZAhFklCyZHMTtQsOS1TTkAAk+R/0z7wXKE9SroicxepK30knVkfWJfTSA5TdgvqAEk+EphnLYC5og8sbJOikAnSRIcgDbfhkpvuFjQBksd8QGrnF9bDlCDTCzF4vhbS0btJyqhkGVg1XZiCLh1mk2QOSiOgCZK0EinmECI55wOumCApGKVGuojXpdXF82nBAj/jXJykSZIc93WRSpPZImfnKhn3UX8MWZKajEoxXJVyVc3D1bl1dEnK7ZWLgC+G4lmNGdKtJLsUogpkmNNIg5PFFP0HwuKSm3U1Kcj8Sbsq/a2AwkAhcjxPSnGS5AdDlSjL4KGCUGjxrPy6IA++X3m+JZDrWtGmUmPc0wW5653Kdi+B9+QTK65ySTomKe3Buqn+GH1sd0hy4pAopWludQyzs89SJWWeE4mEb42VgwzFB6OC71BLrvEfayWQTu+IjguSorCqvIonq8Fes88qkJTiXLQExNPVIIdn4ueNcSbsd5eX/qP5DpBcy4pdz4id7LIPvVSKasVSXwybhrpyMs+u7FgpSDeyonqYE+qOyKRhc0vq/KrSeYru6mHGQvqy5zWXD2eT58pXD9+CGVCe6Sp0F+mIk/tLQLd9jxvron13k/Pisx2bSQ6Se3y7G+jsTgtSWnO59eT0JsG9ftDy6t05Usoxt0+1eCaZ5/BMFZDX5/Zft50Guf1IUknQGctyOFsNHppc3k5q5ODR0xtesmgbHPY9rLASW8LufjLjHei7K0GSz6+qbgFQVVd+YGezfCO55i2SfP4bVcDtiUVDnzCZGSuy80N1jSD53APVLehYHprUilk6o30vYns/OWreWh2Drq4N/Z351Jzd/8lhbN9iFV80Vf9ErR/RN9uJS/Lk2ZVQt1jFF+F7Lb6GNjUseNcu74WdK6EsPbmhBuiIqLGhoW27jNc6f4QYPn5Yb/G9L0yoz9y+Q5um6OgMAzjQgw5fC0/hytbIfSJJ66ftMewDwi1+cAhAGKnTjpErgxt94ICC5P1IFB0ndxuwD51hfMe3qtMK0vcpY/mxvHsH8BpiUGK+Fs6hZf/tapfdPchHASAGxHwtJDG8dvW1m4aG7uWjVwKIdaDFdwwWwti+ujU5ZU9l3CvQis4OoLoFcwB9Pwg/95KVOTPtXnFtK2JA9UxaPAdErx75zcvZ7PuFZS9CeQFQfCfMtBJbtmd4zctZeebUZh2qDiylf3cPqOqPeVf/7lOntqQBYKleHaQZ7klfhYfHh7bSeXkBRNZXgJzk7B59+bYfjouZFOc/eVAHYuH1vi7yKmLusrHBS2c4/5/vmUA7enyb92ALsFvt9C6+YnXMf9iDcASoasHFughwce+A4DtjFz42gchN1UCSbjuU48MDXXTeenyFiWtaWxTf+WBe1Qn1gz8ORBXnjjvu+FAHdGWv/5XUgfg+uTEykX+8bTSnA1AmfaO4qgdxTF1QzOOb2kZzaQAIVQNTAlAOXlInRnY/txJpAFCrQI4EoPxll/ryN9cl0ToBILykugVXjQHKd3/zoLZ07brV6AEQifsv3jrQsnlV34qlHdcsQw+A1hpgAh33bOu7xnsVoRvuaQDSQF9ywOwUb6DtBgDlFbe4HtJAZP/GyevFm0BLKwD4Uhg9WgCWHvj++o7Nb4aBlXWAhQFgyXVt2LRV+RMQ2wfAly2avx8A2te0tGzdqBLAPsRUzR/kNHD1bcAHSdhHAACqUQ3+jVbgxptiiCTx26M9PQCW1CRBLvBgayewBPvWnTYbAJq4R9GBPdBv9kwsbovF7a+aiAA9APSbb+kB4E+rcypNlD+RJX2PhDFY04UEAHQCQCT8RC68WKAozaQOFwAGVCAGbBtoDWk1LZh7dQA/ARCLoBPoqgEXoOrlGJZMdgJd9T+qL4Lw5FqgvjyR6yx9H8O7nQtJTPX7oh2YXRynuXi8+LrIl/sIm8CVhXjtPOjKCwCANvQAWBatbcEk3ygBLJ5w/nv1qy2ofKxa4CLqjFS+v7Nxqait/L268/N4I7Cp9H1L4s7F3NgHZjoA4KbtaqXM41tyiAMApgejlV+Ka/KLtLq8e9806ZlqQLFJ04xsk4IXECIzx11EgytiBUCp/OofWFMbaQ4KVRW1WpCGIuaDg6waXLYBSFdin2v0uCcqOyhqNAkSomllMK01Lx2evUxt8enLFB8roeXizae6Os2qBwXEm9U302heANUvUyEd/n9Vac3mwFW+qlZ/WcH/ADT9vVqjZ2RdAAAAAElFTkSuQmCC"; - -fn head(ident: ast::ident) (void | error) = { - const id = unparse::identstr(ident); - defer free(id); - - let breadcrumb = breadcrumb(ident); - defer free(breadcrumb); - - const title = - if (len(id) == 0) - fmt::asprintf("Hare documentation") - else - fmt::asprintf("{} — Hare documentation", id); - defer free(title); - - // TODO: Move bits to +embed? - fmt::printfln("<!doctype html> -<html lang='en'> -<meta charset='utf-8' /> -<meta name='viewport' content='width=device-width, initial-scale=1' /> -<title>{}</title> -<link rel='icon' type='image/png' href='data:image/png;base64,{}'>", title, harriet_b64)?; - fmt::println("<style> -body { - font-family: sans-serif; - line-height: 1.3; - margin: 0 auto; - padding: 0 1rem; -} - -nav:not(#TableOfContents) { - max-width: calc(800px + 128px + 128px); - margin: 1rem auto 0; - display: grid; - grid-template-rows: auto auto 1fr; - grid-template-columns: auto 1fr; - grid-template-areas: - 'logo header' - 'logo nav' - 'logo none'; -} - -nav:not(#TableOfContents) img { - grid-area: logo; -} - -nav:not(#TableOfContents) h1 { - grid-area: header; - margin: 0; - padding: 0; -} - -nav:not(#TableOfContents) ul { - grid-area: nav; - margin: 0.5rem 0 0 0; - padding: 0; - list-style: none; - display: flex; - flex-direction: row; - justify-content: left; - flex-wrap: wrap; -} - -nav:not(#TableOfContents) li:not(:first-child) { - margin-left: 2rem; -} - -#TableOfContents { - font-size: 1.1rem; -} - -main { - padding: 0 128px; - max-width: 800px; - margin: 0 auto; - -} - -pre { - background-color: #eee; - padding: 0.25rem 1rem; - margin: 0 -1rem 1rem; - font-size: 1.2rem; - max-width: calc(100% + 1rem); - overflow-x: auto; -} - -pre .keyword { - color: #008; -} - -pre .type { - color: #44F; -} - -ol { - padding-left: 0; - list-style: none; -} - -ol li { - padding-left: 0; -} - -h2, h3, h4 { - display: flex; -} - -h3 { - border-bottom: 1px solid #ccc; - padding-bottom: 0.25rem; -} - -.invalid { - color: red; -} - -.heading-body { - word-wrap: anywhere; -} - -.heading-extra { - align-self: flex-end; - flex-grow: 1; - padding-left: 0.5rem; - text-align: right; - font-size: 0.8rem; - color: #444; -} - -h4:target + pre { - background: #ddf; -} - -details { - background: #eee; - margin: 1rem -1rem 1rem; -} - -summary { - cursor: pointer; - padding: 0.5rem 1rem; -} - -details pre { - margin: 0; -} - -.comment { - color: #000; - font-weight: bold; -} - -@media(max-width: 1000px) { - main { - padding: 0; - } -} - -@media(prefers-color-scheme: dark) { - body { - background: #121415; - color: #e1dfdc; - } - - img.mascot { - filter: invert(.92); - } - - a { - color: #78bef8; - } - - a:visited { - color: #48a7f5; - } - - summary { - background: #16191c; - } - - h3 { - border-bottom: solid #16191c; - } - - h4:target + pre { - background: #162329; - } - - pre { - background-color: #16191c; - } - - pre .keyword { - color: #69f; - } - - pre .type { - color: #3cf; - } - - .comment { - color: #fff; - } - - .heading-extra { - color: #9b9997; - } -} -</style>")?; - fmt::printfln("<nav> - <img src='data:image/png;base64,{}' - class='mascot' - alt='An inked drawing of the Hare mascot, a fuzzy rabbit' - width='128' height='128' /> - <h1>Hare documentation</h1> - <ul> - <li> - <a href='https://harelang.org'>Home</a> - </li>", harriet_b64)?; - fmt::printf("<li>{}</li>", breadcrumb)?; - fmt::print("</ul> -</nav> -<main>")?; - return; -}; diff --git a/cmd/haredoc/main.ha b/cmd/haredoc/main.ha @@ -3,6 +3,7 @@ // (c) 2021 Drew DeVault <sir@cmpwn.com> // (c) 2021 Ember Sawady <ecs@d2evs.net> // (c) 2022 Sebastian <sebastian@sebsite.pw> +use cmd::haredoc::doc; use fmt; use fs; use getopt; @@ -16,56 +17,53 @@ use memio; use os; use os::exec; use path; +use strconv; use strings; use unix::tty; -type format = enum { - HARE, - TTY, - HTML, -}; +const help: []getopt::help = [ + "reads and formats Hare documentation", + ('a', "show undocumented members (only applies to -Fhare and -Ftty)"), + ('t', "disable HTML template (requires postprocessing)"), + ('F', "format", "specify output format (hare, tty, or html)"), + ('T', "tagset", "set/unset build tags"), + "[identifiers...]", +]; -type context = struct { - mctx: *module::context, - ident: ast::ident, - tags: []module::tag, - version: module::version, - summary: summary, - format: format, - template: bool, - show_undocumented: bool, - readme: (io::file | void), - out: io::handle, - pager: (exec::process | void), +export fn main() void = { + const cmd = getopt::parse(os::args, help...); + defer getopt::finish(&cmd); + match (doc(os::args[0], &cmd)) { + case void => void; + case let e: doc::error => + fmt::fatal(doc::strerror(e)); + case let e: exec::error => + fmt::fatal(exec::strerror(e)); + case let e: fs::error => + fmt::fatal(fs::strerror(e)); + case let e: io::error => + fmt::fatal(io::strerror(e)); + case let e: module::error => + fmt::fatal(module::strerror(e)); + case let e: path::error => + fmt::fatal(path::strerror(e)); + case let e: parse::error => + fmt::fatal(parse::strerror(e)); + case let e: strconv::error => + fmt::fatal(strconv::strerror(e)); + }; }; -export fn main() void = { +fn doc(name: str, cmd: *getopt::command) (void | error) = { let fmt = if (tty::isatty(os::stdout_file)) { - yield format::TTY; + yield doc::format::TTY; } else { - yield format::HARE; + yield doc::format::HARE; }; let template = true; let show_undocumented = false; - let tags = match (default_tags()) { - case let t: []module::tag => - yield t; - case let err: exec::error => - fmt::fatal(strerror(err)); - }; - defer module::tags_free(tags); - - const help: [_]getopt::help = [ - "reads and formats Hare documentation", - ('F', "format", "specify output format (hare, tty, or html)"), - ('T', "tags...", "set build tags"), - ('X', "tags...", "unset build tags"), - ('a', "show undocumented members (only applies to -Fhare and -Ftty)"), - ('t', "disable HTML template (requires postprocessing)"), - "[identifiers...]", - ]; - const cmd = getopt::parse(os::args, help...); - defer getopt::finish(&cmd); + let tags: []str = default_tags()?; + defer free(tags); for (let i = 0z; i < len(cmd.opts); i += 1) { let opt = cmd.opts[i]; @@ -73,28 +71,16 @@ export fn main() void = { case 'F' => switch (opt.1) { case "hare" => - fmt = format::HARE; + fmt = doc::format::HARE; case "tty" => - fmt = format::TTY; + fmt = doc::format::TTY; case "html" => - fmt = format::HTML; + fmt = doc::format::HTML; case => fmt::fatal("Invalid format", opt.1); }; case 'T' => - tags = match (addtags(tags, opt.1)) { - case void => - fmt::fatal("Error parsing tags"); - case let t: []module::tag => - yield t; - }; - case 'X' => - tags = match (deltags(tags, opt.1)) { - case void => - fmt::fatal("Error parsing tags"); - case let t: []module::tag => - yield t; - }; + merge_tags(&tags, opt.1)?; case 't' => template = false; case 'a' => @@ -104,7 +90,7 @@ export fn main() void = { }; if (show_undocumented) switch (fmt) { - case format::HARE, format::TTY => void; + case doc::format::HARE, doc::format::TTY => void; case => fmt::fatal("Option -a must be used only with -Fhare or -Ftty"); }; @@ -112,51 +98,44 @@ export fn main() void = { let decls: []ast::decl = []; defer free(decls); - let ctx = module::context_init(tags, [], default_harepath()); - defer module::context_finish(&ctx); - - const id: ast::ident = - if (len(cmd.args) < 1) [] - else match (parseident(cmd.args[0])) { - case let err: parse::error => - fmt::fatal(parse::strerror(err)); - case let id: ast::ident => - yield id; - }; + let ctx = module::context { + harepath = harepath(), + harecache = harecache(), + tags = tags, + }; let decl = ""; - let dirname: ast::ident = if (len(id) < 2) [] else id[..len(id) - 1]; - const version = match (module::lookup(&ctx, id)) { - case let ver: module::version => - yield ver; - case let err: module::error => - yield match (module::lookup(&ctx, dirname)) { - case let ver: module::version => - assert(len(id) >= 1); - decl = id[len(id) - 1]; - yield ver; - case let err: module::error => - fmt::fatal("Error scanning input module:", - module::strerror(err)); + let (modpath, srcs, id) = if (len(cmd.args) == 0) { + let (modpath, srcs) = module::find(&ctx, []: ast::ident)?; + yield (modpath, srcs, []: ast::ident); + } else match (parseident(cmd.args[0])) { + case let id: ast::ident => + // first assume it's a module + yield match (module::find(&ctx, id)) { + case let r: (str, module::srcset) => + yield (r.0, r.1, id); + case let e: module::error => + module::finish_error(e); + // then assume it's an ident inside a module + decl = id[len(id)-1]; + id = id[..len(id)-1]; + let (modpath, srcs) = module::find(&ctx, id)?; + yield (modpath, srcs, id); }; + case => + let buf = path::buffer { ... }; + path::set(&buf, cmd.args[0])?; + let (modpath, srcs) = module::find(&ctx, &buf)?; + yield (modpath, srcs, []: ast::ident); }; - for (let i = 0z; i < len(version.inputs); i += 1) { - const in = version.inputs[i]; - const ext = path::peek_ext(&path::init(in.path)!); - if (ext is void || ext as str != "ha") { - continue; - }; - match (scan(in.path)) { - case let u: ast::subunit => - ast::imports_finish(u.imports); - append(decls, u.decls...); - case let err: error => - fmt::fatal("Error:", strerror(err)); - }; + for (let i = 0z; i < len(srcs.ha); i += 1) { + let u = doc::scan(srcs.ha[i])?; + ast::imports_finish(u.imports); + append(decls, u.decls...); }; - const rpath = path::init(version.basedir, "README")!; + const rpath = path::init(modpath, "README")!; const readme: (io::file | void) = if (decl == "") { yield match (os::open(path::string(&rpath))) { case let err: fs::error => @@ -183,7 +162,7 @@ export fn main() void = { }; if (len(new) == 0) { fmt::fatalf("Could not find {}::{}", - unparse::identstr(dirname), decl); + unparse::identstr(id), decl); }; free(decls); decls = new; @@ -195,12 +174,14 @@ export fn main() void = { ast::decl_finish(decls[i]); }; - const ctx = context { + const ctx = doc::context { mctx = &ctx, ident = id, tags = tags, - version = version, - summary = sort_decls(decls), + modpath = modpath, + srcs = srcs, + submods = if (decl == "") doc::submodules(modpath)? else [], + summary = doc::sort_decls(decls), format = fmt, template = template, readme = readme, @@ -209,17 +190,13 @@ export fn main() void = { pager = void, }; - if (fmt == format::TTY) { + if (fmt == doc::format::TTY) { ctx.out = init_tty(&ctx); }; - match (emit(&ctx)) { - case void => void; - case let err: error => - fmt::fatal("Error:", strerror(err)); - }; + emit(&ctx)?; - io::close(ctx.out)!; + io::close(ctx.out)?; match (ctx.pager) { case void => void; case let proc: exec::process => @@ -235,7 +212,10 @@ fn parseident(in: str) (ast::ident | parse::error) = { const buf = memio::fixed(strings::toutf8(in)); const lexer = lex::init(&buf, "<string>"); defer lex::finish(&lexer); - let ident: []str = []; // TODO: errdefer + // XXX: errdefer + let success = false; + let ident: ast::ident = []; + defer if (!success) ast::ident_free(ident); let z = 0z; for (true) { const tok = lex::lex(&lexer)?; @@ -270,10 +250,11 @@ fn parseident(in: str) (ast::ident | parse::error) = { const why = "Identifier exceeds maximum length"; return (loc, why): lex::syntax: parse::error; }; + success = true; return ident; }; -fn init_tty(ctx: *context) io::handle = { +fn init_tty(ctx: *doc::context) io::handle = { const pager = match (os::getenv("PAGER")) { case let name: str => yield match (exec::cmd(name)) { @@ -340,32 +321,22 @@ fn has_decl(decl: ast::decl, name: str) bool = { return false; }; -fn scan(path: str) (ast::subunit | error) = { - const input = match (os::open(path)) { - case let f: io::file => - yield f; - case let err: fs::error => - fmt::fatalf("Error reading {}: {}", path, fs::strerror(err)); - }; - defer io::close(input)!; - const lexer = lex::init(input, path, lex::flag::COMMENTS); - return parse::subunit(&lexer)?; -}; - -fn emit(ctx: *context) (void | error) = { +fn emit(ctx: *doc::context) (void | error) = { switch (ctx.format) { - case format::HARE => - emit_hare(ctx)?; - case format::TTY => - emit_tty(ctx)?; - case format::HTML => - emit_html(ctx)?; + case doc::format::HARE => + doc::emit_hare(ctx)?; + case doc::format::TTY => + doc::emit_tty(ctx)?; + case doc::format::HTML => + doc::emit_html(ctx)?; }; }; @test fn parseident() void = { - assert(parseident("hare::lex") is ast::ident); + assert(ast::ident_eq(parseident("hare::lex") as ast::ident, + ["hare", "lex"])); + assert(ast::ident_eq(parseident("rt::abort") as ast::ident, + ["rt", "abort"])); assert(parseident("strings::dup*{}&@") is parse::error); assert(parseident("foo::bar::") is parse::error); - assert(parseident("rt::abort") is ast::ident); }; diff --git a/cmd/haredoc/resolver.ha b/cmd/haredoc/resolver.ha @@ -1,178 +0,0 @@ -// License: GPL-3.0 -// (c) 2021 Drew DeVault <sir@cmpwn.com> -// (c) 2021 Ember Sawady <ecs@d2evs.net> -// (c) 2022 Alexey Yerin <yyp@disroot.org> -use fmt; -use hare::ast; -use hare::module; -use path; - -type symkind = enum { - LOCAL, - MODULE, - SYMBOL, - ENUM_LOCAL, - ENUM_REMOTE, -}; - -// Resolves a reference. Given an identifier, determines if it refers to a local -// symbol, a module, or a symbol in a remote module, then returns this -// information combined with a corrected ident if necessary. -fn resolve(ctx: *context, what: ast::ident) ((ast::ident, symkind) | void) = { - if (is_local(ctx, what)) { - return (what, symkind::LOCAL); - }; - - if (len(what) > 1) { - // Look for symbol in remote module - let partial = what[..len(what) - 1]; - - match (module::lookup(ctx.mctx, partial)) { - case let ver: module::version => - return (what, symkind::SYMBOL); - case module::error => void; - }; - }; - if (len(what) == 2) { - match (lookup_local_enum(ctx, what)) { - case let id: ast::ident => - return (id, symkind::ENUM_LOCAL); - case => void; - }; - }; - if (len(what) > 2) { - match (lookup_remote_enum(ctx, what)) { - case let id: ast::ident => - return (id, symkind::ENUM_REMOTE); - case => void; - }; - }; - - match (module::lookup(ctx.mctx, what)) { - case let ver: module::version => - return (what, symkind::MODULE); - case module::error => void; - }; - - return; -}; - -fn is_local(ctx: *context, what: ast::ident) bool = { - if (len(what) != 1) { - return false; - }; - - const summary = ctx.summary; - for (let i = 0z; i < len(summary.constants); i += 1) { - const name = decl_ident(summary.constants[i])[0]; - if (name == what[0]) { - return true; - }; - }; - for (let i = 0z; i < len(summary.errors); i += 1) { - const name = decl_ident(summary.errors[i])[0]; - if (name == what[0]) { - return true; - }; - }; - for (let i = 0z; i < len(summary.types); i += 1) { - const name = decl_ident(summary.types[i])[0]; - if (name == what[0]) { - return true; - }; - }; - for (let i = 0z; i < len(summary.globals); i += 1) { - const name = decl_ident(summary.globals[i])[0]; - if (name == what[0]) { - return true; - }; - }; - for (let i = 0z; i < len(summary.funcs); i += 1) { - const name = decl_ident(summary.funcs[i])[0]; - if (name == what[0]) { - return true; - }; - }; - - return false; -}; - -fn lookup_local_enum(ctx: *context, what: ast::ident) (ast::ident | void) = { - for (let i = 0z; i < len(ctx.summary.types); i += 1) { - const decl = ctx.summary.types[i]; - const name = decl_ident(decl)[0]; - if (name == what[0]) { - const t = (decl.decl as []ast::decl_type)[0]; - const e = match (t._type.repr) { - case let e: ast::enum_type => - yield e; - case => - return; - }; - for (let i = 0z; i < len(e.values); i += 1) { - if (e.values[i].name == what[1]) { - return what; - }; - }; - }; - }; -}; - -fn lookup_remote_enum(ctx: *context, what: ast::ident) (ast::ident | void) = { - // mod::decl_name::member - const mod = what[..len(what) - 2]; - const decl_name = what[len(what) - 2]; - const member = what[len(what) - 1]; - - const version = match (module::lookup(ctx.mctx, mod)) { - case let ver: module::version => - yield ver; - case module::error => - abort(); - }; - - // This would take a lot of memory to load - let decls: []ast::decl = []; - defer { - for (let i = 0z; i < len(decls); i += 1) { - ast::decl_finish(decls[i]); - }; - free(decls); - }; - for (let i = 0z; i < len(version.inputs); i += 1) { - const in = version.inputs[i]; - const ext = path::peek_ext(&path::init(in.path)!); - if (ext is void || ext as str != "ha") { - continue; - }; - match (scan(in.path)) { - case let u: ast::subunit => - append(decls, u.decls...); - case let err: error => - fmt::fatal("Error:", strerror(err)); - }; - }; - - for (let i = 0z; i < len(decls); i += 1) { - const decl = match (decls[i].decl) { - case let t: []ast::decl_type => - yield t; - case => continue; - }; - for (let i = 0z; i < len(decl); i += 1) { - if (decl[i].ident[0] == decl_name) { - const e = match (decl[i]._type.repr) { - case let e: ast::enum_type => - yield e; - case => - abort(); - }; - for (let i = 0z; i < len(e.values); i += 1) { - if (e.values[i].name == member) { - return what; - }; - }; - }; - }; - }; -}; diff --git a/cmd/haredoc/sort.ha b/cmd/haredoc/sort.ha @@ -1,103 +0,0 @@ -// License: GPL-3.0 -// (c) 2021 Drew DeVault <sir@cmpwn.com> -// (c) 2021 Ember Sawady <ecs@d2evs.net> -use hare::ast; -use sort; -use strings; - -type summary = struct { - constants: []ast::decl, - errors: []ast::decl, - types: []ast::decl, - globals: []ast::decl, - funcs: []ast::decl, -}; - -// Sorts declarations by removing unexported declarations, moving undocumented -// declarations to the end, sorting by identifier, and ensuring that only one -// member is present in each declaration (so that "let x: int = 10, y: int = 20" -// becomes two declarations: "let x: int = 10; let y: int = 20;"). -fn sort_decls(decls: []ast::decl) summary = { - let sorted = summary { ... }; - - for (let i = 0z; i < len(decls); i += 1) { - let decl = decls[i]; - if (!decl.exported) { - continue; - }; - - match (decl.decl) { - case let f: ast::decl_func => - append(sorted.funcs, decl); - case let t: []ast::decl_type => - for (let j = 0z; j < len(t); j += 1) { - let bucket = &sorted.types; - if (t[j]._type.flags & ast::type_flag::ERROR == ast::type_flag::ERROR) { - bucket = &sorted.errors; - }; - append(bucket, ast::decl { - exported = true, - start = decl.start, - end = decl.end, - decl = alloc([t[j]]), - docs = decl.docs, - }); - }; - case let c: []ast::decl_const => - for (let j = 0z; j < len(c); j += 1) { - append(sorted.constants, ast::decl { - exported = true, - start = decl.start, - end = decl.end, - decl = alloc([c[j]]), - docs = decl.docs, - }); - }; - case let g: []ast::decl_global => - for (let j = 0z; j < len(g); j += 1) { - append(sorted.globals, ast::decl { - exported = true, - start = decl.start, - end = decl.end, - decl = alloc([g[j]]), - docs = decl.docs, - }); - }; - }; - }; - - sort::sort(sorted.constants, size(ast::decl), &decl_cmp); - sort::sort(sorted.errors, size(ast::decl), &decl_cmp); - sort::sort(sorted.types, size(ast::decl), &decl_cmp); - sort::sort(sorted.globals, size(ast::decl), &decl_cmp); - sort::sort(sorted.funcs, size(ast::decl), &decl_cmp); - return sorted; -}; - -fn decl_cmp(a: const *opaque, b: const *opaque) int = { - const a = *(a: const *ast::decl); - const b = *(b: const *ast::decl); - if (a.docs == "" && b.docs != "") { - return 1; - } else if (a.docs != "" && b.docs == "") { - return -1; - }; - const id_a = decl_ident(a), id_b = decl_ident(b); - return strings::compare(id_a[len(id_a) - 1], id_b[len(id_b) - 1]); -}; - -fn decl_ident(decl: ast::decl) ast::ident = { - match (decl.decl) { - case let f: ast::decl_func => - return f.ident; - case let t: []ast::decl_type => - assert(len(t) == 1); - return t[0].ident; - case let c: []ast::decl_const => - assert(len(c) == 1); - return c[0].ident; - case let g: []ast::decl_global => - assert(len(g) == 1); - return g[0].ident; - }; -}; diff --git a/cmd/haredoc/tty.ha b/cmd/haredoc/tty.ha @@ -1,591 +0,0 @@ -// License: GPL-3.0 -// (c) 2021 Alexey Yerin <yyp@disroot.org> -// (c) 2021 Drew DeVault <sir@cmpwn.com> -// (c) 2021 Ember Sawady <ecs@d2evs.net> -use ascii; -use bufio; -use fmt; -use hare::ast; -use hare::ast::{variadism}; -use hare::lex; -use hare::unparse; -use io; -use memio; -use os; -use strings; - -let firstline: bool = true; - -// Formats output as Hare source code (prototypes) with syntax highlighting -fn emit_tty(ctx: *context) (void | error) = { - init_colors(); - const summary = ctx.summary; - - match (ctx.readme) { - case let readme: io::file => - for (true) match (bufio::scanline(readme)?) { - case io::EOF => break; - case let b: []u8 => - defer free(b); - firstline = false; - insert(b[0], ' '); - comment_tty(ctx.out, strings::fromutf8(b)!)?; - }; - case void => void; - }; - - emit_submodules_tty(ctx)?; - - // XXX: Should we emit the dependencies, too? - for (let i = 0z; i < len(summary.types); i += 1) { - details_tty(ctx, summary.types[i])?; - }; - for (let i = 0z; i < len(summary.constants); i += 1) { - details_tty(ctx, summary.constants[i])?; - }; - for (let i = 0z; i < len(summary.errors); i += 1) { - details_tty(ctx, summary.errors[i])?; - }; - for (let i = 0z; i < len(summary.globals); i += 1) { - details_tty(ctx, summary.globals[i])?; - }; - for (let i = 0z; i < len(summary.funcs); i += 1) { - details_tty(ctx, summary.funcs[i])?; - }; -}; - -fn emit_submodules_tty(ctx: *context) (void | error) = { - const submodules = submodules(ctx)?; - defer strings::freeall(submodules); - - if (len(submodules) != 0) { - fmt::fprintln(ctx.out)?; - if (len(ctx.ident) == 0) { - render(ctx.out, syn::COMMENT)?; - fmt::fprintln(ctx.out, "// Modules")?; - render(ctx.out, syn::NORMAL)?; - } else { - render(ctx.out, syn::COMMENT)?; - fmt::fprintln(ctx.out, "// Submodules")?; - render(ctx.out, syn::NORMAL)?; - }; - for (let i = 0z; i < len(submodules); i += 1) { - let submodule = if (len(ctx.ident) != 0) { - const s = unparse::identstr(ctx.ident); - defer free(s); - yield strings::concat(s, "::", submodules[i]); - } else { - yield strings::dup(submodules[i]); - }; - defer free(submodule); - - render(ctx.out, syn::COMMENT)?; - fmt::fprintfln(ctx.out, "// - [[{}]]", submodule)?; - render(ctx.out, syn::NORMAL)?; - }; - }; -}; - -fn comment_tty(out: io::handle, s: str) (size | io::error) = { - let n = 0z; - n += render(out, syn::COMMENT)?; - n += fmt::fprintfln(out, "//{}", s)?; - n += render(out, syn::NORMAL)?; - return n; -}; - -fn docs_tty(out: io::handle, s: str, indent: size) (size | io::error) = { - const iter = strings::tokenize(s, "\n"); - let z = 0z; - for (true) match (strings::next_token(&iter)) { - case let s: str => - if (!(strings::peek_token(&iter) is void)) { - z += comment_tty(out, s)?; - for (let i = 0z; i < indent; i += 1) { - z += fmt::fprint(out, "\t")?; - }; - }; - case void => break; - }; - - return z; -}; - -fn isws(s: str) bool = { - const iter = strings::iter(s); - for (true) { - match (strings::next(&iter)) { - case let r: rune => - if (!ascii::isspace(r)) { - return false; - }; - case void => break; - }; - }; - return true; -}; - -fn details_tty(ctx: *context, decl: ast::decl) (void | error) = { - if (len(decl.docs) == 0 && !ctx.show_undocumented) { - return; - }; - - if (!firstline) { - fmt::fprintln(ctx.out)?; - }; - firstline = false; - - docs_tty(ctx.out, decl.docs, 0)?; - unparse_tty(ctx.out, decl)?; - fmt::fprintln(ctx.out)?; -}; - -// Forked from [[hare::unparse]] -fn unparse_tty(out: io::handle, d: ast::decl) (size | io::error) = { - let n = 0z; - match (d.decl) { - case let g: []ast::decl_global => - n += render(out, syn::KEYWORD)?; - n += fmt::fprint(out, if (g[0].is_const) "const " else "let ")?; - for (let i = 0z; i < len(g); i += 1) { - if (len(g[i].symbol) != 0) { - n += render(out, syn::ATTRIBUTE)?; - n += fmt::fprintf(out, "@symbol(")?; - n += render(out, syn::STRING)?; - n += fmt::fprintf(out, `"{}"`, g[i].symbol)?; - n += render(out, syn::ATTRIBUTE)?; - n += fmt::fprintf(out, ") ")?; - n += render(out, syn::NORMAL)?; - }; - n += render(out, syn::GLOBAL)?; - n += unparse::ident(out, g[i].ident)?; - match (g[i]._type) { - case null => - yield; - case let ty: *ast::_type => - n += render(out, syn::PUNCTUATION)?; - n += fmt::fprint(out, ": ")?; - n += type_tty(out, 0, *ty)?; - }; - if (i + 1 < len(g)) { - n += render(out, syn::PUNCTUATION)?; - n += fmt::fprint(out, ", ")?; - }; - n += render(out, syn::NORMAL)?; - }; - case let c: []ast::decl_const => - n += render(out, syn::KEYWORD)?; - n += fmt::fprintf(out, "def ")?; - for (let i = 0z; i < len(c); i += 1) { - n += render(out, syn::CONSTANT)?; - n += unparse::ident(out, c[i].ident)?; - n += render(out, syn::PUNCTUATION)?; - match (c[i]._type) { - case null => - yield; - case let ty: *ast::_type => - n += render(out, syn::PUNCTUATION)?; - n += fmt::fprint(out, ": ")?; - n += type_tty(out, 0, *ty)?; - }; - if (i + 1 < len(c)) { - n += render(out, syn::PUNCTUATION)?; - n += fmt::fprint(out, ", ")?; - }; - }; - case let t: []ast::decl_type => - n += render(out, syn::KEYWORD)?; - n += fmt::fprint(out, "type ")?; - for (let i = 0z; i < len(t); i += 1) { - n += render(out, syn::TYPEDEF)?; - n += unparse::ident(out, t[i].ident)?; - n += render(out, syn::PUNCTUATION)?; - n += fmt::fprint(out, " = ")?; - n += type_tty(out, 0, t[i]._type)?; - if (i + 1 < len(t)) { - n += render(out, syn::PUNCTUATION)?; - n += fmt::fprint(out, ", ")?; - }; - }; - case let f: ast::decl_func => - n += render(out, syn::ATTRIBUTE)?; - n += fmt::fprint(out, switch (f.attrs) { - case ast::fndecl_attrs::NONE => - yield ""; - case ast::fndecl_attrs::FINI => - yield "@fini "; - case ast::fndecl_attrs::INIT => - yield "@init "; - case ast::fndecl_attrs::TEST => - yield "@test "; - })?; - n += render(out, syn::NORMAL)?; - - let p = f.prototype.repr as ast::func_type; - if (len(f.symbol) != 0) { - n += render(out, syn::ATTRIBUTE)?; - n += fmt::fprintf(out, "@symbol(")?; - n += render(out, syn::STRING)?; - n += fmt::fprintf(out, `"{}"`, f.symbol)?; - n += render(out, syn::ATTRIBUTE)?; - n += fmt::fprintf(out, ") ")?; - n += render(out, syn::NORMAL)?; - }; - n += render(out, syn::KEYWORD)?; - n += fmt::fprint(out, "fn ")?; - n += render(out, syn::FUNCTION)?; - n += unparse::ident(out, f.ident)?; - n += fmt::fprint(out, "\x1b[0m")?; - n += prototype_tty(out, 0, - f.prototype.repr as ast::func_type)?; - }; - n += render(out, syn::PUNCTUATION)?; - n += fmt::fprint(out, ";")?; - return n; -}; - -fn prototype_tty( - out: io::handle, - indent: size, - t: ast::func_type, -) (size | io::error) = { - let n = 0z; - n += render(out, syn::PUNCTUATION)?; - n += fmt::fprint(out, "(")?; - - let typenames: []str = []; - // TODO: https://todo.sr.ht/~sircmpwn/hare/581 - if (len(t.params) > 0) { - typenames = alloc([""...], len(t.params)); - }; - defer strings::freeall(typenames); - let retname = ""; - defer free(retname); - - // estimate length of prototype to determine if it should span multiple - // lines - const linelen = if (len(t.params) == 0) { - let strm = memio::dynamic(); - defer io::close(&strm)!; - type_tty(&strm, indent, *t.result)?; - retname = strings::dup(memio::string(&strm)!); - yield 0z; // only use one line if there's no parameters - } else { - let strm = memio::dynamic(); - defer io::close(&strm)!; - let linelen = indent * 8 + 5; - linelen += if (len(t.params) != 0) len(t.params) * 3 - 1 else 0; - for (let i = 0z; i < len(t.params); i += 1) { - const param = t.params[i]; - linelen += unparse::_type(&strm, indent, *param._type)?; - typenames[i] = strings::dup(memio::string(&strm)!); - linelen += if (param.name == "") 1 else len(param.name); - memio::reset(&strm); - }; - switch (t.variadism) { - case variadism::NONE => void; - case variadism::HARE => - linelen += 3; - case variadism::C => - linelen += 5; - }; - linelen += type_tty(&strm, indent, *t.result)?; - retname = strings::dup(memio::string(&strm)!); - yield linelen; - }; - - // use 72 instead of 80 to give a bit of leeway for preceding text - if (linelen > 72) { - indent += 1; - for (let i = 0z; i < len(t.params); i += 1) { - const param = t.params[i]; - n += newline(out, indent)?; - n += render(out, syn::SECONDARY)?; - n += fmt::fprint(out, - if (param.name == "") "_" else param.name)?; - n += render(out, syn::PUNCTUATION)?; - n += fmt::fprint(out, ": ")?; - n += render(out, syn::TYPE)?; - n += fmt::fprint(out, typenames[i])?; - if (i + 1 == len(t.params) - && t.variadism == variadism::HARE) { - n += render(out, syn::OPERATOR)?; - n += fmt::fprint(out, "...")?; - } else { - n += render(out, syn::PUNCTUATION)?; - n += fmt::fprint(out, ",")?; - }; - }; - if (t.variadism == variadism::C) { - n += newline(out, indent)?; - n += render(out, syn::OPERATOR)?; - n += fmt::fprint(out, "...")?; - }; - indent -= 1; - n += newline(out, indent)?; - } else for (let i = 0z; i < len(t.params); i += 1) { - const param = t.params[i]; - n += render(out, syn::SECONDARY)?; - n += fmt::fprint(out, - if (param.name == "") "_" else param.name)?; - n += render(out, syn::PUNCTUATION)?; - n += fmt::fprint(out, ": ")?; - n += render(out, syn::TYPE)?; - n += fmt::fprint(out, typenames[i])?; - if (i + 1 == len(t.params)) { - switch (t.variadism) { - case variadism::NONE => void; - case variadism::HARE => - n += render(out, syn::OPERATOR)?; - n += fmt::fprint(out, "...")?; - case variadism::C => - n += render(out, syn::PUNCTUATION)?; - n += fmt::fprint(out, ", ")?; - n += render(out, syn::OPERATOR)?; - n += fmt::fprint(out, "...")?; - }; - } else { - n += render(out, syn::PUNCTUATION)?; - n += fmt::fprint(out, ", ")?; - }; - }; - - n += render(out, syn::PUNCTUATION)?; - n += fmt::fprint(out, ")", retname)?; - return n; -}; - -// Forked from [[hare::unparse]] -fn struct_union_type_tty( - out: io::handle, - indent: size, - t: ast::_type, -) (size | io::error) = { - let n = 0z; - let membs = match (t.repr) { - case let st: ast::struct_type => - n += render(out, syn::TYPE)?; - n += fmt::fprint(out, "struct")?; - if (st.packed) { - n += render(out, syn::ATTRIBUTE)?; - n += fmt::fprint(out, " @packed")?; - }; - n += render(out, syn::PUNCTUATION)?; - n += fmt::fprint(out, " {")?; - yield st.members: []ast::struct_member; - case let ut: ast::union_type => - n += render(out, syn::TYPE)?; - n += fmt::fprint(out, "union")?; - n += render(out, syn::PUNCTUATION)?; - n += fmt::fprint(out, " {")?; - yield ut: []ast::struct_member; - }; - - indent += 1z; - for (let i = 0z; i < len(membs); i += 1) { - n += newline(out, indent)?; - if (membs[i].docs != "") { - n += docs_tty(out, membs[i].docs, indent)?; - }; - - match (membs[i]._offset) { - case null => void; - case let ex: *ast::expr => - n += render(out, syn::ATTRIBUTE)?; - n += fmt::fprint(out, "@offset(")?; - n += render(out, syn::NUMBER)?; - n += unparse::expr(out, indent, *ex)?; - n += render(out, syn::ATTRIBUTE)?; - n += fmt::fprint(out, ")")?; - n += render(out, syn::NORMAL)?; - }; - - match (membs[i].member) { - case let se: ast::struct_embedded => - n += type_tty(out, indent, *se)?; - case let sa: ast::struct_alias => - n += unparse::ident(out, sa)?; - case let sf: ast::struct_field => - n += render(out, syn::SECONDARY)?; - n += fmt::fprint(out, sf.name)?; - n += render(out, syn::PUNCTUATION)?; - n += fmt::fprint(out, ": ")?; - n += type_tty(out, indent, *sf._type)?; - }; - - n += render(out, syn::PUNCTUATION)?; - n += fmt::fprint(out, ",")?; - }; - - indent -= 1; - n += newline(out, indent)?; - n += render(out, syn::PUNCTUATION)?; - n += fmt::fprint(out, "}")?; - return n; -}; - -// Forked from [[hare::unparse]] -fn type_tty( - out: io::handle, - indent: size, - t: ast::_type, -) (size | io::error) = { - let n = 0z; - if (t.flags & ast::type_flag::CONST != 0 - && !(t.repr is ast::func_type)) { - n += render(out, syn::TYPE)?; - n += fmt::fprint(out, "const ")?; - }; - if (t.flags & ast::type_flag::ERROR != 0) { - n += render(out, syn::OPERATOR)?; - n += fmt::fprint(out, "!")?; - }; - - match (t.repr) { - case let a: ast::alias_type => - if (a.unwrap) { - n += render(out, syn::OPERATOR)?; - n += fmt::fprint(out, "...")?; - }; - n += render(out, syn::TYPE)?; - n += unparse::ident(out, a.ident)?; - case let b: ast::builtin_type => - n += render(out, syn::TYPE)?; - n += fmt::fprintf(out, "{}", unparse::builtin_type(b))?; - case let e: ast::enum_type => - n += render(out, syn::TYPE)?; - n += fmt::fprint(out, "enum ")?; - if (e.storage != ast::builtin_type::INT) { - n += fmt::fprintf(out, - "{} ", unparse::builtin_type(e.storage))?; - }; - n += render(out, syn::PUNCTUATION)?; - n += fmt::fprintln(out, "{")?; - indent += 1; - for (let i = 0z; i < len(e.values); i += 1) { - for (let i = 0z; i < indent; i += 1) { - n += fmt::fprint(out, "\t")?; - }; - let value = e.values[i]; - let wrotedocs = false; - if (value.docs != "") { - // Check if comment should go above or next to - // field - if (multiline_comment(value.docs)) { - n += docs_tty(out, value.docs, indent)?; - wrotedocs = true; - }; - }; - n += render(out, syn::SECONDARY)?; - n += fmt::fprint(out, value.name)?; - match (value.value) { - case null => void; - case let e: *ast::expr => - n += render(out, syn::OPERATOR)?; - n += fmt::fprint(out, " = ")?; - n += render(out, syn::NORMAL)?; - n += unparse::expr(out, indent, *e)?; - }; - n += render(out, syn::PUNCTUATION)?; - n += fmt::fprint(out, ",")?; - if (value.docs != "" && !wrotedocs) { - n += fmt::fprint(out, " ")?; - n += docs_tty(out, value.docs, 0)?; - } else { - n += fmt::fprintln(out)?; - }; - }; - indent -= 1; - for (let i = 0z; i < indent; i += 1) { - n += fmt::fprint(out, "\t")?; - }; - n += render(out, syn::PUNCTUATION)?; - n += fmt::fprint(out, "}")?; - case let f: ast::func_type => - n += render(out, syn::TYPE)?; - n += fmt::fprint(out, "fn")?; - n += prototype_tty(out, indent, f)?; - case let l: ast::list_type => - n += render(out, syn::OPERATOR)?; - n += fmt::fprint(out, "[")?; - match (l.length) { - case ast::len_slice => void; - case ast::len_unbounded => - n += fmt::fprint(out, "*")?; - case ast::len_contextual => - n += fmt::fprint(out, "_")?; - case let e: *ast::expr => - n += unparse::expr(out, indent, *e)?; - }; - n += render(out, syn::OPERATOR)?; - n += fmt::fprint(out, "]")?; - n += type_tty(out, indent, *l.members)?; - case let p: ast::pointer_type => - if (p.flags & ast::pointer_flag::NULLABLE != 0) { - n += render(out, syn::TYPE)?; - n += fmt::fprint(out, "nullable ")?; - }; - n += render(out, syn::OPERATOR)?; - n += fmt::fprint(out, "*")?; - n += type_tty(out, indent, *p.referent)?; - case ast::struct_type => - n += struct_union_type_tty(out, indent, t)?; - case ast::union_type => - n += struct_union_type_tty(out, indent, t)?; - case let t: ast::tagged_type => - // rough estimate of current line length - let linelen: size = n + (indent + 1) * 8; - n = 0; - n += render(out, syn::PUNCTUATION)?; - linelen += fmt::fprint(out, "(")?; - for (let i = 0z; i < len(t); i += 1) { - linelen += type_tty(out, indent, *t[i])?; - if (i + 1 == len(t)) break; - n += render(out, syn::PUNCTUATION)?; - linelen += fmt::fprint(out, " |")?; - // use 72 instead of 80 to give a bit of leeway for long - // type names - if (linelen > 72) { - n += linelen; - linelen = (indent + 1) * 8; - n += fmt::fprintln(out)?; - for (let i = 0z; i <= indent; i += 1) { - n += fmt::fprint(out, "\t")?; - }; - } else { - linelen += fmt::fprint(out, " ")?; - }; - }; - n += linelen; - n += render(out, syn::PUNCTUATION)?; - n += fmt::fprint(out, ")")?; - case let t: ast::tuple_type => - // rough estimate of current line length - let linelen: size = n + (indent + 1) * 8; - n = 0; - n += render(out, syn::PUNCTUATION)?; - linelen += fmt::fprint(out, "(")?; - for (let i = 0z; i < len(t); i += 1) { - linelen += type_tty(out, indent, *t[i])?; - if (i + 1 == len(t)) break; - n += render(out, syn::PUNCTUATION)?; - linelen += fmt::fprint(out, ",")?; - // use 72 instead of 80 to give a bit of leeway for long - // type names - if (linelen > 72) { - n += linelen; - linelen = (indent + 1) * 8; - n += fmt::fprintln(out)?; - for (let i = 0z; i <= indent; i += 1) { - n += fmt::fprint(out, "\t")?; - }; - } else { - linelen += fmt::fprint(out, " ")?; - }; - }; - n += linelen; - n += render(out, syn::PUNCTUATION)?; - n += fmt::fprint(out, ")")?; - }; - return n; -}; diff --git a/cmd/haredoc/util.ha b/cmd/haredoc/util.ha @@ -1,70 +1,50 @@ -// License: GPL-3.0 -// (c) 2022 Byron Torres <b@torresjrjr.com> -// (c) 2022 Sebastian <sebastian@sebsite.pw> -use fmt; -use hare::ast; +use ascii; +use dirs; +use errors; use hare::module; -use io; -use memio; +use os; use strings; -// Forked from [[hare::unparse]]. -fn newline(out: io::handle, indent: size) (size | io::error) = { - let n = 0z; - n += fmt::fprint(out, "\n")?; - for (let i = 0z; i < indent; i += 1) { - n += fmt::fprint(out, "\t")?; - }; - return n; -}; - -fn multiline_comment(s: str) bool = - strings::byteindex(s, '\n') as size != len(s) - 1; +def HAREPATH: str = "."; -fn trim_comment(s: str) str = { - let trimmed = memio::dynamic(); - let tok = strings::tokenize(s, "\n"); - for (true) { - const line = match (strings::next_token(&tok)) { - case void => - break; - case let line: str => - yield line; +fn merge_tags(current: *[]str, new: str) (void | module::error) = { + let trimmed = strings::ltrim(new, '^'); + if (trimmed != new) { + strings::freeall(*current); + *current = []; + }; + let newtags = module::parse_tags(trimmed)?; + for (let i = 0z; i < len(newtags); i += 1) :new { + for (let j = 0z; j < len(current); j += 1) { + if (newtags[i].name == current[j]) { + if (!newtags[i].include) { + free(current[j]); + static delete(current[j]); + }; + continue :new; + }; + }; + if (newtags[i].include) { + append(current, strings::dup(newtags[i].name)); }; - memio::concat(&trimmed, strings::trimprefix(line, " "), "\n")!; }; - return strings::dup(memio::string(&trimmed)!); }; -fn submodules(ctx: *context) ([]str | error) = { - let identpath = module::identpath(ctx.ident); - defer free(identpath); +fn harepath() str = os::tryenv("HAREPATH", HAREPATH); - let submodules: []str = []; - for (let i = 0z; i < len(ctx.version.subdirs); i += 1) { - let dir = ctx.version.subdirs[i]; - // XXX: the list of reserved directory names is not yet - // finalized. See https://todo.sr.ht/~sircmpwn/hare/516 - if (dir == "contrib") continue; - if (dir == "cmd") continue; - if (dir == "docs") continue; - if (dir == "ext") continue; - if (dir == "vendor") continue; - if (dir == "scripts") continue; - - let submod = [identpath, dir]: ast::ident; - match (module::lookup(ctx.mctx, submod)) { - case let ver: module::version => - // TODO: free version data - void; - case module::notfound => - continue; - case let err: module::error => - return err; - }; - - append(submodules, dir); +fn harecache() str = { + match (os::getenv("HARECACHE")) { + case let s: str => + return s; + case void => + return dirs::cache("hare"); }; +}; - return submodules; +// result must be freed with strings::freeall +fn default_tags() ([]str | error) = { + let arch = os::machine(); + let platform = ascii::strlower(os::sysname()); + let tags: []str = alloc([strings::dup(arch), platform]); + return tags; }; diff --git a/crypto/aes/+x86_64/ni_native.s b/crypto/aes/+x86_64/ni.s diff --git a/docs/hare-doc.5.scd b/docs/hare-doc.5.scd @@ -0,0 +1,29 @@ +hare-doc(5) + +# NAME + +hare-doc - hare documentation format. + +# DESCRIPTION + +The Hare formatting markup is a very simple markup language. Text may be written +normally, broken into several lines to conform to the column limit. Repeated +whitespace will be collapsed. To begin a new paragraph, insert an empty line. + +Links to Hare symbols may be written in brackets, like this: [[os::stdout]]. A +bulleted list can be started by opening a line with "-". To complete the list, +insert an empty line. Code samples may be used by using more than one space +character at the start of a line (a tab character counts as 8 spaces). + +This markup language is extracted from Hare comments preceding exported symbols +in your source code, and from a file named "README" in your module directory, if +present. + +``` +// Foos the bars. See also [[foobar]]. +export fn example() int; +``` + +# SEE ALSO + +*hare*(1) diff --git a/docs/hare.1.scd b/docs/hare.1.scd @@ -0,0 +1,357 @@ +hare(1) + +# NAME + +hare - compiles, runs, and tests Hare programs + +# SYNOPSIS + +*hare* build [-hqv]++ + [-a _arch_]++ + [-D _ident[:type]=value_]++ + [-j _jobs_]++ + [-L _libdir_]++ + [-l _libname_]++ + [-N _namespace_]++ + [-o _path_]++ + [-T _tagset_]++ + [-t _type_]++ + [_path_] + +*hare* cache [-hc] + +*hare* deps [-hd] [-T _tagset_] [_path_|_module_] + +*hare* run [-hqv]++ + [-a _arch_]++ + [-D _ident[:type]=value_]++ + [-j _jobs_]++ + [-L _libdir_]++ + [-l _libname_]++ + [-T _tagset_]++ + [_path_ [_args_...]] + +*hare* test [-hqv]++ + [-a _arch_]++ + [-D _ident[:type]=value_]++ + [-j _jobs_]++ + [-L _libdir_]++ + [-l _libname_]++ + [-o _path_]++ + [-T _tagset_]++ + [_path_] + +*hare* version [-hv] + +# DESCRIPTION + +; TODO: Decide on and document driver exit statuses +*hare build* compiles a Hare program into an executable. The _path_ argument is +a path to a Hare source file or a directory which contains a Hare module (see +*MODULES* below). If no path is given, the Hare module contained in the current +working directory is built. + +*hare cache* displays information about the build cache. + +*hare deps* displays the dependency graph of a Hare program. The _path_ argument +is equivalent in usage to *hare build*. + +*hare run* compiles and runs a Hare program. The _path_ argument is equivalent +in usage to *hare build*. If provided, any additional _args_ are passed to the +Hare program which is run. os::args[0] is set to the _path_ argument. + +*hare test* compiles and runs tests for Hare code. All Hare modules in the +current working directory are recursively discovered, built, and their tests +made eligible for the test run. If the _tests_ argument is omitted, all tests +are run. Otherwise, each argument is interpreted as a *glob*(7) pattern, giving +the names of the tests that should be run. *hare test* adds the +test tag to the +default build tags. + +*hare version* prints version information for the *hare* program. If *-v* is +supplied, it also prints information about the build parameters. The output +format is consistent for machine reading: the first line is always +"hare $version". Subsequent lines give configuration values in the form of a +name, value, and optional context, separated by tabs. + +# OPTIONS + +## hare build + +*-h* + Prints the help text. + +*-q* + Outside of errors, don't write anything to stdout while building. + +*-v* + Enable verbose logging. Specify twice to increase verbosity. + +*-a* _arch_ + Set the desired architecture for cross-compiling. See *ARCHITECTURES* + for supported architecture names. + +*-D* _ident[:type]=value_ + Passed to *harec*(1) to define a constant in the type system. _ident_ is + parsed as a Hare identifier (e.g. "foo::bar::baz"), _type_ as a Hare + type (e.g. "str" or "struct { x: int, y: int }"), and _value_ as a Hare + expression (e.g. "42"). Take care to address any necessary escaping to + avoid conflicts between your shell syntax and Hare syntax. + +*-j* _jobs_ + Defines the maximum number of jobs which *hare* will execute in + parallel. The default is the number of processors available on the host. + +*-L libdir* + Add directory to the linker library search path. + +*-l* _libname_ + Link with the named system library. The name is passed to + *pkg-config --libs* (see *pkg-config*(1)) to obtain the appropriate + linker flags. + +*-N* _namespace_ + Override the namespace for the module. + +*-o* _path_ + Set the output file to the given path. + +*-T* _tagset_ + Sets or unsets build tags. See *CUSTOMIZING BUILD TAGS*. + +*-t* _type_ + Set the build type. Should be one of s/o/bin, for assembly, compiled + object, or compiled binary, respectively. + +## hare cache + +*-h* + Prints the help text. + +*-c* + Clears the cache. + +## hare deps + +*-h* + Prints the help text. + +*-d* + Print dependency graph as a dot file for use with *graphviz*(1). + +*-T* _tags_ + Sets or unsets build tags. See *CUSTOMIZING BUILD TAGS*. + +## hare run + +*-h* + Prints the help text. + +*-q* + Outside of errors, don't write anything to stdout while building. + +*-v* + Enable verbose logging. Specify twice to increase verbosity. + +*-a* _arch_ + Set the desired architecture for cross-compiling. See *ARCHITECTURES* + for supported architecture names. + +*-D* _ident[:type]=value_ + Passed to *harec*(1) to define a constant in the type system. _ident_ is + parsed as a Hare identifier (e.g. "foo::bar::baz"), _type_ as a Hare + type (e.g. "str" or "struct { x: int, y: int }"), and _value_ as a Hare + expression (e.g. "42"). Take care to address any necessary escaping to + avoid conflicts between your shell syntax and Hare syntax. + +*-j* _jobs_ + Defines the maximum number of jobs which *hare* will execute in + parallel. The default is the number of processors available on the host. + +*-l* _name_ + Link with the named system library. The name is passed to + *pkg-config --libs* (see *pkg-config*(1)) to obtain the appropriate + linker flags. + +*-L libdir* + Add directory to the linker library search path. + +*-T* _tags_ + Sets or unsets build tags. See *CUSTOMIZING BUILD TAGS*. + +## hare test + +*-h* + Prints the help text. + +*-q* + Outside of errors, don't write anything to stdout while building. + +*-v* + Enable verbose logging. Specify twice to increase verbosity. + +*-a* _arch_ + Set the desired architecture for cross-compiling. See *ARCHITECTURES* + for supported architecture names. + +*-D* _ident[:type]=value_ + Passed to *harec*(1) to define a constant in the type system. _ident_ is + parsed as a Hare identifier (e.g. "foo::bar::baz"), _type_ as a Hare + type (e.g. "str" or "struct { x: int, y: int }"), and _value_ as a Hare + expression (e.g. "42"). Take care to address any necessary escaping to + avoid conflicts between your shell syntax and Hare syntax. + +*-j* _jobs_ + Defines the maximum number of jobs which *hare* will execute in + parallel. The default is the number of processors available on the host. + +*-l* _name_ + Link with the named system library. The name is passed to + *pkg-config --libs* (see *pkg-config*(1)) to obtain the appropriate + linker flags. + +*-L libdir* + Add directory to the linker library search path. + +*-T* _tags_ + Adds additional build tags. See *CUSTOMIZING BUILD TAGS*. + +*-X* _tags_ + Unsets build tags. See *CUSTOMIZING BUILD TAGS*. + +## hare version + +*-h* + Prints the help text. + +*-v* + Show build parameters. + +# MODULES + +The _path_ argument to *hare build* and *hare run* are used to identify the +inputs for the build. If this path is a file, it is treated as a single Hare +source file. If it is a directory, the directory is treated as a module, and is +placed in the global namespace for the build. + +All files which end in *.ha*, *.s*, and *.o* are treated as inputs to the +module, and are respectively treated as Hare sources, assembly sources, and +objects to be linked into the final binary. There must either be at least one +Hare source or a file named 'README' in the module's root directory. + +The list of files considered eligible may be filtered by build tags. The format +for the filename is _name_[+_tags_]._ext_, where the _name_ is user-defined, the +_ext_ is either 'ha' or 's', and a list of tags are provided after the name. A +plus symbol ('+') will cause a file to be included only if that tag is present, +and a minus symbol ('-') will cause a file to be excluded if that tag is +present. + +Only one file for a given combination of _name_ and _ext_ will be selected for +the build, and among files with eligible tags, the one with the most tag +specifiers is selected. If there are two or more such files, the build driver +will error out. + +For example, if the following files are present in a directory: + +- foo.ha +- bar.ha +- bar+linux.ha +- bar+plan9.ha +- baz+x86_64.s +- bat-x86_64.ha + +If the build tags are +linux+x86_64, then the files which are included in the +module are foo.ha, bar+linux.ha, and baz+x86_64.s. + +Additionally, subdirectories in a module will be considered part of that module +if their name consists *only* of a tag set, e.g. "+linux" or "-x86_64". A +directory with a name *and* tag set is never considered as part of any module, +such as "example+linux". A directory with only a name (e.g. "example") is +considered a sub-module of its parent directory and must be imported separately, +so "foo::bar" refers to foo/bar/. + +# DEPENDENCY RESOLUTION + +The "use" statements in each source file which is used as an input to *hare +build* or *hare run* are scanned and used to determine the dependencies for the +program, and this process is repeated for each dependency to obtain a complete +dependency graph. + +Dependencies are searched for by examining first the current working directory, +then each component of the *HAREPATH* environment variable in order, which is a +list of paths separated by colons. The default value of the *HAREPATH* may be +found with the *hare version -v* command. Typically, it is set to include the +path to the standard library installed on the system, as well as a +system-provided storage location for third-party modules installed via the +system package manager. + +# ARCHITECTURES + +The *-a* flag for *hare build* is used for cross-compilation, and selects a +architecture different from the host to target. The list of supported +architectures is: + +- aarch64 +- riscv64 +- x86_64 + +The system usually provides reasonable defaults for the *AR*, *AS*, and *LD* +tools based on the desired target. However, you may wish to set these variables +yourself to control the cross toolchain in use. +; TODO: sysroots + +# CUSTOMIZING BUILD TAGS + +Build tags allow you to add constraints on what features or platforms are +enabled for your build. A tag is a name, consisting of characters which aren't +any of '+', '-', or '.', and a + or - prefix to signal inclusivity or +exclusivity. See *MODULES* for details on how build tags affect module input +selection. + +To add or remove build tags, use the *-T* flag. For example, "-T +foo-bar" will +add the 'foo' tag and remove the 'bar' tag. + +Some tags are enabled by default, enabling features for the host platform. You +can view the default tagset by running *hare version -v*. To remove all default +tags, use "-T^". + +# ENVIRONMENT + +The following environment variables affect *hare*'s execution: + +|[ *HARECACHE* +:< The path to the build cache. Defaults to _$XDG_CACHE_HOME/hare_, or + _~/.cache/hare_ if that doesn't exist. +| *HAREPATH* +: See *DEPENDENCY RESOLUTION*. +| *HAREFLAGS* +: Applies additional flags to the command line arguments. +| *HAREC* +: Name of the *harec*(1) command to use. +| *QBE* +: Name of the *qbe*(1) command to use. +| *QBEFLAGS* +: Additional flags to pass to *QBE*(1). +| *AR* +: Name of the *ar*(1) command to use. +| *ARFLAGS* +: Additional flags to pass to *ar*(1). +| *AS* +: Name of the *as*(1) command to use. +| *ASFLAGS* +: Additional flags to pass to *as*(1). +| *CC* +: Name of the *cc*(1) command to use when linking external libraries. +| *LDFLAGS* +: Additional linker flags to pass to *cc*(1). +| *LD* +: Name of the *ld*(1) command to use. +| *LDLINKFLAGS* +: Additional flags to pass to *ld*(1). +| *CC* +: Name of the *cc*(1) command to use. +| *LDFLAGS* +: Additional flags to pass to *cc*(1). + +# SEE ALSO + +*harec*(1), *as*(1), *ld*(1), *cc*(1), *hare-doc*(5), *as*(1) diff --git a/docs/hare.scd b/docs/hare.scd @@ -1,310 +0,0 @@ -hare(1) - -# NAME - -hare - compiles, runs, and tests Hare programs - -# SYNOPSIS - -*hare* build [-cv]++ - [-D _ident[:type]=value_]++ - [-j _jobs_]++ - [-L libdir]++ - [-l _name_]++ - [-o _path_]++ - [-t _arch_]++ - [-T _tags_] [-X _tags_]++ - [_path_] - -*hare* deps [-Mm] [-T _tags_] [-X _tags_] _path_ - -*hare* run [-v]++ - [-D _ident[:type]=value_]++ - [-l _name_]++ - [-L libdir]++ - [-j _jobs_]++ - [-T _tags_] [-X _tags_]++ - [_path_] [_args_...] - -*hare* test [-v]++ - [-D _ident[:type]=value_]++ - [-l _name_]++ - [-L libdir]++ - [-j _jobs_]++ - [-T _tags_] [-X _tags_]++ - _tests_ - -*hare* version [-v] - -# DESCRIPTION - -; TODO: Decide on and document driver exit statuses -*hare build* compiles a Hare program into an executable. The _path_ argument is -a path to a Hare source file or a directory which contains a Hare module (see -*MODULES* below). If no path is given, the Hare module contained in the current -working directory is built. - -*hare deps* queries the dependencies graph of a Hare program. The _path_ argument -is equivalent in usage to *hare build*. - -*hare run* compiles and runs a Hare program. The _path_ argument is equivalent -in usage to *hare build*. If provided, any additional _args_ are passed to the -Hare program which is run. os::args[0] is set to the _path_ argument. - -*hare test* compiles and runs tests for Hare code. All Hare modules in the -current working directory are recursively discovered, built, and their tests -made eligible for the test run. If the _tests_ argument is omitted, all tests -are run. Otherwise, each argument is interpreted as a *glob*(7) pattern, giving -the names of the tests that should be run. *hare test* adds the +test tag to the -default build tags. - -*hare version* prints version information for the *hare* program. If *-v* is -supplied, it also prints information about the build parameters. The output -format is consistent for machine reading: the first line is always "Hare version -$version". Subsequent lines give configuration values in the form of a name, -value, and optional context, separated by tabs. - -# OPTIONS - -## hare build - -*-c* - Compile only, do not link. The output is an object file (for Hare-only - modules) or archive (for mixed source modules). - -*-v* - Enable verbose logging. Prints every command to stderr before executing - it. - -*-D* _ident[:type]=value_ - Passed to *harec*(1) to define a constant in the type system. _ident_ is - parsed as a Hare identifier (e.g. "foo::bar::baz"), _type_ as a Hare - type (e.g. "str" or "struct { x: int, y: int }"), and _value_ as a Hare - expression (e.g. "42"). Take care to address any necessary escaping to - avoid conflicts between your shell syntax and Hare syntax. - -*-j* _jobs_ - Defines the maximum number of jobs which *hare* will execute in - parallel. The default is the number of processors available on the host. - -*-l* _name_ - Link with the named system library. The name is passed to - *pkg-config --libs* (see *pkg-config*(1)) to obtain the appropriate - linker flags. - -*-L libdir* - Add directory to the linker library search path. - -*-o* _path_ - Set the output file to the given path. - -*-t* _arch_ - Set the desired architecture for cross-compiling. See *ARCHITECTURES* - for supported architecture names. - -*-T* _tags_ - Adds additional build tags. See *CUSTOMIZING BUILD TAGS*. - -*-X* _tags_ - Unsets build tags. See *CUSTOMIZING BUILD TAGS*. - -## hare deps - -*-d* - Print dependency graph as a dot file for use with *graphviz*(1). - -*-M* - Print rules compatible with POSIX *make*(1). - -*-T* _tags_ - Adds additional build tags. See *CUSTOMIZING BUILD TAGS*. - -*-X* _tags_ - Unsets build tags. See *CUSTOMIZING BUILD TAGS*. - -## hare run - -*-v* - Enable verbose logging. Prints every command to stderr before executing - it. - -*-D* _ident[:type]=value_ - Passed to *harec*(1) to define a constant in the type system. _ident_ is - parsed as a Hare identifier (e.g. "foo::bar::baz"), _type_ as a Hare - type (e.g. "str" or "struct { x: int, y: int }"), and _value_ as a Hare - expression (e.g. "42"). Take care to address any necessary escaping to - avoid conflicts between your shell syntax and Hare syntax. - -*-j* _jobs_ - Defines the maximum number of jobs which *hare* will execute in - parallel. The default is the number of processors available on the host. - -*-l* _name_ - Link with the named system library. The name is passed to - *pkg-config --libs* (see *pkg-config*(1)) to obtain the appropriate - linker flags. - -*-L libdir* - Add directory to the linker library search path. - -*-T* _tags_ - Adds additional build tags. See *CUSTOMIZING BUILD TAGS*. - -*-X* _tags_ - Unsets build tags. See *CUSTOMIZING BUILD TAGS*. - -## hare test - -*-v* - Enable verbose logging. Prints every command to stderr before executing - it. - -*-D* _ident[:type]=value_ - Passed to *harec*(1) to define a constant in the type system. _ident_ is - parsed as a Hare identifier (e.g. "foo::bar::baz"), _type_ as a Hare - type (e.g. "str" or "struct { x: int, y: int }"), and _value_ as a Hare - expression (e.g. "42"). Take care to address any necessary escaping to - avoid conflicts between your shell syntax and Hare syntax. - -*-j* _jobs_ - Defines the maximum number of jobs which *hare* will execute in - parallel. The default is the number of processors available on the host. - -*-l* _name_ - Link with the named system library. The name is passed to - *pkg-config --libs* (see *pkg-config*(1)) to obtain the appropriate - linker flags. - -*-L libdir* - Add directory to the linker library search path. - -*-T* _tags_ - Adds additional build tags. See *CUSTOMIZING BUILD TAGS*. - -*-X* _tags_ - Unsets build tags. See *CUSTOMIZING BUILD TAGS*. - -## hare version - -*-v* - Show build parameters. - -# MODULES - -The _path_ argument to *hare build* and *hare run* are used to identify the -inputs for the build. If this path is a file, it is treated as a single Hare -source file. If it is a directory, the directory is treated as a module, and is -placed in the global namespace for the build. - -All files which end in *.ha* and *.s* are treated as inputs to the module, and -are respectively treated as Hare sources and assembly sources. A module with a -mix of assembly and Hare sources are considered *mixed* modules, and have some -special semantics. - -The list of files considered eligible may be filtered by build tags. The format -for the filename is _name_[+_tags_]._ext_, where the _name_ is user-defined, the -_ext_ is either 'ha' or 's', and a list of tags are provided after the name. A -plus symbol ('+') will cause a file to be included only if that tag is present, -and a minus symbol ('-') will cause a file to be excluded if that tag is -present. Only one file for a given _name_ will be selected for the build, and -among files with eligible tags, the one with the most tag specifiers is -selected. - -For example, if the following files are present in a directory: - -- foo.ha -- bar.ha -- bar+linux.ha -- bar+plan9.ha -- baz+x86_64.s -- bat-x86_64.ha - -If the build tags are +linux+x86_64, then the files which are included in the -module are foo.ha, bar+linux.ha, and baz+x86_64.s. - -Additionally, subdirectories in a module will be considered part of that module -if their name consists *only* of a tag set, e.g. "+linux" or "-x86_64". A -directory with a name *and* tag set is never considered as part of any module, -such as "example+linux". A directory with only a name (e.g. "example") is -considered a sub-module of its parent directory and must be imported separately, -so "foo::bar" refers to foo/bar/. - -# DEPENDENCY RESOLUTION - -The "use" statements in each source file which is used as an input to *hare -build* or *hare run* are scanned and used to determine the dependencies for the -program, and this process is repeated for each dependency to obtain a complete -dependency graph. - -Dependencies are searched for by examining first the current working directory, -then each component of the *HAREPATH* environment variable in order, which is a -list of paths separated by colons. The default value of the *HAREPATH* may be -found with the *hare version -v* command. Typically, it is set to include the -path to the standard library installed on the system, as well as a -system-provided storage location for third-party modules installed via the -system package manager. - -# ARCHITECTURES - -The *-t* flag for *hare build* is used for cross-compilation, and selects a -architecture different from the host to target. The list of supported -architectures is: - -- aarch64 -- riscv64 -- x86_64 - -The system usually provides reasonable defaults for the *AR*, *AS*, and *LD* -tools based on the desired target. However, you may wish to set these variables -yourself to control the cross toolchain in use. -; TODO: sysroots - -# CUSTOMIZING BUILD TAGS - -Build tags allow you to add constraints on what features or platforms are -enabled for your build. A tag is a name, consisting of alphanumeric characters -and underscores, and a + or - prefix to signal inclusivity or exclusivity. See -*MODULES* for details on how build tags affect module input selection. - -To add new tag constraints, inclusive or exclusive, use the *-T* flag. "-T -+foo-bar" will include the 'foo' tag and exclude the 'bar' tag. To remove -constraints, use the *-X* in a similar fashion; "-X +foo-bar" will reverse the -previous *-T* example. - -Some tags are enabled by default, enabling features for the host platform. You -can view the default tagset by running *hare version -v*. To remove all default -tags, use "-X^". - -# ENVIRONMENT - -The following environment variables affect *hare*'s execution: - -|[ *HARECACHE* -:< The path to the object cache. Defaults to _$XDG_CACHE_HOME/hare_, or - _~/.cache/hare_ if that doesn't exist. -| *HAREPATH* -: See *DEPENDENCY RESOLUTION*. -| *HAREFLAGS* -: Applies additional flags to the command line arguments. -| *HAREC* -: Name of the *harec*(1) command to use. -| *AR* -: Name of the *ar*(1) command to use. -| *ARFLAGS* -: Additional flags to pass to *ar*(1). -| *AS* -: Name of the *as*(1) command to use. -| *ASFLAGS* -: Additional flags to pass to *as*(1). -| *CC* -: Name of the *cc*(1) command to use when linking external libraries. -| *LDFLAGS* -: Additional linker flags to pass to *cc*(1). -| *LD* -: Name of the *ld*(1) command to use. -| *LDLINKFLAGS* -: Additional flags to pass to *ld*(1). - -# SEE ALSO - -*harec*(1), *haredoc*(1), *ar*(1), *as*(1), *cc*(1), *ld*(1), *make*(1) diff --git a/docs/haredoc.1.scd b/docs/haredoc.1.scd @@ -0,0 +1,75 @@ +haredoc(1) + +# NAME + +haredoc - create documentation from hare source code + +# SYNOPSIS + +*haredoc* [-hat] [-F _format_] [-T _tagset_] [_identifiers_...] + +# DESCRIPTION + +*haredoc* reads documentation for a set of identifiers from Hare source code, +and optionally prepares it for viewing in various output formats. By default, +*haredoc* will format documentation for your terminal. See *hare-doc*(5) for +details on the format. + +# OPTIONS + +*-h* + Prints the help text. + +*-a* + Show undocumented members (only applies to -Fhare and -Ftty). + +*-F* _format_ + Select output format (one of "html", "hare", or "tty"). + +*-t* + Disable HTML template. + +*-T* _tags_ + Sets or unsets build tags. See *CUSTOMIZING BUILD TAGS*. + +# TTY COLORS + +The TTY output format of *haredoc* renders colors in the terminal with ANSI +SGR escape sequences, behaving similarly to this shell command: + + printf '\\033[0;%sm' '_seq_' + +These sequences can be customised with the *HAREDOC_COLORS* environment +variable, which follows this whitespace-delimited format: + + HAREDOC\_COLORS='_key_=_seq_ _key_=_seq_ _..._' + +where each _key_=_seq_ entry assigns a valid _seq_ SGR sequence to a _key_ +syntax category. A valid _seq_ must contain either a single underscore "\_"; or +digits and/or semicolons ";". Here are the initial default entries. +; TODO: what even is this wording + +. normal "0" +. comment "1" +. primary "0" +. secondary "0" +. keyword "94" +. type "96" +. attribute "33" +. operator "1" +. punctuation "0" +. constant "91" +. string "91" +. number "95" + +Any number of entries can be specified. If a _seq_ is an underscore "\_", the +sequence specified for "normal" is used. Otherwise, if a _seq_ is invalid, +blank, empty or absent, its corresponding default sequence is used. + +For example: + + HAREDOC\_COLORS='comment=3 primary=1;4 attribute=41' haredoc -Ftty log + +# SEE ALSO + +*hare*(1), *hare-doc*(5) diff --git a/docs/haredoc.scd b/docs/haredoc.scd @@ -1,116 +0,0 @@ -haredoc(1) - -# NAME - -haredoc - reads and formats Hare documentation - -# SYNOPSIS - -*haredoc* [-at] [-F _format_] [_identifiers_...] - -# DESCRIPTION - -*haredoc* reads documentation for a set of identifiers from Hare source code, -and optionally prepares it for viewing in various output formats. By default, -*haredoc* will format documentation for your terminal. - -See *DOCUMENTATION FORMAT* for details on the format. - -# OPTIONS - -*-a* - Show undocumented members (only applies to -Fhare and -Ftty). - -*-F* _format_ - Select output format (one of "html", "hare", or "tty"). - -*-t* - Disable HTML template. - -*-T* _tags_ - Adds additional build tags. See *CUSTOMIZING BUILD TAGS* in *hare*(1). - -*-X* _tags_ - Unsets build tags. See *CUSTOMIZING BUILD TAGS* in *hare*(1). - -# DOCUMENTATION FORMAT - -The Hare formatting markup is a very simple markup language. Text may be written -normally, broken into several lines to conform to the column limit. Repeated -whitespace will be collapsed. To begin a new paragraph, insert an empty line. - -Links to Hare symbols may be written in brackets, like this: [[os::stdout]]. A -bulleted list can be started by opening a line with "-". To complete the list, -insert an empty line. Code samples may be used by using more than one space -character at the start of a line (a tab character counts as 8 spaces). - -This markup language is extracted from Hare comments preceding exported symbols -in your source code, and from a file named "README" in your module directory, if -present. - -``` -// Foos the bars. See also [[foobar]]. -export fn example() int; -``` - -# TTY COLORS - -The TTY output format renders colors in the terminal with ANSI SGR escape -sequences, behaving similarly to this shell command: - - printf '\\033[0;%sm' '_seq_' - -These sequences can be customised with the *HAREDOC_COLORS* environment -variable, which follows this whitespace-delimited format: - - HAREDOC\_COLORS='_key_=_seq_ _key_=_seq_ _..._' - -where each _key_=_seq_ entry assigns a valid _seq_ SGR sequence to a _key_ -syntax category. A valid _seq_ must contain either a single underscore "\_"; or -digits and/or semicolons ";". Here are the initial default _key_=_seq_ entries. - -. normal "0" -. comment "1" -. primary "0" -. secondary "0" -. keyword "94" -. type "96" -. attribute "33" -. operator "1" -. punctuation "0" -. constant "91" -. string "91" -. number "95" - -Any number of entries can be specified. If a _seq_ is an underscore "\_", the -sequence specified for "normal" is used. Otherwise, if a _seq_ is invalid, -blank, empty or absent, its corresponding default sequence is used. - -For example: - - HAREDOC\_COLORS='comment=3 primary=1;4 attribute=41' haredoc -Ftty log - -# ENVIRONMENT - -The following environment variables affect *haredoc*'s execution: - -|[ *HAREDOC_COLORS* -:< Customizes TTY format color rendering. See *TTY COLORS*. - -# EXAMPLES - -Read the documentation for _io_: - - haredoc io - -Read the documentation for _hash::fnv_: - - haredoc hash::fnv - -Prepare documentation for _hare::parse_ as HTML: - - haredoc -Fhtml hare::parse >parse.html - -# SEE ALSO - -*hare*(1) diff --git a/docs/modules.md b/docs/modules.md @@ -1,161 +0,0 @@ -# Hare Modules - -*This document is informative. It describes the behavior of the upstream Hare -distribution's build driver, but other Hare implementations may differ, and we -may revise this behavior in the future.* - -TODO: - -- Describe caching mechanism -- hare.ini considerations and linking to static libraries - -The **host** is the machine which is running the build driver and Hare -toolchain. The **target** is the machine which the completed program is expected -to run on. This may not be the same as the host configuration, for example when -**cross-compiling**. The **build driver**, located at `cmd/hare`, orchestrates -this process by collecting the necessary source files to build a Hare program, -resolving its dependencies, and executing the necessary parts of the toolchain -in the appropriate order. - -## The build driver and the Hare specification - -The Hare language specification is defined at a layer of abstraction that does -not include filesystems, leaving it to the implementation to define how Hare -sources are organized. The upstream Hare distribution maps the concept of a -"module" onto what the spec defines as a *unit*, and each Hare source file in -the filesystem provides what the specification refers to as a *subunit*. - -The upstream Hare distribution provides the "hosted" translation environment. -Hare programs prepared for the "freestanding" environment may also be compiled -with the upstream distribution, but the standard library is not used in this -situation. - -## Build tags - -The upstream distribution defines the concept of a **build tag**, or "tag", -which is an alphanumeric string and an "inclusive" or "exclusive" bit, which is -used to control the list of source files considered for inclusion in a Hare -module. - -The environment defines a number of default build tags depending on the target -system it was configured for. For example, a Linux system running on an x86\_64 -processor defines +linux and +x86\_64 by default, which causes files tagged -+linux or +x86\_64 to be included, and files tagged -linux or -x86\_64 to be -excluded. - -The host configuration defines a set of default build tags, which may be -overridden by specifying an alternate target. The `hare version -v` command -prints out the defaults. - -It is important to note that Hare namespaces and build tags are mutually -exclusive grammars, thanks to the fact that the + and - symbols may not appear -in a Hare identifier. - -## Locating modules on the filesystem - -Each module, identified by its namespace, is organized into a "**module root**" -directory, where all of its source files may be found, either as members or -descendants. This directory corresponds to a file path which is formed by -replacing the namespace delimiters (`::`) with the path separator for the target -host system (e.g. `/`). This forms a relative path, which is then applied to -each of several possible **source roots**. - -A source root is a directory which forms the top of a hierarchy of Hare modules -and their sources. This directory may also itself be a module, namely the **root -module**: it provides the unit for the empty namespace, where, for example, the -"main" function can be found. Generally speaking, there will be at least two -source roots to choose from: the user's program, and the standard library. - -The current working directory (`.`) is always assigned the highest priority. If -the `HAREPATH` environment variable is set, it specifies a colon-delimited (`:`) -list of additional candidates in descending order of preference. If unset, a -default value is used, which depends on the host configuration, generally -providing at least the path to the standard library's installation location, as -well the installation location of third-party Hare modules. The `hare version --v` command prints out the defaults configured for this host. - -Each of these source roots is considered in order of precedence by concatenating -the source root path and the relative path of the desired module, and checking -if a **valid** Hare module is present. A module is considered valid if it -contains any regular files, or symlinks to regular files, whose names end in -`.ha` or `.s`; or if it contains any directories, or symlinks to directories; -whose names begin with `+` or `-` and which would also be considered valid under -these criteria, applied recursively. - -The user's program, or any dependency, may *shadow* a module from the standard -library (or another dependency) by providing a suitably named directory in a -source root with a higher level of precedence. - -## Assembling the list of source files - -A source file is named with the following convention: - -`<name>[<tags...>].<ext>` - -The \< and \> symbols denote a required parameter, and \[ and \] denote optional -parameters. Some example names which follow this convention are: - -- `main.ha` -- `pipe+linux.ha` -- `example-freebsd.ha` -- `longjmp.s` -- `foo+linux-x86_64.ha` - -The build driver examines the list of files in a given module's root directory, -eliminating those with incompatible build tags, and produces a list of -applicable files. Once files with incompatible build tag have been eliminated, -only one file for a given "name" may be provided, such that a module with the -files `hello.ha` and `hello.s` is invalid. Only the "ha" and "s" extensions are -used, respectively denoting Hare sources and assembly sources. - -If any sub-directories of the module's root directory begin with `-` or `+`, -they are treated as a set of build tags and considered for their compatibility -with the build driver's active set of build tags. If compatible, the process is -repeated within that directory, treating its contents as members of the desired -module. - -## Semantics of specific tools - -A summary of how the mechanisms documented above are applied by each tool is -provided. - -### hare build, hare run - -The input to this command is the location of the root module for the Hare -program to be built or run. If the path provided identifies a file, that file is -used as the sole input file for the root module. If the path identifies a -directory, the directory is used as the root directory for the root module, -whose source files are assembled according to the algorithm described above. - -### hare test - -`hare test` walks the current source root (i.e. the current working directory) -by recursively checking if that directory, and every directory which is a -descendant of it, is a valid Hare module. Each of these modules is compiled with -the special +test build tag defined. Dependencies of these modules are also -built, but with the +test tag unspecified, with the exception of the rt module, -which provides a special test runner in this mode. The resulting executable is -executed, which causes all of the `@test` functions in the current source root -to be executed. - -The command line arguments for hare test, if given at all, are interpreted by -rt+test as a list of namespace wildcards (see [fnmatch]) defining which subsets -of the test suite to run. - -[fnmatch]: https://docs.harelang.org/fnmatch - -### haredoc - -The `haredoc` command accepts a list of identifiers to fetch documentation for, -using the same identifiers which the user might use in a Hare source file to -utilize the corresponding module or declaration. - -The desired identifier is converted to a path. If this path refers to a -directory which is a valid Hare module, documentation for that module is shown. -If that path refers to a directory which is not a valid Hare module, it is -walked to determine if any of its sub-directories are valid Hare modules; if so, -a list of those sub-directories is shown. If the path does not exist, the most -specific component of the identifier is removed, and looked up as a module, -within which the least-significant component is looked up as a declaration -exported from that module. If the module or this declaration still is not found, -the identifier is deemed unresolvable and an error is shown. diff --git a/hare/module/README b/hare/module/README @@ -1,10 +1,3 @@ -hare::module implements the module resolution algorithm used by Hare. Given that -it is run within a Hare environment (i.e. with HAREPATH et al filled in), this -module will resolve module references from their identifiers, producing a list -of the source files which are necessary, including any necessary considerations -for build tags. This interface is stable, but specific to this Hare -implementation, and may not be portable to other Hare implementations. - -This module also provides access to the Hare cache via [[manifest]]s and their -related functions, but this is not considered stable, and may be changed if we -overhaul the cache format to implement better caching strategies. +hare::module provides an interface to the module system used by this Hare +implementation, as well as to the Hare cache. Note that these interfaces may not +be portable to other Hare implementations. diff --git a/hare/module/cache.ha b/hare/module/cache.ha @@ -0,0 +1,10 @@ +use path; + +// Gets the cache directory for a given module, given the value of 'harecache'. +// The result is statically allocated and will be overwritten on subsequent +// calls. An error is returned if the resulting path would be longer than +// [[path::MAX]]. +export fn get_cache(harecache: str, modpath: str) (str | error) = { + static let buf = path::buffer { ... }; + return path::set(&buf, harecache, modpath)?; +}; diff --git a/hare/module/context.ha b/hare/module/context.ha @@ -1,129 +0,0 @@ -// License: MPL-2.0 -// (c) 2022 Alexey Yerin <yyp@disroot.org> -// (c) 2021-2022 Drew DeVault <sir@cmpwn.com> -// (c) 2021 Ember Sawady <ecs@d2evs.net> -use dirs; -use fmt; -use fs; -use glob; -use hare::ast; -use memio; -use os; -use path; -use strings; - -export type context = struct { - // Filesystem to use for the cache and source files. - fs: *fs::fs, - // List of paths to search, generally populated from HAREPATH plus some - // baked-in defaults. - paths: []str, - // Path to the Hare cache, generally populated from HARECACHE and - // defaulting to $XDG_CACHE_HOME/hare. - cache: str, - // Build tags to apply to this context. - tags: []tag, - // List of -D arguments passed to harec - defines: []str, -}; - -// Initializes a new context with the system default configuration. The tag list -// and list of defines (arguments passed with harec -D) is borrowed from the -// caller. The harepath parameter is not borrowed, but it is ignored if HAREPATH -// is set in the process environment. -export fn context_init(tags: []tag, defs: []str, harepath: str) context = { - let ctx = context { - fs = os::cwd, - tags = tags, - defines = defs, - paths = { - let harepath = match (os::getenv("HAREPATH")) { - case void => - yield harepath; - case let s: str => - yield s; - }; - - let path: []str = []; - let tok = strings::tokenize(harepath, ":"); - for (true) match (strings::next_token(&tok)) { - case void => - break; - case let s: str => - append(path, strings::dup(s)); - }; - - let vendor = glob::glob("vendor/*"); - defer glob::finish(&vendor); - for (true) match (glob::next(&vendor)) { - case void => - break; - case glob::failure => - void; // XXX: Anything else? - case let s: str => - append(path, strings::dup(s)); - }; - - append(path, strings::dup(".")); - yield path; - }, - cache: str = match (os::getenv("HARECACHE")) { - case void => - yield strings::dup(dirs::cache("hare")); - case let s: str => - yield strings::dup(s); - }, - ... - }; - return ctx; -}; - -// Frees resources associated with this context. -export fn context_finish(ctx: *context) void = { - for (let i = 0z; i < len(ctx.paths); i += 1) { - free(ctx.paths[i]); - }; - free(ctx.paths); - free(ctx.cache); -}; - -// Converts an identifier to a partial path (e.g. foo::bar becomes foo/bar). The -// return value must be freed by the caller. -export fn identpath(name: ast::ident) str = { - if (len(name) == 0) { - return strings::dup("."); - }; - let buf = path::init()!; - for (let i = 0z; i < len(name); i += 1) { - path::push(&buf, name[i])!; - }; - return strings::dup(path::string(&buf)); -}; - -@test fn identpath() void = { - let ident: ast::ident = ["foo", "bar", "baz"]; - let p = identpath(ident); - defer free(p); - assert(p == "foo/bar/baz"); -}; - -// Joins an ident string with underscores instead of double colons. The return -// value must be freed by the caller. -// -// This is used for module names in environment variables and some file names. -export fn identuscore(ident: ast::ident) str = { - let buf = memio::dynamic(); - for (let i = 0z; i < len(ident); i += 1) { - fmt::fprintf(&buf, "{}{}", ident[i], - if (i + 1 < len(ident)) "_" - else "") as size; - }; - return memio::string(&buf)!; -}; - -@test fn identuscore() void = { - let ident: ast::ident = ["foo", "bar", "baz"]; - let p = identuscore(ident); - defer free(p); - assert(p == "foo_bar_baz"); -}; diff --git a/hare/module/deps.ha b/hare/module/deps.ha @@ -0,0 +1,204 @@ +use bufio; +use fmt; +use fs; +use hare::ast; +use hare::lex; +use hare::parse; +use hare::unparse; +use io; +use memio; +use os; +use path; +use sort; +use strings; + +// A hare module. +export type module = struct { + name: str, + ns: ast::ident, + path: str, + srcs: srcset, + deps: [](size, ast::ident), +}; + +// Get the list of dependencies referred to by a set of source files. +// The list will be sorted alphabetically and deduplicated. +export fn parse_deps(files: str...) ([]ast::ident | error) = { + let deps: []ast::ident = []; + for (let i = 0z; i < len(files); i += 1) { + let handle = match (os::open(files[i])) { + case let f: io::file => + yield f; + case let e: fs::error => + return attach(strings::dup(files[i]), e); + }; + defer io::close(handle)!; + + let lexer = lex::init(handle, files[i]); + defer lex::finish(&lexer); + let imports = parse::imports(&lexer)?; + defer ast::imports_finish(imports); + + // dedupe + insertion sort + for (let i = 0z; i < len(imports); i += 1) { + let id = imports[i].ident; + let idx = sort::rbisect(deps, size(ast::ident), &id, &idcmp); + if (idx == 0 || idcmp(&deps[idx - 1], &id) != 0) { + insert(deps[idx], ast::ident_dup(id)); + }; + }; + }; + return deps; +}; + +fn idcmp(a: const *opaque, b: const *opaque) int = { + const a = a: const *ast::ident, b = b: const *ast::ident; + for (let i = 0z; i < len(a) && i < len(b); i += 1) { + let cmp = strings::compare(a[i], b[i]); + if (cmp != 0) { + return cmp; + }; + }; + if (len(a) < len(b)) { + return -1; + } else if (len(a) == len(b)) { + return 0; + } else { + return 1; + }; +}; + +// Get the dependencies for a module from the cache, recalculating +// them if necessary. cachedir should be calculated with [[get_cache]], +// and srcset should be calculated with [[find]]. +fn get_deps(cachedir: str, srcs: *srcset) ([]ast::ident | error) = { + static let buf = path::buffer{...}; + path::set(&buf, cachedir, "deps")?; + let rest = memio::fixed(buf.buf[buf.end..]); + buf.end += format_tags(&rest, srcs.seentags)?; + buf.end += memio::concat(&rest, ".txt")?; + + let outofdate = outdated(path::string(&buf), srcs.ha, srcs.mtime); + os::mkdirs(cachedir, 0o755)?; + let depsfile = os::create(path::string(&buf), 0o644, fs::flag::RDWR)?; + defer io::close(depsfile)!; + io::lock(depsfile, true, io::lockop::EXCLUSIVE)?; + + let deps: []ast::ident = []; + if (outofdate) { + deps = parse_deps(srcs.ha...)?; + io::trunc(depsfile, 0)?; + let out = bufio::init(depsfile, [], buf.buf); + for (let i = 0z; i < len(deps); i += 1) { + unparse::ident(&out, deps[i])?; + fmt::fprintln(&out)?; + }; + } else { + let in = bufio::newscanner_static(depsfile, buf.buf); + for (true) match (bufio::scan_line(&in)?) { + case io::EOF => + break; + case let s: const str => + append(deps, parse::identstr(s)?); + }; + }; + return deps; +}; + +// Gather a [[module]] and all its dependencies, appending them to an existing +// slice, deduplicated, in reverse topological order, returning the index of the +// input module within the slice. Dependencies will also be written to the +// cache. +export fn gather( + ctx: *context, + out: *[]module, + mod: location, +) (size | error) = { + let stack: []str = []; + defer free(stack); + return _gather(ctx, out, &stack, mod)?; +}; + +fn _gather( + ctx: *context, + out: *[]module, + stack: *[]str, + mod: location, +) (size | error) = { + let (modpath, srcs) = match (find(ctx, mod)) { + case let r: (str, srcset) => + yield r; + case let e: error => + let e = attach(locstr(mod), e); + if (len(stack) == 0) { + return e; + }; + return attach(strings::dup(stack[len(stack) - 1]), e); + }; + modpath = strings::dup(modpath); + defer free(modpath); + + for (let j = 0z; j < len(stack); j += 1) { + if (modpath == stack[j]) { + append(stack, modpath); + return strings::dupall(stack[j..]): dep_cycle; + }; + }; + for (let j = 0z; j < len(out); j += 1) { + if (modpath == out[j].path) { + return j; + }; + }; + append(stack, modpath); + defer delete(stack[len(stack) - 1]); + + let cache = get_cache(ctx.harecache, modpath)?; + let depids = get_deps(cache, &srcs)?; + defer free(depids); + let deps: [](size, ast::ident) = alloc([], len(depids)); + for (let i = 0z; i < len(depids); i += 1) { + static append(deps, + (_gather(ctx, out, stack, depids[i])?, depids[i]) + ); + }; + + append(out, module { + name = match (mod) { + case let mod: *path::buffer => + yield strings::dup(path::string(mod)); + case let mod: ast::ident => + yield unparse::identstr(mod); + }, + ns = match (mod) { + case let mod: *path::buffer => + yield []; + case let mod: ast::ident => + yield ast::ident_dup(mod); + }, + path = strings::dup(modpath), + srcs = srcs, + deps = deps, + }); + return len(out) - 1; +}; + +// Free the resources associated with a [[module]]. +export fn finish(mod: *module) void = { + free(mod.name); + ast::ident_free(mod.ns); + free(mod.path); + finish_srcset(&mod.srcs); + for (let i = 0z; i < len(mod.deps); i += 1) { + ast::ident_free(mod.deps[i].1); + }; + free(mod.deps); +}; + +// Free all the [[module]]s in a slice of modules, and then the slice itself. +export fn free_slice(mods: []module) void = { + for (let i = 0z; i < len(mods); i += 1) { + finish(&mods[i]); + }; + free(mods); +}; + diff --git a/hare/module/format.ha b/hare/module/format.ha @@ -0,0 +1,65 @@ +use time::date; +use fmt; +use hare::unparse; +use io; +use time::chrono; + +// Formats a set of tags to an [[io::handle]] in "+tag1-tag2" format. +export fn format_tags( + out: io::handle, + tags: ([]str | []tag), +) (size | io::error) = { + let n = 0z; + match (tags) { + case let tags: []str => + for (let i = 0z; i < len(tags); i += 1) { + n += fmt::fprintf(out, "+{}", tags[i])?; + }; + case let tags: []tag => + for (let i = 0z; i < len(tags); i += 1) { + n += fmt::fprintf( + out, + if (tags[i].include) "+{}" else "-{}", + tags[i].name)?; + }; + }; + return n; +}; + +// Formats a [[srcset]] to an [[io::handle]]. +export fn format_srcset(out: io::handle, srcs: *srcset) (size | io::error) = { + let n = 0z; + n += fmt::fprint(out, "relevant tags: ")?; + n += format_tags(out, srcs.seentags)?; + n += fmt::fprintln(out)?; + const dt = date::from_instant(time::chrono::LOCAL, srcs.mtime); + n += date::format(out, "last change to source list: %F %T\n", &dt)?; + n += fmt::fprintln(out, "hare sources:")?; + for (let i = 0z; i < len(srcs.ha); i += 1) { + n += fmt::fprintln(out, " ", srcs.ha[i])?; + }; + n += fmt::fprintln(out, "assembly sources:")?; + for (let i = 0z; i < len(srcs.s); i += 1) { + n += fmt::fprintln(out, " ", srcs.s[i])?; + }; + n += fmt::fprintln(out, "object sources:")?; + for (let i = 0z; i < len(srcs.o); i += 1) { + n += fmt::fprintln(out, " ", srcs.o[i])?; + }; + return n; +}; + +// Formats a [[module]] to an [[io::handle]]. +export fn format(out: io::handle, mod: *module) (size | io::error) = { + let n = 0z; + n += fmt::fprintln(out, "module:", mod.name)?; + n += fmt::fprintln(out, "path:", mod.path)?; + n += format_srcset(out, &mod.srcs)?; + n += fmt::fprintln(out, "dependencies:")?; + for (let i = 0z; i < len(mod.deps); i += 1) { + n += fmt::fprint(out, " ")?; + n += unparse::ident(out, mod.deps[i].1)?; + n += fmt::fprintln(out)?; + }; + return n; +}; diff --git a/hare/module/manifest.ha b/hare/module/manifest.ha @@ -1,403 +0,0 @@ -// License: MPL-2.0 -// (c) 2021-2022 Alexey Yerin <yyp@disroot.org> -// (c) 2021 Bor Grošelj Simić <bor.groseljsimic@telemach.net> -// (c) 2021 Drew DeVault <sir@cmpwn.com> -// (c) 2021 Ember Sawady <ecs@d2evs.net> -// (c) 2021 Thomas Bracht Laumann Jespersen <t@laumann.xyz> -use bufio; -use bytes; -use encoding::hex; -use encoding::utf8; -use errors; -use fmt; -use fs; -use hare::ast; -use hare::unparse; -use io; -use os; -use path; -use strconv; -use strings; -use time; -use temp; - -// The manifest file format is a series of line-oriented records. Lines starting -// with # are ignored. -// -// - "version" indicates the manifest format version, currently 2. -// - "input" is an input file, and its fields are the file hash, path, inode, -// and mtime as a Unix timestamp. -// - "module" is a version of a module, and includes the module hash and the set -// of input hashes which produce it. -// - "tags" is a list of tags associated with a module version - -def VERSION: int = 2; - -fn getinput(in: []input, hash: []u8) nullable *input = { - for (let i = 0z; i < len(in); i += 1) { - if (bytes::equal(in[i].hash, hash)) { - return &in[i]; - }; - }; - return null; -}; - -// Loads the module manifest from the build cache for the given ident. The -// return value borrows the ident parameter. If the module is not found, an -// empty manifest is returned. -export fn manifest_load(ctx: *context, ident: ast::ident) (manifest | error) = { - let manifest = manifest { - ident = ident, - inputs = [], - versions = [], - }; - let ipath = identpath(manifest.ident); - defer free(ipath); - let cachedir = path::init(ctx.cache, ipath)!; - let cachedir = path::string(&cachedir); - - let mpath = path::init(cachedir, "manifest")!; - let mpath = path::string(&mpath); - - let truefile = match (fs::open(ctx.fs, mpath, fs::flag::RDONLY)) { - case errors::noentry => - return manifest; - case let err: fs::error => - return err; - case let file: io::handle => - yield file; - }; - defer io::close(truefile)!; - - let inputs: []input = [], versions: []version = []; - - let buf: [4096]u8 = [0...]; - let file = bufio::init(truefile, buf, []); - for (true) { - let line = match (bufio::scanline(&file)) { - case io::EOF => - break; - case let err: io::error => - return err; - case let line: []u8 => - yield line; - }; - defer free(line); - - let line = match (strings::fromutf8(line)) { - case utf8::invalid => - // Treat an invalid manifest as empty - return manifest; - case let s: str => - yield s; - }; - - if (strings::hasprefix(line, "#")) { - continue; - }; - - let tok = strings::tokenize(line, " "); - let kind = match (strings::next_token(&tok)) { - case void => - continue; - case let s: str => - yield s; - }; - - switch (kind) { - case "version" => - let ver = match (strings::next_token(&tok)) { - case void => - return manifest; - case let s: str => - yield s; - }; - match (strconv::stoi(ver)) { - case let v: int => - if (v != VERSION) { - return manifest; - }; - case => - return manifest; - }; - case "input" => - let hash = match (strings::next_token(&tok)) { - case void => - return manifest; - case let s: str => - yield s; - }, path = match (strings::next_token(&tok)) { - case void => - return manifest; - case let s: str => - yield s; - }, inode = match (strings::next_token(&tok)) { - case void => - return manifest; - case let s: str => - yield s; - }, mtime = match (strings::next_token(&tok)) { - case void => - return manifest; - case let s: str => - yield s; - }; - - let hash = match (hex::decodestr(hash)) { - case let b: []u8 => - yield b; - case => - return manifest; - }; - let inode = match (strconv::stoz(inode)) { - case let z: size => - yield z; - case => - return manifest; - }; - let mtime = match (strconv::stoi64(mtime)) { - case let i: i64 => - yield time::from_unix(i); - case => - return manifest; - }; - - let parsed = parsename(path); - let ftype = match (type_for_ext(path)) { - case void => - return manifest; - case let ft: filetype => - yield ft; - }; - - append(inputs, input { - hash = hash, - path = strings::dup(path), - ft = ftype, - stat = fs::filestat { - mask = fs::stat_mask::MTIME | fs::stat_mask::INODE, - mtime = mtime, - inode = inode, - ... - }, - basename = strings::dup(parsed.0), - tags = parsed.2, - }); - case "module" => - let modhash = match (strings::next_token(&tok)) { - case void => - return manifest; - case let s: str => - yield s; - }; - let modhash = match (hex::decodestr(modhash)) { - case let b: []u8 => - yield b; - case => - return manifest; - }; - - let minputs: []input = []; - for (true) { - let hash = match (strings::next_token(&tok)) { - case void => - break; - case let s: str => - yield s; - }; - let hash = match (hex::decodestr(hash)) { - case let b: []u8 => - yield b; - case => - return manifest; - }; - defer free(hash); - - let input = match (getinput(inputs, hash)) { - case null => - return manifest; - case let i: *input => - yield i; - }; - append(minputs, *input); - }; - - append(versions, version { - hash = modhash, - inputs = minputs, - ... - }); - case "tags" => - let modhash = match (strings::next_token(&tok)) { - case void => - return manifest; - case let s: str => - yield s; - }; - let modhash = match (hex::decodestr(modhash)) { - case let b: []u8 => - yield b; - case => - return manifest; - }; - - const tags = strings::remaining_tokens(&tok); - const tags = parsetags(tags) as []tag; - let found = false; - for (let i = 0z; i < len(versions); i += 1) { - if (bytes::equal(versions[i].hash, modhash)) { - versions[i].tags = tags; - found = true; - break; - }; - }; - // Implementation detail: tags always follows module - // directive for a given module version - assert(found); - - // Drain tokenizer - for (strings::next_token(&tok) is str) void; - case => - return manifest; - }; - - // Check for extra tokens - match (strings::next_token(&tok)) { - case void => void; - case str => - return manifest; - }; - }; - - manifest.inputs = inputs; - manifest.versions = versions; - return manifest; -}; - -// Returns true if the desired module version is present and current in this -// manifest. -export fn current(man: *manifest, ver: *version) bool = { - // TODO: This is kind of dumb. What we really need to do is: - // 1. Update scan to avoid hashing the file if a manifest is present, - // and indicate that the hash is cached somewhere in the type. Get an - // up-to-date stat. - // 2. In [current], test if the inode and mtime are equal to the - // manifest version. If so, presume the file is up-to-date. If not, - // check the hash and update the manifest to the new inode/mtime if - // the hash matches. If not, the module is not current; rebuild. - let cached: nullable *version = null; - for (let i = 0z; i < len(man.versions); i += 1) { - if (bytes::equal(man.versions[i].hash, ver.hash)) { - cached = &man.versions[i]; - break; - }; - }; - let cached = match (cached) { - case null => - return false; - case let v: *version => - yield v; - }; - - assert(len(cached.inputs) == len(ver.inputs)); - for (let i = 0z; i < len(cached.inputs); i += 1) { - let a = cached.inputs[i], b = cached.inputs[i]; - assert(a.path == b.path); - let ast = a.stat, bst = b.stat; - if (ast.inode != bst.inode - || time::compare(ast.mtime, bst.mtime) != 0) { - return false; - }; - }; - return true; -}; - -// Writes a module manifest to the build cache. -export fn manifest_write(ctx: *context, man: *manifest) (void | error) = { - let ipath = identpath(man.ident); - defer free(ipath); - let cachedir = path::init(ctx.cache, ipath)!; - let cachedir = path::string(&cachedir); - - let mpath = path::init(cachedir, "manifest")!; - let mpath = path::string(&mpath); - - let (truefile, name) = temp::named(ctx.fs, cachedir, io::mode::WRITE, 0o644)?; - let wbuf: [os::BUFSZ]u8 = [0...]; - let file = &bufio::init(truefile, [], wbuf); - defer { - bufio::flush(file)!; - fs::remove(ctx.fs, name): void; - io::close(truefile)!; - }; - - let ident = unparse::identstr(man.ident); - defer free(ident); - fmt::fprintfln(file, "# {}", ident)?; - fmt::fprintln(file, "# This file is an internal Hare implementation detail.")?; - fmt::fprintln(file, "# The format is not stable.")?; - fmt::fprintfln(file, "version {}", VERSION)?; - for (let i = 0z; i < len(man.inputs); i += 1) { - const input = man.inputs[i]; - let hash = hex::encodestr(input.hash); - defer free(hash); - - const want = fs::stat_mask::INODE | fs::stat_mask::MTIME; - assert(input.stat.mask & want == want); - fmt::fprintfln(file, "input {} {} {} {}", - hash, input.path, input.stat.inode, - time::unix(input.stat.mtime))?; - }; - - for (let i = 0z; i < len(man.versions); i += 1) { - const ver = man.versions[i]; - let hash = hex::encodestr(ver.hash); - defer free(hash); - - fmt::fprintf(file, "module {}", hash)?; - - for (let j = 0z; j < len(ver.inputs); j += 1) { - let hash = hex::encodestr(ver.inputs[i].hash); - defer free(hash); - - fmt::fprintf(file, " {}", hash)?; - }; - - fmt::fprintln(file)?; - - fmt::fprintf(file, "tags {} ", hash)?; - for (let i = 0z; i < len(ver.tags); i += 1) { - const tag = &ver.tags[i]; - fmt::fprintf(file, "{}{}", - switch (tag.mode) { - case tag_mode::INCLUSIVE => - yield "+"; - case tag_mode::EXCLUSIVE => - yield "-"; - }, - tag.name)?; - }; - fmt::fprintln(file)!; - }; - - fs::move(ctx.fs, name, mpath)?; -}; - -fn input_finish(in: *input) void = { - free(in.hash); - free(in.path); - free(in.basename); - tags_free(in.tags); -}; - -// Frees resources associated with this manifest. -export fn manifest_finish(m: *manifest) void = { - for (let i = 0z; i < len(m.inputs); i += 1) { - input_finish(&m.inputs[i]); - }; - - for (let i = 0z; i < len(m.versions); i += 1) { - free(m.versions[i].inputs); - tags_free(m.versions[i].tags); - }; -}; diff --git a/hare/module/scan.ha b/hare/module/scan.ha @@ -1,495 +0,0 @@ -// License: MPL-2.0 -// (c) 2021-2022 Alexey Yerin <yyp@disroot.org> -// (c) 2022 Bor Grošelj Simić <bor.groseljsimic@telemach.net> -// (c) 2021-2022 Drew DeVault <sir@cmpwn.com> -// (c) 2021 Ember Sawady <ecs@d2evs.net> -// (c) 2021 Kiëd Llaentenn <kiedtl@tilde.team> -// (c) 2021 Thomas Bracht Laumann Jespersen <t@laumann.xyz> -use ascii; -use crypto::sha256; -use fs; -use hare::ast; -use hare::lex; -use hare::parse; -use hash; -use io; -use memio; -use path; -use sort; -use strings; -use bufio; -use os; - -def ABI_VERSION: u8 = 6; - -// Scans the files in a directory for eligible build inputs and returns a -// [[version]] which includes all applicable files and their dependencies. -export fn scan(ctx: *context, path: str) (version | error) = { - // TODO: Incorporate defines into the hash - let sha = sha256::sha256(); - for (let i = 0z; i < len(ctx.tags); i += 1) { - const tag = &ctx.tags[i]; - hash::write(&sha, if (tag.mode == tag_mode::INCLUSIVE) { - yield [1]; - } else { - yield [0]; - }); - hash::write(&sha, strings::toutf8(tag.name)); - }; - let iter = match (fs::iter(ctx.fs, path)) { - case fs::wrongtype => - // Single file case - let inputs: []input = []; - let deps: []ast::ident = []; - let ft = match (type_for_ext(path)) { - case void => - return notfound; - case let ft: filetype => - yield ft; - }; - let path = fs::resolve(ctx.fs, path); - let st = fs::stat(ctx.fs, path)?; - let in = input { - path = strings::dup(path), - stat = st, - ft = ft, - hash = scan_file(ctx, path, ft, &deps)?, - ... - }; - append(inputs, in); - - let sumbuf: [sha256::SZ]u8 = [0...]; - hash::write(&sha, in.hash); - hash::sum(&sha, sumbuf); - - return version { - hash = sumbuf, - basedir = strings::dup(path::dirname(path)), - depends = deps, - inputs = inputs, - tags = tags_dup(ctx.tags), - ... - }; - case let err: fs::error => - return err; - case let iter: *fs::iterator => - yield iter; - }; - defer fs::finish(iter); - let ver = version { - basedir = strings::dup(path), - tags = tags_dup(ctx.tags), - ... - }; - scan_directory(ctx, &ver, &sha, path, iter)?; - - let buf = path::init(path, "README")!; - if (len(ver.inputs) == 0 && !fs::exists(ctx.fs, path::string(&buf))) { - // TODO: HACK: README is a workaround for haredoc issues - return notfound; - }; - - let tmp: [sha256::SZ]u8 = [0...]; - hash::sum(&sha, tmp); - ver.hash = alloc([], sha.sz); - append(ver.hash, tmp...); - return ver; -}; - -// Given a file or directory name, parses it into the basename, extension, and -// tag set. -export fn parsename(name: str) (str, str, []tag) = { - static let buf = path::buffer {...}; - path::set(&buf, name)!; - let ext = match (path::pop_ext(&buf)) { - case void => yield ""; - case let s: str => yield strings::dup(s); - }; - let base = match (path::peek(&buf)) { - case void => yield ""; - case let s: str => yield strings::dup(s); - }; - - let p = strings::index(base, '+'); - let m = strings::index(base, '-'); - if (p is void && m is void) { - return (base, ext, []); - }; - let i: size = - if (p is void && m is size) m: size - else if (m is void && p is size) p: size - else if (m: size < p: size) m: size - else p: size; - let tags = strings::sub(base, i, strings::end); - let tags = match (parsetags(tags)) { - case void => - return (base, ext, []); - case let t: []tag => - yield t; - }; - let base = strings::sub(base, 0, i); - return (base, ext, tags); -}; - -fn scan_directory( - ctx: *context, - ver: *version, - sha: *hash::hash, - path: str, - iter: *fs::iterator, -) (void | error) = { - let files: []str = [], dirs: []str = []; - defer { - strings::freeall(files); - strings::freeall(dirs); - }; - - let pathbuf = path::init()!; - for (true) { - const ent = match (fs::next(iter)) { - case void => - break; - case let ent: fs::dirent => - yield ent; - }; - - switch (ent.ftype) { - case fs::mode::LINK => - let linkpath = path::set(&pathbuf, path, ent.name)!; - linkpath = fs::readlink(ctx.fs, linkpath)?; - if (!path::abs(linkpath)) { - linkpath = path::set(&pathbuf, path, linkpath)!; - }; - - const st = fs::stat(ctx.fs, linkpath)?; - if (fs::isfile(st.mode)) { - append(files, strings::dup(ent.name)); - } else if (fs::isdir(st.mode)) { - append(dirs, strings::dup(ent.name)); - } else if (fs::islink(st.mode)) { - abort(); // TODO: Resolve recursively - }; - case fs::mode::DIR => - append(dirs, strings::dup(ent.name)); - case fs::mode::REG => - append(files, strings::dup(ent.name)); - case => void; - }; - }; - - // Sorted to keep the hash consistent - sort::strings(dirs); - sort::strings(files); - - // Tuple of is_directory, basename, tags, and path to a candidate input. - let inputs: [](bool, str, []tag, str) = []; - defer for (let i = 0z; i < len(inputs); i += 1) { - // For file paths, these are assigned to the input, which - // assumes ownership over them. - if (inputs[i].0) { - free(inputs[i].1); - tags_free(inputs[i].2); - free(inputs[i].3); - }; - }; - - // For a given basename, only the most specific path (i.e. with the most - // tags) is used. - // - // foo.ha - // foo+linux.ha - // foo+linux+x86_64/ - // bar.ha - // baz.ha - // - // In this case, foo+linux+x86_64 is the most specific, and so its used - // as the build input and the other two files are discarded. - - for (let i = 0z; i < len(dirs); i += 1) { - let name = dirs[i]; - let parsed = parsename(name); - let base = parsed.0, tags = parsed.2; - - if (!strings::hasprefix(name, "+") - && !strings::hasprefix(name, "-")) { - if (!strings::hasprefix(name, ".")) { - append(ver.subdirs, strings::dup(name)); - }; - continue; - }; - if (!tagcompat(ctx.tags, tags)) { - continue; - }; - - const buf = path::init(path, name)!; - let path = strings::dup(path::string(&buf)); - let tuple = (true, strings::dup(base), tags, path); - let superceded = false; - for (let j = 0z; j < len(inputs); j += 1) { - if (inputs[j].1 != base) { - continue; - }; - let theirs = inputs[j].2; - if (len(theirs) < len(tags)) { - free(inputs[j].1); - tags_free(inputs[j].2); - free(inputs[j].3); - inputs[j] = tuple; - superceded = true; - break; - } else if (len(theirs) > len(tags)) { - // They are more specific - superceded = true; - break; - } else if (len(base) != 0) { - return (path, inputs[j].3): ambiguous; - }; - }; - if (!superceded) { - append(inputs, tuple); - }; - }; - - for (let i = 0z; i < len(files); i += 1) { - let name = files[i]; - let parsed = parsename(name); - let base = parsed.0, ext = parsed.1, tags = parsed.2; - - let eligible = false; - static const exts = ["ha", "s"]; - for (let i = 0z; i < len(exts); i += 1) { - if (exts[i] == ext) { - eligible = true; - break; - }; - }; - if (!eligible || !tagcompat(ctx.tags, tags)) { - tags_free(tags); - continue; - }; - - const buf = path::init(path, name)!; - let path = strings::dup(path::string(&buf)); - let tuple = (false, strings::dup(base), tags, path); - let superceded = false; - for (let j = 0z; j < len(inputs); j += 1) { - if (inputs[j].1 != base) { - continue; - }; - let theirs = inputs[j].2; - if (len(theirs) < len(tags)) { - // We are more specific - free(inputs[j].1); - tags_free(inputs[j].2); - free(inputs[j].3); - inputs[j] = tuple; - superceded = true; - break; - } else if (len(theirs) > len(tags)) { - // They are more specific - superceded = true; - break; - } else if (len(base) != 0) { - return (path, inputs[j].3): ambiguous; - }; - }; - if (!superceded) { - append(inputs, tuple); - }; - }; - - for (let i = 0z; i < len(inputs); i += 1) { - let isdir = inputs[i].0, path = inputs[i].3; - if (isdir) { - let iter = fs::iter(ctx.fs, path)?; - defer fs::finish(iter); - scan_directory(ctx, ver, sha, path, iter)?; - } else { - let path = fs::resolve(ctx.fs, path); - let st = fs::stat(ctx.fs, path)?; - let ftype = type_for_ext(path) as filetype; - let in = input { - path = strings::dup(path), - stat = st, - ft = ftype, - hash = scan_file(ctx, path, ftype, &ver.depends)?, - basename = inputs[i].1, - tags = inputs[i].2, - ... - }; - append(ver.inputs, in); - hash::write(sha, in.hash); - }; - }; -}; - -// Looks up a module by its identifier from HAREPATH, and returns a [[version]] -// which includes all eligible build inputs. -export fn lookup(ctx: *context, name: ast::ident) (version | error) = { - let ipath = identpath(name); - defer free(ipath); - for (let i = len(ctx.paths); i > 0; i -= 1) { - let cand = path::init(ctx.paths[i - 1], ipath)!; - match (scan(ctx, path::string(&cand))) { - case let v: version => - return v; - case error => void; - }; - }; - return notfound; -}; - -fn type_for_ext(name: str) (filetype | void) = { - static let buf = path::buffer {...}; - path::set(&buf, name)!; - match (path::peek_ext(&buf)) { - case let ext: str => - switch (ext) { - case "ha" => return filetype::HARE; - case "s" => return filetype::ASSEMBLY; - case => void; - }; - case => void; - }; -}; - -fn scan_file( - ctx: *context, - path: str, - ftype: filetype, - deps: *[]ast::ident, -) ([]u8 | error) = { - let truef = fs::open(ctx.fs, path)?; - defer io::close(truef)!; - let rbuf: [os::BUFSZ]u8 = [0...]; - let f = &bufio::init(truef, rbuf, []); - let sha = sha256::sha256(); - hash::write(&sha, strings::toutf8(path)); - hash::write(&sha, [ABI_VERSION]); - - if (ftype == filetype::HARE) { - let tee = io::tee(f, &sha); - let lexer = lex::init(&tee, path); - defer lex::finish(&lexer); - let imports = match (parse::imports(&lexer)) { - case let im: []ast::import => - yield im; - case let err: parse::error => - return err; - }; - for (let i = 0z; i < len(imports); i += 1) { - if (!have_ident(deps, imports[i].ident)) { - append(deps, imports[i].ident); - }; - }; - // Finish spooling out the file for the SHA - match (io::copy(io::empty, &tee)) { - case size => void; - case let err: io::error => - return err; - }; - } else { - match (io::copy(&sha, f)) { - case size => void; - case let err: io::error => - return err; - }; - }; - - let tmp: [sha256::SZ]u8 = [0...]; - hash::sum(&sha, tmp); - - let checksum: []u8 = alloc([], sha.sz); - append(checksum, tmp...); - return checksum; -}; - -fn have_ident(sl: *[]ast::ident, id: ast::ident) bool = { - for (let i = 0z; i < len(sl); i += 1) { - if (ast::ident_eq(sl[i], id)) { - return true; - }; - }; - return false; -}; - -// Parses a set of build tags, returning void if the string is an invalid tag -// set. The caller must free the return value with [[tags_free]]. -export fn parsetags(in: str) ([]tag | void) = { - let tags: []tag = []; - let iter = strings::iter(in); - for (true) { - let t = tag { ... }; - let m = match (strings::next(&iter)) { - case void => - break; - case let r: rune => - yield r; - }; - t.mode = switch (m) { - case => - tags_free(tags); - return; - case '+' => - yield tag_mode::INCLUSIVE; - case '-' => - yield tag_mode::EXCLUSIVE; - }; - let buf = memio::dynamic(); - for (true) match (strings::next(&iter)) { - case void => - break; - case let r: rune => - if (ascii::isalnum(r) || r == '_') { - memio::appendrune(&buf, r)!; - } else { - strings::prev(&iter); - break; - }; - }; - t.name = memio::string(&buf)!; - append(tags, t); - }; - return tags; -}; - -// Frees a set of tags. -export fn tags_free(tags: []tag) void = { - for (let i = 0z; i < len(tags); i += 1) { - free(tags[i].name); - }; - free(tags); -}; - -// Duplicates a set of tags. -export fn tags_dup(tags: []tag) []tag = { - let new: []tag = alloc([], len(tags)); - for (let i = 0z; i < len(tags); i += 1) { - append(new, tag { - name = strings::dup(tags[i].name), - mode = tags[i].mode, - }); - }; - return new; -}; - -// Compares two tag sets and tells you if they are compatible. -export fn tagcompat(have: []tag, want: []tag) bool = { - // XXX: O(n²), lame - for (let i = 0z; i < len(want); i += 1) { - let present = false; - for (let j = 0z; j < len(have); j += 1) { - if (have[j].name == want[i].name) { - present = have[j].mode == tag_mode::INCLUSIVE; - break; - }; - }; - switch (want[i].mode) { - case tag_mode::INCLUSIVE => - if (!present) return false; - case tag_mode::EXCLUSIVE => - if (present) return false; - }; - }; - return true; -}; diff --git a/hare/module/srcs.ha b/hare/module/srcs.ha @@ -0,0 +1,363 @@ +use bytes; +use fs; +use hare::ast; +use os; +use path; +use sort; +use strings; +use time; + +// A file tag, e.g. +x86_64, or -libc. +export type tag = struct { + // The name of the tag. + name: str, + // Whether the tag is inclusive (+tag) or not (-tag). + include: bool, +}; + +// A set of sources for a module, filtered by a set of tags. +export type srcset = struct { + // The last time the list of source files changed. Note that this is not + // the last time that the source files themselves changed. + mtime: time::instant, + // Source directories traversed while finding these source files. + dirs: []str, + // The list of tags that were actually encountered while finding these + // source files. These are sorted alphabetically, and are the set of + // tags that should be used to find this module in the cache. + seentags: []str, + // hare source files (.ha) + ha: []str, + // assembly source files (.s) + s: []str, + // object source files (.o) + o: []str, + // linker scripts (.sc) + sc: []str, +}; + +// Frees the resources associated with a [[srcset]]. +export fn finish_srcset(srcs: *srcset) void = { + strings::freeall(srcs.dirs); + strings::freeall(srcs.seentags); + strings::freeall(srcs.ha); + strings::freeall(srcs.s); + strings::freeall(srcs.o); + strings::freeall(srcs.sc); +}; + +// Find the on-disk path and set of source files for a given module. The path is +// statically allocated and may be overwritten on subsequent calls. +export fn find(ctx: *context, loc: location) ((str, srcset) | error) = { + match (loc) { + case let buf: *path::buffer => + return (path::string(buf), path_find(ctx, buf)?); + case let mod: ast::ident => + let tok = strings::tokenize(ctx.harepath, ":"); + let next: (str | void) = "."; + for (next is str; next = strings::next_token(&tok)) { + if (!os::exists(next as str)) { + continue; + }; + + static let buf = path::buffer { ... }; + path::set(&buf, os::realpath(next as str)?)?; + for (let i = 0z; i < len(mod); i += 1) { + path::push(&buf, mod[i])?; + }; + + match (path_find(ctx, &buf)) { + case let s: srcset => + return (path::string(&buf), s); + case not_found => void; + case let e: error => + return e; + }; + }; + return not_found; + }; +}; + +fn path_find(ctx: *context, buf: *path::buffer) (srcset | error) = { + // list of sources to return, with 3 extra fields prepended to allow + // quick lookup and comparison. each item is e.g.: + // ("basename", "ha", 2 (# of tags), ["path/-tag1/basename+tag2.ha"]) + // if len(srcs.3) != 1 at the end of _findsrcs() then there's a conflict + let srcs: [](str, str, size, []str) = []; + defer { + for (let i = 0z; i < len(srcs); i += 1) { + free(srcs[i].0); + free(srcs[i].1); + free(srcs[i].3); + }; + free(srcs); + }; + let mtime = time::INSTANT_MIN; + let res = srcset { mtime = time::INSTANT_MIN, ... }; + + _findsrcs(buf, ctx.tags, &srcs, &res, 0)?; + for (let i = 0z; i < len(srcs); i += 1) { + if (len(srcs[i].3) != 1) { + return alloc(srcs[i].3...): file_conflict; + }; + let out = switch (srcs[i].1) { + case "ha" => + yield &res.ha; + case "s" => + yield &res.s; + case "o" => + yield &res.o; + case "sc" => + yield &res.sc; + case => abort(); + }; + append(out, srcs[i].3[0]); + }; + + // module needs either a hare source file or a README in order to be + // valid. used to allow eg. shadowing foo::bar:: without accidentally + // shadowing foo:: + if (len(res.ha) == 0) { + path::push(buf, "README")?; + defer path::pop(buf); + if (!os::exists(path::string(buf))) { + finish_srcset(&res); + return not_found; + }; + }; + + sort::strings(res.dirs); + sort::strings(res.ha); + sort::strings(res.s); + sort::strings(res.o); + sort::strings(res.sc); + return res; +}; + +// simple implementation but the reasons for it are delicate +// +// finding the right sources is conceptually simple: just collect all the +// files compatible with the tagset and then pick the best ones for each +// conflicting filename +// +// the mtime is the first weird part: you want to find the last time a file +// was added, moved, or deleted, but only for parts of the module relevant to +// the input tags. the edge-case here is "what if i renamed a subdirectory so +// that it's tags don't match". the solution is that you can just find the +// latest mtime of directories that get traversed, i.e. have matching tags. +// this is because the tag-compatible subset of the filetree constitutes +// a filetree in its own right, where a file being renamed to no longer be +// part of the tag-filetree is equivalent to it being deleted from the +// tag-filetree. the mtime-checking does not distinguish between renames +// and deletions, so we get this for free by checking mtimes in the underlying +// filesystem +// +// the second weird part is the seentags: the goal here is finding the subset +// of the input tags which were actually used while finding the srcset, +// so that the cache can be reused for two sets of input tags which don't +// produce different srcsets. the method used here is just to take note of +// the tags which were encountered while traversing the tree, and not to +// continue down a file path beyond the first incompatible tag. exploring +// this method, you could look at e.g "mod/+linux/+x86_64.ha". you might think +// that this should, in theory, produce 4 different cache versions, since +// there are 2 tags, each of which has 2 states; and using the method here +// would produce only 3: none, +linux, and +linux+x86_64. however, there are +// actually only 2 options: none, and +linux+x86_64, and the method here adds +// one redundant slot for +linux. this is because either tag on their own +// doesn't change whether the path matches, only both together. in practice, +// the redundancy in the method used here will cause minimal overhead, because +// it's likely that you do actually have a file with just one of the tags +// somewhere else in your module, or else you would have combined them into +// one tag. in any case, the method used here is fast because it gets to stop +// searching as soon as it can +fn _findsrcs( + buf: *path::buffer, + in_tags: []str, + srcs: *[](str, str, size, []str), + res: *srcset, + tagdepth: size, +) (void | error) = { + const pathstr = path::string(buf); + const stat = match (os::stat(pathstr)) { + case let stat: fs::filestat => + yield stat; + case fs::error => + return; + }; + + let tmp = pathstr; + for (fs::islink(stat.mode)) { + if (time::compare(res.mtime, stat.mtime) < 0) { + res.mtime = stat.mtime; + }; + tmp = os::readlink(tmp)?; + stat = os::stat(tmp)?; + }; + + if (fs::isfile(stat.mode)) { + let ext = match (path::pop_ext(buf)) { + case void => + return; + case let ext: str => + yield ext; + }; + switch (ext) { + case "ha", "s", "o", "sc" => void; + case => + return; + }; + let filebytes = strings::toutf8(path::peek(buf) as str); + path::push_ext(buf, ext)?; + + let split = tagindex(filebytes); + let (base, tags) = ( + strings::fromutf8_unsafe(filebytes[..split]), + strings::fromutf8_unsafe(filebytes[split..]), + ); + + let wanttags = match (parse_tags(tags)) { + case let tags: []tag => + yield tags; + case let e: error => + return attach(strings::dup(path::string(buf)), e); + }; + defer free(wanttags); + if (!seentags_compat(in_tags, wanttags, &res.seentags)) { + return; + }; + + let ntags = tagdepth + len(wanttags); + let bufstr = path::string(buf); + for (let i = 0z; i < len(srcs); i += 1) { + if (srcs[i].0 == base && srcs[i].1 == ext) { + if (srcs[i].2 > ntags) { + return; + }; + if (srcs[i].2 < ntags) { + srcs[i].2 = ntags; + strings::freeall(srcs[i].3); + srcs[i].3 = []; + }; + append(srcs[i].3, strings::dup(bufstr)); + return; + }; + }; + + append(srcs, ( + strings::dup(base), + strings::dup(ext), + ntags, + alloc([strings::dup(bufstr)]), + )); + return; + }; + + if (!fs::isdir(stat.mode)) return; // ignore special files + + append(res.dirs, strings::dup(pathstr)); + if (time::compare(res.mtime, stat.mtime) < 0) { + res.mtime = stat.mtime; + }; + + let iter = match (os::iter(pathstr)) { + case let i: *fs::iterator => + yield i; + case let e: fs::error => + return attach(strings::dup(pathstr), e); + }; + defer fs::finish(iter); + for (true) match (fs::next(iter)) { + case void => + break; + case let d: fs::dirent => + if (d.name == "." || d.name == "..") { + continue; + }; + path::push(buf, d.name)?; + defer path::pop(buf); + + if (fs::isdir(d.ftype)) { + if (tagindex(strings::toutf8(d.name)) != 0) { + continue; + }; + let wanttags = match (parse_tags(d.name)) { + case let tags: []tag => + yield tags; + case let e: error => + return attach(strings::dup(path::string(buf)), e); + }; + defer free(wanttags); + if (!seentags_compat(in_tags, wanttags, &res.seentags)) { + continue; + }; + + _findsrcs(buf, in_tags, srcs, res, + tagdepth+len(wanttags))?; + } else if (fs::isfile(d.ftype)) { + _findsrcs(buf, in_tags, srcs, res, tagdepth)?; + }; + }; +}; + +fn tagindex(bs: []u8) size = { + let i = 0z; + for (i < len(bs) && bs[i] != '+' && bs[i] != '-'; i += 1) void; + return i; +}; + +// Parses tags from a string. The tag themselves are borrowed from the input, +// but the caller must free the slice returned. +export fn parse_tags(s: str) ([]tag | error) = { + let bs = strings::toutf8(s); + if (bytes::contains(bs, '.')) { + return tag_has_dot; + }; + let tags: []tag = []; + let start = tagindex(bs); + if (start != 0) { + return tag_bad_format; + }; + for (start < len(bs)) { + const end = start + 1 + tagindex(bs[start+1..]); + append(tags, tag { + name = strings::fromutf8_unsafe(bs[start+1..end]), + include = bs[start] == '+', + }); + start = end; + }; + return tags; +}; + +// Checks if a set of tags are compatible with a tag requirement. +export fn tags_compat(have: []str, want: []tag) bool = { + for (let i = 0z; i < len(want); i += 1) { + let t = want[i]; + let found = false; + for (let j = 0z; j < len(have); j += 1) if (have[j] == t.name) { + found = true; + break; + }; + if (t.include ^^ found) { + return false; + }; + }; + return true; +}; + +// same as tags_compat, but also adds any relevant tags to a seentags list +// for use in _findsrcs. +fn seentags_compat(have: []str, want: []tag, seen: *[]str) bool = { + for (let i = 0z; i < len(want); i += 1) { + let t = want[i]; + let found = false; + for (let j = 0z; j < len(have); j += 1) if (have[j] == t.name) { + insert_uniq(seen, t.name); + found = true; + break; + }; + if (t.include ^^ found) { + return false; + }; + }; + return true; +}; diff --git a/hare/module/types.ha b/hare/module/types.ha @@ -1,90 +1,129 @@ -// License: MPL-2.0 -// (c) 2021 Drew DeVault <sir@cmpwn.com> -// (c) 2021 Ember Sawady <ecs@d2evs.net> +use encoding::utf8; use fs; use hare::ast; use hare::parse; +use hare::unparse; use io; -use fmt; +use memio; +use path; +use strings; -// The inclusive/exclusive state for a build tag. -export type tag_mode = enum { - INCLUSIVE, - EXCLUSIVE, -}; +// A module was not found. +export type not_found = !void; -// A build tag, e.g. +x86_64. -export type tag = struct { - name: str, - mode: tag_mode, -}; +// A tag contains a dot. +export type tag_has_dot = !void; -// The manifest for a particular module, with some number of inputs, and -// versions. -export type manifest = struct { - ident: ast::ident, - inputs: []input, - versions: []version, -}; +// Generic badly formatted tag error. +export type tag_bad_format = !void; -// A module version: a set of possible input files for that module. -export type version = struct { - hash: []u8, - basedir: str, - depends: []ast::ident, - inputs: []input, - subdirs: []str, - tags: []tag, -}; +// A dependency cycle error. +export type dep_cycle = ![]str; + +// Two files in a module have the same basename and extension, and the +// same number of compatible tags with the input tagset, so it is unknown +// which should be used. +export type file_conflict = ![]str; -// The filetype of a file within a module. -export type filetype = enum { - HARE, - ASSEMBLY, +// Context for another error. +export type errcontext = !(str, *error); + +// Tagged union of all possible error types. Must be freed with [[finish_error]] +// unless it's passed to [[strerror]]. +export type error = !( + fs::error | + io::error | + path::error | + parse::error | + utf8::invalid | + file_conflict | + not_found | + dep_cycle | + tag_has_dot | + tag_bad_format | + errcontext | +); + +// A container struct for context, used by [[gather]]. +export type context = struct { + harepath: str, + harecache: str, + tags: []str, }; -// An input to a module, generally a source file. -export type input = struct { - hash: []u8, - path: str, - ft: filetype, - stat: fs::filestat, +// The location of a module +export type location = (*path::buffer | ast::ident); - // Name without any tags - basename: str, - // Tags applicable to input - tags: []tag, +// Returns a string representation of a [[location]]. The result must be freed +// by the caller. +export fn locstr(loc: location) str = { + match (loc) { + case let buf: *path::buffer => + return strings::dup(path::string(buf)); + case let id: ast::ident => + return unparse::identstr(id); + }; }; -// The requested module could not be found. -export type notfound = !void; +// XXX: this shouldn't be necessary, the language should have some built-in way +// to carry context with errors +fn attach(ctx: str, e: error) errcontext = (ctx, alloc(e)): errcontext; -// We are unable to select from two ambiguous options for an input file. -export type ambiguous = !(str, str); +// Free the resources associated with an [[error]]. +export fn finish_error(e: error) void = { + match (e) { + case let e: dep_cycle => + strings::freeall(e); + case let e: file_conflict => + strings::freeall(e); + case let ctx: errcontext => + finish_error(*ctx.1); + free(ctx.0); + free(ctx.1); + case => void; + }; +}; -// All possible error types. -export type error = !( - fs::error | - io::error | - parse::error | - notfound | - ambiguous); +// Turns an [[error]] into a human-readable string. The result is +// statically allocated. Consumes the error. +export fn strerror(e: error) str = { + defer finish_error(e); + static let buf: [2*path::MAX]u8 = [0...]; + let buf = memio::fixed(buf[..]); + _strerror(e, &buf); + return memio::string(&buf)!; +}; -// Returns a human-friendly representation of an error. -export fn strerror(err: error) const str = { - // Should be more than enough for PATH_MAX * 2 - static let buf: [4096]u8 = [0...]; - match (err) { - case let err: fs::error => - return fs::strerror(err); - case let err: io::error => - return io::strerror(err); - case let err: parse::error => - return parse::strerror(err); - case notfound => - return "Module not found"; - case let amb: ambiguous => - return fmt::bsprintf(buf, "Cannot choose between {} and {}", - amb.0, amb.1); +fn _strerror(e: error, buf: *memio::stream) void = { + let s = match (e) { + case let e: fs::error => + yield fs::strerror(e); + case let e: io::error => + yield io::strerror(e); + case let e: parse::error => + yield parse::strerror(e); + case let e: path::error => + yield path::strerror(e); + case utf8::invalid => + yield "Invalid UTF-8"; + case not_found => + yield "Module not found"; + case tag_has_dot => + yield "Tag contains a '.'"; + case tag_bad_format => + yield "Bad tag format"; + case let e: dep_cycle => + memio::concat(buf, "Dependency cycle: ")!; + memio::join(buf, " -> ", e...)!; + return; + case let e: file_conflict => + memio::concat(buf, "File conflict: ")!; + memio::join(buf, ", ", e...)!; + return; + case let ctx: errcontext => + memio::concat(buf, ctx.0, ": ")!; + _strerror(*ctx.1, buf); + return; }; + memio::concat(buf, s)!; }; diff --git a/hare/module/util.ha b/hare/module/util.ha @@ -0,0 +1,70 @@ +use ascii; +use fs; +use os; +use strings; +use time; + +// insert a string into a sorted list of strings, deduplicated. +fn insert_uniq(into: *[]str, s: str) void = { + let i = 0z; + // XXX: could use binary search + for (i < len(into) && strings::compare(into[i], s) < 0) { + i += 1; + }; + if (i == len(into) || into[i] != s) { + insert(into[i], strings::dup(s)); + }; +}; + +// Checks if the file at 'target' is out-of-date, given a list of dependency +// files, and the last time the deps list changed. If "target" doesn't exist, +// returns true. If any of the deps don't exist, they are skipped. +export fn outdated(target: str, deps: []str, mtime: time::instant) bool = { + let current = match (os::stat(target)) { + case fs::error => + return true; + case let stat: fs::filestat => + yield stat.mtime; + }; + if (time::compare(current, mtime) < 0) { + return true; + }; + for (let i = 0z; i < len(deps); i += 1) match (os::stat(deps[i])) { + case fs::error => + continue; + case let stat: fs::filestat => + if (time::compare(current, stat.mtime) < 0) { + return true; + }; + }; + return false; +}; + +// Wrapper for [[fs::next]] that only returns valid submodule directories. +export fn next(it: *fs::iterator) (fs::dirent | void) = { + for (true) match (fs::next(it)) { + case void => + return void; + case let d: fs::dirent => + if (!fs::isdir(d.ftype)) { + continue; + }; + if (is_submodule(d.name)) { + return d; + }; + }; +}; + +fn is_submodule(path: str) bool = { + let it = strings::iter(path); + for (let first = true; true; first = false) match (strings::next(&it)) { + case void => + break; + case let r: rune => + if (!ascii::isalpha(r) && r != '_' + && (first || !ascii::isdigit(r))) { + return false; + }; + }; + return true; +}; diff --git a/hare/module/walk.ha b/hare/module/walk.ha @@ -1,91 +0,0 @@ -// License: MPL-2.0 -// (c) 2021-2022 Drew DeVault <sir@cmpwn.com> -// (c) 2021 Ember Sawady <ecs@d2evs.net> -use errors; -use fs; -use hare::ast; -use path; -use strings; - -// Recursively scans the filesystem to find valid Hare modules for the given -// [[context]], given the path to the entry point. The caller must free the -// return value with [[walk_free]]. -export fn walk(ctx: *context, path: str) ([]ast::ident | error) = { - let items: []ast::ident = []; - _walk(ctx, path, &items, [])?; - return items; -}; - -fn _walk( - ctx: *context, - path: str, - items: *[]ast::ident, - ns: ast::ident, -) (void | error) = { - match (scan(ctx, path)) { - case error => - void; - case let ver: version => - append(items, ns); - }; - - let iter = match (fs::iter(ctx.fs, path)) { - case fs::wrongtype => - return; // Single file "module" - case let err: fs::error => - return err; - case let iter: *fs::iterator => - yield iter; - }; - defer fs::finish(iter); - - // TODO: Refactor me to use path::buffer - for (true) { - const ent = match (fs::next(iter)) { - case void => - break; - case let ent: fs::dirent => - yield ent; - }; - - if (strings::hasprefix(ent.name, "+") - || strings::hasprefix(ent.name, "-") - || strings::hasprefix(ent.name, ".")) { - continue; - }; - - switch (ent.ftype) { - case fs::mode::DIR => - // TODO: Test that this is a valid name (grammar) - let subpath = path::init(path, ent.name)!; - let newns = ast::ident_dup(ns); - append(newns, strings::dup(ent.name)); - _walk(ctx, path::string(&subpath), items, newns)?; - case fs::mode::LINK => - let linkbuf = path::init(path, ent.name)!; - path::set(&linkbuf, fs::readlink(ctx.fs, path::string(&linkbuf))?)!; - if (!path::abs(&linkbuf)) { - path::prepend(&linkbuf, path)!; - }; - - const st = fs::stat(ctx.fs, path::string(&linkbuf))?; - if (fs::isdir(st.mode)) { - let subpath = path::init(path, ent.name)!; - let newns = ast::ident_dup(ns); - append(newns, strings::dup(ent.name)); - _walk(ctx, path::string(&subpath), items, newns)?; - }; - case fs::mode::REG => - void; // no-op - case => abort(); - }; - }; -}; - -// Frees resources associated with the return value of [[walk]]. -export fn walk_free(items: []ast::ident) void = { - for (let i = 0z; i < len(items); i += 1) { - ast::ident_free(items[i]); - }; - free(items); -}; diff --git a/hare/parse/decl.ha b/hare/parse/decl.ha @@ -29,27 +29,29 @@ fn attr_symbol(lexer: *lex::lexer) (str | error) = { return s; }; +// Parses a command-line definition +export fn define(lexer: *lex::lexer) (ast::decl_const | error) = { + const ident = ident(lexer)?; + const _type: nullable *ast::_type = match (try(lexer, ltok::COLON)?) { + case lex::token => yield alloc(_type(lexer)?); + case void => yield null; + }; + want(lexer, ltok::EQUAL)?; + const init: *ast::expr = alloc(expr(lexer)?); + return ast::decl_const { + ident = ident, + _type = _type, + init = init, + }; +}; + fn decl_const( lexer: *lex::lexer, tok: ltok, ) ([]ast::decl_const | error) = { let decl: []ast::decl_const = []; for (true) { - const ident = ident(lexer)?; - const _type: nullable *ast::_type = - match (try(lexer, ltok::COLON)?) { - case lex::token => - yield alloc(_type(lexer)?); - case void => - yield null; - }; - want(lexer, ltok::EQUAL)?; - const init: *ast::expr = alloc(expr(lexer)?); - append(decl, ast::decl_const { - ident = ident, - _type = _type, - init = init, - }); + append(decl, define(lexer)?); if (try(lexer, ltok::COMMA)? is void) { break; diff --git a/hare/parse/doc/doc.ha b/hare/parse/doc/doc.ha @@ -0,0 +1,255 @@ +// License: GPL-3.0 +// (c) 2022 Alexey Yerin <yyp@disroot.org> +// (c) 2021 Drew DeVault <sir@cmpwn.com> +// (c) 2021 Ember Sawady <ecs@d2evs.net> +// (c) 2021 Thomas Bracht Laumann Jespersen <t@laumann.xyz> +// (c) 2022 Umar Getagazov <umar@handlerug.me> +use ascii; +use bufio; +use encoding::utf8; +use fmt; +use hare::ast; +use hare::parse; +use io; +use memio; +use strings; + +export type paragraph = void; +export type text = str; +export type reference = ast::ident; +export type sample = str; +export type listitem = void; +export type token = (paragraph | text | reference | sample | listitem); + +export type docstate = enum { + PARAGRAPH, + TEXT, + LIST, +}; + +export type parser = struct { + src: bufio::stream, + state: docstate, +}; + +export fn parse(in: io::handle) parser = { + static let buf: [4096]u8 = [0...]; + return parser { + src = bufio::init(in, buf[..], []), + state = docstate::PARAGRAPH, + }; +}; + +export fn scan(par: *parser) (token | void) = { + const rn = match (bufio::scanrune(&par.src)!) { + case let rn: rune => + yield rn; + case io::EOF => + return; + }; + + bufio::unreadrune(&par.src, rn); + switch (par.state) { + case docstate::TEXT => + switch (rn) { + case '[' => + return scanref(par); + case => + return scantext(par); + }; + case docstate::LIST => + switch (rn) { + case '[' => + return scanref(par); + case '-' => + return scanlist(par); + case => + return scantext(par); + }; + case docstate::PARAGRAPH => + switch (rn) { + case ' ', '\t' => + return scansample(par); + case '-' => + return scanlist(par); + case => + return scantext(par); + }; + }; +}; + +fn scantext(par: *parser) (token | void) = { + if (par.state == docstate::PARAGRAPH) { + par.state = docstate::TEXT; + return paragraph; + }; + // TODO: Collapse whitespace + const buf = memio::dynamic(); + for (true) { + const rn = match (bufio::scanrune(&par.src)!) { + case io::EOF => + break; + case let rn: rune => + yield rn; + }; + switch (rn) { + case '[' => + bufio::unreadrune(&par.src, rn); + break; + case '\n' => + memio::appendrune(&buf, rn)!; + const rn = match (bufio::scanrune(&par.src)!) { + case io::EOF => + break; + case let rn: rune => + yield rn; + }; + if (rn == '\n') { + par.state = docstate::PARAGRAPH; + break; + }; + bufio::unreadrune(&par.src, rn); + if (rn == '-' && par.state == docstate::LIST) { + break; + }; + case => + memio::appendrune(&buf, rn)!; + }; + }; + let result = memio::string(&buf)!; + if (len(result) == 0) { + return; + }; + return result: text; +}; + +fn scanref(par: *parser) (token | void) = { + match (bufio::scanrune(&par.src)!) { + case io::EOF => + return; + case let rn: rune => + if (rn != '[') { + abort(); + }; + }; + match (bufio::scanrune(&par.src)!) { + case io::EOF => + return; + case let rn: rune => + if (rn != '[') { + bufio::unreadrune(&par.src, rn); + return strings::dup("["): text; + }; + }; + + const buf = memio::dynamic(); + defer io::close(&buf)!; + // TODO: Handle invalid syntax here + for (true) { + match (bufio::scanrune(&par.src)!) { + case let rn: rune => + switch (rn) { + case ']' => + bufio::scanrune(&par.src) as rune; // ] + break; + case => + memio::appendrune(&buf, rn)!; + }; + case io::EOF => + break; + }; + }; + let id = parse::identstr(memio::string(&buf)!) as ast::ident; + return id: reference; +}; + +fn scansample(par: *parser) (token | void) = { + let nws = 0z; + for (true) { + match (bufio::scanrune(&par.src)!) { + case io::EOF => + return; + case let rn: rune => + switch (rn) { + case ' ' => + nws += 1; + case '\t' => + nws += 8; + case => + bufio::unreadrune(&par.src, rn); + break; + }; + }; + }; + if (nws <= 1) { + return scantext(par); + }; + + let cont = true; + let buf = memio::dynamic(); + for (cont) { + const rn = match (bufio::scanrune(&par.src)!) { + case io::EOF => + break; + case let rn: rune => + yield rn; + }; + switch (rn) { + case '\n' => + memio::appendrune(&buf, rn)!; + case => + memio::appendrune(&buf, rn)!; + continue; + }; + + // Consume whitespace + for (let i = 0z; i < nws) { + match (bufio::scanrune(&par.src)!) { + case io::EOF => + break; + case let rn: rune => + switch (rn) { + case ' ' => + i += 1; + case '\t' => + i += 8; + case '\n' => + memio::appendrune(&buf, rn)!; + i = 0; + case => + bufio::unreadrune(&par.src, rn); + cont = false; + break; + }; + }; + }; + }; + + let buf = memio::string(&buf)!; + // Trim trailing newlines + buf = strings::rtrim(buf, '\n'); + return buf: sample; +}; + +fn scanlist(par: *parser) (token | void) = { + match (bufio::scanrune(&par.src)!) { + case io::EOF => + return void; + case let rn: rune => + if (rn != '-') { + abort(); + }; + }; + const rn = match (bufio::scanrune(&par.src)!) { + case io::EOF => + return void; + case let rn: rune => + yield rn; + }; + if (rn != ' ') { + bufio::unreadrune(&par.src, rn); + return strings::dup("-"): text; + }; + par.state = docstate::LIST; + return listitem; +}; diff --git a/rt/+aarch64/cpuid_native.s b/rt/+aarch64/cpuid.s diff --git a/rt/+riscv64/cpuid_native.s b/rt/+riscv64/cpuid.s diff --git a/rt/+x86_64/cpuid_native.s b/rt/+x86_64/cpuid.s diff --git a/scripts/gen-stdlib b/scripts/gen-stdlib @@ -103,7 +103,7 @@ ${stdlib}_asm = \$($cache)/rt/syscall.o \\ \$($cache)/rt/getfp.o \\ \$($cache)/rt/fenv.o \\ \$($cache)/rt/start.o \\ - \$($cache)/rt/cpuid_native.o + \$($cache)/rt/cpuid.o \$($cache)/rt/syscall.o: \$(STDLIB)/rt/+\$(PLATFORM)/syscall+\$(ARCH).s @printf 'AS \t%s\n' "\$@" @@ -135,10 +135,10 @@ ${stdlib}_asm = \$($cache)/rt/syscall.o \\ @mkdir -p \$($cache)/rt @\$(AS) -o \$@ \$(STDLIB)/rt/+\$(ARCH)/getfp.s -\$($cache)/rt/cpuid_native.o: \$(STDLIB)/rt/+\$(ARCH)/cpuid_native.s +\$($cache)/rt/cpuid.o: \$(STDLIB)/rt/+\$(ARCH)/cpuid.s @printf 'AS \t%s\n' "\$@" @mkdir -p \$($cache)/rt - @\$(AS) -o \$@ \$(STDLIB)/rt/+\$(ARCH)/cpuid_native.s + @\$(AS) -o \$@ \$(STDLIB)/rt/+\$(ARCH)/cpuid.s \$($cache)/rt/rt-linux.a: \$($cache)/rt/rt-linux.o \$(${stdlib}_asm) @printf 'AR \t%s\n' "\$@" @@ -205,6 +205,17 @@ bytes() { gen_ssa bytes types } +cmd_hare_build() { + gen_srcs cmd::hare::build \ + gather.ha \ + queue.ha \ + types.ha \ + util.ha + gen_ssa cmd::hare::build encoding::hex crypto::sha256 errors fmt fs \ + hare::ast hare::module hare::unparse hash io memio os os::exec path \ + sort strings shlex unix::tty +} + crypto() { if [ $testing -eq 0 ] then @@ -763,15 +774,16 @@ hare_lex() { hare_module() { gen_srcs hare::module \ + cache.ha \ + deps.ha \ types.ha \ - context.ha \ - scan.ha \ - manifest.ha \ - walk.ha + format.ha \ + srcs.ha \ + util.ha gen_ssa hare::module \ - hare::ast hare::lex hare::parse hare::unparse memio fs io strings hash \ - crypto::sha256 dirs bytes encoding::utf8 ascii fmt time bufio \ - strconv os encoding::hex sort errors temp path + ascii memio bytes datetime encoding::utf8 fmt fs hare::ast hare::lex \ + hare::parse hare::unparse io os path strings time time::chrono \ + time::date types encoding::hex } gensrcs_hare_parse() { @@ -869,6 +881,12 @@ hare_unparse() { gen_ssa hare::unparse fmt io strings memio hare::ast hare::lex } +hare_parse_doc() { + gen_srcs hare::parse::doc \ + doc.ha + gen_ssa hare::parse::doc ascii encoding::utf8 fmt hare::ast hare::parse io strings memio +} + hash() { gen_srcs hash \ hash.ha @@ -1611,6 +1629,7 @@ uuid() { modules="ascii bufio bytes +cmd::hare::build crypto crypto::aes crypto::aes::xts @@ -1656,6 +1675,7 @@ hare::ast hare::lex hare::module hare::parse +hare::parse::doc hare::types hare::unit hare::unparse diff --git a/stdlib.mk b/stdlib.mk @@ -85,7 +85,7 @@ stdlib_asm = $(HARECACHE)/rt/syscall.o \ $(HARECACHE)/rt/getfp.o \ $(HARECACHE)/rt/fenv.o \ $(HARECACHE)/rt/start.o \ - $(HARECACHE)/rt/cpuid_native.o + $(HARECACHE)/rt/cpuid.o $(HARECACHE)/rt/syscall.o: $(STDLIB)/rt/+$(PLATFORM)/syscall+$(ARCH).s @printf 'AS \t%s\n' "$@" @@ -117,10 +117,10 @@ $(HARECACHE)/rt/getfp.o: $(STDLIB)/rt/+$(ARCH)/getfp.s @mkdir -p $(HARECACHE)/rt @$(AS) -o $@ $(STDLIB)/rt/+$(ARCH)/getfp.s -$(HARECACHE)/rt/cpuid_native.o: $(STDLIB)/rt/+$(ARCH)/cpuid_native.s +$(HARECACHE)/rt/cpuid.o: $(STDLIB)/rt/+$(ARCH)/cpuid.s @printf 'AS \t%s\n' "$@" @mkdir -p $(HARECACHE)/rt - @$(AS) -o $@ $(STDLIB)/rt/+$(ARCH)/cpuid_native.s + @$(AS) -o $@ $(STDLIB)/rt/+$(ARCH)/cpuid.s $(HARECACHE)/rt/rt-linux.a: $(HARECACHE)/rt/rt-linux.o $(stdlib_asm) @printf 'AR \t%s\n' "$@" @@ -157,6 +157,13 @@ stdlib_deps_any += $(stdlib_bytes_any) stdlib_bytes_linux = $(stdlib_bytes_any) stdlib_bytes_freebsd = $(stdlib_bytes_any) +# gen_lib cmd::hare::build (any) +stdlib_cmd_hare_build_any = $(HARECACHE)/cmd/hare/build/cmd_hare_build-any.o +stdlib_env += HARE_TD_cmd::hare::build=$(HARECACHE)/cmd/hare/build/cmd_hare_build.td +stdlib_deps_any += $(stdlib_cmd_hare_build_any) +stdlib_cmd_hare_build_linux = $(stdlib_cmd_hare_build_any) +stdlib_cmd_hare_build_freebsd = $(stdlib_cmd_hare_build_any) + # gen_lib crypto (any) stdlib_crypto_any = $(HARECACHE)/crypto/crypto-any.o stdlib_env += HARE_TD_crypto=$(HARECACHE)/crypto/crypto.td @@ -475,6 +482,13 @@ stdlib_deps_any += $(stdlib_hare_parse_any) stdlib_hare_parse_linux = $(stdlib_hare_parse_any) stdlib_hare_parse_freebsd = $(stdlib_hare_parse_any) +# gen_lib hare::parse::doc (any) +stdlib_hare_parse_doc_any = $(HARECACHE)/hare/parse/doc/hare_parse_doc-any.o +stdlib_env += HARE_TD_hare::parse::doc=$(HARECACHE)/hare/parse/doc/hare_parse_doc.td +stdlib_deps_any += $(stdlib_hare_parse_doc_any) +stdlib_hare_parse_doc_linux = $(stdlib_hare_parse_doc_any) +stdlib_hare_parse_doc_freebsd = $(stdlib_hare_parse_doc_any) + # gen_lib hare::types (any) stdlib_hare_types_any = $(HARECACHE)/hare/types/hare_types-any.o stdlib_env += HARE_TD_hare::types=$(HARECACHE)/hare/types/hare_types.td @@ -935,6 +949,19 @@ $(HARECACHE)/bytes/bytes-any.ssa: $(stdlib_bytes_any_srcs) $(stdlib_rt) $(stdlib @$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nbytes \ -t$(HARECACHE)/bytes/bytes.td $(stdlib_bytes_any_srcs) +# cmd::hare::build (+any) +stdlib_cmd_hare_build_any_srcs = \ + $(STDLIB)/cmd/hare/build/gather.ha \ + $(STDLIB)/cmd/hare/build/queue.ha \ + $(STDLIB)/cmd/hare/build/types.ha \ + $(STDLIB)/cmd/hare/build/util.ha + +$(HARECACHE)/cmd/hare/build/cmd_hare_build-any.ssa: $(stdlib_cmd_hare_build_any_srcs) $(stdlib_rt) $(stdlib_encoding_hex_$(PLATFORM)) $(stdlib_crypto_sha256_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM)) $(stdlib_fs_$(PLATFORM)) $(stdlib_hare_ast_$(PLATFORM)) $(stdlib_hare_module_$(PLATFORM)) $(stdlib_hare_unparse_$(PLATFORM)) $(stdlib_hash_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_memio_$(PLATFORM)) $(stdlib_os_$(PLATFORM)) $(stdlib_os_exec_$(PLATFORM)) $(stdlib_path_$(PLATFORM)) $(stdlib_sort_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_shlex_$(PLATFORM)) $(stdlib_unix_tty_$(PLATFORM)) + @printf 'HAREC \t$@\n' + @mkdir -p $(HARECACHE)/cmd/hare/build + @$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Ncmd::hare::build \ + -t$(HARECACHE)/cmd/hare/build/cmd_hare_build.td $(stdlib_cmd_hare_build_any_srcs) + # crypto (+any) stdlib_crypto_any_srcs = \ $(STDLIB)/crypto/authenc.ha \ @@ -1423,13 +1450,14 @@ $(HARECACHE)/hare/lex/hare_lex-any.ssa: $(stdlib_hare_lex_any_srcs) $(stdlib_rt) # hare::module (+any) stdlib_hare_module_any_srcs = \ + $(STDLIB)/hare/module/cache.ha \ + $(STDLIB)/hare/module/deps.ha \ $(STDLIB)/hare/module/types.ha \ - $(STDLIB)/hare/module/context.ha \ - $(STDLIB)/hare/module/scan.ha \ - $(STDLIB)/hare/module/manifest.ha \ - $(STDLIB)/hare/module/walk.ha + $(STDLIB)/hare/module/format.ha \ + $(STDLIB)/hare/module/srcs.ha \ + $(STDLIB)/hare/module/util.ha -$(HARECACHE)/hare/module/hare_module-any.ssa: $(stdlib_hare_module_any_srcs) $(stdlib_rt) $(stdlib_hare_ast_$(PLATFORM)) $(stdlib_hare_lex_$(PLATFORM)) $(stdlib_hare_parse_$(PLATFORM)) $(stdlib_hare_unparse_$(PLATFORM)) $(stdlib_memio_$(PLATFORM)) $(stdlib_fs_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_hash_$(PLATFORM)) $(stdlib_crypto_sha256_$(PLATFORM)) $(stdlib_dirs_$(PLATFORM)) $(stdlib_bytes_$(PLATFORM)) $(stdlib_encoding_utf8_$(PLATFORM)) $(stdlib_ascii_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM)) $(stdlib_time_$(PLATFORM)) $(stdlib_bufio_$(PLATFORM)) $(stdlib_strconv_$(PLATFORM)) $(stdlib_os_$(PLATFORM)) $(stdlib_encoding_hex_$(PLATFORM)) $(stdlib_sort_$(PLATFORM)) $(stdlib_errors_$(PLATFORM)) $(stdlib_temp_$(PLATFORM)) $(stdlib_path_$(PLATFORM)) +$(HARECACHE)/hare/module/hare_module-any.ssa: $(stdlib_hare_module_any_srcs) $(stdlib_rt) $(stdlib_ascii_$(PLATFORM)) $(stdlib_memio_$(PLATFORM)) $(stdlib_bytes_$(PLATFORM)) $(stdlib_datetime_$(PLATFORM)) $(stdlib_encoding_utf8_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM)) $(stdlib_fs_$(PLATFORM)) $(stdlib_hare_ast_$(PLATFORM)) $(stdlib_hare_lex_$(PLATFORM)) $(stdlib_hare_parse_$(PLATFORM)) $(stdlib_hare_unparse_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_os_$(PLATFORM)) $(stdlib_path_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_time_$(PLATFORM)) $(stdlib_time_chrono_$(PLATFORM)) $(stdlib_time_date_$(PLATFORM)) $(stdlib_types_$(PLATFORM)) $(stdlib_encoding_hex_$(PLATFORM)) @printf 'HAREC \t$@\n' @mkdir -p $(HARECACHE)/hare/module @$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nhare::module \ @@ -1451,6 +1479,16 @@ $(HARECACHE)/hare/parse/hare_parse-any.ssa: $(stdlib_hare_parse_any_srcs) $(stdl @$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nhare::parse \ -t$(HARECACHE)/hare/parse/hare_parse.td $(stdlib_hare_parse_any_srcs) +# hare::parse::doc (+any) +stdlib_hare_parse_doc_any_srcs = \ + $(STDLIB)/hare/parse/doc/doc.ha + +$(HARECACHE)/hare/parse/doc/hare_parse_doc-any.ssa: $(stdlib_hare_parse_doc_any_srcs) $(stdlib_rt) $(stdlib_ascii_$(PLATFORM)) $(stdlib_encoding_utf8_$(PLATFORM)) $(stdlib_fmt_$(PLATFORM)) $(stdlib_hare_ast_$(PLATFORM)) $(stdlib_hare_parse_$(PLATFORM)) $(stdlib_io_$(PLATFORM)) $(stdlib_strings_$(PLATFORM)) $(stdlib_memio_$(PLATFORM)) + @printf 'HAREC \t$@\n' + @mkdir -p $(HARECACHE)/hare/parse/doc + @$(stdlib_env) $(HAREC) $(HAREFLAGS) -o $@ -Nhare::parse::doc \ + -t$(HARECACHE)/hare/parse/doc/hare_parse_doc.td $(stdlib_hare_parse_doc_any_srcs) + # hare::types (+any) stdlib_hare_types_any_srcs = \ $(STDLIB)/hare/types/+$(ARCH)/writesize.ha \ @@ -2491,7 +2529,7 @@ testlib_asm = $(TESTCACHE)/rt/syscall.o \ $(TESTCACHE)/rt/getfp.o \ $(TESTCACHE)/rt/fenv.o \ $(TESTCACHE)/rt/start.o \ - $(TESTCACHE)/rt/cpuid_native.o + $(TESTCACHE)/rt/cpuid.o $(TESTCACHE)/rt/syscall.o: $(STDLIB)/rt/+$(PLATFORM)/syscall+$(ARCH).s @printf 'AS \t%s\n' "$@" @@ -2523,10 +2561,10 @@ $(TESTCACHE)/rt/getfp.o: $(STDLIB)/rt/+$(ARCH)/getfp.s @mkdir -p $(TESTCACHE)/rt @$(AS) -o $@ $(STDLIB)/rt/+$(ARCH)/getfp.s -$(TESTCACHE)/rt/cpuid_native.o: $(STDLIB)/rt/+$(ARCH)/cpuid_native.s +$(TESTCACHE)/rt/cpuid.o: $(STDLIB)/rt/+$(ARCH)/cpuid.s @printf 'AS \t%s\n' "$@" @mkdir -p $(TESTCACHE)/rt - @$(AS) -o $@ $(STDLIB)/rt/+$(ARCH)/cpuid_native.s + @$(AS) -o $@ $(STDLIB)/rt/+$(ARCH)/cpuid.s $(TESTCACHE)/rt/rt-linux.a: $(TESTCACHE)/rt/rt-linux.o $(testlib_asm) @printf 'AR \t%s\n' "$@" @@ -2563,6 +2601,13 @@ testlib_deps_any += $(testlib_bytes_any) testlib_bytes_linux = $(testlib_bytes_any) testlib_bytes_freebsd = $(testlib_bytes_any) +# gen_lib cmd::hare::build (any) +testlib_cmd_hare_build_any = $(TESTCACHE)/cmd/hare/build/cmd_hare_build-any.o +testlib_env += HARE_TD_cmd::hare::build=$(TESTCACHE)/cmd/hare/build/cmd_hare_build.td +testlib_deps_any += $(testlib_cmd_hare_build_any) +testlib_cmd_hare_build_linux = $(testlib_cmd_hare_build_any) +testlib_cmd_hare_build_freebsd = $(testlib_cmd_hare_build_any) + # gen_lib crypto (any) testlib_crypto_any = $(TESTCACHE)/crypto/crypto-any.o testlib_env += HARE_TD_crypto=$(TESTCACHE)/crypto/crypto.td @@ -2881,6 +2926,13 @@ testlib_deps_any += $(testlib_hare_parse_any) testlib_hare_parse_linux = $(testlib_hare_parse_any) testlib_hare_parse_freebsd = $(testlib_hare_parse_any) +# gen_lib hare::parse::doc (any) +testlib_hare_parse_doc_any = $(TESTCACHE)/hare/parse/doc/hare_parse_doc-any.o +testlib_env += HARE_TD_hare::parse::doc=$(TESTCACHE)/hare/parse/doc/hare_parse_doc.td +testlib_deps_any += $(testlib_hare_parse_doc_any) +testlib_hare_parse_doc_linux = $(testlib_hare_parse_doc_any) +testlib_hare_parse_doc_freebsd = $(testlib_hare_parse_doc_any) + # gen_lib hare::types (any) testlib_hare_types_any = $(TESTCACHE)/hare/types/hare_types-any.o testlib_env += HARE_TD_hare::types=$(TESTCACHE)/hare/types/hare_types.td @@ -3343,6 +3395,19 @@ $(TESTCACHE)/bytes/bytes-any.ssa: $(testlib_bytes_any_srcs) $(testlib_rt) $(test @$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nbytes \ -t$(TESTCACHE)/bytes/bytes.td $(testlib_bytes_any_srcs) +# cmd::hare::build (+any) +testlib_cmd_hare_build_any_srcs = \ + $(STDLIB)/cmd/hare/build/gather.ha \ + $(STDLIB)/cmd/hare/build/queue.ha \ + $(STDLIB)/cmd/hare/build/types.ha \ + $(STDLIB)/cmd/hare/build/util.ha + +$(TESTCACHE)/cmd/hare/build/cmd_hare_build-any.ssa: $(testlib_cmd_hare_build_any_srcs) $(testlib_rt) $(testlib_encoding_hex_$(PLATFORM)) $(testlib_crypto_sha256_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_fs_$(PLATFORM)) $(testlib_hare_ast_$(PLATFORM)) $(testlib_hare_module_$(PLATFORM)) $(testlib_hare_unparse_$(PLATFORM)) $(testlib_hash_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_memio_$(PLATFORM)) $(testlib_os_$(PLATFORM)) $(testlib_os_exec_$(PLATFORM)) $(testlib_path_$(PLATFORM)) $(testlib_sort_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_shlex_$(PLATFORM)) $(testlib_unix_tty_$(PLATFORM)) + @printf 'HAREC \t$@\n' + @mkdir -p $(TESTCACHE)/cmd/hare/build + @$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Ncmd::hare::build \ + -t$(TESTCACHE)/cmd/hare/build/cmd_hare_build.td $(testlib_cmd_hare_build_any_srcs) + # crypto (+any) testlib_crypto_any_srcs = \ $(STDLIB)/crypto/authenc.ha \ @@ -3867,13 +3932,14 @@ $(TESTCACHE)/hare/lex/hare_lex-any.ssa: $(testlib_hare_lex_any_srcs) $(testlib_r # hare::module (+any) testlib_hare_module_any_srcs = \ + $(STDLIB)/hare/module/cache.ha \ + $(STDLIB)/hare/module/deps.ha \ $(STDLIB)/hare/module/types.ha \ - $(STDLIB)/hare/module/context.ha \ - $(STDLIB)/hare/module/scan.ha \ - $(STDLIB)/hare/module/manifest.ha \ - $(STDLIB)/hare/module/walk.ha + $(STDLIB)/hare/module/format.ha \ + $(STDLIB)/hare/module/srcs.ha \ + $(STDLIB)/hare/module/util.ha -$(TESTCACHE)/hare/module/hare_module-any.ssa: $(testlib_hare_module_any_srcs) $(testlib_rt) $(testlib_hare_ast_$(PLATFORM)) $(testlib_hare_lex_$(PLATFORM)) $(testlib_hare_parse_$(PLATFORM)) $(testlib_hare_unparse_$(PLATFORM)) $(testlib_memio_$(PLATFORM)) $(testlib_fs_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_hash_$(PLATFORM)) $(testlib_crypto_sha256_$(PLATFORM)) $(testlib_dirs_$(PLATFORM)) $(testlib_bytes_$(PLATFORM)) $(testlib_encoding_utf8_$(PLATFORM)) $(testlib_ascii_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_time_$(PLATFORM)) $(testlib_bufio_$(PLATFORM)) $(testlib_strconv_$(PLATFORM)) $(testlib_os_$(PLATFORM)) $(testlib_encoding_hex_$(PLATFORM)) $(testlib_sort_$(PLATFORM)) $(testlib_errors_$(PLATFORM)) $(testlib_temp_$(PLATFORM)) $(testlib_path_$(PLATFORM)) +$(TESTCACHE)/hare/module/hare_module-any.ssa: $(testlib_hare_module_any_srcs) $(testlib_rt) $(testlib_ascii_$(PLATFORM)) $(testlib_memio_$(PLATFORM)) $(testlib_bytes_$(PLATFORM)) $(testlib_datetime_$(PLATFORM)) $(testlib_encoding_utf8_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_fs_$(PLATFORM)) $(testlib_hare_ast_$(PLATFORM)) $(testlib_hare_lex_$(PLATFORM)) $(testlib_hare_parse_$(PLATFORM)) $(testlib_hare_unparse_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_os_$(PLATFORM)) $(testlib_path_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_time_$(PLATFORM)) $(testlib_time_chrono_$(PLATFORM)) $(testlib_time_date_$(PLATFORM)) $(testlib_types_$(PLATFORM)) $(testlib_encoding_hex_$(PLATFORM)) @printf 'HAREC \t$@\n' @mkdir -p $(TESTCACHE)/hare/module @$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nhare::module \ @@ -3901,6 +3967,16 @@ $(TESTCACHE)/hare/parse/hare_parse-any.ssa: $(testlib_hare_parse_any_srcs) $(tes @$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nhare::parse \ -t$(TESTCACHE)/hare/parse/hare_parse.td $(testlib_hare_parse_any_srcs) +# hare::parse::doc (+any) +testlib_hare_parse_doc_any_srcs = \ + $(STDLIB)/hare/parse/doc/doc.ha + +$(TESTCACHE)/hare/parse/doc/hare_parse_doc-any.ssa: $(testlib_hare_parse_doc_any_srcs) $(testlib_rt) $(testlib_ascii_$(PLATFORM)) $(testlib_encoding_utf8_$(PLATFORM)) $(testlib_fmt_$(PLATFORM)) $(testlib_hare_ast_$(PLATFORM)) $(testlib_hare_parse_$(PLATFORM)) $(testlib_io_$(PLATFORM)) $(testlib_strings_$(PLATFORM)) $(testlib_memio_$(PLATFORM)) + @printf 'HAREC \t$@\n' + @mkdir -p $(TESTCACHE)/hare/parse/doc + @$(testlib_env) $(HAREC) $(TESTHAREFLAGS) -o $@ -Nhare::parse::doc \ + -t$(TESTCACHE)/hare/parse/doc/hare_parse_doc.td $(testlib_hare_parse_doc_any_srcs) + # hare::types (+any) testlib_hare_types_any_srcs = \ $(STDLIB)/hare/types/+$(ARCH)/writesize.ha \