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:
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 "&";
+ case '<' =>
+ yield "<";
+ case '>' =>
+ yield ">";
+ case '"' =>
+ yield """;
+ case '\'' =>
+ yield "'";
+ 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)! == ""hello world!"");
+
+ let sink = memio::dynamic();
+ defer io::close(&sink)!;
+ html_escape(&sink, "<hello & 'world'!>")!;
+ assert(memio::string(&sink)! == "<hello & 'world'!>");
+};
+
+// 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 "&";
- case '<' =>
- yield "<";
- case '>' =>
- yield ">";
- case '"' =>
- yield """;
- case '\'' =>
- yield "'";
- 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)! == ""hello world!"");
-
- let sink = memio::dynamic();
- defer io::close(&sink)!;
- html_escape(&sink, "<hello & 'world'!>")!;
- assert(memio::string(&sink)! == "<hello & 'world'!>");
-};
-
-// 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 \