Ember Sawady: 1 Rewrite build driver and hare::module 46 files changed, 2399 insertions(+), 3398 deletions(-)
thanks for the feedback! there were some things i wanted to follow up on, but you can assume that if i didn't mention something i'll be implementing the change you suggested in v4
nope, if you've got a better suggestion i'm all ears. i chose it cause it's what we use for type ids. i remember that i used fnv32 at one point in the past, and i remember changing it to fnv64, but i don't remember if i had any rationale there
(things i didn't bring up were either addressed elsewhere in the thread or are now in my todo list)
to assume the sequence of commands involved in the build process, one of which is qbe. so i think leaving this as-is is fine (...)
non-qbe-based implementations to ignore qbe files in modules.
haven't yet looked through the rest of the feedback, but
Got it. Could improve the error message but not a big deal.
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~sircmpwn/hare-dev/patches/41706/mbox | git am -3Learn more about email & git
Co-authored-by: Autumn! <autumnull@posteo.net> Co-authored-by: Sebastian <sebastian@sebsite.pw> Signed-off-by: Ember Sawady <ecs@d2evs.net> --- honestly i kinda forgot all the stuff i did since v2, probably should've kept a better changelog. p sure i got everything from v2 review, and i fixed a pretty tricky caching bug .gitignore | 1 + Makefile | 60 +- cmd/hare/arch.ha | 62 ++ cmd/hare/build.ha | 713 ++++++++++++++++++ cmd/hare/cache.ha | 49 ++ cmd/hare/deps.ha | 129 ++-- cmd/{haredoc/main.ha => hare/doc.ha} | 194 ++--- cmd/{haredoc => hare/doc}/color.ha | 0 cmd/{haredoc => hare/doc}/hare.ha | 13 +- cmd/{haredoc => hare/doc}/html.ha | 71 +- .../resolver.ha => hare/doc/resolve.ha} | 63 +- cmd/{haredoc => hare/doc}/sort.ha | 10 +- cmd/{haredoc => hare/doc}/tty.ha | 13 +- cmd/hare/doc/types.ha | 55 ++ cmd/{haredoc => hare/doc}/util.ha | 39 +- cmd/hare/error.ha | 23 + cmd/hare/main.ha | 107 ++- cmd/hare/plan.ha | 328 -------- cmd/hare/progress.ha | 64 -- cmd/hare/release.ha | 51 ++ cmd/hare/schedule.ha | 388 ---------- cmd/hare/subcmds.ha | 624 --------------- cmd/hare/target.ha | 81 -- cmd/hare/util.ha | 36 + cmd/hare/version.ha | 43 ++ cmd/harec/gen.ha | 2 +- cmd/haredoc/env.ha | 104 --- cmd/haredoc/errors.ha | 26 - docs/hare-doc.5.scd | 29 + docs/{hare.scd => hare.1.scd} | 172 ++++- docs/haredoc.scd | 116 --- hare/module/README | 15 +- hare/module/cache.ha | 29 + hare/module/context.ha | 129 ---- hare/module/deps.ha | 214 ++++++ hare/module/format.ha | 71 ++ hare/module/manifest.ha | 403 ---------- hare/module/scan.ha | 491 ------------ hare/module/srcs.ha | 296 ++++++++ hare/module/types.ha | 152 ++-- hare/module/util.ha | 46 ++ hare/module/walk.ha | 91 --- hare/parse/decl.ha | 32 +- .../docstr.ha => hare/parse/doc/doc.ha | 20 +- scripts/gen-stdlib | 38 +- stdlib.mk | 104 ++- 46 files changed, 2399 insertions(+), 3398 deletions(-) create mode 100644 cmd/hare/arch.ha create mode 100644 cmd/hare/build.ha create mode 100644 cmd/hare/cache.ha rename cmd/{haredoc/main.ha => hare/doc.ha} (59%) rename cmd/{haredoc => hare/doc}/color.ha (100%) rename cmd/{haredoc => hare/doc}/hare.ha (94%) rename cmd/{haredoc => hare/doc}/html.ha (94%) rename cmd/{haredoc/resolver.ha => hare/doc/resolve.ha} (75%) rename cmd/{haredoc => hare/doc}/sort.ha (93%) rename cmd/{haredoc => hare/doc}/tty.ha (98%) create mode 100644 cmd/hare/doc/types.ha rename cmd/{haredoc => hare/doc}/util.ha (52%) create mode 100644 cmd/hare/error.ha delete mode 100644 cmd/hare/plan.ha delete mode 100644 cmd/hare/progress.ha delete mode 100644 cmd/hare/schedule.ha delete mode 100644 cmd/hare/subcmds.ha delete mode 100644 cmd/hare/target.ha create mode 100644 cmd/hare/util.ha create mode 100644 cmd/hare/version.ha delete mode 100644 cmd/haredoc/env.ha delete mode 100644 cmd/haredoc/errors.ha create mode 100644 docs/hare-doc.5.scd rename docs/{hare.scd => hare.1.scd} (74%) delete mode 100644 docs/haredoc.scd create mode 100644 hare/module/cache.ha delete mode 100644 hare/module/context.ha create mode 100644 hare/module/deps.ha create mode 100644 hare/module/format.ha delete mode 100644 hare/module/manifest.ha delete mode 100644 hare/module/scan.ha create mode 100644 hare/module/srcs.ha create mode 100644 hare/module/util.ha delete mode 100644 hare/module/walk.ha rename cmd/haredoc/docstr.ha => hare/parse/doc/doc.ha (92%) diff --git a/.gitignore b/.gitignore index 30310524..06d477e0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ config.mk .cache .bin *.1 +*.5 docs/html diff --git a/Makefile b/Makefile index 728d2d26..1d4b9940 100644 --- 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,35 +18,29 @@ 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/doc.ha \ + ./cmd/hare/error.ha \ ./cmd/hare/main.ha \ - ./cmd/hare/plan.ha \ - ./cmd/hare/progress.ha \ ./cmd/hare/release.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 \ ./cmd/harec/errors.ha -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 - include targets.mk $(HARECACHE)/hare.ssa: $(hare_srcs) $(stdlib_deps_any) $(stdlib_deps_$(PLATFORM)) scripts/version @@ -77,26 +71,16 @@ $(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) - @mkdir -p $(BINOUT) - @printf 'HARE\t%s\n' "$@" - @env HAREPATH=. HAREC=$(HAREC) QBE=$(QBE) $(BINOUT)/hare build \ - $(HARE_DEFINES) -o $(BINOUT)/haredoc ./cmd/haredoc - -docs/html: $(BINOUT)/haredoc scripts/gen-docs.sh +docs/html: $(BINOUT)/hare 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/hare-doc.5: docs/hare-doc.5.scd -docs: docs/hare.1 docs/haredoc.1 +docs: docs/hare.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/hare-doc.5 docs/html check: $(BINOUT)/hare-tests @$(BINOUT)/hare-tests @@ -104,22 +88,22 @@ 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 new file mode 100644 index 00000000..f7b0d515 --- /dev/null +++ b/cmd/hare/arch.ha @@ -0,0 +1,62 @@ +use hare::module; +use os; + +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, +}; + +type unknown_arch = !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, 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 new file mode 100644 index 00000000..187e10f8 --- /dev/null +++ b/cmd/hare/build.ha @@ -0,0 +1,713 @@ +use bufio; +use bytes; +use encoding::hex; +use errors; +use fmt; +use fs; +use getopt; +use hare::ast; +use hare::lex; +use hare::module; +use hare::module::{cachekind}; +use hare::parse; +use hare::unparse; +use hash::fnv; +use hash; +use io; +use os::exec; +use os; +use path; +use shlex; +use sort; +use strconv; +use strings; +use strio; +use unix::tty; + +type unknown_type = !str; + +type output_not_file = !str; + +type unknown_output = !void; + +type task = struct { + rdeps: []*task, + ndeps: size, + kind: cachekind, + mod: *modctx, +}; + +type job = struct { + pid: exec::process, + task: *task, + out: str, + tmp: io::file, +}; + +type modctx = struct { + mod: module::module, + cache: str, + prefixes: [cachekind::BIN + 1]str,
Given that the length of all of those is fixed and known i wonder if it would be better to have this be prefixes: [cachekind::NKINDS][16]u8
+ flags: [cachekind::BIN + 1][]str, + libs: []str, +}; + +type output = enum { + DEFAULT, + VERBOSE, + VVERBOSE, +}; + +type context = struct { + ctx: module::context, + arch: *arch, + output: str, + goal: cachekind, + defines: []ast::decl_const, + libdirs: []str, + libs: []str, + jobs: size, + ns: ast::ident, + top: size, + name: str, + version: []u8, + buf: path::buffer, + submods: bool, + + cmds: [cachekind::BIN + 1]str, + envs: [cachekind::BIN + 1]str,
cachekind::NKINDS once more
+ + mode: output, + interactive: bool, + completed: size, + total: size, + nlines: size, +}; + +fn build(name: str, cmd: *getopt::command) (void | error) = { + let ctx = context { + ctx = module::context { + harepath = harepath(), + harecache = harecache(), + tags = default_tags()?, + }, + arch = get_arch(os::machine())?, + goal = cachekind::BIN, + jobs = match (os::cpucount()) { + case errors::error => yield 1; + case let ncpu: int => yield if (ncpu <= 0) 1 else ncpu: size; + }, + cmds = ["", "harec", "qbe", "as", "ld"], + envs = ["", "HARECFLAGS", "QBEFLAGS", "ASFLAGS", "LDLINKFLAGS"], + name = name, + mode = output::DEFAULT, + interactive = tty::isatty(os::stderr_file), + ... + }; + defer ctx_finish(&ctx); + for (let i = 0z; i < len(cmd.opts); i += 1) { + let opt = cmd.opts[i]; + switch (opt.0) { + case 'a' => + ctx.arch = get_arch(opt.1)?; + case 'D' => + const buf = bufio::fixed(strings::toutf8(opt.1), + io::mode::READ); + const lexer = lex::init(&buf, "<define>"); + defer lex::finish(&lexer); + append(ctx.defines, parse::define(&lexer)?); + case 'j' => + ctx.jobs = strconv::stoz(opt.1)?; + case 'L' => + append(ctx.libdirs, opt.1); + case 'l' => + append(ctx.libs, opt.1); + case 'N' => + ctx.ns = parse::identstr(opt.1)?; + case 'o' => + ctx.output = opt.1; + case 'T' => + merge_tags(&ctx.ctx.tags, opt.1)?; + case 't' => + switch (opt.1) { + case "ssa" => ctx.goal = cachekind::SSA; + case "s" => ctx.goal = cachekind::S; + case "o" => ctx.goal = cachekind::O; + case "bin" => ctx.goal = cachekind::BIN; + case => return opt.1: unknown_type; + }; + case 'v' => + ctx.interactive = tty::isatty(os::stdout_file); + if (ctx.mode == output::DEFAULT) { + ctx.mode = output::VERBOSE; + } else { + ctx.mode = output::VVERBOSE; + }; + case => + abort(); + }; + }; + if (len(cmd.args) > 1 && ctx.name == "build") { + getopt::printusage(os::stderr, ctx.name, cmd.help...)!; + os::exit(1); + }; + + ctx.cmds[cachekind::O] = ctx.arch.as_cmd; + set_arch_tags(&ctx.ctx.tags, ctx.arch); + if (len(ctx.libs) > 0) { + merge_tags(&ctx.ctx.tags, "+libc")?; + ctx.cmds[cachekind::BIN] = os::tryenv("CC", ctx.arch.cc_cmd); + ctx.envs[cachekind::BIN] = "LDFLAGS"; + } else { + ctx.cmds[cachekind::BIN] = os::tryenv("LD", ctx.arch.ld_cmd); + }; + let envs = ["", "HAREC", "QBE", "AS"]; + for (let i = cachekind::SSA; i < cachekind::BIN; i += 1) { + ctx.cmds[i] = os::tryenv(envs[i], ctx.cmds[i]); + }; + if (ctx.name == "test") merge_tags(&ctx.ctx.tags, "+test")?; + + const input = if (len(cmd.args) == 0) os::getcwd() else cmd.args[0]; + ctx.submods = ctx.name == "test" && len(cmd.args) == 0; + + let mods: []module::module = []; + path::set(&ctx.buf, os::realpath(input)?)?; + module::gather(&ctx.ctx, &mods, ["rt"])?; + if (ctx.name == "test") module::gather(&ctx.ctx, &mods, ["test"])?; + if (ctx.submods) { + let id: ast::ident = []; + defer ast::ident_free(id); + gather_submodules(&ctx, &mods, &ctx.buf, &id)?; + }; + ctx.top = module::gather(&ctx.ctx, &mods, &ctx.buf)?; + let mods = getcontexts(&ctx, mods)?; + defer modctx_finish(mods); + + let q: []*task = []; + queue(&q, mods, ctx.goal, ctx.top); + // sort by cachekind, harec then qbe then as then ld + sort::sort(q, size(*task), &task_cmp); + ctx.total = len(q); + + let jobs: [](job | void) = alloc([void...], ctx.jobs); + defer free(jobs); + + let version = exec::cmd("harec", "-v")?; + let pipe = exec::pipe(); + exec::addfile(&version, os::stdout_file, pipe.1); + let proc = exec::start(&version)?; + io::close(pipe.1)?; + ctx.version = io::drain(pipe.0)?; + let status = exec::wait(&proc)?; + io::close(pipe.0)?; + match (exec::check(&status)) { + case void => void; + case let status: !exec::exit_status => + fmt::fatal("harec -v", exec::exitstr(status)); + }; + + if (len(os::tryenv("NO_COLOR", "")) == 0 + && os::getenv("HAREC_COLOR") is void + && ctx.interactive) { + if (ctx.mode == output::VVERBOSE) { + fmt::printfln("# HAREC_COLOR=1")?; + }; + os::setenv("HAREC_COLOR", "1")!; + }; + + if (ctx.mode == output::DEFAULT && ctx.interactive) fmt::errorln()?; + for (let i = 0z; len(q) != 0; i += 1) { + if (i == len(q)) { + await_task(&ctx, &jobs)?; + i = 0; + }; + if (runtask(&ctx, &jobs, mods, q[i])?) { + delete(q[i]); + i = -1; + }; + }; + ui_update(&ctx, jobs)?; + for (await_task(&ctx, &jobs) is size) ui_update(&ctx, jobs)?; + ui_finish(&ctx)?; + + let built = getcache(&mods[ctx.top], ctx.goal); + + if (ctx.output == "" && ctx.name == "build") { + let stat = os::stat(input)?; + path::set(&ctx.buf, os::realpath(input)?)?; + if (!fs::isdir(stat.mode)) { + path::pop(&ctx.buf); + }; + if (ctx.goal != cachekind::BIN) { + path::push_ext(&ctx.buf, module::cachekind_ext[ctx.goal])?; + }; + ctx.output = match (path::peek(&ctx.buf)) { + case let s: str => yield s; + case void => return unknown_output; + }; + }; + + if (ctx.output == "") { + const run = exec::cmd(built)?; + if (len(cmd.args) != 0) append(run.argv, cmd.args[1..]...); + exec::setname(&run, input); + exec::exec(&run); + }; + + match (os::stat(ctx.output)) { + case let stat: fs::filestat => + if (!fs::isfile(stat.mode)) return ctx.output: output_not_file; + case => void; + }; + + let src = os::open(built)?; + defer io::close(src)!; + // TODO needs replacing with os::copy whenever that appears + let mode = fs::mode::USER_RW | fs::mode::GROUP_R; + if (ctx.goal == cachekind::BIN) { + mode |= fs::mode::USER_X | fs::mode::GROUP_X | fs::mode::OTHER_X; + }; + let dest = os::stdout_file; + if (ctx.output != "-") dest = os::create(ctx.output, mode)?; + defer if (dest != os::stdout_file) io::close(dest)!; + io::copy(dest, src)?; +}; + +fn gather_submodules( + ctx: *context, + mods: *[]module::module, + buf: *path::buffer, + mod: *ast::ident, +) (void | error) = { + 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]); + module::gather(&ctx.ctx, mods, *mod)?; + gather_submodules(ctx, mods, buf, mod)?; + }; +}; + +fn getcontexts(ctx: *context, mods: []module::module) ([]modctx | error) = { + if (len(ctx.ns) != 0) mods[ctx.top].ns = ctx.ns; + defer free(mods); + let ret: []modctx = alloc([], len(mods)); + for (let i = 0z; i < len(mods); i += 1) { + let flags: [cachekind::BIN + 1][]str = [[]...]; + if (len(mods[i].ns) != 0 || len(ctx.libs) != 0) { + append(flags[cachekind::SSA], strings::dup("-N")); + append(flags[cachekind::SSA], unparse::identstr(mods[i].ns)); + }; + path::set(&ctx.buf, mods[i].path)?; + if (path::trimprefix(&ctx.buf, os::getcwd()) is str + && ctx.submods) { + append(flags[cachekind::SSA], strings::dup("-T")); + }; + for (let j = 0z; j < len(ctx.defines); j += 1) :next { + let ident = ctx.defines[j].ident; + if (len(ident) != len(mods[i].ns) + 1) continue; + for (let k = 0z; k < len(mods[i].ns); k += 1) { + if (ident[k] != mods[i].ns[k]) continue :next; + }; + let buf = strio::dynamic(); + strio::concat(&buf, "-D", ident[len(ident) - 1])!; + match (ctx.defines[j]._type) { + case null => void; + case let t: *ast::_type => + strio::concat(&buf, ":")!; + unparse::_type(&buf, 0, *t)!; + }; + strio::concat(&buf, "=")!; + unparse::expr(&buf, 0, *ctx.defines[j].init)!; + append(flags[cachekind::SSA], strio::string(&buf)); + }; + for (let i = cachekind::SSA; i <= cachekind::BIN; i += 1) { + let env = shlex::split(os::tryenv(ctx.envs[i], ""))!; + defer free(env); + append(flags[i], env...); + }; + let cache = module::getcache(ctx.ctx.harecache, mods[i].path)?; + os::mkdirs(cache, 0o755)!; + append(ret, modctx { + mod = mods[i], + flags = flags, + libs = [], + cache = strings::dup(cache), + prefixes = [""...], + }); + }; + for (let i = 0z; i < len(ctx.libdirs); i += 1) { + append(ret[ctx.top].flags[cachekind::BIN], + strings::concat("-L", ctx.libdirs[i])); + }; + for (let i = 0z; i < len(ctx.libs); i += 1) { + append(ret[ctx.top].libs, strings::concat("-l", ctx.libs[i])); + }; + if (ctx.name == "test") { + append(ret[ctx.top].flags[cachekind::SSA], strings::dup("-T")); + }; + return ret; +}; + +fn queue(q: *[]*task, mods: []modctx, kind: cachekind, idx: size,) *task = { + for (let i = 0z; i < len(q); i += 1) { + if (q[i].kind == kind && q[i].mod == &mods[idx]) return q[i]; + }; + let t = alloc(task { + kind = kind, + mod = &mods[idx], + ... + }); + switch (kind) { + case cachekind::BIN => + t.ndeps = len(mods); + for (let i = 0z; i < len(mods); i += 1) { + append(queue(q, mods, cachekind::O, i).rdeps, t); + }; + case cachekind::O, cachekind::S => + t.ndeps = 1; + append(queue(q, mods, kind - 1, idx).rdeps, t); + case cachekind::SSA => + t.ndeps = len(mods[idx].mod.deps); + for (let i = 0z; i < len(mods[idx].mod.deps); i += 1) { + let j = mods[idx].mod.deps[i].0; + append(queue(q, mods, cachekind::SSA, j).rdeps, t); + }; + case cachekind::TD => abort(); + }; + append(q, t); + return t; +}; + +fn runtask(
I don't like that this function basically hashes everything about the command, because that includes a lot of useless noise such as hashing "-o", "", "-t", "" over and over again. What is hashed and what isn't should be carefully thought out and documented. By not hashing the entire cmd.argv we can also avoid the outidx/tdidx hacks because the out/td filenames won't depend on the entire argv anymore. Afaict the hash will also depend on the order of files as returned by getdents, which is not guaranteed to be the same across filesystems. The correct solution to this is probably making hare::module::findsrcs sort the filenames before they are returned
+ ctx: *context, + jobs: *[](job | void), + mods: []modctx, + t: *task, +) (bool | error) = { + let slot = getslot(ctx, jobs)?; + if (t.ndeps != 0) return false; + let mod = t.mod.mod; + + let depfiles: []str = []; + defer free(depfiles); + let cmd = exec::cmd(ctx.cmds[t.kind], t.mod.flags[t.kind]...)?;
cmd's argv doesn't get freed, causing a few leaked allocations per task
+ let exec = true; + let outidx = 0z; + let tdidx: (size | void) = void; + + let h = fnv::fnv64a();
Are we sure that fnv is a good choice here?
+ switch (t.kind) { + case cachekind::TD => abort(); + case cachekind::SSA => + hash::write(&h, ctx.version); + hash::write(&h, [0]); + append(depfiles, mod.srcs.ha...); + for (let i = 0z; i < len(mod.deps); i += 1) { + let dep = &mods[mod.deps[i].0]; + append(depfiles, getcache(dep, cachekind::TD)); + }; + append(cmd.argv, ["-o", ""]...); + outidx = len(cmd.argv) - 1; + append(cmd.argv, ["-t", ""]...); + tdidx = len(cmd.argv) - 1; + append(cmd.argv, mod.srcs.ha...); + if (len(mod.srcs.ha) == 0) exec = false; + case cachekind::S, cachekind::O => + let srcs = mod.srcs; + let srcs = if (t.kind == cachekind::S) srcs.ssa else srcs.s; + let in = getcache(t.mod, t.kind - 1); + append(depfiles, strings::dup(in)); + append(depfiles, srcs...); + append(cmd.argv, ["-o", ""]...); + outidx = len(cmd.argv) - 1; + if (t.kind == cachekind::S) { + append(cmd.argv, ["-t", ctx.arch.qbe_name]...); + }; + append(cmd.argv, in); + append(cmd.argv, srcs...); + case cachekind::BIN => + append(cmd.argv, ["-o", ""]...); + outidx = len(cmd.argv) - 1; + for (let i = 0z; i < len(mods); i += 1) { + let srcs = mods[i].mod.srcs; + let o = getcache(&mods[i], cachekind::O); + append(depfiles, strings::dup(o)); + append(depfiles, srcs.o...); + for (let i = 0z; i < len(srcs.sc); i += 1) { + append(cmd.argv,["-T", srcs.sc[i]]...); + }; + append(cmd.argv, o); + append(cmd.argv, srcs.o...); + }; + append(cmd.argv, t.mod.libs...); + }; + + for (let i = 0z; i < len(mod.deps); i += 1) { + let ns = unparse::identstr(mod.deps[i].1); + defer free(ns); + let var = strings::concat("HARE_TD_", ns);
This is one of the things that shouldn't be hashed imo
+ defer free(var); + hash::write(&h, strings::toutf8(var)); + hash::write(&h, strings::toutf8("=")); + hash::write(&h, strings::toutf8(os::getenv(var) as str));
Not really problematic, but suboptimal: lots of calls to os::getenv - each does a linear search over all variables
+ hash::write(&h, [0]); + }; + + for (let i = 0z; i < len(cmd.argv); i += 1) { + hash::write(&h, strings::toutf8(cmd.argv[i])); + hash::write(&h, [0]); + }; + + if (t.mod.prefixes[t.kind] == "") { + let prefix: [8]u8 = [0...]; + hash::sum(&h, prefix); + t.mod.prefixes[t.kind] = hex::encodestr(prefix); + }; + + let out = getcache(t.mod, t.kind); + let tmp = strings::concat(out, ".tmp"); + cmd.argv[outidx] = tmp; + let tmp = os::create(tmp, 0o644, fs::flags::WRONLY)?; + if (!io::lock(tmp, false, io::lockop::EXCLUSIVE)?) { + io::close(tmp)?; + exec::finish(&cmd); + return false; + }; + + match (tdidx) { + case void => void; + case let tdidx: size => + let td = getcache(t.mod, cachekind::SSA); + cmd.argv[tdidx] = strings::concat(td, ".td.tmp"); + }; + + let outdated = module::outdated(out, depfiles, mod.srcs.mtime); + if (!exec || !outdated) { + io::close(tmp)?; + exec::finish(&cmd); + if (outdated) { + cleanup_task(ctx, t)?; + } else if (t.kind == cachekind::SSA) { + get_td(ctx, t.mod)?; + }; + free_task(t); + ctx.total -= 1; + return true; + }; + path::set(&ctx.buf, out)?; + let stderr = os::create(path::push_ext(&ctx.buf, "log")?, 0o644)?; + defer io::close(stderr)!; + exec::addfile(&cmd, os::stderr_file, stderr); + switch (ctx.mode) { + case output::DEFAULT => void; + case output::VERBOSE => + if (ctx.interactive) { + fmt::printfln("\x1b[1m" "{}" "\x1b[0m" "\t{}", + cmd.argv[0], mod.id)?; + } else { + fmt::printfln("{}\t{}", cmd.argv[0], mod.id)?; + }; + case output::VVERBOSE => + fmt::print(cmd.argv[0])?; + for (let i = 1z; i < len(cmd.argv); i += 1) { + fmt::print("", cmd.argv[i])?; + }; + fmt::println()?; + }; + let proc = exec::start(&cmd)?; + jobs[slot] = job { pid = proc, task = t, out = out, tmp = tmp }; + ui_update(ctx, *jobs)?; + return true; +}; + +fn getslot(ctx: *context, jobs: *[](job | void)) (size | error) = { + for (let i = 0z; i < len(jobs); i += 1) if (jobs[i] is void) return i;
Two expressions per line are somewhat ok, but this is too much imo.
+ return await_task(ctx, jobs)? as size; +}; + +fn await_task(ctx: *context, jobs: *[](job | void)) (size | void | error) = { + let havejob = false; + for (let i = 0z; i < len(jobs); i += 1) if (jobs[i] is job) { + havejob = true; + }; + if (!havejob) return void; + let (proc, status) = exec::waitany()?; + let i = 0z; + for (i < len(jobs); i += 1) match (jobs[i]) { + case void => void; + case let j: job => if (j.pid == proc) break; + }; + assert(i < len(jobs), "Unknown PID returned from waitany"); + let j = jobs[i] as job; + let t = j.task; + jobs[i] = void; + path::set(&ctx.buf, j.out)?; + let stderr = path::push_ext(&ctx.buf, "log")?; + defer os::remove(stderr): void; + match (exec::check(&status)) { + case void => void; + case let e: !exec::exit_status => + let stderr = os::open(stderr)?; + defer io::close(stderr)!; + ui_finish(ctx)?; + io::copy(os::stderr, stderr)?; + fmt::fatal(t.mod.mod.id, ctx.cmds[t.kind], exec::exitstr(e)); + }; + cleanup_task(ctx, t)?; + free_task(t); + io::close(j.tmp)?; + ctx.completed += 1; + return i; +}; + +// update the cache after a task has been run +fn cleanup_task(ctx: *context, t: *task) (void | fs::error) = { + let out = getcache(t.mod, t.kind); + defer free(out); + let tmp = strings::concat(out, ".tmp"); + defer free(tmp); + os::move(tmp, out)?; + if (t.kind != cachekind::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 + let tmp = strings::concat(out, ".td.tmp"); + defer free(tmp); + if (!os::exists(tmp)) return; + + let f = os::open(tmp)?; + defer io::close(f)!; + let contents = io::drain(f)?; + defer free(contents); + let h = fnv::fnv64a(); + hash::write(&h, contents); + let prefix: [8]u8 = [0...]; + hash::sum(&h, prefix); + t.mod.prefixes[cachekind::TD] = hex::encodestr(prefix); + let td = getcache(t.mod, cachekind::TD); + defer free(td); + + let ptr = strings::concat(out, ".td"); + defer free(ptr); + let ptr = os::create(ptr, 0o644)?; + defer io::close(ptr)!; + io::writeall(ptr, strings::toutf8(td))?; + + let ns = unparse::identstr(t.mod.mod.ns); + defer free(ns); + if (ctx.mode == output::VVERBOSE) { + fmt::printfln("# HARE_TD_{}={}", ns, td)?; + }; + let var = strings::concat("HARE_TD_", ns); + defer free(var); + os::setenv(var, td)!; + if (os::exists(td)) { + os::remove(tmp)?; + } else { + os::move(tmp, td)?; + }; +}; + +fn get_td(ctx: *context, mod: *modctx) (void | fs::error) = { + let ssa = getcache(mod, cachekind::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 path = strings::fromutf8(io::drain(ptr)?)!; + defer free(path); + let ns = unparse::identstr(mod.mod.ns); + defer free(ns); + if (ctx.mode == output::VVERBOSE) { + fmt::printfln("# HARE_TD_{}={}", ns, path)?; + }; + let var = strings::concat("HARE_TD_", ns); + defer free(var); + os::setenv(var, path)!; +}; + +fn ui_update(ctx: *context, jobs: [](job | void)) (void | io::error) = { + if (ctx.mode != output::DEFAULT || !ctx.interactive) return; + let buf = strio::dynamic(); + defer io::close(&buf)!; + fmt::fprintfln(&buf, "\x1b[{}A" "\x1b[0J" "{}/{} tasks completed ({}%)", + ctx.nlines + 1, ctx.completed, ctx.total, + if (ctx.total == 0) 100 else ctx.completed * 100 / ctx.total)?; + for (let i = ctx.nlines; i < len(jobs); i += 1) { + if (jobs[i] is job) ctx.nlines = i + 1; + }; + for (let i = 0z; i < ctx.nlines; i += 1) match (jobs[i]) { + case void => + fmt::fprintln(&buf)?; + case let job: job => + fmt::fprintfln(&buf, "\x1b[1m" "{}" "\x1b[0m\t{}", + ctx.cmds[job.task.kind], job.task.mod.mod.id)?; + }; + fmt::error(strio::string(&buf))?; +}; + +fn ui_finish(ctx: *context) (void | error) = { + if (ctx.mode != output::DEFAULT) return; + if (ctx.interactive) { + if (ctx.completed == ctx.total) ctx.nlines += 1; + if (ctx.nlines != 0) fmt::errorf("\x1b[{}A" "\x1b[0J", ctx.nlines)?; + return; + }; + fmt::errorfln("{}/{} tasks completed ({}%)", ctx.completed, ctx.total, + if (ctx.total == 0) 100 else ctx.completed * 100 / ctx.total)?; +}; + +fn task_cmp(a: const *void, b: const *void) int = { + let a = a: const **task, b = b: const **task; + return if (a.kind < b.kind) -1 else if (a.kind == b.kind) 0 else 1;
There's no risk for overflow, so this can safely be a.kind - b.kind
+}; + +fn ctx_finish(ctx: *context) void = { + free(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); +}; + +fn getcache(mod: *modctx, kind: cachekind) str = + module::cache_getfile(mod.cache, mod.prefixes[kind], kind); + +fn modctx_finish(mods: []modctx) void = { + for (let i = 0z; i < len(mods); i += 1) { + module::finish(&mods[i].mod); + free(mods[i].cache); + for (let j = 0z; j < len(mods[i].prefixes); j += 1) { + free(mods[i].prefixes[j]); + }; + strings::freeall(mods[i].libs); + for (let j = 0z; j < len(mods[i].flags); j += 1) { + strings::freeall(mods[i].flags[j]); + }; + }; + free(mods); +}; + +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); +}; diff --git a/cmd/hare/cache.ha b/cmd/hare/cache.ha new file mode 100644 index 00000000..5ce69133 --- /dev/null +++ b/cmd/hare/cache.ha @@ -0,0 +1,49 @@ +use fmt; +use fs; +use getopt; +use os; +use path; + +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) && 1024 < sz; 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 index f54c9d1e..6073d3fe 100644 --- a/cmd/hare/deps.ha +++ b/cmd/hare/deps.ha @@ -1,79 +1,93 @@ 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 (1 < len(cmd.args)) { + 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); - - static delete(stack[len(stack)-1]); - append(visited, this); - return len(visited) - 1; -}; + const input = if (len(cmd.args) == 0) os::getcwd() else cmd.args[0]; -// sorts in reverse -fn cmpsz(a: const *void, b: const *void) int = (*(b: *size) - *(a: *size)): int; + let mods: []module::module = []; + static let buf = path::buffer { ... }; + path::set(&buf, input)?; + let ctx = module::context { + harepath = harepath(), + harecache = harecache(), + tags = tags, + }; + module::gather(&ctx, &mods, &buf)?; + 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].id, child.id)!; + }; + }; + 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 (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; @@ -83,11 +97,11 @@ fn show_deps(depnodes: *[]depnode) void = { }; 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; @@ -110,13 +124,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].id)!; + 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 *void, b: const *void) int = { + let a = *(a: *(size, str)); + let b = *(b: *(size, str)); + return (b.0 - a.0): int; +}; diff --git a/cmd/haredoc/main.ha b/cmd/hare/doc.ha similarity index 59% rename from cmd/haredoc/main.ha rename to cmd/hare/doc.ha index eaf9dedd..119a609c 100644 --- a/cmd/haredoc/main.ha +++ b/cmd/hare/doc.ha @@ -4,6 +4,7 @@ // (c) 2021 Ember Sawady <ecs@d2evs.net> // (c) 2022 Sebastian <sebastian@sebsite.pw> use bufio; +use cmd::hare::doc; use fmt; use fs; use getopt; @@ -19,53 +20,16 @@ use path; use strings; use unix::tty; -type format = enum { - HARE, - TTY, - HTML, -}; - -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 = { +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 +37,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 +56,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 +64,39 @@ 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, id) = if (len(cmd.args) == 0) { + yield (".", []: ast::ident); + } else match (parseident(cmd.args[0])) { + case let id: ast::ident => + // first assume it's a module + yield match (module::findinpath(ctx.harepath, id)) { + case let modpath: str => + yield (modpath, 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]; + yield (module::findinpath(ctx.harepath, id)?, id); }; + case => yield (cmd.args[0], []: 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)); - }; + const srcs = module::findsrcs(modpath, tags)?; + 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 +123,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 +135,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 +151,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 +173,10 @@ fn parseident(in: str) (ast::ident | parse::error) = { const buf = bufio::fixed(strings::toutf8(in), io::mode::READ); 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 +211,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 +282,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::flags::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/color.ha b/cmd/hare/doc/color.ha similarity index 100% rename from cmd/haredoc/color.ha rename to cmd/hare/doc/color.ha diff --git a/cmd/haredoc/hare.ha b/cmd/hare/doc/hare.ha similarity index 94% rename from cmd/haredoc/hare.ha rename to cmd/hare/doc/hare.ha index e1a9cf4f..16c99e68 100644 --- a/cmd/haredoc/hare.ha +++ b/cmd/hare/doc/hare.ha @@ -14,7 +14,7 @@ use strings; use strio; // Formats output as Hare source code (prototypes) -fn emit_hare(ctx: *context) (void | error) = { +export fn emit_hare(ctx: *context) (void | error) = { const summary = ctx.summary; let first = true; @@ -74,23 +74,20 @@ fn emit_hare(ctx: *context) (void | error) = { }; fn emit_submodules_hare(ctx: *context) (void | error) = { - const submodules = submodules(ctx)?; - defer strings::freeall(submodules); - - if (len(submodules) != 0) { + 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(submodules); i += 1) { + 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, "::", submodules[i]); + yield strings::concat(s, "::", ctx.submods[i]); } else { - yield strings::dup(submodules[i]); + yield strings::dup(ctx.submods[i]); }; defer free(submodule); diff --git a/cmd/haredoc/html.ha b/cmd/hare/doc/html.ha similarity index 94% rename from cmd/haredoc/html.ha rename to cmd/hare/doc/html.ha index a5ff75ab..5718d407 100644 --- a/cmd/haredoc/html.ha +++ b/cmd/hare/doc/html.ha @@ -14,6 +14,7 @@ use hare::ast; use hare::ast::{variadism}; use hare::lex; use hare::module; +use hare::parse::doc; use hare::unparse; use io; use net::ip; @@ -69,7 +70,7 @@ fn html_escape(out: io::handle, in: str) (size | io::error) = { }; // Formats output as HTML -fn emit_html(ctx: *context) (void | error) = { +export fn emit_html(ctx: *context) (void | error) = { const decls = ctx.summary; const ident = unparse::identstr(ctx.ident); defer free(ident); @@ -82,13 +83,7 @@ fn emit_html(ctx: *context) (void | error) = { 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::fprintf(ctx.out, "+{} ", ctx.tags[i])?; }; fmt::fprintln(ctx.out, "</span></h2>")?; @@ -100,40 +95,18 @@ fn emit_html(ctx: *context) (void | error) = { fmt::fprintln(ctx.out, "</div>")?; }; - let identpath = module::identpath(ctx.ident); + let identpath = strings::join("/", 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.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(submodules); i += 1) { - let submodule = submodules[i]; + 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='")?; @@ -340,9 +313,9 @@ fn details(ctx: *context, decl: ast::decl) (void | error) = { return; }; -fn htmlref(ctx: *context, ref: ast::ident) (void | io::error) = { +fn htmlref(ctx: *context, ref: ast::ident) (void | error) = { const ik = - match (resolve(ctx, ref)) { + match (resolve(ctx, ref)?) { case let ik: (ast::ident, symkind) => yield ik; case void => @@ -363,12 +336,12 @@ fn htmlref(ctx: *context, ref: ast::ident) (void | io::error) = { case symkind::LOCAL => fmt::fprintf(ctx.out, "<a href='#{0}' class='ref'>{0}</a>", ident)?; case symkind::MODULE => - let ipath = module::identpath(id); + let ipath = strings::join("/", 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]); + 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)?; @@ -376,7 +349,7 @@ fn htmlref(ctx: *context, ref: ast::ident) (void | io::error) = { 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]); + 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)?; @@ -384,28 +357,28 @@ fn htmlref(ctx: *context, ref: ast::ident) (void | io::error) = { free(ident); }; -fn markup_html(ctx: *context, in: io::handle) (void | io::error) = { - let parser = parsedoc(in); +fn markup_html(ctx: *context, in: io::handle) (void | error) = { + let parser = doc::parse(in); let waslist = false; for (true) { - const tok = match (scandoc(&parser)) { + const tok = match (doc::scan(&parser)) { case void => if (waslist) { fmt::fprintln(ctx.out, "</ul>")?; }; break; - case let tok: token => + case let tok: doc::token => yield tok; }; match (tok) { - case paragraph => + case doc::paragraph => if (waslist) { fmt::fprintln(ctx.out, "</ul>")?; waslist = false; }; fmt::fprintln(ctx.out)?; fmt::fprint(ctx.out, "<p>")?; - case let tx: text => + case let tx: doc::text => defer free(tx); match (uri::parse(strings::trim(tx))) { case let uri: uri::uri => @@ -422,9 +395,9 @@ fn markup_html(ctx: *context, in: io::handle) (void | io::error) = { case uri::invalid => html_escape(ctx.out, tx)?; }; - case let re: reference => + case let re: doc::reference => htmlref(ctx, re)?; - case let sa: sample => + case let sa: doc::sample => if (waslist) { fmt::fprintln(ctx.out, "</ul>")?; waslist = false; @@ -433,7 +406,7 @@ fn markup_html(ctx: *context, in: io::handle) (void | io::error) = { html_escape(ctx.out, sa)?; fmt::fprint(ctx.out, "</pre>")?; free(sa); - case listitem => + case doc::listitem => if (!waslist) { fmt::fprintln(ctx.out, "<ul>")?; waslist = true; @@ -857,7 +830,7 @@ fn breadcrumb(ident: ast::ident) str = { let buf = strio::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]); + let ipath = strings::join("/", ident[..i+1]...); defer free(ipath); fmt::fprintf(&buf, "<a href='/{}'>{}</a>::", ipath, ident[i])!; }; diff --git a/cmd/haredoc/resolver.ha b/cmd/hare/doc/resolve.ha similarity index 75% rename from cmd/haredoc/resolver.ha rename to cmd/hare/doc/resolve.ha index 0e3be7af..b0fc2114 100644 --- a/cmd/haredoc/resolver.ha +++ b/cmd/hare/doc/resolve.ha @@ -3,8 +3,13 @@ // (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 { @@ -18,7 +23,7 @@ type symkind = enum { // 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) = { +fn resolve(ctx: *context, what: ast::ident) ((ast::ident, symkind) | void | error) = { if (is_local(ctx, what)) { return (what, symkind::LOCAL); }; @@ -27,8 +32,8 @@ fn resolve(ctx: *context, what: ast::ident) ((ast::ident, symkind) | void) = { // Look for symbol in remote module let partial = what[..len(what) - 1]; - match (module::lookup(ctx.mctx, partial)) { - case let ver: module::version => + match (module::findinpath(ctx.mctx.harepath, partial)) { + case str => return (what, symkind::SYMBOL); case module::error => void; }; @@ -41,15 +46,15 @@ fn resolve(ctx: *context, what: ast::ident) ((ast::ident, symkind) | void) = { }; }; if (len(what) > 2) { - match (lookup_remote_enum(ctx, what)) { + 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 => + match (module::findinpath(ctx.mctx.harepath, what)) { + case str => return (what, symkind::MODULE); case module::error => void; }; @@ -118,17 +123,23 @@ fn lookup_local_enum(ctx: *context, what: ast::ident) (ast::ident | void) = { }; }; -fn lookup_remote_enum(ctx: *context, what: ast::ident) (ast::ident | void) = { +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 version = match (module::lookup(ctx.mctx, mod)) { - case let ver: module::version => - yield ver; - case module::error => - abort(); + const modpath = match (module::findinpath(ctx.mctx.harepath, mod)) { + case let s: str => yield s; + case let e: module::error => + module::finish_error(e); + return void; + }; + const srcs = match (module::findsrcs(modpath, ctx.tags)) { + case let s: module::srcset => yield s; + case let e: module::error => + module::finish_error(e); + return void; }; // This would take a lot of memory to load @@ -139,18 +150,10 @@ fn lookup_remote_enum(ctx: *context, what: ast::ident) (ast::ident | void) = { }; 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(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) { @@ -176,3 +179,15 @@ fn lookup_remote_enum(ctx: *context, what: ast::ident) (ast::ident | void) = { }; }; }; + +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::flags::COMMENTS); + return parse::subunit(&lexer)?; +}; diff --git a/cmd/haredoc/sort.ha b/cmd/hare/doc/sort.ha similarity index 93% rename from cmd/haredoc/sort.ha rename to cmd/hare/doc/sort.ha index cc5a0928..c6ed617a 100644 --- a/cmd/haredoc/sort.ha +++ b/cmd/hare/doc/sort.ha @@ -5,19 +5,11 @@ 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 = { +export fn sort_decls(decls: []ast::decl) summary = { let sorted = summary { ... }; for (let i = 0z; i < len(decls); i += 1) { diff --git a/cmd/haredoc/tty.ha b/cmd/hare/doc/tty.ha similarity index 98% rename from cmd/haredoc/tty.ha rename to cmd/hare/doc/tty.ha index d983ca17..e48dc978 100644 --- a/cmd/haredoc/tty.ha +++ b/cmd/hare/doc/tty.ha @@ -17,7 +17,7 @@ use strio; let firstline: bool = true; // Formats output as Hare source code (prototypes) with syntax highlighting -fn emit_tty(ctx: *context) (void | error) = { +export fn emit_tty(ctx: *context) (void | error) = { init_colors(); const summary = ctx.summary; @@ -55,10 +55,7 @@ fn emit_tty(ctx: *context) (void | error) = { }; fn emit_submodules_tty(ctx: *context) (void | error) = { - const submodules = submodules(ctx)?; - defer strings::freeall(submodules); - - if (len(submodules) != 0) { + if (len(ctx.submods) != 0) { fmt::fprintln(ctx.out)?; if (len(ctx.ident) == 0) { render(ctx.out, syn::COMMENT)?; @@ -69,13 +66,13 @@ fn emit_submodules_tty(ctx: *context) (void | error) = { fmt::fprintln(ctx.out, "// Submodules")?; render(ctx.out, syn::NORMAL)?; }; - for (let i = 0z; i < len(submodules); i += 1) { + 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, "::", submodules[i]); + yield strings::concat(s, "::", ctx.submods[i]); } else { - yield strings::dup(submodules[i]); + yield strings::dup(ctx.submods[i]); }; defer free(submodule); diff --git a/cmd/hare/doc/types.ha b/cmd/hare/doc/types.ha new file mode 100644 index 00000000..8398f6e3 --- /dev/null +++ b/cmd/hare/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/util.ha b/cmd/hare/doc/util.ha similarity index 52% rename from cmd/haredoc/util.ha rename to cmd/hare/doc/util.ha index ba17bab0..623713a3 100644 --- a/cmd/haredoc/util.ha +++ b/cmd/hare/doc/util.ha @@ -2,9 +2,11 @@ // (c) 2022 Byron Torres <b@torresjrjr.com> // (c) 2022 Sebastian <sebastian@sebsite.pw> use fmt; -use hare::ast; +use fs; use hare::module; use io; +use os; +use sort; use strings; use strio; @@ -36,35 +38,14 @@ fn trim_comment(s: str) str = { return strings::dup(strio::string(&trimmed)); }; -fn submodules(ctx: *context) ([]str | error) = { - let identpath = module::identpath(ctx.ident); - defer free(identpath); - +export fn submodules(path: str) ([]str | error) = { 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); + 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/hare/error.ha b/cmd/hare/error.ha new file mode 100644 index 00000000..57c4f984 --- /dev/null +++ b/cmd/hare/error.ha @@ -0,0 +1,23 @@ +use cmd::hare::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 | + unknown_arch | + unknown_output | + unknown_type | + output_not_file | + doc::error | +); diff --git a/cmd/hare/main.ha b/cmd/hare/main.ha index 0164779e..8f2da0e5 100644 --- 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 cmd::hare::doc; +use fmt; +use fs; use getopt; +use hare::module; +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,31 +21,35 @@ const help: []getopt::help = [ "args...", ("build", [ "compiles the Hare program at <path>", - ('c', "build object instead of executable"), - ('v', "print executed commands"), + ('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 (ssa/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), + ("doc", [ + "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...]", ]: []getopt::help), ("release", [ "prepares a new release for a program or library", @@ -48,26 +58,26 @@ const help: []getopt::help = [ ]: []getopt::help), ("run", [ "compiles and runs the Hare program at <path>", - ('v', "print executed commands"), + ('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"), + ('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", @@ -84,21 +94,38 @@ export fn main() void = { os::exit(1); case let subcmd: (str, *getopt::command) => const task = switch (subcmd.0) { - case "build" => - yield &build; - case "cache" => - yield &cache; - case "deps" => - yield &deps; - case "release" => - yield &release; - case "run" => - yield &run; - case "test" => - yield &test; - case "version" => - yield &version; + case "build", "run", "test" => yield &build; + case "cache" => yield &cache; + case "deps" => yield &deps; + case "doc" => yield &doc; + case "release" => yield &release; + case "version" => yield &version; + case => abort(); + }; + match (task(subcmd.0, subcmd.1)) { + 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: strconv::error => + fmt::fatal(strconv::strerror(e)); + case let e: unknown_arch => + fmt::fatalf("unknown arch: {}", e); + case unknown_output => + fmt::fatal("can't guess output in root directory"); + case let e: unknown_type => + fmt::fatalf("unknown build type: {}", e); + case let e: output_not_file => + fmt::fatalf("output '{}' exists and is not a file", e); + case void => void; }; - task(subcmd.1); }; }; diff --git a/cmd/hare/plan.ha b/cmd/hare/plan.ha deleted file mode 100644 index d8e7ac5d..00000000 --- a/cmd/hare/plan.ha @@ -1,328 +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; - ld_tool.0 = if (len(libs) > 0) { - yield target.cc_cmd; - } else { - yield 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::BUFSIZ]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 deleted file mode 100644 index 9d6cbd31..00000000 --- a/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/release.ha b/cmd/hare/release.ha index 08b033b5..5fd46ec3 100644 --- a/cmd/hare/release.ha +++ b/cmd/hare/release.ha @@ -6,6 +6,7 @@ use bufio; use errors; use fmt; use fs; +use getopt; use io; use os::exec; use os; @@ -49,6 +50,56 @@ const initial_template: str = "# These are the release notes for the initial rel {0} version {1} "; +fn release(name: str, cmd: *getopt::command) (void | error) = { + let dryrun = false; + for (let i = 0z; i < len(cmd.opts); i += 1) { + let opt = cmd.opts[i]; + switch (opt.0) { + case 'd' => + dryrun = true; + case => abort(); + }; + }; + + if (len(cmd.args) == 0) { + getopt::printusage(os::stderr, name, cmd.help)!; + os::exit(1); + }; + + const next = switch (cmd.args[0]) { + case "major" => + yield increment::MAJOR; + case "minor" => + yield increment::MINOR; + case "patch" => + yield increment::PATCH; + case => + yield match (parseversion(cmd.args[0])) { + case badversion => + getopt::printusage(os::stderr, "release", cmd.help)!; + os::exit(1); + case let ver: modversion => + yield ver; + }; + }; + + match (do_release(next, dryrun)) { + case void => void; + case let err: exec::error => + fmt::fatal(exec::strerror(err)); + case let err: errors::error => + fmt::fatal(errors::strerror(err)); + case let err: io::error => + fmt::fatal(io::strerror(err)); + case let err: fs::error => + fmt::fatal(fs::strerror(err)); + case let err: git_error => + fmt::fatal("git:", exec::exitstr(err)); + case badversion => + fmt::fatal("Error: invalid format string. Hare uses semantic versioning, in the form major.minor.patch."); + }; +}; + fn parseversion(in: str) (modversion | badversion) = { const items = strings::split(in, "."); defer free(items); diff --git a/cmd/hare/schedule.ha b/cmd/hare/schedule.ha deleted file mode 100644 index f0656605..00000000 --- a/cmd/hare/schedule.ha @@ -1,388 +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; -use strio; - -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 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 task = alloc(task { - status = status::SCHEDULED, - output = output, - depend = alloc(depend...), - cmd = getcmd(&ld_tool, - "-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 will not work when using cc as the linker - if (len(plan.libs) == 0 && task.cmd[0] == plan.target.ld_cmd) { - append(task.cmd, "--gc-sections"); - }; - - 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 deleted file mode 100644 index 79a3439e..00000000 --- a/cmd/hare/subcmds.ha @@ -1,624 +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 => - let task = sched_hare_object(&plan, ver, - namespace, output, depends...); - append(plan.scheduled, task); - }; - 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 release(cmd: *getopt::command) void = { - let dryrun = false; - for (let i = 0z; i < len(cmd.opts); i += 1) { - let opt = cmd.opts[i]; - switch (opt.0) { - case 'd' => - dryrun = true; - case => abort(); - }; - }; - - if (len(cmd.args) == 0) { - getopt::printusage(os::stderr, "release", cmd.help)!; - os::exit(1); - }; - - const next = switch (cmd.args[0]) { - case "major" => - yield increment::MAJOR; - case "minor" => - yield increment::MINOR; - case "patch" => - yield increment::PATCH; - case => - yield match (parseversion(cmd.args[0])) { - case badversion => - getopt::printusage(os::stderr, "release", cmd.help)!; - os::exit(1); - case let ver: modversion => - yield ver; - }; - }; - - match (do_release(next, dryrun)) { - case void => void; - case let err: exec::error => - fmt::fatal(exec::strerror(err)); - case let err: errors::error => - fmt::fatal(errors::strerror(err)); - case let err: io::error => - fmt::fatal(io::strerror(err)); - case let err: fs::error => - fmt::fatal(fs::strerror(err)); - case let err: git_error => - fmt::fatal("git:", exec::exitstr(err)); - case badversion => - fmt::fatal("Error: invalid format string. Hare uses semantic versioning, in the form major.minor.patch."); - }; -}; - -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 deleted file mode 100644 index 58d8cef1..00000000 --- a/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 new file mode 100644 index 00000000..9b66c6a3 --- /dev/null +++ b/cmd/hare/util.ha @@ -0,0 +1,36 @@ +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) static delete (current[..]); + let newtags = module::parsetags(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) static delete(current[j]); + continue :new; + }; + }; + if (newtags[i].include) append(current, newtags[i].name); + }; +}; + +fn harepath() str = os::tryenv("HAREPATH", HAREPATH); + +fn harecache() str = match (os::getenv("HARECACHE")) { +case let s: str => yield s; +case void => yield dirs::cache("hare"); +}; + +// result must be freed, but internal strings are statically allocated +fn default_tags() ([]str | error) = { + let arch = get_arch(os::machine())?; + let platform = ascii::strlower(os::sysname()); + let tags: []str = alloc([arch.name, platform]); + return tags; +}; diff --git a/cmd/hare/version.ha b/cmd/hare/version.ha new file mode 100644 index 00000000..169fafda --- /dev/null +++ b/cmd/hare/version.ha @@ -0,0 +1,43 @@ +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 index 89ff60ee..814efed4 100644 --- a/cmd/harec/gen.ha +++ b/cmd/harec/gen.ha @@ -52,7 +52,7 @@ fn gen_func(ctx: *context, decl: *unit::decl) void = { assert(fntype.flags == 0); 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/env.ha b/cmd/haredoc/env.ha deleted file mode 100644 index a28f0ab4..00000000 --- a/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/errors.ha b/cmd/haredoc/errors.ha deleted file mode 100644 index 79e98e49..00000000 --- a/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/docs/hare-doc.5.scd b/docs/hare-doc.5.scd new file mode 100644 index 00000000..9ca1fc3b --- /dev/null +++ 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 indents). + +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.scd b/docs/hare.1.scd similarity index 74% rename from docs/hare.scd rename to docs/hare.1.scd index 2ad9057a..f715424e 100644 --- a/docs/hare.scd +++ b/docs/hare.1.scd @@ -6,35 +6,46 @@ hare - compiles, runs, and tests Hare programs # SYNOPSIS -*hare* build [-cv]++ +*hare* build [-hv]++ + [-a _arch_]++ [-D _ident[:type]=value_]++ [-j _jobs_]++ - [-L libdir]++ - [-l _name_]++ + [-L _libdir_]++ + [-l _libname_]++ + [-N _namespace_]++ [-o _path_]++ - [-t _arch_]++ - [-T _tags_] [-X _tags_]++ + [-T _tagset_]++ + [-t _type_]++ [_path_] -*hare* deps [-Mm] [-T _tags_] [-X _tags_] _path_ +*hare* cache [-hc] -*hare* run [-v]++ +*hare* deps [-hd] [-T _tagset_] [_path_|_module_] + +*hare* doc [-hat] [-F _format_] [-T _tagset_] [_identifiers_...] + +*hare* release [-hd] _major_|_minor_|_patch_|_x.y.z_ + +*hare* run [-hv]++ + [-a _arch_]++ [-D _ident[:type]=value_]++ - [-l _name_]++ - [-L libdir]++ [-j _jobs_]++ - [-T _tags_] [-X _tags_]++ - [_path_] [_args_...] + [-L _libdir_]++ + [-l _libname_]++ + [-T _tagset_]++ + [_path_ [_args_...]] -*hare* test [-v]++ +*hare* test [-hv]++ + [-a _arch_]++ [-D _ident[:type]=value_]++ - [-l _name_]++ - [-L libdir]++ [-j _jobs_]++ - [-T _tags_] [-X _tags_]++ - _tests_ + [-L _libdir_]++ + [-l _libname_]++ + [-o _path_]++ + [-T _tagset_]++ + [_path_] -*hare* version [-v] +*hare* version [-hv] # DESCRIPTION @@ -44,9 +55,19 @@ 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* queries the dependencies graph of a Hare program. The _path_ argument is equivalent in usage to *hare build*. +*hare doc* reads documentation for a set of identifiers from Hare source code, +and optionally prepares it for viewing in various output formats. By default, +*hare doc* will format documentation for your terminal. See *hare-doc*(5) for +details on the format. + +; *hare release* +; TODO + *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. @@ -68,14 +89,17 @@ value, and optional context, separated by tabs. ## hare build -*-c* - Compile only, do not link. The output is an object file (for Hare-only - modules) or archive (for mixed source modules). +*-h* + Prints the help text. *-v* Enable verbose logging. Prints every command to stderr before executing it. +*-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 @@ -87,43 +111,72 @@ value, and optional context, separated by tabs. 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_ +*-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. -*-L libdir* - Add directory to the linker library search path. +*-N* _namespace_ + Override the namespace for the module. *-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* _tagset_ + Sets or unsets build tags. See *CUSTOMIZING BUILD TAGS*. -*-T* _tags_ - Adds additional build tags. See *CUSTOMIZING BUILD TAGS*. +*-t* _type_ + Set the build type. Should be one of ssa/s/o/bin, for qbe IL, assembly, + compiled object, or compiled binary, respectively. -*-X* _tags_ - Unsets build tags. See *CUSTOMIZING BUILD TAGS*. +## 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). -*-M* - Print rules compatible with POSIX *make*(1). +*-T* _tags_ + Sets or unsets build tags. See *CUSTOMIZING BUILD TAGS*. + +## hare doc + +*-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_ - Adds additional build tags. See *CUSTOMIZING BUILD TAGS*. + Sets or unsets build tags. See *CUSTOMIZING BUILD TAGS*. -*-X* _tags_ - Unsets build tags. See *CUSTOMIZING BUILD TAGS*. +## hare release + +TODO ## hare run +*-h* + Prints the help text. + *-v* Enable verbose logging. Prints every command to stderr before executing it. @@ -155,6 +208,9 @@ value, and optional context, separated by tabs. ## hare test +*-h* + Prints the help text. + *-v* Enable verbose logging. Prints every command to stderr before executing it. @@ -186,6 +242,9 @@ value, and optional context, separated by tabs. ## hare version +*-h* + Prints the help text. + *-v* Show build parameters. @@ -275,17 +334,56 @@ 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^". +# DOC TTY COLORS + +The TTY output format of *hare doc* 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. + +. 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' hare doc -Ftty log + # ENVIRONMENT The following environment variables affect *hare*'s execution: |[ *HARECACHE* -:< The path to the object cache. Defaults to _$XDG_CACHE_HOME/hare_, or +:< 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. +| *HAREDOC_COLORS* +: Customizes TTY format color rendering for *hare doc*. See *TTY COLORS*. | *HAREC* : Name of the *harec*(1) command to use. | *AR* @@ -303,4 +401,4 @@ The following environment variables affect *hare*'s execution: # SEE ALSO -*harec*(1), *haredoc*(1), *as*(1), *ld*(1), *ar*(1), *make*(1) +*harec*(1), *hare-doc*(5), *as*(1), *ld*(1), *ar*(1), *make*(1) diff --git a/docs/haredoc.scd b/docs/haredoc.scd deleted file mode 100644 index 9269a317..00000000 --- a/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 indents). - -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/hare/module/README b/hare/module/README index a51dd556..db29d979 100644 --- a/hare/module/README +++ b/hare/module/README @@ -1,10 +1,7 @@ -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. +hare::module provides an interface to the module system used by Hare. 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. +This module also provides basic access to the Hare cache, but this is not +considered stable, and may be changed if we overhaul the cache format to +implement better caching strategies. diff --git a/hare/module/cache.ha b/hare/module/cache.ha new file mode 100644 index 00000000..76ebba61 --- /dev/null +++ b/hare/module/cache.ha @@ -0,0 +1,29 @@ +use path; +use strings; + +// A kind of cache file. used by [[cache_getfile]]. +export type cachekind = enum { + TD, + SSA, + S, + O, + BIN, +}; + +// File extensions corresponding to each [[cachekind]]. +export const cachekind_ext = ["td", "ssa", "s", "o", "bin"]; + +// Gets the cache directory for a given module, given the location 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
Their numerical values are used a lot, so it makes sense to add NKIND at the end. I would also explicitly initialize TD to 0 but that's just my preference.
+// than [[path::PATH_MAX]]. +export fn getcache(harecache: str, modpath: str) (str | error) = { + static let buf = path::buffer { ... }; + return path::set(&buf, harecache, modpath)?; +}; + +// Given the cache directory for a given module and the basename of the file, +// returns the location of the cache file of the given [[cachekind]]. The +// result must be freed by the caller. +export fn cache_getfile(modcache: str, basename: str, k: cachekind) str = + strings::concat(modcache, "/", basename, ".", cachekind_ext[k]); diff --git a/hare/module/context.ha b/hare/module/context.ha deleted file mode 100644 index ac39ffbc..00000000 --- a/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 os; -use path; -use strings; -use strio; - -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 = strio::dynamic(); - for (let i = 0z; i < len(ident); i += 1) { - fmt::fprintf(&buf, "{}{}", ident[i], - if (i + 1 < len(ident)) "_" - else "") as size; - }; - return strio::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 new file mode 100644 index 00000000..2f990450 --- /dev/null +++ b/hare/module/deps.ha @@ -0,0 +1,214 @@ +use bufio; +use fmt; +use fs; +use hare::ast; +use hare::lex; +use hare::parse; +use hare::unparse; +use io; +use os; +use path; +use sort; +use strings; +use strio; + +// A hare module. +export type module = struct { + id: str, + ns: ast::ident, + path: str, + srcs: srcset, + deps: [](size, ast::ident), +}; + +// Free the resources associated with a [[module]]. +export fn finish(mod: *module) void = { + free(mod.id); + free(mod.path); + finish_srcset(&mod.srcs); + for (let i = 0z; i < len(mod.deps); i += 1) { + 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); +}; + +// Finds the directory for a module in a given HAREPATH. The current directory +// will be searched before any of the items in harepath. The result will be an +// absolute path, and will be statically allocated. +export fn findinpath(harepath: str, mod: ast::ident) (str | error) = { + let tok = strings::tokenize(harepath, ":"); + let next: (str | void) = "."; + for (next is str; next = strings::next_token(&tok)) { + static let buf = path::buffer { ... };
Cleanup functions normally belong to the end of file.
+ if (!os::exists(next as str)) continue; + path::set(&buf, os::realpath(next as str)?)?; + for (let i = 0z; i < len(mod); i += 1) { + path::push(&buf, mod[i])?; + }; + match (os::stat(path::string(&buf))) { + case fs::error => continue; + case let stat: fs::filestat => + if (fs::isdir(stat.mode)) return path::string(&buf); + }; + }; + return not_found; +}; + +// Get the list of dependencies referred to by a set of source files. +// The list will be sorted alphabetically and deduplicated. Use +// [[strings::freeall]] to free the result. +export fn parsedeps(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)!;
We have ast::ident_free for this. Uses of this function should also be updated to utilize that.
+ let lexer = lex::init(handle, files[i]); + defer lex::finish(&lexer); + let imports = parse::imports(&lexer)?; + defer ast::imports_finish(imports); + 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 *void, b: const *void) 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;
Not actually an issue, but worth pointing out that this loop is actually quatdratic because of the insert.
+ }; + return if (len(a) < len(b)) -1 else if (len(a) == len(b)) 0 else 1; +}; + +// Get the dependencies for a module from the cache, recalculating +// them if necessary. cachedir should be calculated with [[getcache]], +// and srcset should be calculated with [[findsrcs]]. +fn getcachedeps(cachedir: str, srcs: *srcset) ([]ast::ident | error) = { + static let buf = path::buffer{...}; + path::set(&buf, cachedir, "deps")?; + let rest = strio::fixed(buf.buf[buf.end..]); + buf.end += format_tags(&rest, srcs.seentags)?; + buf.end += strio::concat(&rest, ".txt")?; + let outofdate = outdated(path::string(&buf), srcs.ha, srcs.mtime); + os::mkdirs(cachedir, 0o777)?; + let depsfile = os::create(path::string(&buf), 0o666, fs::flags::RDWR)?; + defer io::close(depsfile)!; + io::lock(depsfile, true, io::lockop::EXCLUSIVE)?; + let deps: []ast::ident = []; + if (outofdate) { + deps = parsedeps(srcs.ha...)?; + io::trunc(depsfile, 0)?; + let out = bufio::buffered(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. The result will be sorted in reverse topological +// order, with the toplevel module last. The result should be freed with +// [[free_slice]]. +export fn gather( + ctx: *context, + out: *[]module, + mod: (*path::buffer | ast::ident), +) (size | error) = { + let stack: []str = []; + defer free(stack); + return _gather(ctx, out, &stack, mod)?; +}; + +fn _gather( + ctx: *context, + out: *[]module, + stack: *[]str, + mod: (*path::buffer | ast::ident), +) (size | error) = { + let modpath = match (mod) { + case let mod: *path::buffer => + yield strings::dup(path::string(mod)); + case let mod: ast::ident => + yield match (findinpath(ctx.harepath, mod)) { + case let s: str => + yield strings::dup(s); + case let e: error => + let e = attach(unparse::identstr(mod), e); + if (len(stack) == 0) { + return e; + } else { + return attach(stack[len(stack) - 1], e); + }; + }; + }; + + for (let j = 0z; j < len(stack); j += 1) { + if (modpath == stack[j]) { + defer free(modpath); + append(stack, modpath); + return strings::dupall(stack[j..]): dep_cycle; + }; + }; + for (let j = 0z; j < len(out); j += 1) { + if (modpath == out[j].path) {