diff --git a/Makefile b/Makefile index d8d0c008..3797d3d7 100644 --- a/Makefile +++ b/Makefile @@ -563,6 +563,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_lib_byteid \ $(BIN)/test_m2wwi_run \ $(BIN)/test_m3sep_run \ + $(BIN)/test_sepbuild_run \ $(BIN)/test_floatlit_run \ $(BIN)/test_checked_run \ $(BIN)/test_floatarr_run \ @@ -3097,6 +3098,15 @@ $(BIN)/test_m3sep_run: test/wcc/989_m3sep_run.c $(BIN)/ww $(BIN)/w6c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# 989_sepbuild_run drives `ww build --sep` + `ww_ww build --sep` (the +# M3-tail commit-3 build_one_sep driver) end-to-end on the real lib chain +# root->os->{rt,time}. Needs BOTH driver stages + both compiler stages +# (the keystone re-runs w6c -c on the bodies-unit). +$(BIN)/test_sepbuild_run: test/wcc/989_sepbuild_run.c $(BIN)/ww $(BIN)/ww_ww \ + $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6a_ww \ + $(BIN)/w6l $(BIN)/w6l_ww $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_floatlit_run: test/wcc/989_floatlit_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/cmd/ww/main.c b/cmd/ww/main.c index 7ac93804..e9da5ecc 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -570,6 +570,451 @@ build_one(const char *src, int entry_is_dir, const char *out, return 0; } +/* ==================================================================== + * ww build --sep — M3-tail separate-compilation driver (task #46/c3). + * ==================================================================== + * The `--sep` path materializes each imported package's `.wwi` + * interface and compiles every package on its own (`w6c -c`), then + * flat-links the `.o` set. combined.ww stays the DEFAULT live path; + * --sep is purely additive (no existing invocation reaches it). + * + * Each w6c pass is BOTH consumer (reads dep `.wwi` as import scope) AND + * producer (writes this package's `.wwi` for its importers via -I), so + * a package's interface is materialized as a side effect of compiling + * it. Reverse-topo order guarantees a package's deps' `.wwi` exist + * before it compiles. + * + * The load-bearing rule (ken #56, rob-resolved): every dep is tagged by + * its FULL DOTTED import path on prepend (`//ww:module `), so the + * definer's qualified symbol (#53) equals the consumer's qualified + * reference (#40) and the sep `.o`s link. A dep is NEVER bare-embedded. + * + * The prepend is the TRANSITIVE closure of a package's deps (lead- + * ratified, superseding rob-c3-spec §1.3 "direct deps"): a dep's public + * interface can name a transitive dep's type (os exposes time.instant), + * so the consuming unit needs the whole closure for name RESOLUTION — + * direct-deps-only does not type-check. This mirrors harec reading the + * transitive `.td` closure and is consistent with the current flat-unit + * transitive-namespace model (the visibility tighten is task #45, + * deferred post-M4). + */ +#define SEP_MAXPKG 256 + +struct seppkg { + char path[256]; /* dotted import path; "" == root/primary */ + char entry[1024]; /* resolved package dir (or file, for a file root) */ + int is_dir; + int deps[SEP_MAXPKG]; /* direct-dep indices into sepgraph.pkg */ + int ndeps; + int color; /* tri-color DFS: 0 white, 1 gray, 2 black */ +}; + +struct sepgraph { + struct seppkg pkg[SEP_MAXPKG]; + int n; +}; + +/* Find a package by dotted path, or add it. Returns its index, -1 full. */ +static int +sep_find_or_add(struct sepgraph *g, const char *path, const char *entry, + int is_dir) +{ + for (int i = 0; i < g->n; i++) + if (strcmp(g->pkg[i].path, path) == 0) return i; + if (g->n >= SEP_MAXPKG) { + fprintf(stderr, "ww --sep: too many packages (limit %d)\n", + SEP_MAXPKG); + return -1; + } + struct seppkg *p = &g->pkg[g->n]; + snprintf(p->path, sizeof p->path, "%s", path); + snprintf(p->entry, sizeof p->entry, "%s", entry); + p->is_dir = is_dir; + p->ndeps = 0; + p->color = 0; + return g->n++; +} + +/* Sanitize a package's dotted path into a scratch-file basename. Dots + * stay (legal in filenames); the root's empty path becomes "__root". */ +static void +sep_fname(const struct sepgraph *g, int pi, const char *scratch, + const char *suffix, char *out, size_t outsz) +{ + const char *base = g->pkg[pi].path[0] ? g->pkg[pi].path : "__root"; + snprintf(out, outsz, "%s/%s%s", scratch, base, suffix); +} + +/* Scan one source file for top-level `import IDENT;`, resolving each. A + * DIRECTORY import is a package boundary: add it as a direct dep of pkg + * `pi`. A FILE import is an intra-package split — fold its imports into + * `pi` (its bytes join pi's body at emit time). Mirrors expand's scan + * but collects package PATHS instead of concatenating bytes (§1.1). */ +static int +sep_scan_file(struct sepgraph *g, int pi, const char *file, + const char *searchpath, struct ImportSet *filevisit) +{ + if (import_seen(filevisit, file)) return 0; + import_add(filevisit, file); + FILE *in = fopen(file, "rb"); + if (in == NULL) { + fprintf(stderr, "ww --sep: cannot read %s\n", file); + return -1; + } + char line[2048]; + int rc = 0; + while (fgets(line, sizeof line, in)) { + const char *p = line; + while (*p == ' ' || *p == '\t') p++; + if (strncmp(p, "import ", 7) != 0 && strncmp(p, "import\t", 7) != 0) + continue; + p += 7; + while (*p == ' ' || *p == '\t') p++; + char name[256] = {0}; + int j = 0; + while ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') + || *p == '_' || *p == '.' || (*p >= '0' && *p <= '9')) + if (j + 1 < (int)sizeof name) name[j++] = *p++; + if (j == 0) continue; + char path_form[256]; + import_path_form(name, path_form, sizeof path_form); + char ipath[1024]; + int is_dir = 0; + /* a locate-miss is an inline-satisfied (single-file multi- + * package) import; --sep targets directory packages, so the + * combined path owns that case. Skip, mirroring expand. */ + if (!locate_import(searchpath, path_form, ipath, sizeof ipath, + &is_dir)) + continue; + if (is_dir) { + int di = sep_find_or_add(g, name, ipath, 1); + if (di < 0) { rc = -1; break; } + int seen = 0; + for (int k = 0; k < g->pkg[pi].ndeps; k++) + if (g->pkg[pi].deps[k] == di) { seen = 1; break; } + if (!seen) { + if (g->pkg[pi].ndeps >= SEP_MAXPKG) { rc = -1; break; } + g->pkg[pi].deps[g->pkg[pi].ndeps++] = di; + } + } else if (sep_scan_file(g, pi, ipath, searchpath, filevisit) < 0) { + rc = -1; break; + } + } + fclose(in); + return rc; +} + +/* Discover pkg pi's direct deps + recurse. Enumerate the package's own + * source files (dir → *.ww less *test.ww; file → the file) and scan + * each. `color` doubles as a scanned-marker here (2 == scanned); it is + * reset to white before the topo pass. */ +static int +sep_scan_pkg(struct sepgraph *g, int pi, const char *searchpath) +{ + if (g->pkg[pi].color == 2) return 0; + g->pkg[pi].color = 2; + struct ImportSet fv = {0}; + int rc = 0; + if (g->pkg[pi].is_dir) { + char **files = NULL; + int n = enumerate_dir_ww(g->pkg[pi].entry, &files); + for (int i = 0; i < n && rc == 0; i++) { + char fp[1024]; + snprintf(fp, sizeof fp, "%s/%s", g->pkg[pi].entry, files[i]); + rc = sep_scan_file(g, pi, fp, searchpath, &fv); + } + for (int i = 0; i < n; i++) free(files[i]); + free(files); + } else { + rc = sep_scan_file(g, pi, g->pkg[pi].entry, searchpath, &fv); + } + for (int i = 0; i < fv.n; i++) free(fv.paths[i]); + free(fv.paths); + if (rc < 0) return rc; + /* recurse into freshly-added deps (sep_find_or_add may have grown + * g->n during the scan; iterate by index). */ + for (int k = 0; k < g->pkg[pi].ndeps; k++) + if (sep_scan_pkg(g, g->pkg[pi].deps[k], searchpath) < 0) + return -1; + return 0; +} + +/* DFS post-order over the dep DAG → reverse-topo (deps before importer), + * cite Hare gather (deps.ha:123). Tri-color: a back-edge BAILS rather + * than spinning. Commit 3 assumes acyclic; the LOUD cycle reject is + * commit 4 (this just must not infinite-loop). */ +static int +sep_topo_visit(struct sepgraph *g, int pi, int *order, int *no) +{ + if (g->pkg[pi].color == 2) return 0; + if (g->pkg[pi].color == 1) { + fprintf(stderr, + "ww --sep: import cycle (loud reject lands commit 4)\n"); + return -1; + } + g->pkg[pi].color = 1; + for (int k = 0; k < g->pkg[pi].ndeps; k++) + if (sep_topo_visit(g, g->pkg[pi].deps[k], order, no) < 0) + return -1; + g->pkg[pi].color = 2; + order[(*no)++] = pi; + return 0; +} + +/* Mark pi's transitive deps (excluding pi) in inset[]. */ +static void +sep_mark_deps(struct sepgraph *g, int pi, char *inset) +{ + for (int k = 0; k < g->pkg[pi].ndeps; k++) { + int di = g->pkg[pi].deps[k]; + if (!inset[di]) { inset[di] = 1; sep_mark_deps(g, di, inset); } + } +} + +/* Emit one of pi's own source files into the sep-unit under the + * //ww:module-reset primary boundary (so -c emits its decls, imported + * ==0). DIRECTORY imports are skipped (provided as `.wwi` ahead of the + * body); FILE imports fold in (intra-package split). */ +static void +sep_emit_body(FILE *out, const char *path, struct ImportSet *visited, + const char *searchpath) +{ + if (import_seen(visited, path)) return; + import_add(visited, path); + FILE *in = fopen(path, "rb"); + if (in == NULL) { + fprintf(stderr, "ww --sep: cannot read %s\n", path); + return; + } + char line[2048]; + while (fgets(line, sizeof line, in)) { + const char *p = line; + while (*p == ' ' || *p == '\t') p++; + if (strncmp(p, "import ", 7) != 0 && strncmp(p, "import\t", 7) != 0) + continue; + p += 7; + while (*p == ' ' || *p == '\t') p++; + char name[256] = {0}; + int j = 0; + while ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') + || *p == '_' || *p == '.' || (*p >= '0' && *p <= '9')) + if (j + 1 < (int)sizeof name) name[j++] = *p++; + if (j == 0) continue; + char path_form[256]; + import_path_form(name, path_form, sizeof path_form); + char ipath[1024]; + int is_dir = 0; + if (!locate_import(searchpath, path_form, ipath, sizeof ipath, + &is_dir)) + continue; + if (!is_dir) + sep_emit_body(out, ipath, visited, searchpath); + } + fputs("//ww:module-reset\n", out); + rewind(in); + int ch; + while ((ch = fgetc(in)) != EOF) fputc(ch, out); + fputc('\n', out); + fclose(in); +} + +static void +sep_emit_dir_body(FILE *out, const char *dir, struct ImportSet *visited, + const char *searchpath) +{ + char **files = NULL; + int n = enumerate_dir_ww(dir, &files); + for (int i = 0; i < n; i++) { + char fp[1024]; + snprintf(fp, sizeof fp, "%s/%s", dir, files[i]); + sep_emit_body(out, fp, visited, searchpath); + free(files[i]); + } + free(files); +} + +/* Compose pi's sep-unit at `unitf`: the transitive-closure `.wwi`s + * (reverse-topo order, each tagged by its dotted path), then pi's own + * body under //ww:module-reset. */ +static int +sep_compose_unit(struct sepgraph *g, int pi, const char *scratch, + const int *order, int norder, const char *searchpath, const char *unitf) +{ + FILE *u = fopen(unitf, "wb"); + if (u == NULL) { + fprintf(stderr, "ww --sep: cannot open %s\n", unitf); + return -1; + } + char inset[SEP_MAXPKG] = {0}; + sep_mark_deps(g, pi, inset); + for (int oi = 0; oi < norder; oi++) { + int dj = order[oi]; + if (dj == pi || !inset[dj]) continue; + char wwi[1024]; + sep_fname(g, dj, scratch, ".wwi", wwi, sizeof wwi); + FILE *wf = fopen(wwi, "rb"); + if (wf == NULL) { + fprintf(stderr, "ww --sep: missing %s\n", wwi); + fclose(u); + return -1; + } + fprintf(u, "//ww:module %s\n", g->pkg[dj].path); + int ch; + while ((ch = fgetc(wf)) != EOF) fputc(ch, u); + fputc('\n', u); + fclose(wf); + } + struct ImportSet bodyvisit = {0}; + if (g->pkg[pi].is_dir) + sep_emit_dir_body(u, g->pkg[pi].entry, &bodyvisit, searchpath); + else + sep_emit_body(u, g->pkg[pi].entry, &bodyvisit, searchpath); + for (int i = 0; i < bodyvisit.n; i++) free(bodyvisit.paths[i]); + free(bodyvisit.paths); + fclose(u); + return 0; +} + +/* build_one_sep — the --sep orchestration: discover_deps, reverse_topo, + * the transitive producer loop (one `w6c -c -I` per package, dep-first), + * then a flat `w6l` of the `.o` set (per-pkg `.a` + multi-archive link + * is commit 4). Side files land in a cold `.sepwork` scratch dir + * (the structured cache is commit 5). */ +static int +build_one_sep(const char *src, int entry_is_dir, const char *out, + const char *objstem, const char *extra_includes, const char *extra_libs, + const char *extra_libdirs) +{ + const char *c6 = toolpath("WW_W6C", "w6c"); + const char *a6 = toolpath("WW_W6A", "w6a"); + const char *l6 = toolpath("WW_W6L", "w6l"); + const char *libdir = getenv("WW_LIB"); + if (libdir == NULL || libdir[0] == 0) { + static char libbuf[1024]; + snprintf(libbuf, sizeof libbuf, "%s/../lib", self_dir); + libdir = libbuf; + } + const char *srcdir = getenv("WW_SRCLIB"); + static char srcbuf[1024]; + if (srcdir == NULL || srcdir[0] == 0) { + snprintf(srcbuf, sizeof srcbuf, "%s/../../lib", self_dir); + if (access(srcbuf, 0) == 0) srcdir = srcbuf; + else if (access("lib", 0) == 0) srcdir = "lib"; + else srcdir = libdir; + } + /* search path: source-dir, then -I dirs, then srcdir (mirrors + * build_one). */ + char srcd[1024]; + if (entry_is_dir) { + snprintf(srcd, sizeof srcd, "%s", src); + size_t n = strlen(srcd); + while (n > 1 && srcd[n-1] == '/') srcd[--n] = '\0'; + } else { + const char *slash = strrchr(src, '/'); + if (slash) { + size_t n = (size_t)(slash - src); + if (n >= sizeof srcd) n = sizeof srcd - 1; + memcpy(srcd, src, n); + srcd[n] = '\0'; + } else { srcd[0] = '.'; srcd[1] = '\0'; } + } + static char searchpath[4096]; + if (extra_includes && extra_includes[0]) + snprintf(searchpath, sizeof searchpath, "%s:%s:%s", + srcd, extra_includes, srcdir); + else + snprintf(searchpath, sizeof searchpath, "%s:%s", srcd, srcdir); + srcdir = searchpath; + + char stem[1024]; + if (entry_is_dir) { + const char *b = strrchr(srcd, '/'); + const char *base = b ? b + 1 : srcd; + snprintf(stem, sizeof stem, "%s/%s", srcd, base); + } else { + snprintf(stem, sizeof stem, "%s", src); + char *dot = strrchr(stem, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + } + const char *ostem = (objstem && objstem[0]) ? objstem : stem; + char scratch[1100]; + snprintf(scratch, sizeof scratch, "%s.sepwork", ostem); + { char m[1200]; snprintf(m, sizeof m, "rm -rf %s", scratch); run(m); } + if (mkdir(scratch, 0755) != 0) { + fprintf(stderr, "ww --sep: cannot create scratch %s\n", scratch); + return 1; + } + + struct sepgraph *g = calloc(1, sizeof *g); + if (g == NULL) return 1; + int root = sep_find_or_add(g, "", src, entry_is_dir); + if (root < 0 || sep_scan_pkg(g, root, srcdir) < 0) { free(g); return 1; } + for (int i = 0; i < g->n; i++) g->pkg[i].color = 0; + int *order = calloc((size_t)g->n, sizeof *order); + int norder = 0; + if (order == NULL || sep_topo_visit(g, root, order, &norder) < 0) { + free(order); free(g); return 1; + } + + /* producer loop — dep-first, one `w6c -c -I` pass per package. */ + for (int oi = 0; oi < norder; oi++) { + int pi = order[oi]; + char unitf[1024], wwi[1024], asmf[1024], obj[1024], cmd[8192]; + sep_fname(g, pi, scratch, ".unit.ww", unitf, sizeof unitf); + sep_fname(g, pi, scratch, ".wwi", wwi, sizeof wwi); + sep_fname(g, pi, scratch, ".s", asmf, sizeof asmf); + sep_fname(g, pi, scratch, ".o", obj, sizeof obj); + if (sep_compose_unit(g, pi, scratch, order, norder, srcdir, + unitf) < 0) { free(order); free(g); return 1; } + snprintf(cmd, sizeof cmd, "%s -c -I %s -o %s %s", + c6, wwi, asmf, unitf); + if (run(cmd) != 0) { + fprintf(stderr, "ww --sep: w6c failed for %s\n", + g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)"); + free(order); free(g); return 1; + } + snprintf(cmd, sizeof cmd, "%s -o %s %s", a6, obj, asmf); + if (run(cmd) != 0) { + fprintf(stderr, "ww --sep: w6a failed for %s\n", + g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)"); + free(order); free(g); return 1; + } + } + + /* flat link: root.o first (order[norder-1]), deps after; runtime + * archive selectively pulls only undefined runtime symbols (commit 4 + * adds per-pkg `.a` + multi-archive reverse-topo link). */ + char rtargs[2048] = {0}; + char rtpath[1024]; + snprintf(rtpath, sizeof rtpath, "%s/libwwrt.a", libdir); + if (access(rtpath, 0) == 0) { + snprintf(rtargs, sizeof rtargs, "%s", rtpath); + } else { + char a1[1024], a2[1024]; + snprintf(a1, sizeof a1, "%s/../obj/rt/start.o", self_dir); + snprintf(a2, sizeof a2, "%s/../obj/rt/syscall.o", self_dir); + snprintf(rtargs, sizeof rtargs, "%s %s", a1, a2); + } + char objs[8192] = {0}; + for (int oi = norder - 1; oi >= 0; oi--) { + char obj[1024]; + sep_fname(g, order[oi], scratch, ".o", obj, sizeof obj); + size_t n = strlen(objs); + snprintf(objs + n, sizeof objs - n, "%s%s", n ? " " : "", obj); + } + const char *libargs = (extra_libs && extra_libs[0]) ? extra_libs : ""; + const char *libdirset = (extra_libdirs && extra_libdirs[0]) ? extra_libdirs : ""; + char cmd[16384]; + snprintf(cmd, sizeof cmd, "%s -o %s %s %s%s%s%s%s", + l6, out, objs, rtargs, + libdirset[0] ? " " : "", libdirset, + libargs[0] ? " " : "", libargs); + int rc = run(cmd); + free(order); free(g); + if (rc != 0) { fprintf(stderr, "ww --sep: w6l failed\n"); return 1; } + return 0; +} + static int do_version(void) { @@ -694,12 +1139,25 @@ parse_build_flags(const char *cmd, int argc, char **argv, char *libdirs, size_t libdirsz, char *libs, size_t libsz, char *outpath, size_t outsz, - const char **src_out) + const char **src_out, int *want_sep) { *src_out = NULL; int i = 0; for (; i < argc; i++) { - if (strncmp(argv[i], "-l", 2) == 0 && argv[i][2]) { + if (strcmp(argv[i], "--sep") == 0) { + /* M3-tail c3: separate-compilation path, build-only. + * `run` has no sep-compile-then-run path (out of #46 + * commit-3 scope), so it LOUD-REJECTS rather than + * silently swallowing a typed flag — both stages reject + * identically (rule 10; want_sep==NULL marks the run + * caller). */ + if (want_sep == NULL) { + fprintf(stderr, + "ww %s: --sep is only valid with build\n", cmd); + return -1; + } + *want_sep = 1; + } else if (strncmp(argv[i], "-l", 2) == 0 && argv[i][2]) { size_t n = strlen(libs); snprintf(libs + n, libsz - n, "%s%s", n ? " " : "", argv[i]); @@ -764,9 +1222,10 @@ do_build(int argc, char **argv) char libdirs[2048] = {0}; char incs[2048] = {0}; char outflag[1024] = {0}; + int want_sep = 0; if (parse_build_flags("build", argc, argv, incs, sizeof incs, libdirs, sizeof libdirs, libs, sizeof libs, - outflag, sizeof outflag, &src) < 0) + outflag, sizeof outflag, &src, &want_sep) < 0) return 2; if (src == NULL) src = "."; /* default: build cwd */ char resolved[1024]; @@ -792,6 +1251,9 @@ do_build(int argc, char **argv) } else { basename_no_ext(resolved, out, sizeof out); } + if (want_sep) + return build_one_sep(resolved, is_dir, out, objstem, incs, + libs, libdirs); return build_one(resolved, is_dir, out, objstem, incs, libs, libdirs, 0); } @@ -805,7 +1267,7 @@ do_run(int argc, char **argv) char outflag[1024] = {0}; /* -o accepted+ignored: run always uses the temp */ int next = parse_build_flags("run", argc, argv, incs, sizeof incs, libdirs, sizeof libdirs, libs, sizeof libs, - outflag, sizeof outflag, &src); + outflag, sizeof outflag, &src, NULL); if (next < 0) return 2; if (src == NULL) src = "."; char resolved[1024]; diff --git a/selfhost/cmd/ww/main.combined.ww b/selfhost/cmd/ww/main.combined.ww index 7e7b18ae..7b5667da 100644 --- a/selfhost/cmd/ww/main.combined.ww +++ b/selfhost/cmd/ww/main.combined.ww @@ -3720,6 +3720,545 @@ type lflags = struct { nlibs: i32, }; +// ---- ww build --sep — M3-tail separate-compilation driver ------------ +// +// Port of cmd/ww/main.c build_one_sep (task #46/c3). The `--sep` path +// materializes each imported package's `.wwi` interface and compiles +// every package on its own (`w6c -c`), then flat-links the `.o` set. +// combined.ww stays the DEFAULT live path; --sep is additive. +// +// Each w6c pass is BOTH consumer (reads dep `.wwi` as import scope) AND +// producer (writes this package's `.wwi` via -I). Reverse-topo order +// guarantees a package's deps' `.wwi` exist before it compiles. +// +// The load-bearing rule (#56, rob-resolved): every dep is tagged by its +// FULL DOTTED import path on prepend (`//ww:module `), so the +// definer's qualified symbol (#53) equals the consumer's qualified +// reference (#40) and the sep `.o`s link. The prepend is the TRANSITIVE +// closure of a package's deps (lead-ratified): a dep's interface can +// name a transitive dep's type, so the consuming unit needs the whole +// closure for resolution. The unit composition is byte-identical to the +// cstage driver (rule 10) so w6c/w6c_ww emit identical `.s`. + +def SEP_MAXPKG: i32 = 256; + +type seppkg = struct { + path: *u8, // dotted import path, NUL-term; root path[0]==0 + entry: *u8, // resolved package dir (or file, file root), NUL-term + isdir: i32, + deps: []i32, // direct-dep indices into sepgraph.pkg + ndeps: i32, + color: i32, // tri-color DFS: 0 white, 1 gray, 2 black +}; + +type sepgraph = struct { + pkg: []seppkg, // alloc'd SEP_MAXPKG + n: i32, +}; + +// Find a package by dotted path, or add it. Returns index, -1 if full. +fn sepfindoradd(g: *sepgraph, path: *u8, entry: *u8, isdir: i32) i32 = { + let i: i32 = 0; + for (i < g.n) { + if (cstreq(g.pkg[i].path, path)) { return i; }; + i += 1; + }; + if (g.n >= SEP_MAXPKG) { + cerr("ww --sep: too many packages\n"); + return -1; + }; + let plen: u64 = cstrlen(path); + let elen: u64 = cstrlen(entry); + g.pkg[g.n].path = arenadupcstr(path, plen); + g.pkg[g.n].entry = arenadupcstr(entry, elen); + g.pkg[g.n].isdir = isdir; + let dslot: []i32 = alloc([], SEP_MAXPKG: u64)!; + dslot.len = SEP_MAXPKG; + g.pkg[g.n].deps = dslot; + g.pkg[g.n].ndeps = 0; + g.pkg[g.n].color = 0; + let r: i32 = g.n; + g.n += 1; + return r; +}; + +// Build "/" NUL-term; base = path, or "__root" +// for the empty root path. +fn sepfname(g: *sepgraph, pi: i32, scratch: *u8, suffix: str) *u8 = { + let buf: []u8 = alloc([], (os.PATH_MAX: u64))!; + buf.len = os.PATH_MAX; + let off: u64 = cstrinto(buf.ptr, 0u64, scratch); + off = byteinto(buf.ptr, off, 47u8); // '/' + if (g.pkg[pi].path[0u64] != 0u8) { + off = cstrinto(buf.ptr, off, g.pkg[pi].path); + } else { + off = strinto(buf.ptr, off, "__root"); + }; + off = strinto(buf.ptr, off, suffix); + cstrseal(buf.ptr, off); + return buf.ptr; +}; + +// Scan one source file for top-level `import IDENT;`. A DIRECTORY import +// is a package boundary: add as a direct dep of pi. A FILE import is an +// intra-package split: fold its imports into pi. Mirrors cstage +// sep_scan_file (collects PATHS, not bytes). +fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8, + fv: *expctx) i32 = { + let fview: str; + fview.ptr = file; + fview.len = cstrlen(file): i32; + let fdup: str = strings.dup(fview); + if (visitseen(fv, fdup)) { return 0; }; + visitadd(fv, fdup); + let bufp: *u8; + let blen: u64; + bufp, blen = slurp(file); + if (bufp == nil) { + cerr("ww --sep: cannot read source\n"); + return -1; + }; + let i: u64 = 0u64; + for (i < blen) { + let j: u64 = i; + for (j < blen) { + if (bufp[j] == 10u8) { break; }; // '\n' + j += 1u64; + }; + let idp: *u8; + let idn: u64; + idp, idn = scanuse(bufp + i, j - i); + if (idp != nil) { + let isdir: i32 = 0; + let ipath: *u8 = locateimport(searchpath, idp, idn, &isdir); + if (ipath != nil) { + if (isdir != 0) { + let nm: []u8 = alloc([], idn + 1u64)!; + let k: u64 = 0u64; + for (k < idn) { nm[k] = idp[k]; k += 1u64; }; + nm[idn] = 0u8; + let di: i32 = sepfindoradd(g, nm.ptr, ipath, 1); + if (di < 0) { return -1; }; + let seen: bool = false; + let m: i32 = 0; + for (m < g.pkg[pi].ndeps) { + if (g.pkg[pi].deps[m] == di) { seen = true; }; + m += 1; + }; + if (!seen) { + if (g.pkg[pi].ndeps >= SEP_MAXPKG) { return -1; }; + g.pkg[pi].deps[g.pkg[pi].ndeps] = di; + g.pkg[pi].ndeps += 1; + }; + } else { + if (sepscanfile(g, pi, ipath, searchpath, fv) < 0) { + return -1; + }; + }; + }; + }; + i = j + 1u64; + }; + return 0; +}; + +// Discover pi's direct deps + recurse. Enumerate the package's own +// files (dir → *.ww less *test.ww; file → the file) and scan each. +// `color` doubles as a scanned-marker (2); reset to white before topo. +fn sepscanpkg(g: *sepgraph, pi: i32, searchpath: *u8) i32 = { + if (g.pkg[pi].color == 2) { return 0; }; + g.pkg[pi].color = 2; + let fv: expctx; + fv.out = -1; + fv.dirs = searchpath; + fv.visit = nil; + let rc: i32 = 0; + if (g.pkg[pi].isdir != 0) { + let names: **u8; + let n: i32; + names, n = enumeratedir(g.pkg[pi].entry); + let dlen: u64 = cstrlen(g.pkg[pi].entry); + let i: i32 = 0; + for (i < n) { + if (rc == 0) { + let nlen: u64 = cstrlen(names[i]); + let fp: []u8 = alloc([], dlen + 1u64 + nlen + 1u64)!; + let k: u64 = 0u64; + for (k < dlen) { fp[k] = g.pkg[pi].entry[k]; k += 1u64; }; + fp[dlen] = 47u8; // '/' + k = 0u64; + for (k < nlen) { fp[dlen + 1u64 + k] = names[i][k]; k += 1u64; }; + fp[dlen + 1u64 + nlen] = 0u8; + rc = sepscanfile(g, pi, fp.ptr, searchpath, &fv); + }; + i += 1; + }; + } else { + rc = sepscanfile(g, pi, g.pkg[pi].entry, searchpath, &fv); + }; + if (rc < 0) { return rc; }; + let k: i32 = 0; + for (k < g.pkg[pi].ndeps) { + if (sepscanpkg(g, g.pkg[pi].deps[k], searchpath) < 0) { return -1; }; + k += 1; + }; + return 0; +}; + +// DFS post-order over the dep DAG → reverse-topo (deps before importer). +// Tri-color: a back-edge BAILS rather than spinning (loud cycle reject is +// commit 4). Cite Hare gather (deps.ha:123). +fn septopovisit(g: *sepgraph, pi: i32, order: []i32, no: *i32) i32 = { + if (g.pkg[pi].color == 2) { return 0; }; + if (g.pkg[pi].color == 1) { + cerr("ww --sep: import cycle (loud reject lands commit 4)\n"); + return -1; + }; + g.pkg[pi].color = 1; + let k: i32 = 0; + for (k < g.pkg[pi].ndeps) { + if (septopovisit(g, g.pkg[pi].deps[k], order, no) < 0) { return -1; }; + k += 1; + }; + g.pkg[pi].color = 2; + order[*no] = pi; + *no += 1; + return 0; +}; + +// Mark pi's transitive deps (excluding pi) in inset[]. +fn sepmarkdeps(g: *sepgraph, pi: i32, inset: []u8) void = { + let k: i32 = 0; + for (k < g.pkg[pi].ndeps) { + let di: i32 = g.pkg[pi].deps[k]; + if (inset[di] == 0u8) { + inset[di] = 1u8; + sepmarkdeps(g, di, inset); + }; + k += 1; + }; +}; + +// Emit one of pi's own source files into the sep-unit under the +// //ww:module-reset primary boundary (so -c emits its decls, imported +// ==0). DIRECTORY imports are skipped (provided as `.wwi` ahead); +// FILE imports fold in (intra-package split). +fn sepemitbody(fd: i32, path: *u8, visit: *expctx, searchpath: *u8) void = { + let pview: str; + pview.ptr = path; + pview.len = cstrlen(path): i32; + let pdup: str = strings.dup(pview); + if (visitseen(visit, pdup)) { return; }; + visitadd(visit, pdup); + let bufp: *u8; + let blen: u64; + bufp, blen = slurp(path); + if (bufp == nil) { + cerr("ww --sep: cannot read source\n"); + return; + }; + let i: u64 = 0u64; + for (i < blen) { + let j: u64 = i; + for (j < blen) { + if (bufp[j] == 10u8) { break; }; // '\n' + j += 1u64; + }; + let idp: *u8; + let idn: u64; + idp, idn = scanuse(bufp + i, j - i); + if (idp != nil) { + let isdir: i32 = 0; + let ipath: *u8 = locateimport(searchpath, idp, idn, &isdir); + if (ipath != nil) { + if (isdir == 0) { + sepemitbody(fd, ipath, visit, searchpath); + }; + }; + }; + i = j + 1u64; + }; + let d: str = "//ww:module-reset\n"; + os.writeall(fd, d.ptr, d.len: u64); + os.writeall(fd, bufp, blen); + os.writeall(fd, "\n".ptr, 1u64); +}; + +fn sepemitdirbody(fd: i32, dir: *u8, visit: *expctx, searchpath: *u8) void = { + let names: **u8; + let n: i32; + names, n = enumeratedir(dir); + let dlen: u64 = cstrlen(dir); + let i: i32 = 0; + for (i < n) { + let nlen: u64 = cstrlen(names[i]); + let fp: []u8 = alloc([], dlen + 1u64 + nlen + 1u64)!; + let k: u64 = 0u64; + for (k < dlen) { fp[k] = dir[k]; k += 1u64; }; + fp[dlen] = 47u8; // '/' + k = 0u64; + for (k < nlen) { fp[dlen + 1u64 + k] = names[i][k]; k += 1u64; }; + fp[dlen + 1u64 + nlen] = 0u8; + sepemitbody(fd, fp.ptr, visit, searchpath); + i += 1; + }; +}; + +// Compose pi's sep-unit at `unitf`: the transitive-closure `.wwi`s +// (reverse-topo order, each tagged by its dotted path), then pi's own +// body under //ww:module-reset. +fn sepcomposeunit(g: *sepgraph, pi: i32, scratch: *u8, order: []i32, + norder: i32, searchpath: *u8, unitf: *u8) i32 = { + let u: i32 = os.open(pathstr(unitf), os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 + if (u < 0) { + cerr("ww --sep: cannot open unit\n"); + return -1; + }; + let inset: []u8 = alloc([], g.n: u64)!; + inset.len = g.n; + let z: i32 = 0; + for (z < g.n) { inset[z] = 0u8; z += 1; }; + sepmarkdeps(g, pi, inset); + let oi: i32 = 0; + for (oi < norder) { + let dj: i32 = order[oi]; + if (dj != pi) { + if (inset[dj] != 0u8) { + let wwi: *u8 = sepfname(g, dj, scratch, ".wwi"); + let wb: *u8; + let wn: u64; + wb, wn = slurp(wwi); + if (wb == nil) { + cerr("ww --sep: missing wwi\n"); + os.close(u); + return -1; + }; + let dm: str = "//ww:module "; + os.writeall(u, dm.ptr, dm.len: u64); + os.writeall(u, g.pkg[dj].path, cstrlen(g.pkg[dj].path)); + os.writeall(u, "\n".ptr, 1u64); + os.writeall(u, wb, wn); + os.writeall(u, "\n".ptr, 1u64); + }; + }; + oi += 1; + }; + let bv: expctx; + bv.out = u; + bv.dirs = searchpath; + bv.visit = nil; + if (g.pkg[pi].isdir != 0) { + sepemitdirbody(u, g.pkg[pi].entry, &bv, searchpath); + } else { + sepemitbody(u, g.pkg[pi].entry, &bv, searchpath); + }; + os.close(u); + return 0; +}; + +// buildonesep — the --sep orchestration: discover deps, reverse-topo, +// the transitive producer loop (one `w6c -c -I` per package, dep-first), +// then a flat `w6l` of the `.o` set. Side files land in a cold +// `.sepwork` scratch dir. Twin of cstage build_one_sep. +fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, + objstem: *u8, incs: *u8, lf: *lflags) i32 = { + let c6: *u8 = joinpathlit(selfdir, "w6c_ww"); + let a6: *u8 = joinpathlit(selfdir, "w6a_ww"); + let l6: *u8 = joinpathlit(selfdir, "w6l_ww"); + + // Default lib search path: /../../lib + let dotdotlib: []u8 = alloc([], (os.PATH_MAX: u64))!; + dotdotlib.len = os.PATH_MAX; + { + let off: u64 = cstrinto(dotdotlib.ptr, 0u64, selfdir); + off = strinto(dotdotlib.ptr, off, "/../../lib"); + cstrseal(dotdotlib.ptr, off); + }; + + // Source directory (mirrors buildone). + let srcd: []u8 = alloc([], (os.PATH_MAX: u64))!; + srcd.len = os.PATH_MAX; + if (entryisdir != 0) { + let slen: u64 = cstrlen(src); + let k: u64 = 0u64; + for (k < slen) { srcd[k] = src[k]; k += 1u64; }; + for (slen > 1u64) { + if (srcd[slen - 1u64] != 47u8) { break; }; + slen -= 1u64; + }; + srcd[slen] = 0u8; + } else { + let slen: u64 = cstrlen(src); + let last: u64 = slen; + let found: bool = false; + let i: u64 = slen; + for (i > 0u64) { + i -= 1u64; + if (src[i] == 47u8) { last = i; found = true; i = 0u64; }; + }; + if (found) { + let k: u64 = 0u64; + for (k < last) { srcd[k] = src[k]; k += 1u64; }; + srcd[last] = 0u8; + } else { + srcd[0] = 46u8; // '.' + srcd[1] = 0u8; + }; + }; + + // searchpath = srcd + ':' + incs + ':' + dotdotlib. + let searchpath: []u8 = alloc([], (os.PATH_MAX: u64) * 3u64)!; + searchpath.len = ((os.PATH_MAX: u64) * 3u64): i32; + { + let off: u64 = cstrinto(searchpath.ptr, 0u64, srcd.ptr); + off = byteinto(searchpath.ptr, off, 58u8); // ':' + if (incs[0u64] != 0u8) { + off = cstrinto(searchpath.ptr, off, incs); + off = byteinto(searchpath.ptr, off, 58u8); + }; + off = cstrinto(searchpath.ptr, off, dotdotlib.ptr); + cstrseal(searchpath.ptr, off); + }; + + // Stem for the scratch dir (mirrors buildone). + let stem: []u8 = alloc([], (os.PATH_MAX: u64))!; + stem.len = os.PATH_MAX; + if (entryisdir != 0) { + let dlen: u64 = cstrlen(srcd.ptr); + let bo: u64 = basenameoff(srcd.ptr, dlen); + let off: u64 = cstrinto(stem.ptr, 0u64, srcd.ptr); + stem[off] = 47u8; off += 1u64; // '/' + let i: u64 = bo; + for (i < dlen) { stem[off] = srcd[i]; off += 1u64; i += 1u64; }; + cstrseal(stem.ptr, off); + } else { + makestem(stem.ptr, src); + }; + let effstem: *u8 = stem.ptr; + if (objstem != nil) { effstem = objstem; }; + let scratch: *u8 = appendlit(effstem, ".sepwork"); + + // rm -rf scratch (cold); recreate. Reuse os.removeall if present; + // here we mkdir and rely on TRUNC opens to overwrite stale files. + os.mkdir(pathstr(scratch), 493i32); // 0o755 (idempotent; stale files TRUNC'd) + + // libwwrt.a path: /../lib/libwwrt.a + let libwwrt: []u8 = alloc([], (os.PATH_MAX: u64))!; + libwwrt.len = os.PATH_MAX; + { + let off: u64 = cstrinto(libwwrt.ptr, 0u64, selfdir); + off = strinto(libwwrt.ptr, off, "/../lib/libwwrt.a"); + cstrseal(libwwrt.ptr, off); + }; + + // Discover. + let pkgslot: []seppkg = alloc([], SEP_MAXPKG: u64)!; + pkgslot.len = SEP_MAXPKG; + let g: *sepgraph = alloc(sepgraph{pkg = pkgslot, n = 0})!; + let root: i32 = sepfindoradd(g, "\0".ptr, src, entryisdir); + if (root < 0) { return 1; }; + if (sepscanpkg(g, root, searchpath.ptr) < 0) { return 1; }; + + // Reset colors, reverse-topo. + let ci: i32 = 0; + for (ci < g.n) { g.pkg[ci].color = 0; ci += 1; }; + let order: []i32 = alloc([], g.n: u64)!; + order.len = g.n; + let norder: i32 = 0; + if (septopovisit(g, root, order, &norder) < 0) { return 1; }; + + // Producer loop — dep-first, one `w6c -c -I` per package. + let oi: i32 = 0; + for (oi < norder) { + let pi: i32 = order[oi]; + let unitf: *u8 = sepfname(g, pi, scratch, ".unit.ww"); + let wwi: *u8 = sepfname(g, pi, scratch, ".wwi"); + let asmf: *u8 = sepfname(g, pi, scratch, ".s"); + let objf: *u8 = sepfname(g, pi, scratch, ".o"); + if (sepcomposeunit(g, pi, scratch, order, norder, searchpath.ptr, unitf) < 0) { + return 1; + }; + { + let argv: []*u8 = alloc([], 8u64)!; + argv.len = 8; + argv[0] = "w6c\0".ptr; + argv[1] = "-c\0".ptr; + argv[2] = "-I\0".ptr; + argv[3] = wwi; + argv[4] = "-o\0".ptr; + argv[5] = asmf; + argv[6] = unitf; + argv[7] = nil; + if (procrun(c6, argv.ptr) != 0) { + cerr("ww --sep: w6c failed\n"); + return 1; + }; + }; + { + let argv: []*u8 = alloc([], 5u64)!; + argv.len = 5; + argv[0] = "w6a\0".ptr; + argv[1] = "-o\0".ptr; + argv[2] = objf; + argv[3] = asmf; + argv[4] = nil; + if (procrun(a6, argv.ptr) != 0) { + cerr("ww --sep: w6a failed\n"); + return 1; + }; + }; + oi += 1; + }; + + // Flat link: root.o first (order[norder-1]), deps after; libwwrt.a + // selectively pulls runtime symbols. argv: 4 fixed (w6l,-o,out) + 1 + // per .o + 1 libwwrt + 2*nlibdirs + 2*nlibs + 1 nil. + let nldirs: i32 = 0; + let nllibs: i32 = 0; + let ldirs: **u8 = nil; + let llibs: **u8 = nil; + if (lf != nil) { + nldirs = lf.nlibdirs; + nllibs = lf.nlibs; + ldirs = lf.libdirs; + llibs = lf.libs; + }; + let total: i32 = 3 + norder + 1 + 2 * nldirs + 2 * nllibs + 1; + let largv: []*u8 = alloc([], total: u64)!; + largv.len = total; + largv[0] = "w6l\0".ptr; + largv[1] = "-o\0".ptr; + largv[2] = out; + let pos: i32 = 3; + let li: i32 = norder - 1; + for (li >= 0) { + largv[pos] = sepfname(g, order[li], scratch, ".o"); + pos += 1; + li -= 1; + }; + largv[pos] = libwwrt.ptr; pos += 1; + let k: i32 = 0; + for (k < nldirs) { + largv[pos] = "-L\0".ptr; + largv[pos + 1] = ldirs[k]; + pos += 2; + k += 1; + }; + k = 0; + for (k < nllibs) { + largv[pos] = "-l\0".ptr; + largv[pos + 1] = llibs[k]; + pos += 2; + k += 1; + }; + largv[pos] = nil; + if (procrun(l6, largv.ptr) != 0) { + cerr("ww --sep: w6l failed\n"); + return 1; + }; + return 0; +}; + // buildone — compile `src` (file or directory) into the executable // named `out`. // selfdir: NUL-terminated dir containing this driver and the @@ -4088,6 +4627,7 @@ fn defaultoutpath(src: *u8) *u8 = { fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { let src: *u8 = nil; + let wantsep: i32 = 0; // --sep: M3-tail separate-compilation path let outflag: *u8 = nil; // -o target (binary + intermediate stem); T3 let incs: []u8 = alloc([], (os.PATH_MAX: u64) * 2u64)!; incs.len = ((os.PATH_MAX: u64) * 2u64): i32; @@ -4106,7 +4646,9 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { for (i < argc) { let p: *u8 = argv[i]; if (p[0u64] == 45u8) { // '-' - if (p[1u64] == 73u8) { // '-I' + if (cstreqlit(p, "--sep")) { // M3-tail separate-compile + wantsep = 1; + } else { if (p[1u64] == 73u8) { // '-I' let dir: *u8 = nil; if (p[2u64] != 0u8) { dir = p + 2u64; @@ -4174,7 +4716,7 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { } else { cerr("ww build: unknown flag\n"); return 2; - }; }; }; }; + }; }; }; }; }; } else { if (src == nil) { src = p; }; }; @@ -4221,6 +4763,9 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { lf.nlibdirs = nlibdirs; lf.libs = libs.ptr; lf.nlibs = nlibs; + if (wantsep != 0) { + return buildonesep(selfdir, resolved, isdir, out, objstem, incs.ptr, &lf); + }; return buildone(selfdir, resolved, isdir, out, objstem, incs.ptr, &lf, 0i32); }; @@ -4276,7 +4821,10 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { else { let p: *u8 = argv[i]; if (p[0u64] == 45u8) { - if (p[1u64] == 73u8) { + if (cstreqlit(p, "--sep")) { // build-only; run rejects (rule 10) + cerr("ww run: --sep is only valid with build\n"); + return 2; + } else { if (p[1u64] == 73u8) { let dir: *u8 = nil; if (p[2u64] != 0u8) { dir = p + 2u64; @@ -4343,7 +4891,7 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { } else { cerr("ww run: unknown flag\n"); return 2; - }; }; }; }; + }; }; }; }; }; i += 1; } else { if (src == nil) { diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index f4aca4f2..82f51b2a 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -846,6 +846,545 @@ type lflags = struct { nlibs: i32, }; +// ---- ww build --sep — M3-tail separate-compilation driver ------------ +// +// Port of cmd/ww/main.c build_one_sep (task #46/c3). The `--sep` path +// materializes each imported package's `.wwi` interface and compiles +// every package on its own (`w6c -c`), then flat-links the `.o` set. +// combined.ww stays the DEFAULT live path; --sep is additive. +// +// Each w6c pass is BOTH consumer (reads dep `.wwi` as import scope) AND +// producer (writes this package's `.wwi` via -I). Reverse-topo order +// guarantees a package's deps' `.wwi` exist before it compiles. +// +// The load-bearing rule (#56, rob-resolved): every dep is tagged by its +// FULL DOTTED import path on prepend (`//ww:module `), so the +// definer's qualified symbol (#53) equals the consumer's qualified +// reference (#40) and the sep `.o`s link. The prepend is the TRANSITIVE +// closure of a package's deps (lead-ratified): a dep's interface can +// name a transitive dep's type, so the consuming unit needs the whole +// closure for resolution. The unit composition is byte-identical to the +// cstage driver (rule 10) so w6c/w6c_ww emit identical `.s`. + +def SEP_MAXPKG: i32 = 256; + +type seppkg = struct { + path: *u8, // dotted import path, NUL-term; root path[0]==0 + entry: *u8, // resolved package dir (or file, file root), NUL-term + isdir: i32, + deps: []i32, // direct-dep indices into sepgraph.pkg + ndeps: i32, + color: i32, // tri-color DFS: 0 white, 1 gray, 2 black +}; + +type sepgraph = struct { + pkg: []seppkg, // alloc'd SEP_MAXPKG + n: i32, +}; + +// Find a package by dotted path, or add it. Returns index, -1 if full. +fn sepfindoradd(g: *sepgraph, path: *u8, entry: *u8, isdir: i32) i32 = { + let i: i32 = 0; + for (i < g.n) { + if (cstreq(g.pkg[i].path, path)) { return i; }; + i += 1; + }; + if (g.n >= SEP_MAXPKG) { + cerr("ww --sep: too many packages\n"); + return -1; + }; + let plen: u64 = cstrlen(path); + let elen: u64 = cstrlen(entry); + g.pkg[g.n].path = arenadupcstr(path, plen); + g.pkg[g.n].entry = arenadupcstr(entry, elen); + g.pkg[g.n].isdir = isdir; + let dslot: []i32 = alloc([], SEP_MAXPKG: u64)!; + dslot.len = SEP_MAXPKG; + g.pkg[g.n].deps = dslot; + g.pkg[g.n].ndeps = 0; + g.pkg[g.n].color = 0; + let r: i32 = g.n; + g.n += 1; + return r; +}; + +// Build "/" NUL-term; base = path, or "__root" +// for the empty root path. +fn sepfname(g: *sepgraph, pi: i32, scratch: *u8, suffix: str) *u8 = { + let buf: []u8 = alloc([], (os.PATH_MAX: u64))!; + buf.len = os.PATH_MAX; + let off: u64 = cstrinto(buf.ptr, 0u64, scratch); + off = byteinto(buf.ptr, off, 47u8); // '/' + if (g.pkg[pi].path[0u64] != 0u8) { + off = cstrinto(buf.ptr, off, g.pkg[pi].path); + } else { + off = strinto(buf.ptr, off, "__root"); + }; + off = strinto(buf.ptr, off, suffix); + cstrseal(buf.ptr, off); + return buf.ptr; +}; + +// Scan one source file for top-level `import IDENT;`. A DIRECTORY import +// is a package boundary: add as a direct dep of pi. A FILE import is an +// intra-package split: fold its imports into pi. Mirrors cstage +// sep_scan_file (collects PATHS, not bytes). +fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8, + fv: *expctx) i32 = { + let fview: str; + fview.ptr = file; + fview.len = cstrlen(file): i32; + let fdup: str = strings.dup(fview); + if (visitseen(fv, fdup)) { return 0; }; + visitadd(fv, fdup); + let bufp: *u8; + let blen: u64; + bufp, blen = slurp(file); + if (bufp == nil) { + cerr("ww --sep: cannot read source\n"); + return -1; + }; + let i: u64 = 0u64; + for (i < blen) { + let j: u64 = i; + for (j < blen) { + if (bufp[j] == 10u8) { break; }; // '\n' + j += 1u64; + }; + let idp: *u8; + let idn: u64; + idp, idn = scanuse(bufp + i, j - i); + if (idp != nil) { + let isdir: i32 = 0; + let ipath: *u8 = locateimport(searchpath, idp, idn, &isdir); + if (ipath != nil) { + if (isdir != 0) { + let nm: []u8 = alloc([], idn + 1u64)!; + let k: u64 = 0u64; + for (k < idn) { nm[k] = idp[k]; k += 1u64; }; + nm[idn] = 0u8; + let di: i32 = sepfindoradd(g, nm.ptr, ipath, 1); + if (di < 0) { return -1; }; + let seen: bool = false; + let m: i32 = 0; + for (m < g.pkg[pi].ndeps) { + if (g.pkg[pi].deps[m] == di) { seen = true; }; + m += 1; + }; + if (!seen) { + if (g.pkg[pi].ndeps >= SEP_MAXPKG) { return -1; }; + g.pkg[pi].deps[g.pkg[pi].ndeps] = di; + g.pkg[pi].ndeps += 1; + }; + } else { + if (sepscanfile(g, pi, ipath, searchpath, fv) < 0) { + return -1; + }; + }; + }; + }; + i = j + 1u64; + }; + return 0; +}; + +// Discover pi's direct deps + recurse. Enumerate the package's own +// files (dir → *.ww less *test.ww; file → the file) and scan each. +// `color` doubles as a scanned-marker (2); reset to white before topo. +fn sepscanpkg(g: *sepgraph, pi: i32, searchpath: *u8) i32 = { + if (g.pkg[pi].color == 2) { return 0; }; + g.pkg[pi].color = 2; + let fv: expctx; + fv.out = -1; + fv.dirs = searchpath; + fv.visit = nil; + let rc: i32 = 0; + if (g.pkg[pi].isdir != 0) { + let names: **u8; + let n: i32; + names, n = enumeratedir(g.pkg[pi].entry); + let dlen: u64 = cstrlen(g.pkg[pi].entry); + let i: i32 = 0; + for (i < n) { + if (rc == 0) { + let nlen: u64 = cstrlen(names[i]); + let fp: []u8 = alloc([], dlen + 1u64 + nlen + 1u64)!; + let k: u64 = 0u64; + for (k < dlen) { fp[k] = g.pkg[pi].entry[k]; k += 1u64; }; + fp[dlen] = 47u8; // '/' + k = 0u64; + for (k < nlen) { fp[dlen + 1u64 + k] = names[i][k]; k += 1u64; }; + fp[dlen + 1u64 + nlen] = 0u8; + rc = sepscanfile(g, pi, fp.ptr, searchpath, &fv); + }; + i += 1; + }; + } else { + rc = sepscanfile(g, pi, g.pkg[pi].entry, searchpath, &fv); + }; + if (rc < 0) { return rc; }; + let k: i32 = 0; + for (k < g.pkg[pi].ndeps) { + if (sepscanpkg(g, g.pkg[pi].deps[k], searchpath) < 0) { return -1; }; + k += 1; + }; + return 0; +}; + +// DFS post-order over the dep DAG → reverse-topo (deps before importer). +// Tri-color: a back-edge BAILS rather than spinning (loud cycle reject is +// commit 4). Cite Hare gather (deps.ha:123). +fn septopovisit(g: *sepgraph, pi: i32, order: []i32, no: *i32) i32 = { + if (g.pkg[pi].color == 2) { return 0; }; + if (g.pkg[pi].color == 1) { + cerr("ww --sep: import cycle (loud reject lands commit 4)\n"); + return -1; + }; + g.pkg[pi].color = 1; + let k: i32 = 0; + for (k < g.pkg[pi].ndeps) { + if (septopovisit(g, g.pkg[pi].deps[k], order, no) < 0) { return -1; }; + k += 1; + }; + g.pkg[pi].color = 2; + order[*no] = pi; + *no += 1; + return 0; +}; + +// Mark pi's transitive deps (excluding pi) in inset[]. +fn sepmarkdeps(g: *sepgraph, pi: i32, inset: []u8) void = { + let k: i32 = 0; + for (k < g.pkg[pi].ndeps) { + let di: i32 = g.pkg[pi].deps[k]; + if (inset[di] == 0u8) { + inset[di] = 1u8; + sepmarkdeps(g, di, inset); + }; + k += 1; + }; +}; + +// Emit one of pi's own source files into the sep-unit under the +// //ww:module-reset primary boundary (so -c emits its decls, imported +// ==0). DIRECTORY imports are skipped (provided as `.wwi` ahead); +// FILE imports fold in (intra-package split). +fn sepemitbody(fd: i32, path: *u8, visit: *expctx, searchpath: *u8) void = { + let pview: str; + pview.ptr = path; + pview.len = cstrlen(path): i32; + let pdup: str = strings.dup(pview); + if (visitseen(visit, pdup)) { return; }; + visitadd(visit, pdup); + let bufp: *u8; + let blen: u64; + bufp, blen = slurp(path); + if (bufp == nil) { + cerr("ww --sep: cannot read source\n"); + return; + }; + let i: u64 = 0u64; + for (i < blen) { + let j: u64 = i; + for (j < blen) { + if (bufp[j] == 10u8) { break; }; // '\n' + j += 1u64; + }; + let idp: *u8; + let idn: u64; + idp, idn = scanuse(bufp + i, j - i); + if (idp != nil) { + let isdir: i32 = 0; + let ipath: *u8 = locateimport(searchpath, idp, idn, &isdir); + if (ipath != nil) { + if (isdir == 0) { + sepemitbody(fd, ipath, visit, searchpath); + }; + }; + }; + i = j + 1u64; + }; + let d: str = "//ww:module-reset\n"; + os.writeall(fd, d.ptr, d.len: u64); + os.writeall(fd, bufp, blen); + os.writeall(fd, "\n".ptr, 1u64); +}; + +fn sepemitdirbody(fd: i32, dir: *u8, visit: *expctx, searchpath: *u8) void = { + let names: **u8; + let n: i32; + names, n = enumeratedir(dir); + let dlen: u64 = cstrlen(dir); + let i: i32 = 0; + for (i < n) { + let nlen: u64 = cstrlen(names[i]); + let fp: []u8 = alloc([], dlen + 1u64 + nlen + 1u64)!; + let k: u64 = 0u64; + for (k < dlen) { fp[k] = dir[k]; k += 1u64; }; + fp[dlen] = 47u8; // '/' + k = 0u64; + for (k < nlen) { fp[dlen + 1u64 + k] = names[i][k]; k += 1u64; }; + fp[dlen + 1u64 + nlen] = 0u8; + sepemitbody(fd, fp.ptr, visit, searchpath); + i += 1; + }; +}; + +// Compose pi's sep-unit at `unitf`: the transitive-closure `.wwi`s +// (reverse-topo order, each tagged by its dotted path), then pi's own +// body under //ww:module-reset. +fn sepcomposeunit(g: *sepgraph, pi: i32, scratch: *u8, order: []i32, + norder: i32, searchpath: *u8, unitf: *u8) i32 = { + let u: i32 = os.open(pathstr(unitf), os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 + if (u < 0) { + cerr("ww --sep: cannot open unit\n"); + return -1; + }; + let inset: []u8 = alloc([], g.n: u64)!; + inset.len = g.n; + let z: i32 = 0; + for (z < g.n) { inset[z] = 0u8; z += 1; }; + sepmarkdeps(g, pi, inset); + let oi: i32 = 0; + for (oi < norder) { + let dj: i32 = order[oi]; + if (dj != pi) { + if (inset[dj] != 0u8) { + let wwi: *u8 = sepfname(g, dj, scratch, ".wwi"); + let wb: *u8; + let wn: u64; + wb, wn = slurp(wwi); + if (wb == nil) { + cerr("ww --sep: missing wwi\n"); + os.close(u); + return -1; + }; + let dm: str = "//ww:module "; + os.writeall(u, dm.ptr, dm.len: u64); + os.writeall(u, g.pkg[dj].path, cstrlen(g.pkg[dj].path)); + os.writeall(u, "\n".ptr, 1u64); + os.writeall(u, wb, wn); + os.writeall(u, "\n".ptr, 1u64); + }; + }; + oi += 1; + }; + let bv: expctx; + bv.out = u; + bv.dirs = searchpath; + bv.visit = nil; + if (g.pkg[pi].isdir != 0) { + sepemitdirbody(u, g.pkg[pi].entry, &bv, searchpath); + } else { + sepemitbody(u, g.pkg[pi].entry, &bv, searchpath); + }; + os.close(u); + return 0; +}; + +// buildonesep — the --sep orchestration: discover deps, reverse-topo, +// the transitive producer loop (one `w6c -c -I` per package, dep-first), +// then a flat `w6l` of the `.o` set. Side files land in a cold +// `.sepwork` scratch dir. Twin of cstage build_one_sep. +fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, + objstem: *u8, incs: *u8, lf: *lflags) i32 = { + let c6: *u8 = joinpathlit(selfdir, "w6c_ww"); + let a6: *u8 = joinpathlit(selfdir, "w6a_ww"); + let l6: *u8 = joinpathlit(selfdir, "w6l_ww"); + + // Default lib search path: /../../lib + let dotdotlib: []u8 = alloc([], (os.PATH_MAX: u64))!; + dotdotlib.len = os.PATH_MAX; + { + let off: u64 = cstrinto(dotdotlib.ptr, 0u64, selfdir); + off = strinto(dotdotlib.ptr, off, "/../../lib"); + cstrseal(dotdotlib.ptr, off); + }; + + // Source directory (mirrors buildone). + let srcd: []u8 = alloc([], (os.PATH_MAX: u64))!; + srcd.len = os.PATH_MAX; + if (entryisdir != 0) { + let slen: u64 = cstrlen(src); + let k: u64 = 0u64; + for (k < slen) { srcd[k] = src[k]; k += 1u64; }; + for (slen > 1u64) { + if (srcd[slen - 1u64] != 47u8) { break; }; + slen -= 1u64; + }; + srcd[slen] = 0u8; + } else { + let slen: u64 = cstrlen(src); + let last: u64 = slen; + let found: bool = false; + let i: u64 = slen; + for (i > 0u64) { + i -= 1u64; + if (src[i] == 47u8) { last = i; found = true; i = 0u64; }; + }; + if (found) { + let k: u64 = 0u64; + for (k < last) { srcd[k] = src[k]; k += 1u64; }; + srcd[last] = 0u8; + } else { + srcd[0] = 46u8; // '.' + srcd[1] = 0u8; + }; + }; + + // searchpath = srcd + ':' + incs + ':' + dotdotlib. + let searchpath: []u8 = alloc([], (os.PATH_MAX: u64) * 3u64)!; + searchpath.len = ((os.PATH_MAX: u64) * 3u64): i32; + { + let off: u64 = cstrinto(searchpath.ptr, 0u64, srcd.ptr); + off = byteinto(searchpath.ptr, off, 58u8); // ':' + if (incs[0u64] != 0u8) { + off = cstrinto(searchpath.ptr, off, incs); + off = byteinto(searchpath.ptr, off, 58u8); + }; + off = cstrinto(searchpath.ptr, off, dotdotlib.ptr); + cstrseal(searchpath.ptr, off); + }; + + // Stem for the scratch dir (mirrors buildone). + let stem: []u8 = alloc([], (os.PATH_MAX: u64))!; + stem.len = os.PATH_MAX; + if (entryisdir != 0) { + let dlen: u64 = cstrlen(srcd.ptr); + let bo: u64 = basenameoff(srcd.ptr, dlen); + let off: u64 = cstrinto(stem.ptr, 0u64, srcd.ptr); + stem[off] = 47u8; off += 1u64; // '/' + let i: u64 = bo; + for (i < dlen) { stem[off] = srcd[i]; off += 1u64; i += 1u64; }; + cstrseal(stem.ptr, off); + } else { + makestem(stem.ptr, src); + }; + let effstem: *u8 = stem.ptr; + if (objstem != nil) { effstem = objstem; }; + let scratch: *u8 = appendlit(effstem, ".sepwork"); + + // rm -rf scratch (cold); recreate. Reuse os.removeall if present; + // here we mkdir and rely on TRUNC opens to overwrite stale files. + os.mkdir(pathstr(scratch), 493i32); // 0o755 (idempotent; stale files TRUNC'd) + + // libwwrt.a path: /../lib/libwwrt.a + let libwwrt: []u8 = alloc([], (os.PATH_MAX: u64))!; + libwwrt.len = os.PATH_MAX; + { + let off: u64 = cstrinto(libwwrt.ptr, 0u64, selfdir); + off = strinto(libwwrt.ptr, off, "/../lib/libwwrt.a"); + cstrseal(libwwrt.ptr, off); + }; + + // Discover. + let pkgslot: []seppkg = alloc([], SEP_MAXPKG: u64)!; + pkgslot.len = SEP_MAXPKG; + let g: *sepgraph = alloc(sepgraph{pkg = pkgslot, n = 0})!; + let root: i32 = sepfindoradd(g, "\0".ptr, src, entryisdir); + if (root < 0) { return 1; }; + if (sepscanpkg(g, root, searchpath.ptr) < 0) { return 1; }; + + // Reset colors, reverse-topo. + let ci: i32 = 0; + for (ci < g.n) { g.pkg[ci].color = 0; ci += 1; }; + let order: []i32 = alloc([], g.n: u64)!; + order.len = g.n; + let norder: i32 = 0; + if (septopovisit(g, root, order, &norder) < 0) { return 1; }; + + // Producer loop — dep-first, one `w6c -c -I` per package. + let oi: i32 = 0; + for (oi < norder) { + let pi: i32 = order[oi]; + let unitf: *u8 = sepfname(g, pi, scratch, ".unit.ww"); + let wwi: *u8 = sepfname(g, pi, scratch, ".wwi"); + let asmf: *u8 = sepfname(g, pi, scratch, ".s"); + let objf: *u8 = sepfname(g, pi, scratch, ".o"); + if (sepcomposeunit(g, pi, scratch, order, norder, searchpath.ptr, unitf) < 0) { + return 1; + }; + { + let argv: []*u8 = alloc([], 8u64)!; + argv.len = 8; + argv[0] = "w6c\0".ptr; + argv[1] = "-c\0".ptr; + argv[2] = "-I\0".ptr; + argv[3] = wwi; + argv[4] = "-o\0".ptr; + argv[5] = asmf; + argv[6] = unitf; + argv[7] = nil; + if (procrun(c6, argv.ptr) != 0) { + cerr("ww --sep: w6c failed\n"); + return 1; + }; + }; + { + let argv: []*u8 = alloc([], 5u64)!; + argv.len = 5; + argv[0] = "w6a\0".ptr; + argv[1] = "-o\0".ptr; + argv[2] = objf; + argv[3] = asmf; + argv[4] = nil; + if (procrun(a6, argv.ptr) != 0) { + cerr("ww --sep: w6a failed\n"); + return 1; + }; + }; + oi += 1; + }; + + // Flat link: root.o first (order[norder-1]), deps after; libwwrt.a + // selectively pulls runtime symbols. argv: 4 fixed (w6l,-o,out) + 1 + // per .o + 1 libwwrt + 2*nlibdirs + 2*nlibs + 1 nil. + let nldirs: i32 = 0; + let nllibs: i32 = 0; + let ldirs: **u8 = nil; + let llibs: **u8 = nil; + if (lf != nil) { + nldirs = lf.nlibdirs; + nllibs = lf.nlibs; + ldirs = lf.libdirs; + llibs = lf.libs; + }; + let total: i32 = 3 + norder + 1 + 2 * nldirs + 2 * nllibs + 1; + let largv: []*u8 = alloc([], total: u64)!; + largv.len = total; + largv[0] = "w6l\0".ptr; + largv[1] = "-o\0".ptr; + largv[2] = out; + let pos: i32 = 3; + let li: i32 = norder - 1; + for (li >= 0) { + largv[pos] = sepfname(g, order[li], scratch, ".o"); + pos += 1; + li -= 1; + }; + largv[pos] = libwwrt.ptr; pos += 1; + let k: i32 = 0; + for (k < nldirs) { + largv[pos] = "-L\0".ptr; + largv[pos + 1] = ldirs[k]; + pos += 2; + k += 1; + }; + k = 0; + for (k < nllibs) { + largv[pos] = "-l\0".ptr; + largv[pos + 1] = llibs[k]; + pos += 2; + k += 1; + }; + largv[pos] = nil; + if (procrun(l6, largv.ptr) != 0) { + cerr("ww --sep: w6l failed\n"); + return 1; + }; + return 0; +}; + // buildone — compile `src` (file or directory) into the executable // named `out`. // selfdir: NUL-terminated dir containing this driver and the @@ -1214,6 +1753,7 @@ fn defaultoutpath(src: *u8) *u8 = { fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { let src: *u8 = nil; + let wantsep: i32 = 0; // --sep: M3-tail separate-compilation path let outflag: *u8 = nil; // -o target (binary + intermediate stem); T3 let incs: []u8 = alloc([], (os.PATH_MAX: u64) * 2u64)!; incs.len = ((os.PATH_MAX: u64) * 2u64): i32; @@ -1232,7 +1772,9 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { for (i < argc) { let p: *u8 = argv[i]; if (p[0u64] == 45u8) { // '-' - if (p[1u64] == 73u8) { // '-I' + if (cstreqlit(p, "--sep")) { // M3-tail separate-compile + wantsep = 1; + } else { if (p[1u64] == 73u8) { // '-I' let dir: *u8 = nil; if (p[2u64] != 0u8) { dir = p + 2u64; @@ -1300,7 +1842,7 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { } else { cerr("ww build: unknown flag\n"); return 2; - }; }; }; }; + }; }; }; }; }; } else { if (src == nil) { src = p; }; }; @@ -1347,6 +1889,9 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { lf.nlibdirs = nlibdirs; lf.libs = libs.ptr; lf.nlibs = nlibs; + if (wantsep != 0) { + return buildonesep(selfdir, resolved, isdir, out, objstem, incs.ptr, &lf); + }; return buildone(selfdir, resolved, isdir, out, objstem, incs.ptr, &lf, 0i32); }; @@ -1402,7 +1947,10 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { else { let p: *u8 = argv[i]; if (p[0u64] == 45u8) { - if (p[1u64] == 73u8) { + if (cstreqlit(p, "--sep")) { // build-only; run rejects (rule 10) + cerr("ww run: --sep is only valid with build\n"); + return 2; + } else { if (p[1u64] == 73u8) { let dir: *u8 = nil; if (p[2u64] != 0u8) { dir = p + 2u64; @@ -1469,7 +2017,7 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { } else { cerr("ww run: unknown flag\n"); return 2; - }; }; }; }; + }; }; }; }; }; i += 1; } else { if (src == nil) { diff --git a/test/wcc/989_sepbuild_run.c b/test/wcc/989_sepbuild_run.c new file mode 100644 index 00000000..065056bb --- /dev/null +++ b/test/wcc/989_sepbuild_run.c @@ -0,0 +1,368 @@ +/* + * 989_sepbuild_run — M3-tail commit-3 `ww build --sep` driver gate (#46). + * + * Certifies build_one_sep (cmd/ww/main.c + selfhost/cmd/ww/main.ww twin) + * END-TO-END through the REAL `ww` / `ww_ww` drivers on a REAL lib chain + * (NOT the 989_m3sep synth fixture, which exercises the M3-CORE codegen + * via a hand harness). The driver adds ONLY orchestration around that + * proven core: discover_deps, reverse-topo, the transitive `w6c -c -I` + * producer loop, and a flat `w6l`. + * + * Chosen chain (smallest real multi-package graph with a genuine + * multi-level dep edge): root → os → { rt, time }. rt and time are + * true leaves; os imports both AND its public interface exposes a + * transitive type (os returns/takes time.instant in sibling protos), so + * the root's sep-unit needs the TRANSITIVE `.wwi` closure for name + * resolution — direct-deps-only does not type-check (the lead-ratified + * superseding of rob-c3-spec §1.3). The graph thus exercises: + * - discovery of the transitive package set, + * - reverse-topo dep-first ordering (time/rt before os before root), + * - the transitive-closure `.wwi` prepend tagged by dotted path, + * - one-pass `w6c -c -I` (consumer + producer) per package, + * - flat `w6l` of the `.o` set + libwwrt.a. + * + * Asserts (all COLD — `.sepwork` scratch is wiped each run): + * 1. Build + run, BOTH stages → exit EXPECT_EXIT (cross-boundary + * os.getpid resolves + links across sep `.o`s; the program runs). + * 2. cs==ww (rule 10): per-package `.s`/`.wwi`/`.unit.ww` AND the final + * binary are byte-identical between `ww --sep` and `ww_ww --sep`. + * 3. KEYSTONE (load-bearing, §3.1) THROUGH the driver: for each non-leaf + * package P, transform the driver's own `

.unit.ww` by substituting + * each dep's `.wwi` section with that dep's full directory BODIES, + * `w6c -c` it, and cmp vs the driver's `

.s`. Byte-identical proves + * the `.wwi` conveys exactly the dep facts P's codegen needs. (Leaves + * rt/time have no deps → keystone is vacuous; checked on os + root.) + * + * Light wwstage-driver test (CLAUDE.md rule 14): all intermediates are + * `-o`-redirected to /tmp, so it is phase-1 parallel-safe. Models + * 989_m3sep_run.c conventions; 989 prefix per the m3sep/m2wwi precedent. + */ +#include +#include +#include +#include +#include +#include +#include + +#define EXPECT_EXIT 7 + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return 1; +} + +static const char * +absbin(void) +{ + const char *b = getenv("BIN"); + if (!b) b = "out/bin"; + if (b[0] == '/') return b; + static char buf[2048]; + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return NULL; + snprintf(buf, sizeof buf, "%s/%s", cwd, b); + return buf; +} + +static int +slurp(const char *path, char **outbuf, size_t *outlen) +{ + FILE *f = fopen(path, "rb"); + if (!f) return -1; + fseek(f, 0, SEEK_END); + long n = ftell(f); + fseek(f, 0, SEEK_SET); + if (n < 0) { fclose(f); return -1; } + char *b = malloc((size_t)n + 1); + if (!b) { fclose(f); return -1; } + if (fread(b, 1, (size_t)n, f) != (size_t)n) { free(b); fclose(f); return -1; } + b[n] = '\0'; + fclose(f); + *outbuf = b; + *outlen = (size_t)n; + return 0; +} + +static int +files_eq(const char *a, const char *b) +{ + char *ba = NULL, *bb = NULL; + size_t na = 0, nb = 0; + if (slurp(a, &ba, &na) < 0 || slurp(b, &bb, &nb) < 0) { + free(ba); free(bb); + return -1; + } + int eq = (na == nb && memcmp(ba, bb, na) == 0); + free(ba); free(bb); + return eq ? 0 : 1; +} + +static int +write_file(const char *path, const char *body) +{ + FILE *f = fopen(path, "wb"); + if (!f) return -1; + fputs(body, f); + fclose(f); + return 0; +} + +static int +strs_cmp(const void *a, const void *b) +{ + return strcmp(*(const char *const *)a, *(const char *const *)b); +} + +/* Concatenate a package directory's *.ww bodies (less *test.ww and + * *.combined.ww, byte-sorted) into `out`. Mirrors the driver's body + * enumeration so the substituted bodies-unit matches the sep-unit + * structurally. */ +static int +append_dir_bodies(FILE *out, const char *dir) +{ + DIR *d = opendir(dir); + if (!d) return -1; + char *names[256]; + int n = 0; + struct dirent *ent; + while ((ent = readdir(d)) != NULL && n < 256) { + const char *nm = ent->d_name; + size_t nl = strlen(nm); + if (nl <= 3 || strcmp(nm + nl - 3, ".ww") != 0) continue; + if (nl >= 7 && strcmp(nm + nl - 7, "test.ww") == 0) continue; + if (nl >= 12 && strcmp(nm + nl - 12, ".combined.ww") == 0) continue; + names[n++] = strdup(nm); + } + closedir(d); + if (n > 1) qsort(names, (size_t)n, sizeof names[0], strs_cmp); + int rc = 0; + for (int i = 0; i < n; i++) { + char fp[1024]; + snprintf(fp, sizeof fp, "%s/%s", dir, names[i]); + char *b = NULL; + size_t bn = 0; + if (slurp(fp, &b, &bn) == 0) { + fwrite(b, 1, bn, out); + fputc('\n', out); + free(b); + } else rc = -1; + free(names[i]); + } + return rc; +} + +/* Transform the driver's sep-unit at `unitf` into a bodies-unit at + * `bodiesf`: every `//ww:module ` dep section (its `.wwi` content) + * is replaced by 's full directory bodies; the trailing + * `//ww:module-reset` primary body is copied verbatim. The result is the + * SAME unit P would compile with deps-as-bodies — the keystone's other + * arm. roots the dotted-path → dir mapping (dots → slashes). */ +static int +compose_bodies(const char *unitf, const char *libdir, const char *bodiesf) +{ + char *buf = NULL; + size_t n = 0; + if (slurp(unitf, &buf, &n) < 0) return -1; + FILE *out = fopen(bodiesf, "wb"); + if (!out) { free(buf); return -1; } + int rc = 0; + size_t i = 0; + while (i < n) { + size_t j = i; + while (j < n && buf[j] != '\n') j++; + size_t linelen = j - i; + if (strncmp(buf + i, "//ww:module-reset", 17) == 0) { + /* primary body: copy this line + everything after. */ + fwrite(buf + i, 1, n - i, out); + break; + } + if (strncmp(buf + i, "//ww:module ", 12) == 0) { + /* dep section: emit the directive, then dir bodies; + * skip the original `.wwi` content to the next + * directive. */ + fwrite(buf + i, 1, linelen, out); + fputc('\n', out); + char path[256]; + size_t pl = linelen - 12; + if (pl >= sizeof path) pl = sizeof path - 1; + memcpy(path, buf + i + 12, pl); + path[pl] = '\0'; + char dir[1024]; + char form[256]; + size_t k; + for (k = 0; path[k]; k++) + form[k] = (path[k] == '.') ? '/' : path[k]; + form[k] = '\0'; + snprintf(dir, sizeof dir, "%s/%s", libdir, form); + if (append_dir_bodies(out, dir) < 0) rc = -1; + /* advance past the wwi content to the next directive. */ + i = j + 1; + while (i < n) { + if (strncmp(buf + i, "//ww:module", 11) == 0) break; + size_t e = i; + while (e < n && buf[e] != '\n') e++; + i = (e < n) ? e + 1 : n; + } + continue; + } + i = j + 1; + } + fclose(out); + free(buf); + return rc; +} + +static const char *root_src = + "package main;\n" + "import os;\n" + "fn main() i32 = { return os.getpid() - os.getpid() + 7; };\n"; + +int +main(void) +{ + const char *bin = absbin(); + if (!bin) return 1; + char td[64], cmd[8192], libdir[2048]; + int fail = 0; + + snprintf(libdir, sizeof libdir, "%s/../../lib", bin); + snprintf(td, sizeof td, "/tmp/wwsep_%d", getpid()); + snprintf(cmd, sizeof cmd, "rm -rf %s", td); + runwait(cmd); + mkdir(td, 0755); + + char rootww[1024]; + snprintf(rootww, sizeof rootww, "%s/root.ww", td); + if (write_file(rootww, root_src)) { fail++; goto out; } + + /* The two driver stages and their scratch dirs. All per-package paths + * are rebuilt from `td` (a small fixed buffer) + the stage tag rather + * than chained through a large path buffer, so the snprintfs are + * provably non-truncating (warning-clean, like 989_m3sep_run). */ + struct { const char *drv, *tag; char prog[1024]; } + stg[] = { { "ww", "cs", {0} }, { "ww_ww", "ww", {0} } }; + + for (int s = 0; s < 2; s++) { + snprintf(stg[s].prog, sizeof stg[s].prog, "%s/prog.%s", td, stg[s].tag); + snprintf(cmd, sizeof cmd, + "timeout 240 %s/%s build --sep -o %s %s >/dev/null 2>&1", + bin, stg[s].drv, stg[s].prog, rootww); + if (runwait(cmd) != 0) { + fprintf(stderr, "sepbuild FAIL: %s build --sep\n", stg[s].drv); + fail++; + continue; + } + int rc = runwait(stg[s].prog); + if (rc != EXPECT_EXIT) { + fprintf(stderr, "sepbuild FAIL: %s prog exit=%d expected %d\n", + stg[s].drv, rc, EXPECT_EXIT); + fail++; + } + } + + /* reverse-topo correctness is implicit: every package compiled (a + * dep's `.wwi` existed before its importer) → build succeeded above. + * Assert the discovered package set materialized in the scratch. */ + const char *pkgs[] = { "time", "rt", "os", "__root" }; + for (int i = 0; i < 4; i++) { + char p[1024]; + snprintf(p, sizeof p, "%s/prog.cs.sepwork/%s.wwi", td, pkgs[i]); + if (access(p, 0) != 0) { + fprintf(stderr, "sepbuild FAIL: missing %s.wwi (discovery/topo)\n", + pkgs[i]); + fail++; + } + } + + /* cs==ww (rule 10): per-package .s/.wwi/.unit.ww + final binary. */ + for (int i = 0; i < 4; i++) { + const char *suf[] = { ".s", ".wwi", ".unit.ww" }; + for (int k = 0; k < 3; k++) { + char a[1024], b[1024]; + snprintf(a, sizeof a, "%s/prog.%s.sepwork/%s%s", + td, stg[0].tag, pkgs[i], suf[k]); + snprintf(b, sizeof b, "%s/prog.%s.sepwork/%s%s", + td, stg[1].tag, pkgs[i], suf[k]); + if (files_eq(a, b) != 0) { + fprintf(stderr, "sepbuild FAIL: cs!=ww for %s%s (rule 10)\n", + pkgs[i], suf[k]); + fail++; + } + } + } + if (files_eq(stg[0].prog, stg[1].prog) != 0) { + fprintf(stderr, "sepbuild FAIL: cs exe != ww exe (rule 10)\n"); + fail++; + } + + /* KEYSTONE through the driver: bodies-unit == sep-unit codegen, for + * each non-leaf package (os, root). Transform the driver's own + *

.unit.ww (deps as .wwi) into a bodies-unit (deps as full dir + * bodies), `w6c -c` it, cmp vs the driver's

.s. */ + const char *keypkgs[] = { "os", "__root" }; + for (int i = 0; i < 2; i++) { + char unitf[1024], bodiesf[1024], bodies_s[1024], sep_s[1024]; + snprintf(unitf, sizeof unitf, "%s/prog.cs.sepwork/%s.unit.ww", td, keypkgs[i]); + snprintf(bodiesf, sizeof bodiesf, "%s/%s.bodies.ww", td, keypkgs[i]); + snprintf(bodies_s, sizeof bodies_s, "%s/%s.bodies.s", td, keypkgs[i]); + snprintf(sep_s, sizeof sep_s, "%s/prog.cs.sepwork/%s.s", td, keypkgs[i]); + if (compose_bodies(unitf, libdir, bodiesf) < 0) { + fprintf(stderr, "sepbuild FAIL: compose bodies for %s\n", keypkgs[i]); + fail++; + continue; + } + snprintf(cmd, sizeof cmd, + "timeout 240 %s/w6c -c -o %s %s >/dev/null 2>&1", + bin, bodies_s, bodiesf); + if (runwait(cmd) != 0) { + fprintf(stderr, "sepbuild FAIL: %s bodies -c\n", keypkgs[i]); + fail++; + continue; + } + if (files_eq(bodies_s, sep_s) != 0) { + fprintf(stderr, "sepbuild FAIL: %s — bodies.s != driver sep.s " + "(the .wwi does not convey the dep facts P needs)\n", keypkgs[i]); + fail++; + } + } + + /* run --sep symmetry (#46 c3): `run` has no sep-compile-then-run + * path (out of commit-3 scope), so BOTH stages must LOUD-REJECT + * `run --sep` identically (rule 10) — not one silently ignore it. + * The reject exits 2 (the driver's usage/flag-error code); assert + * cs and ww agree AND actually rejected (exit 2, not a build+run + * that happened to exit nonzero). This row is the gate-visible + * guard whose absence let the divergence hide. */ + { + int rc_cs, rc_ww; + snprintf(cmd, sizeof cmd, + "%s/ww run --sep %s >/dev/null 2>&1", bin, rootww); + rc_cs = runwait(cmd); + snprintf(cmd, sizeof cmd, + "%s/ww_ww run --sep %s >/dev/null 2>&1", bin, rootww); + rc_ww = runwait(cmd); + if (rc_cs != rc_ww || rc_cs != 2) { + fprintf(stderr, "sepbuild FAIL: run --sep cs=%d ww=%d " + "(both must loud-reject, exit 2)\n", rc_cs, rc_ww); + fail++; + } + } + +out: + snprintf(cmd, sizeof cmd, "rm -rf %s", td); + runwait(cmd); + if (fail) { + fprintf(stderr, "sepbuild: %d check(s) failed\n", fail); + return 1; + } + printf("sepbuild: real chain root->os->{rt,time} via build_one_sep — " + "build+run (exit %d) + cs==ww per-pkg .s/.wwi/.unit + final binary " + "+ transitive-topo discovery + keystone bodies==.wwi (os,root)\n", + EXPECT_EXIT); + return 0; +}