From 33edc386f194286bcde7d6e2cc71892479844428 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 18 Jun 2026 02:51:31 +0900 Subject: [PATCH] wcc,ww: flip driver to separate-compilation only; delete build_one amalgamator (M4 E3-C1, #22) build_one_sep (per-package compile + .wwi interfaces + link) becomes the sole build path. do_build/do_run/do_test and the ww twins all route through it; --sep is now an accepted no-op and the run-rejects-sep guard is removed. Deleted the single-file amalgamator, both stages: build_one, expand, expand_dir, peek_package (+ the wwstage twins + strictpkgmismatch). unit_has_package is retained -- the sep scan loop's inline-package check needs it. The sep-shared helpers (enumerate_dir_ww, locate_import*, import_path_form, ImportSet, and ww counterparts) stay; they back the surviving sep path. Restores missing-package enforcement under sep by construction: the sep scan loop loudly rejects an unresolvable import (cannot find package ) unless the package is defined inline in the same unit -- matching the deleted amalgamator and closing the silent-accept the flip would otherwise introduce. All 5 wwstage tools relink (each is built via the now-sep `ww build`); emitted asm is byte-identical to the combined build per bootstrap input, so the binary md5 delta is pure link layout, not codegen. selfhost/cmd/ww/main.combined.ww is now stale and unregenerable (its writer build_one is deleted); #90 deletes it next. Test retargets folded in (rule-11 carve-out, #61/#133 precedent): each asserts post-flip-only behavior, is un-pre-migratable unlike #93/#94/#103, and splitting reddens one side. Closes #97. - 989_slttypepref -> dir-package layout (xb imports xa so both same-leaf `invalid` types are in scope at xb.f); inline-multipackage was the amalgamator shape, deleted with the flip. - 989_sepbuild_run -> run --sep now genuinely runs (exit 7), not the old loud-reject (exit 2); + a private per-pid WW_PKGCACHE so the cs/ww per-package byte-id compare on the shared real lib pkgs (rt/time/os) no longer races concurrent siblings on the global out/.pkgcache (the flip made sep the sole path, so every test now contends that cache). - 737_direnum -> the deleted strictpkgmismatch "differs from" wording -> sep's "does not match import path" (shared substring, wwstage terser #68). - 989_lib_byteid -> corpus-completeness scan excludes generated .sepwork scratch (the old `! -name '*.combined.ww'` exclude didn't cover the new sep artifact). --- cmd/ww/main.c | 479 ++++------------------- selfhost/cmd/ww/main.ww | 679 +++++---------------------------- test/wcc/737_direnum.c | 26 +- test/wcc/989_lib_byteid.c | 6 +- test/wcc/989_sepbuild_run.c | 29 +- test/wcc/989_slttypepref_run.c | 103 ++--- 6 files changed, 275 insertions(+), 1047 deletions(-) diff --git a/cmd/ww/main.c b/cmd/ww/main.c index 046b52ee..bb4b0f12 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -236,366 +236,13 @@ enumerate_dir_ww(const char *dirpath, char ***out_files) return n; } -static void expand(FILE *out, const char *path, struct ImportSet *visited, - const char *libdir, const char *modpath); - -/* Scan `path` for its first non-comment-non-blank line; if it starts - * with `package ;` write the name into `out` (NUL-terminated) - * and return 1, else 0. Strict-same-package enforcement (task #23 - * subset) is bundled here because the failure mode is dir-enum's - * own — a non-dir-enum compilation unit cannot trigger it. Hare's - * hare/module/srcs.ha:131 has the same constraint via the README - * gate; we encode it as same-package across all enumerated files. */ -static int -peek_package(const char *path, char *out, size_t outsz) -{ - FILE *in = fopen(path, "rb"); - if (in == NULL) return 0; - char line[2048]; - int found = 0; - while (fgets(line, sizeof line, in)) { - const char *p = line; - while (*p == ' ' || *p == '\t') p++; - if (*p == '\n' || *p == '\0') continue; - if (p[0] == '/' && p[1] == '/') continue; - if (strncmp(p, "package ", 8) != 0 - && strncmp(p, "package\t", 8) != 0) break; - p += 8; - while (*p == ' ' || *p == '\t') p++; - size_t i = 0; - while (i + 1 < outsz && ((p[i] >= 'a' && p[i] <= 'z') - || (p[i] >= 'A' && p[i] <= 'Z') - || p[i] == '_' || (p[i] >= '0' && p[i] <= '9'))) - out[i] = p[i], i++; - out[i] = '\0'; - found = (i > 0); - break; - } - fclose(in); - return found; -} - -/* unit_has_package — does `path` declare `package ;` ANYWHERE? - * #16 ENFORCE-driver (rob A): distinguishes a genuinely-missing import - * from one satisfied by an INLINE package in the same unit. Unlike - * peek_package (stops at the FIRST package decl), this scans every line - * — single-file multi-package fixtures carry several `package` decls. The - * comment-skip line scan + name match mirror peek_package, uncapped. The - * wwstage twin unithaspackage must stay byte-identical (rule 10). */ -static int -unit_has_package(const char *path, const char *leaf) -{ - FILE *in = fopen(path, "rb"); - if (in == NULL) return 0; - char line[2048]; - int found = 0; - while (fgets(line, sizeof line, in)) { - const char *p = line; - while (*p == ' ' || *p == '\t') p++; - if (p[0] == '/' && p[1] == '/') continue; - if (strncmp(p, "package ", 8) != 0 - && strncmp(p, "package\t", 8) != 0) continue; - p += 8; - while (*p == ' ' || *p == '\t') p++; - size_t i = 0; - while (leaf[i] != '\0' && leaf[i] == p[i]) i++; - if (leaf[i] == '\0') { - char c = p[i]; - if (c == ';' || c == ' ' || c == '\t' - || c == '\n' || c == '\0') { found = 1; break; } - } - } - fclose(in); - return found; -} - -/* expand_dir — enumerate /*.ww (skip *test.ww), byte-sort, - * recurse into each. Mirrors ref/hare/hare/module/srcs.ha:183 - * `_findsrcs` minus tag handling. The visited set still keys on - * concrete file paths so multi-file modules are pulled once. - * Strict-same-package: all enumerated files must declare the same - * `package ;` (task #23 subset; failure mode native to - * dir-enum). */ -static void -expand_dir(FILE *out, const char *dirpath, struct ImportSet *visited, - const char *libdir, const char *modpath) -{ - char **files = NULL; - int n = enumerate_dir_ww(dirpath, &files); - char dirpkg[128] = {0}; - for (int i = 0; i < n; i++) { - char fp[1024]; - snprintf(fp, sizeof fp, "%s/%s", dirpath, files[i]); - char pkg[128]; - if (peek_package(fp, pkg, sizeof pkg)) { - if (dirpkg[0] == '\0') { - snprintf(dirpkg, sizeof dirpkg, "%s", pkg); - } else if (strcmp(dirpkg, pkg) != 0) { - fprintf(stderr, - "ww: %s: package %s differs from %s in same module dir %s\n", - fp, pkg, dirpkg, dirpath); - exit(1); - } - } - expand(out, fp, visited, libdir, modpath); - free(files[i]); - } - free(files); -} - -/* Recursively expand `path`: for each top-level `import IDENT;` we - * find, resolve the import and expand it first, then append our own - * bytes. Already-visited paths are skipped. Each source carries its - * own `package ;` declaration (the parser stamps decls from - * it). */ -static void -expand(FILE *out, const char *path, struct ImportSet *visited, - const char *libdir, const char *modpath) -{ - if (import_seen(visited, path)) return; - import_add(visited, path); - - FILE *in = fopen(path, "rb"); - if (in == NULL) { - fprintf(stderr, "ww: 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; - /* #16 ENFORCE-driver (rob A): an unresolvable import is a hard - * error, not a silent skip — the old `continue` let a typo'd/ - * missing package drop its symbols and surface later as a - * confusing downstream failure. BUT a locate-miss is legal when - * the package is defined INLINE in the same unit (single-file - * multi-package: `package aa; ... package main; import aa;`) — - * the bundler can't pull it as a file but the checker binds it. - * So: miss + inline `package ` present -> silent skip; - * miss + not inline -> fatal. */ - if (!locate_import(libdir, path_form, ipath, sizeof ipath, - &is_dir)) { - const char *dot = strrchr(name, '.'); - const char *leaf = dot ? dot + 1 : name; - if (unit_has_package(path, leaf)) - continue; /* inline-satisfied */ - fprintf(stderr, "ww: cannot find package %s\n", name); - exit(1); - } - /* M1 #22 (isdir-gated, rob-ratified): a package IS a directory, so - * only DIRECTORY imports are package boundaries that path-mangle. - * A single-file import (`import opcodes;` → opcodes.ww declaring - * `package w6a`) is an intra-package file-split — it keeps its - * in-file `package` clause as its module (no directive). */ - if (is_dir) expand_dir(out, ipath, visited, libdir, name); - else expand(out, ipath, visited, libdir, NULL); - } - - /* #16 option-B: a package-less file's decls would otherwise inherit - * the preceding bundled module's sticky curmod (parser parse.c). Emit - * a curmod-reset boundary directive so the lexer/parser attribute the - * file to the primary module ("") — fixes the self-import false-fire - * and the leaked-prefix bug, codegen-neutral (bare symbols kept; not - * `package main`, which would main-prefix them). A packaged file's own - * `package` decl already sets curmod, so it needs nothing — keeping - * the directive out of every tracked combined.ww. (Task #11.) */ - if (modpath != NULL && modpath[0] != '\0') { - /* M1 (#22): an import-reached file carries its full dotted - * import path so codegen mangles symbols on the path, not the - * leaf `package` clause. The directive's absence is the root - * marker (#32): root/primary files take the branch below. */ - fprintf(out, "//ww:module %s\n", modpath); - } else { - /* Root/primary file: reset the bundle boundary so a preceding - * imported section's sticky pathmod (M1 #22) is cleared. A - * package-less file then stays primary ("") as before; a - * packaged primary's own `package` clause sets curmod fresh - * (pathmod now NULL → real clause, not an assertion). */ - fputs("//ww:module-reset\n", out); - } - rewind(in); - int ch; - while ((ch = fgetc(in)) != EOF) fputc(ch, out); - fputc('\n', out); - fclose(in); -} - -/* objstem (when non-NULL/non-empty) redirects the .s/.o/.combined.ww - * side files to live beside the build's OUTPUT instead of next to the - * source (task #15/T3). `ww run` and `ww build -o` pass it so concurrent - * builds never share the next-to-source fixed paths; the default - * (objstem == NULL) keeps the old next-to-source layout, load-bearing - * for make's tracked-combined.ww regen + the #110 freshness gate. */ -static int -build_one(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, int is_test) -{ - 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) { - /* in-tree default: ../../lib relative to bin/ */ - 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; - } - - /* Compose the search path: source-file's directory first, then any - * -I dirs, then srcdir. The source-dir lead matches Hare's CWD-first - * convention (its `hare test` is run from the module dir, making CWD - * == module-dir); our wrappers don't cd, so dirname(src) is the - * closest analog. Also matches cc -I. — source-dir wins ties over - * the system path. locate_import walks left-to-right. - * - * For a dir entry the source-dir IS src; for a file entry it's - * the dirname. */ - 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; - - /* Derive a stem for .s/.o/.combined.ww side files. For a file - * entry strip the .ww. For a dir entry use / - * so artifacts land inside the module directory. */ - 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 asmf[1024], obj[1024], combined[1024]; - snprintf(asmf, sizeof asmf, "%s.s", ostem); - snprintf(obj, sizeof obj, "%s.o", ostem); - snprintf(combined, sizeof combined, "%s.combined.ww", ostem); - - /* Resolve imports by concatenating sources into a temp file. The - * compiler then sees one flat source. Dir entry → enumerate the - * module dir's *.ww (less *test.ww); file entry → start at the - * file. */ - { - FILE *cf = fopen(combined, "wb"); - if (cf == NULL) { - fprintf(stderr, "ww: cannot open %s\n", combined); - return 1; - } - struct ImportSet visited = {0}; - /* #17 auto-bundle lib/test: the -T synth's main calls lib/test's - * run(), but @test files don't `import test;`. Pull it like an - * implicit import through the same filename-keyed expand path - * (the visited set dedupes if a fixture imports it explicitly). - * The wwstage twin (selfhost/cmd/ww/main.ww) mirrors this. */ - if (is_test) { - char tpath[1024]; - int tdir = 0; - if (locate_import(srcdir, "test", tpath, sizeof tpath, &tdir)) { - if (tdir) expand_dir(cf, tpath, &visited, srcdir, "test"); - else expand(cf, tpath, &visited, srcdir, NULL); - } - } - if (entry_is_dir) expand_dir(cf, srcd, &visited, srcdir, NULL); - else expand(cf, src, &visited, srcdir, NULL); - fclose(cf); - for (int i = 0; i < visited.n; i++) free(visited.paths[i]); - free(visited.paths); - } - - char cmd[4096]; - snprintf(cmd, sizeof cmd, "%s %s-o %s %s", c6, is_test ? "-T " : "", - asmf, combined); - if (run(cmd) != 0) { - fprintf(stderr, "ww: w6c failed\n"); - return 1; - } - snprintf(cmd, sizeof cmd, "%s -o %s %s", a6, obj, asmf); - if (run(cmd) != 0) { - fprintf(stderr, "ww: w6a failed\n"); - return 1; - } - /* Link runtime: prefer libwwrt.a (selective archive pull) but - * fall back to start.o + syscall.o in the in-tree obj/ dir if - * we're running uninstalled. */ - char rtargs[2048] = {0}; - char path[1024]; - snprintf(path, sizeof path, "%s/libwwrt.a", libdir); - if (access(path, 0) == 0) { - snprintf(rtargs, sizeof rtargs, "%s", path); - } 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); - } - /* -L goes before -l so w6l can resolve the latter. */ - const char *libargs = (extra_libs && extra_libs[0]) ? extra_libs : ""; - const char *libdirset = (extra_libdirs && extra_libdirs[0]) ? extra_libdirs : ""; - snprintf(cmd, sizeof cmd, "%s -o %s %s %s%s%s%s%s", - l6, out, obj, rtargs, - libdirset[0] ? " " : "", libdirset, - libargs[0] ? " " : "", libargs); - if (run(cmd) != 0) { - fprintf(stderr, "ww: w6l failed\n"); - return 1; - } - return 0; -} - /* ==================================================================== - * ww build --sep — M3-tail separate-compilation driver (task #46/c3). + * ww build — 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). + * This is the SOLE build path (E3-C1 flip, task #87): the legacy + * single-file amalgamator (build_one/expand) is gone. Each imported + * package's `.wwi` interface is materialized and every package is + * compiled on its own (`w6c -c`), then the `.o` set is flat-linked. * * 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 @@ -664,6 +311,40 @@ sep_fname(const struct sepgraph *g, int pi, const char *scratch, snprintf(out, outsz, "%s/%s%s", scratch, base, suffix); } +/* unit_has_package — does `path` declare `package ;` ANYWHERE? + * #16 ENFORCE-driver (rob A): distinguishes a genuinely-missing import + * from one satisfied by an INLINE package in the same unit. Unlike + * peek_package (stops at the FIRST package decl), this scans every line + * — single-file multi-package fixtures carry several `package` decls. The + * comment-skip line scan + name match mirror peek_package, uncapped. The + * wwstage twin unithaspackage must stay byte-identical (rule 10). */ +static int +unit_has_package(const char *path, const char *leaf) +{ + FILE *in = fopen(path, "rb"); + if (in == NULL) return 0; + char line[2048]; + int found = 0; + while (fgets(line, sizeof line, in)) { + const char *p = line; + while (*p == ' ' || *p == '\t') p++; + if (p[0] == '/' && p[1] == '/') continue; + if (strncmp(p, "package ", 8) != 0 + && strncmp(p, "package\t", 8) != 0) continue; + p += 8; + while (*p == ' ' || *p == '\t') p++; + size_t i = 0; + while (leaf[i] != '\0' && leaf[i] == p[i]) i++; + if (leaf[i] == '\0') { + char c = p[i]; + if (c == ';' || c == ' ' || c == '\t' + || c == '\n' || c == '\0') { found = 1; break; } + } + } + fclose(in); + return found; +} + /* 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 @@ -699,12 +380,21 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file, 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. */ + /* #16 ENFORCE-driver: an unresolvable import is a hard error, + * not a silent skip — EXCEPT when the package is defined INLINE + * in the same unit (single-file multi-package; leaf = the last + * dotted component). E3-C1: the combined path that used to own + * the genuine-missing case is gone, so the sep producer enforces + * it here (INV-2, by construction). Mirrors the deleted expand. */ if (!locate_import(searchpath, path_form, ipath, sizeof ipath, - &is_dir)) - continue; + &is_dir)) { + const char *dot = strrchr(name, '.'); + const char *leaf = dot ? dot + 1 : name; + if (unit_has_package(file, leaf)) + continue; /* inline-satisfied */ + fprintf(stderr, "ww: cannot find package %s\n", name); + exit(1); + } if (is_dir) { int di = sep_find_or_add(g, name, ipath, 1); if (di < 0) { rc = -1; break; } @@ -1470,24 +1160,17 @@ 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, int *want_sep) + const char **src_out) { *src_out = NULL; int i = 0; for (; i < argc; i++) { 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; + /* E3-C1 flip: separate compilation is now the sole build + * path, so --sep no longer selects anything. Retained as an + * accepted no-op (every subcommand) so the existing --sep + * gate corpus keeps driving the default path. (Task #87.) */ + ; } else if (strncmp(argv[i], "-l", 2) == 0 && argv[i][2]) { size_t n = strlen(libs); snprintf(libs + n, libsz - n, @@ -1553,10 +1236,9 @@ 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, &want_sep) < 0) + outflag, sizeof outflag, &src) < 0) return 2; if (src == NULL) src = "."; /* default: build cwd */ char resolved[1024]; @@ -1582,10 +1264,8 @@ 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, 0); - return build_one(resolved, is_dir, out, objstem, incs, libs, libdirs, 0); + return build_one_sep(resolved, is_dir, out, objstem, incs, libs, + libdirs, 0); } static int @@ -1598,7 +1278,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, NULL); + outflag, sizeof outflag, &src); if (next < 0) return 2; if (src == NULL) src = "."; char resolved[1024]; @@ -1611,7 +1291,7 @@ do_run(int argc, char **argv) snprintf(tmp, sizeof tmp, "/tmp/ww_run_%d", getpid()); /* objstem = tmp → intermediates land at /tmp/ww_run_.{s,o, * combined.ww}, never next to the source (T3). */ - if (build_one(resolved, is_dir, tmp, tmp, incs, libs, libdirs, 0) != 0) + if (build_one_sep(resolved, is_dir, tmp, tmp, incs, libs, libdirs, 0) != 0) return 1; /* exec the built binary with any trailing argv as its argv. */ pid_t pid = fork(); @@ -1648,11 +1328,6 @@ do_test(int argc, char **argv) * wwstage twin (selfhost/cmd/ww/main.ww dotest). */ int compileonly = 0; char outstem[1024] = {0}; - /* #79 E1: --sep routes a single-file/module test through the - * separate-compilation producer (build_one_sep, is_test=1) instead of - * the amalgamator. Additive (combined stays default); dir-mode --sep is - * deferred to E2. */ - int use_sep = 0; /* #17: an optional second positional after the target is a fnmatch * name-filter pattern, forwarded to the test binary as argv[1]. Only * meaningful for a single test file/module — rejected in dir mode. */ @@ -1660,7 +1335,8 @@ do_test(int argc, char **argv) for (int i = 0; i < argc; i++) { if (argv[i][0] == '-') { if (strcmp(argv[i], "--sep") == 0) { - use_sep = 1; + /* E3-C1 flip: sep is the sole path; --sep is an + * accepted no-op (task #87). */ continue; } if (argv[i][1] == 'I') { @@ -1715,11 +1391,8 @@ do_test(int argc, char **argv) const char *outp; if (outstem[0]) outp = outstem; else { snprintf(tmp, sizeof tmp, "/tmp/ww_test_%d", getpid()); outp = tmp; } - int br = use_sep - ? build_one_sep(resolved, is_dir, outp, - outstem[0] ? outstem : NULL, incs, "", "", 1) - : build_one(resolved, is_dir, outp, - outstem[0] ? outstem : NULL, incs, "", "", 1); + int br = build_one_sep(resolved, is_dir, outp, + outstem[0] ? outstem : NULL, incs, "", "", 1); if (br != 0) return 1; if (compileonly) return 0; int rc = run_test_bin(outp, pattern); @@ -1732,11 +1405,8 @@ do_test(int argc, char **argv) const char *outp; if (outstem[0]) outp = outstem; else { snprintf(tmp, sizeof tmp, "/tmp/ww_test_%d", getpid()); outp = tmp; } - int br = use_sep - ? build_one_sep(target, 0, outp, outstem[0] ? outstem : NULL, - incs, "", "", 1) - : build_one(target, 0, outp, outstem[0] ? outstem : NULL, - incs, "", "", 1); + int br = build_one_sep(target, 0, outp, outstem[0] ? outstem : NULL, + incs, "", "", 1); if (br != 0) return 1; if (compileonly) return 0; int rc = run_test_bin(outp, pattern); @@ -1751,13 +1421,6 @@ do_test(int argc, char **argv) fprintf(stderr, "ww test: -c/-o need a single test file\n"); return 2; } - /* #79 E1: dir-mode --sep is deferred to E2 — single-file/module is - * enough to prove the sep test path. Loud-reject rather than silently - * fall back to the combined per-file build. */ - if (use_sep) { - fprintf(stderr, "ww test: --sep needs a single test file\n"); - return 2; - } /* #17: a name-filter pattern is per-binary; directory mode builds one * binary per *_test.ww, so a single pattern can't sensibly route. */ if (pattern) { @@ -1781,7 +1444,7 @@ do_test(int argc, char **argv) snprintf(tmp, sizeof tmp, "/tmp/ww_test_%d_%d", getpid(), i); const char *label = strrchr(files[i], '/'); label = label ? label + 1 : files[i]; - int rc = build_one(files[i], 0, tmp, NULL, target, "", "", 1); + int rc = build_one_sep(files[i], 0, tmp, NULL, target, "", "", 1); if (rc != 0) { fprintf(stderr, "FAIL %s (build)\n", label); fail++; diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index 0eecdda1..5c9c11d2 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -181,12 +181,13 @@ fn procrun(path: *u8, argv: **u8) i32 = { return code; }; -// ---- `use` resolution + source concatenation -------------------------- +// ---- import resolution + visited-set ----------------------------------- // -// Recursive expansion: for each `use IDENT;` we find at the top of -// `path`, resolve via the colon-separated `dirs`, expand the imported -// file first, then append our own bytes. Already-visited paths are -// skipped (linear scan; typical builds visit a handful of modules). +// The separate-compilation producer scans each unit's top-of-file +// `import IDENT;` lines and resolves them via the colon-separated `dirs` +// search path. A per-scan visited set (linear; typical builds visit a +// handful of modules) breaks cycles. (E3-C1: the legacy single-file +// source concatenator was deleted — sep is the sole path. Task #87.) type strnode = struct { s: str, @@ -509,307 +510,6 @@ fn scanuse(src: *u8, len: u64) (*u8, u64) = { return src + idstart, idlen; }; -// expand — emit one file's bytes verbatim into the combined stream, -// after recursive-expanding its top-of-file `import X;` imports. -// Each source declares its own `package ;` (parser stamps -// decls). -fn expand(c: *expctx, pathcs: *u8, modpath: str) void = { - let plen: u64 = cstrlen(pathcs); - let view: str; - view.ptr = pathcs; - view.len = plen: i32; - let pathstr: str = strings.dup(view); - if (visitseen(c, pathstr)) { return; }; - visitadd(c, pathstr); - - let bufp: *u8; - let blen: u64; - bufp, blen = slurp(pathcs); - if (bufp == nil) { - cerr("ww: cannot read source\n"); - return; - }; - - // Pass 1: scan top-of-file `import X;` lines, recursively expand. - 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(c.dirs, idp, idn, - &isdir); - if (ipath != nil) { - // M1 #22 (isdir-gated, rob-ratified): only DIRECTORY - // imports are package boundaries that path-mangle. A - // single-file import (`import opcodes;` → opcodes.ww - // declaring `package w6a`) is an intra-package file-split: - // it keeps its in-file `package` clause as its module - // (no directive → reset → package-clause mangling). - if (isdir != 0) { - let mv: str; - mv.ptr = idp; - mv.len = idn: i32; - let modstr: str = strings.dup(mv); - expanddir(c, ipath, modstr); - } else { - expand(c, ipath, ""); - }; - } else { - // #16 ENFORCE-driver (rob A): a locate-miss is - // legal when the package is defined INLINE in the - // same unit (single-file multi-package). leaf = the - // last dotted component of the import name; if an - // inline `package ` exists -> silent skip - // (the checker binds it), else fatal. cstage twin in - // cmd/ww/main.c; fatal text identical. - let lstart: u64 = 0u64; - let lk: u64 = 0u64; - for (lk < idn) { - if (idp[lk] == 46u8) { lstart = lk + 1u64; }; // '.' - lk += 1u64; - }; - let leafp: *u8 = idp + lstart; - let leafn: u64 = idn - lstart; - if (!unithaspackage(bufp, blen, leafp, leafn)) { - cerr("ww: cannot find package "); - os.write(2, idp, idn); - cerr("\n"); - os.exit(1); - }; - }; - }; - i = j + 1u64; - }; - - // #16 option-B: a package-less file's decls would otherwise inherit - // the preceding bundled module's sticky curmod (parser parse.ww). Emit - // a curmod-reset boundary directive so the lexer/parser attribute the - // file to the primary module ("") — fixes the self-import false-fire - // and the leaked-prefix bug, codegen-neutral (bare symbols kept; not - // `package main`, which would main-prefix them). A packaged file's own - // `package` decl already sets curmod, so it needs nothing — keeping - // the directive out of every tracked combined.ww. (Task #11.) - if (modpath.len != 0) { - // M1 #22: import-reached file carries its full dotted path so - // codegen mangles symbols on the path, not the leaf clause. - let dm: str = "//ww:module "; - os.writeall(c.out, dm.ptr, dm.len: u64); - os.writeall(c.out, modpath.ptr, modpath.len: u64); - os.writeall(c.out, "\n".ptr, 1u64); - } else { - // Root/primary: always reset so a preceding imported section's - // sticky pathmod is cleared; a packaged primary's own `package` - // clause then sets curmod fresh (pathmod NULL → real clause). - let d: str = "//ww:module-reset\n"; - os.writeall(c.out, d.ptr, d.len: u64); - }; - - os.writeall(c.out, bufp, blen); - os.writeall(c.out, "\n".ptr, 1u64); -}; - -// Scan `pathcs` for its first non-comment-non-blank line; if it -// starts with `package ;` return the package name as a fresh -// heap-allocated NUL-terminated *u8, else nil. Same shape as cstage -// peek_package. -// -// Reads the WHOLE file (via slurp), not a fixed prefix: cstage's -// peek_package scans line-by-line with fgets and no total cap, stopping -// at the first non-comment-non-blank line. A prior 2048-byte read cap -// here diverged from cstage on files whose `package` decl sits behind a -// >2048-byte comment header (strconv decimal/ftos/stof, memio) — -// returning nil and making the #16 D-i injection asymmetric (rule-10 -// break, byte-id divergence in the regenerated combined). The corpus has -// no line >2047 chars, so a whole-file line scan matches cstage's -// per-line fgets byte-for-byte on every real input. -fn peekpackage(pathcs: *u8) *u8 = { - let bufp: *u8; - let nu: u64; - bufp, nu = slurp(pathcs); - if (bufp == nil) { return nil; }; - let p: u64 = 0u64; - for (p < nu) { - let q: u64 = p; - for (q < nu) { - if (bufp[q] == 10u8) { break; }; // '\n' - q += 1u64; - }; - let s: u64 = p; - for (s < q) { - if (bufp[s] != 32u8) { - if (bufp[s] != 9u8) { break; }; - }; - s += 1u64; - }; - if (s < q) { - let line: str; - line.ptr = bufp + s; - line.len = (q - s): i32; - // hasprefix("//") subsumes the old s+1= 97u8) { if (ch <= 122u8) { isalpha = true; }; }; - if (ch >= 65u8) { if (ch <= 90u8) { isalpha = true; }; }; - if (ch >= 48u8) { if (ch <= 57u8) { isalpha = true; }; }; - if (ch == 95u8) { isalpha = true; }; - if (!isalpha) { break; }; - t += 1u64; - }; - let plen: u64 = t - start; - if (plen == 0u64) { return nil; }; - let r: []u8 = alloc([], plen + 1u64)!; - let k: u64 = 0u64; - for (k < plen) { r[k] = bufp[start + k]; k += 1u64; }; - r[plen] = 0u8; - return r.ptr; - }; - }; - return nil; - }; - p = q + 1u64; - }; - return nil; -}; - -// unithaspackage — does the unit buffer declare `package ;` ANYWHERE? -// #16 ENFORCE-driver (rob A) cstage unit_has_package twin: distinguishes a -// genuinely-missing import from one satisfied by an INLINE package in the -// same single-file multi-package unit (`package aa; ... package main; -// import aa;`). Scans EVERY line (comment-skip) — not just the first -// package decl (peekpackage stops there). Decision byte-identical to -// cstage so the skip/fatal choice + driver output match (rule 10). -fn unithaspackage(buf: *u8, buflen: u64, leafp: *u8, leafn: u64) bool = { - let p: u64 = 0u64; - for (p < buflen) { - let q: u64 = p; - for (q < buflen) { if (buf[q] == 10u8) { break; }; q += 1u64; }; - let s: u64 = p; - for (s < q) { - if (buf[s] != 32u8) { if (buf[s] != 9u8) { break; }; }; - s += 1u64; - }; - if (s < q) { - let line: str; - line.ptr = buf + s; - line.len = (q - s): i32; - if (strings.hasprefix(line, "//")) { p = q + 1u64; continue; }; - if (s + 8u64 <= q) { - if (strings.hasprefix(line, "package")) { - let sep: u8 = buf[s + 7u64]; - let oksep: bool = false; - if (sep == 32u8) { oksep = true; } - else { if (sep == 9u8) { oksep = true; }; }; - if (oksep) { - let t: u64 = s + 8u64; - for (t < q) { - if (buf[t] != 32u8) { if (buf[t] != 9u8) { break; }; }; - t += 1u64; - }; - let m: u64 = 0u64; - let eq: bool = true; - for (m < leafn) { - if (t + m >= q) { eq = false; break; }; - if (buf[t + m] != leafp[m]) { eq = false; break; }; - m += 1u64; - }; - if (eq) { - let after: u64 = t + leafn; - let term: bool = false; - if (after >= q) { term = true; } - else { - let c: u8 = buf[after]; - if (c == 59u8) { term = true; } // ';' - else { if (c == 32u8) { term = true; } - else { if (c == 9u8) { term = true; }; }; }; - }; - if (term) { return true; }; - }; - }; - }; - }; - }; - p = q + 1u64; - }; - return false; -}; - -// Strict-same-package error helper. Bundled here per task #22 -// brief — failure mode is dir-enum's own. -fn strictpkgmismatch(file: *u8, pkg: *u8, dirpkg: *u8, dirpath: *u8) void = { - cerr("ww: "); - os.write(2, file, cstrlen(file)); - cerr(": package "); - os.write(2, pkg, cstrlen(pkg)); - cerr(" differs from "); - os.write(2, dirpkg, cstrlen(dirpkg)); - cerr(" in same module dir "); - os.write(2, dirpath, cstrlen(dirpath)); - cerr("\n"); - os.exit(1); -}; - -// expanddir — enumerate /*.ww (skip *test.ww and -// *.combined.ww), byte-sort, recurse into each. Mirrors -// ref/hare/hare/module/srcs.ha:183 `_findsrcs` minus tag handling. -// The visited set keys on concrete file paths so multi-file modules -// are pulled once. Strict-same-package: all enumerated files must -// declare the same `package ;` (task #23 subset; failure -// mode native to dir-enum). -fn expanddir(c: *expctx, dirpath: *u8, modpath: str) void = { - let names: **u8; - let n: i32; - names, n = enumeratedir(dirpath); - let dlen: u64 = cstrlen(dirpath); - let dirpkg: *u8 = nil; - 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] = dirpath[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; - let pkg: *u8 = peekpackage(fp.ptr); - if (pkg != nil) { - if (dirpkg == nil) { dirpkg = pkg; } - else { if (!cstreq(dirpkg, pkg)) { - strictpkgmismatch(fp.ptr, pkg, dirpkg, dirpath); - }; }; - }; - expand(c, fp.ptr, modpath); - i += 1; - }; -}; - // ---- Build pipeline --------------------------------------------------- // Strip the trailing ".ww" off `src` (a NUL-terminated path) into @@ -933,6 +633,68 @@ fn sepfname(g: *sepgraph, pi: i32, scratch: *u8, suffix: str) *u8 = { return buf.ptr; }; +// unithaspackage — does the unit buffer declare `package ;` ANYWHERE? +// #16 ENFORCE-driver (rob A) cstage unit_has_package twin: distinguishes a +// genuinely-missing import from one satisfied by an INLINE package in the +// same single-file multi-package unit (`package aa; ... package main; +// import aa;`). Scans EVERY line (comment-skip) — not just the first +// package decl (peekpackage stops there). Decision byte-identical to +// cstage so the skip/fatal choice + driver output match (rule 10). +fn unithaspackage(buf: *u8, buflen: u64, leafp: *u8, leafn: u64) bool = { + let p: u64 = 0u64; + for (p < buflen) { + let q: u64 = p; + for (q < buflen) { if (buf[q] == 10u8) { break; }; q += 1u64; }; + let s: u64 = p; + for (s < q) { + if (buf[s] != 32u8) { if (buf[s] != 9u8) { break; }; }; + s += 1u64; + }; + if (s < q) { + let line: str; + line.ptr = buf + s; + line.len = (q - s): i32; + if (strings.hasprefix(line, "//")) { p = q + 1u64; continue; }; + if (s + 8u64 <= q) { + if (strings.hasprefix(line, "package")) { + let sep: u8 = buf[s + 7u64]; + let oksep: bool = false; + if (sep == 32u8) { oksep = true; } + else { if (sep == 9u8) { oksep = true; }; }; + if (oksep) { + let t: u64 = s + 8u64; + for (t < q) { + if (buf[t] != 32u8) { if (buf[t] != 9u8) { break; }; }; + t += 1u64; + }; + let m: u64 = 0u64; + let eq: bool = true; + for (m < leafn) { + if (t + m >= q) { eq = false; break; }; + if (buf[t + m] != leafp[m]) { eq = false; break; }; + m += 1u64; + }; + if (eq) { + let after: u64 = t + leafn; + let term: bool = false; + if (after >= q) { term = true; } + else { + let c: u8 = buf[after]; + if (c == 59u8) { term = true; } // ';' + else { if (c == 32u8) { term = true; } + else { if (c == 9u8) { term = true; }; }; }; + }; + if (term) { return true; }; + }; + }; + }; + }; + }; + p = q + 1u64; + }; + return false; +}; + // 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 @@ -989,6 +751,29 @@ fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8, return -1; }; }; + } else { + // #16 ENFORCE-driver (rob A): a locate-miss is legal + // when the package is defined INLINE in the same unit + // (single-file multi-package). leaf = last dotted + // component; inline `package ` -> skip (the + // checker binds it), else fatal. E3-C1: the combined + // path that owned the genuine-missing case is gone, so + // the sep producer enforces it here (INV-2). cstage + // twin; fatal text identical. + let lstart: u64 = 0u64; + let lk: u64 = 0u64; + for (lk < idn) { + if (idp[lk] == 46u8) { lstart = lk + 1u64; }; // '.' + lk += 1u64; + }; + let leafp: *u8 = idp + lstart; + let leafn: u64 = idn - lstart; + if (!unithaspackage(bufp, blen, leafp, leafn)) { + cerr("ww: cannot find package "); + os.write(2, idp, idn); + cerr("\n"); + os.exit(1); + }; }; }; i = j + 1u64; @@ -1786,235 +1571,6 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, return 0; }; -// buildone — compile `src` (file or directory) into the executable -// named `out`. -// selfdir: NUL-terminated dir containing this driver and the -// wwstage tools (w6c_ww/w6a_ww/w6l_ww) -// src: NUL-terminated entry path (file or directory). -// entryisdir: non-zero when src is a module directory. -// out: NUL-terminated desired output path -// objstem: when non-nil, redirects the .s/.o/.combined.ww side -// files to live beside this stem instead of next to the -// source (T3 / task #15). `ww run` and `ww build -o` pass -// it so concurrent builds never share next-to-source fixed -// paths; nil keeps the old next-to-source layout (the make -// tracked-combined.ww regen + #110 gate depend on it). Twin -// of cmd/ww/main.c build_one's objstem. -// incs: NUL-terminated colon-list of -I dirs (may be empty) -// lf: extra linker flags (-L, -l); may be nil -// -// The ww-side driver shells to the ww-side tools so a `ww_ww build` -// touches no C-built code at runtime. The C `ww` driver in cmd/ww/ -// still drives the C-built w6c/w6a/w6l. Test 993 pins the two -// pipelines to byte-identical output on a corpus. -fn buildone(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, objstem: *u8, incs: *u8, lf: *lflags, istest: i32) 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); - }; - - // Compute the source directory. For a file entry: bytes of `src` - // up to the last '/' (or "." when src has no '/'). For a dir - // entry: the dir itself (less trailing slashes). Hare's CWD-first - // convention assumes you're running from the module dir; our - // wrappers don't cd, so dirname(src) stands in as the closest - // analog. Source-dir wins ties over the system path (cc -I.). - 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; - }; - }; - - // Compose 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 .s/.o/.combined.ww side files. Dir entry: /; - // file entry: src stripped of .ww. - 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 asmf: *u8 = appendlit(effstem, ".s"); - let objf: *u8 = appendlit(effstem, ".o"); - let combined: *u8 = appendlit(effstem, ".combined.ww"); - - // 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); - }; - - // Step 1: expand imports into the combined file. Dir entry → - // enumerate the module dir; file entry → start at the file. - let cf: i32 = os.open(pathstr(combined), os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 - if (cf < 0) { - cerr("ww: cannot open combined\n"); - return 1; - }; - { - let c: expctx; - c.out = cf; - c.dirs = searchpath.ptr; - c.visit = nil; - // #17 auto-bundle lib/test: the -T synth's main calls lib/test's - // run(), but @test files don't `import test;`. Pull it like an - // implicit import through the same locate+expand path (the visit - // set dedupes a fixture that imports it explicitly). cstage twin - // in cmd/ww/main.c buildone. - if (istest != 0) { - let td: i32 = 0; - let tp: *u8 = locateimport(searchpath.ptr, "test".ptr, "test".len: u64, &td); - if (tp != nil) { - if (td != 0) { expanddir(&c, tp, "test"); } - else { expand(&c, tp, ""); }; - }; - }; - if (entryisdir != 0) { expanddir(&c, srcd.ptr, ""); } - else { expand(&c, src, ""); }; - }; - os.close(cf); - - // Step 2: w6c [-T] -o .s .combined.ww - { - let argv: []*u8 = alloc([], 6u64)!; - let p: i32 = 0; - argv[p] = "w6c\0".ptr; p += 1; - if (istest != 0) { argv[p] = "-T\0".ptr; p += 1; }; - argv[p] = "-o\0".ptr; p += 1; - argv[p] = asmf; p += 1; - argv[p] = combined; p += 1; - argv[p] = nil; p += 1; - argv.len = p; - if (procrun(c6, argv.ptr) != 0) { - cerr("ww: w6c failed\n"); - return 1; - }; - }; - - // Step 3: w6a -o .o .s - { - 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: w6a failed\n"); - return 1; - }; - }; - - // Step 4: w6l -o .o libwwrt.a [-L...] [-l...] - { - 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; - }; - // argv slots: 5 fixed (w6l, -o, out, objf, libwwrt) - // + 2 * nlibdirs (-L, dir) - // + 2 * nlibs (-l, name) - // + 1 nil terminator. - let total: i32 = 5 + 2 * nldirs + 2 * nllibs + 1; - let argv: []*u8 = alloc([], total: u64)!; - argv.len = total; - argv[0] = "w6l\0".ptr; - argv[1] = "-o\0".ptr; - argv[2] = out; - argv[3] = objf; - argv[4] = libwwrt.ptr; - let pos: i32 = 5; - let k: i32 = 0; - for (k < nldirs) { - argv[pos] = "-L\0".ptr; - argv[pos + 1] = ldirs[k]; - pos += 2; - k += 1; - }; - k = 0; - for (k < nllibs) { - argv[pos] = "-l\0".ptr; - argv[pos + 1] = llibs[k]; - pos += 2; - k += 1; - }; - argv[pos] = nil; - if (procrun(l6, argv.ptr) != 0) { - cerr("ww: w6l failed\n"); - return 1; - }; - }; - return 0; -}; - // ---- Module-by-name resolution ---------------------------------------- // // Mirrors cmd/ww/main.c:resolvemodule. Maps a name like "foo", "lib/foo", @@ -2154,7 +1710,6 @@ 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; @@ -2173,8 +1728,7 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { for (i < argc) { let p: *u8 = argv[i]; if (p[0u64] == 45u8) { // '-' - if (cstreqlit(p, "--sep")) { // M3-tail separate-compile - wantsep = 1; + if (cstreqlit(p, "--sep")) { // E3-C1: sep is sole path; accepted no-op (#87) } else { if (p[1u64] == 73u8) { // '-I' let dir: *u8 = nil; if (p[2u64] != 0u8) { @@ -2290,10 +1844,7 @@ 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, 0i32); - }; - return buildone(selfdir, resolved, isdir, out, objstem, incs.ptr, &lf, 0i32); + return buildonesep(selfdir, resolved, isdir, out, objstem, incs.ptr, &lf, 0i32); }; // Format the scratch path /tmp/ww_run_ into buf. Returns NUL- @@ -2348,9 +1899,7 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { else { let p: *u8 = argv[i]; if (p[0u64] == 45u8) { - if (cstreqlit(p, "--sep")) { // build-only; run rejects (rule 10) - cerr("ww run: --sep is only valid with build\n"); - return 2; + if (cstreqlit(p, "--sep")) { // E3-C1: sep is sole path; accepted no-op (#87) } else { if (p[1u64] == 73u8) { let dir: *u8 = nil; if (p[2u64] != 0u8) { @@ -2452,7 +2001,7 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { lf.nlibs = nlibs; // objstem = tmp → intermediates at /tmp/ww_run_.{s,o,combined.ww}, // never next to the source (T3). - if (buildone(selfdir, resolved, isdir, tmp.ptr, tmp.ptr, incs.ptr, &lf, 0i32) != 0) { + if (buildonesep(selfdir, resolved, isdir, tmp.ptr, tmp.ptr, incs.ptr, &lf, 0i32) != 0) { os.remove(pathstr(tmp.ptr)); return 1; }; @@ -2482,7 +2031,7 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { // directory: open the dir, getdents64, build+run each *_test.ww, // report ok/FAIL per file, return 0 iff all pass. -fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, outstem: *u8, pattern: *u8, usesep: i32) i32 = { +fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, outstem: *u8, pattern: *u8) i32 = { let tmp: []u8 = alloc([], (os.PATH_MAX: u64))!; tmp.len = os.PATH_MAX; // -o redirects the binary + its combined (objstem, T3) to ; the @@ -2496,19 +2045,13 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, outstem: * makeruntmp(tmp.ptr); outp = tmp.ptr; }; - // #79 E1: --sep routes the test build through the separate-compilation - // producer (buildonesep, istest=1) instead of the amalgamator. - let bres: i32 = 0; - if (usesep != 0) { - let lf: lflags; - lf.libdirs = nil; - lf.nlibdirs = 0; - lf.libs = nil; - lf.nlibs = 0; - bres = buildonesep(selfdir, src, 0, outp, objstem, incs, &lf, 1i32); - } else { - bres = buildone(selfdir, src, 0, outp, objstem, incs, nil, 1i32); - }; + // E3-C1: separate compilation is the sole build path (task #87). + let lf: lflags; + lf.libdirs = nil; + lf.nlibdirs = 0; + lf.libs = nil; + lf.nlibs = 0; + let bres: i32 = buildonesep(selfdir, src, 0, outp, objstem, incs, &lf, 1i32); if (bres != 0) { if (outstem == nil) { os.remove(pathstr(outp)); }; return 1; @@ -2576,7 +2119,7 @@ fn rundirtests(selfdir: *u8, dir: *u8) i32 = { let tmp: []u8 = alloc([], (os.PATH_MAX: u64))!; tmp.len = os.PATH_MAX; makeruntmp(tmp.ptr); - let bres: i32 = buildone(selfdir, path.ptr, 0, tmp.ptr, nil, tincs.ptr, nil, 1i32); + let bres: i32 = buildonesep(selfdir, path.ptr, 0, tmp.ptr, nil, tincs.ptr, nil, 1i32); if (bres != 0) { fail += 1; cerr("FAIL "); @@ -2639,16 +2182,11 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { // twin: cmd/ww/main.c do_test (error wording identical). let compileonly: i32 = 0; let outstem: *u8 = nil; - // #79 E1: --sep routes a single-file test through the separate- - // compilation producer (buildonesep, istest=1). Dir-mode --sep is - // deferred to E2. cstage twin: do_test use_sep. - let usesep: i32 = 0; let i: i32 = start; for (i < argc) { let p: *u8 = argv[i]; if (p[0u64] == 45u8) { // '-' - if (cstreqlit(p, "--sep")) { // #79: separate-compile test - usesep = 1; + if (cstreqlit(p, "--sep")) { // E3-C1: sep is sole path; accepted no-op (#87) } else { if (p[1u64] == 73u8) { // '-I' let dir: *u8 = nil; if (p[2u64] != 0u8) { @@ -2698,7 +2236,7 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { // single-file mode: literal *.ww that exists if (cstrendswithlit(target, ".ww")) { if (os.access(pathstr(target), 0i32) == 0) { - return runsingletest(selfdir, target, incs.ptr, compileonly, outstem, patarg, usesep); + return runsingletest(selfdir, target, incs.ptr, compileonly, outstem, patarg); }; }; @@ -2706,11 +2244,6 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { cerr("ww test: -c/-o need a single test file\n"); return 2; }; - // #79 E1: dir-mode --sep deferred to E2; single-file proves the path. - if (usesep != 0) { - cerr("ww test: --sep needs a single test file\n"); - return 2; - }; // #17: a name-filter pattern is per-binary; dir mode builds one binary // per *_test.ww, so a single pattern can't route. cstage twin parity. if (patarg != nil) { diff --git a/test/wcc/737_direnum.c b/test/wcc/737_direnum.c index 00073a81..6a76ba86 100644 --- a/test/wcc/737_direnum.c +++ b/test/wcc/737_direnum.c @@ -88,8 +88,12 @@ main(void) (void)tmp; } - /* bad: mismatched package decls in same dir → strict-same-package - * error. Both stages must surface "differs from" in stderr. */ + /* bad: mismatched package decls in same dir → import-path mismatch + * error. E3-C1: the amalgamator's strict-same-package "differs from" + * check is deleted; under sep w6c rejects the composed unit with + * "package

does not match import path " (wwstage is terser per + * #68, so match the shared "does not match import path" substring). + * Both stages must surface it in stderr. */ { const char *src = "test/wcc/data/direnum/bad_entry.ww"; snprintf(errp, sizeof errp, "/tmp/direnum_%d_bad.err", getpid()); @@ -102,8 +106,8 @@ main(void) if (rc == 0) { fprintf(stderr, "737[bad-cstage]: expected build failure, succeeded\n"); fail++; - } else if (!stderr_contains(errp, "differs from")) { - fprintf(stderr, "737[bad-cstage]: stderr missing 'differs from'\n"); + } else if (!stderr_contains(errp, "does not match import path")) { + fprintf(stderr, "737[bad-cstage]: stderr missing 'does not match import path'\n"); fail++; } unlink(errp); @@ -116,8 +120,8 @@ main(void) if (rc == 0) { fprintf(stderr, "737[bad-wwstage]: expected build failure, succeeded\n"); fail++; - } else if (!stderr_contains(errp, "differs from")) { - fprintf(stderr, "737[bad-wwstage]: stderr missing 'differs from'\n"); + } else if (!stderr_contains(errp, "does not match import path")) { + fprintf(stderr, "737[bad-wwstage]: stderr missing 'does not match import path'\n"); fail++; } unlink(errp); @@ -129,7 +133,7 @@ main(void) * (#16): the old 2048-capped peek missed that decl, so the strict- * same-package check skipped the file and the mismatch went * undetected (build wrongly succeeded); the whole-file peek finds it - * → "differs from". Both stages. (reviewer-batcha pin.) */ + * → "does not match import path". Both stages. (reviewer-batcha pin.) */ { const char *src = "test/wcc/data/direnum/bad_deep_entry.ww"; snprintf(errp, sizeof errp, "/tmp/direnum_%d_baddeep.err", getpid()); @@ -142,8 +146,8 @@ main(void) if (rc == 0) { fprintf(stderr, "737[bad-deep-cstage]: expected build failure, succeeded\n"); fail++; - } else if (!stderr_contains(errp, "differs from")) { - fprintf(stderr, "737[bad-deep-cstage]: stderr missing 'differs from'\n"); + } else if (!stderr_contains(errp, "does not match import path")) { + fprintf(stderr, "737[bad-deep-cstage]: stderr missing 'does not match import path'\n"); fail++; } unlink(errp); @@ -156,8 +160,8 @@ main(void) if (rc == 0) { fprintf(stderr, "737[bad-deep-wwstage]: expected build failure, succeeded\n"); fail++; - } else if (!stderr_contains(errp, "differs from")) { - fprintf(stderr, "737[bad-deep-wwstage]: stderr missing 'differs from'\n"); + } else if (!stderr_contains(errp, "does not match import path")) { + fprintf(stderr, "737[bad-deep-wwstage]: stderr missing 'does not match import path'\n"); fail++; } unlink(errp); diff --git a/test/wcc/989_lib_byteid.c b/test/wcc/989_lib_byteid.c index 51a1a032..c4b2fb9b 100644 --- a/test/wcc/989_lib_byteid.c +++ b/test/wcc/989_lib_byteid.c @@ -421,8 +421,12 @@ static int corpus_complete(const char *cwd) { char cmd[2048]; + /* E3-C1: the flip routes lib @test builds through sep, which drops + * .sepwork/.unit.ww scratch next to source; exclude it so + * the scan enrols real source dirs only, not generated work dirs. */ snprintf(cmd, sizeof cmd, "cd %s && find lib -name '*.ww' " - "! -name '*.combined.ww' | sed 's|/[^/]*$||' | sort -u", cwd); + "! -name '*.combined.ww' ! -path '*.sepwork/*' " + "| sed 's|/[^/]*$||' | sort -u", cwd); FILE *p = popen(cmd, "r"); if (!p) return -1; char dir[512]; diff --git a/test/wcc/989_sepbuild_run.c b/test/wcc/989_sepbuild_run.c index eddc98ae..5e070583 100644 --- a/test/wcc/989_sepbuild_run.c +++ b/test/wcc/989_sepbuild_run.c @@ -236,6 +236,18 @@ main(void) runwait(cmd); mkdir(td, 0755); + /* E3-C1: with the flip making sep the sole build path, every concurrent + * test now writes the global out/.pkgcache; this test's cs/ww per-package + * byte-id compare on the heavily-shared real lib pkgs (rt/time/os) then + * races a sibling building the same pkg. Pin a private per-pid cache so + * the cs and ww legs are isolated (system() builds inherit this env). + * pkgcache writes are non-atomic (cp -f), the real torn-read bug; this + * hermetic isolation is correct standalone, pending #104 (atomic + * temp+rename, both stages). */ + char cachedir[80]; + snprintf(cachedir, sizeof cachedir, "%s/pkgcache", td); + setenv("WW_PKGCACHE", cachedir, 1); + char rootww[1024]; snprintf(rootww, sizeof rootww, "%s/root.ww", td); if (write_file(rootww, root_src)) { fail++; goto out; } @@ -339,13 +351,11 @@ main(void) } } - /* 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. */ + /* run --sep symmetry: E3-C1 makes sep the sole compile path, so + * `run` now genuinely sep-compiles-then-runs (the old "no sep run + * path → loud-reject exit 2" is gone with the amalgamator). BOTH + * stages must build+run the cross-boundary fixture identically + * (rule 10) and exit EXPECT_EXIT — `--sep` is an accepted no-op. */ { int rc_cs, rc_ww; snprintf(cmd, sizeof cmd, @@ -354,9 +364,10 @@ main(void) 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) { + if (rc_cs != rc_ww || rc_cs != EXPECT_EXIT) { fprintf(stderr, "sepbuild FAIL: run --sep cs=%d ww=%d " - "(both must loud-reject, exit 2)\n", rc_cs, rc_ww); + "(both must run via sep, exit %d)\n", rc_cs, rc_ww, + EXPECT_EXIT); fail++; } } diff --git a/test/wcc/989_slttypepref_run.c b/test/wcc/989_slttypepref_run.c index 9b42d8b3..341706a0 100644 --- a/test/wcc/989_slttypepref_run.c +++ b/test/wcc/989_slttypepref_run.c @@ -26,14 +26,19 @@ * The shadow row was RED pre-c1 (ww resolved xa's !i8 -> exit 1, the wrong * module's type under rc=0). The noshadow control pins the non-shadow path * (scopelookupprefer lands on the SK_TYPE directly) so the fix can't - * perturb it. Single-file multi-package source is the sanctioned shape - * (cmd/ww/main.c). + * perturb it. + * + * E3-C1 retarget: the single-file multi-package source the original used + * is the amalgamator shape, deleted with the flip (ww is now strictly + * dir=module). Re-expressed as a dir-package tree: xb/ and xa/ both export + * `type invalid`, and xb IMPORTS xa so both `invalid` leaves are in scope + * during xb's sep-compile — the exact two-module same-leaf collision the + * scopelookuptype mod-preference fix governs. A regression flips to xa's + * !i8 -> exit 1. */ #include #include -#include #include -#include #include static int @@ -45,62 +50,70 @@ runwait(const char *cmd) return -1; } -struct row { const char *label; const char *src; int want_exit; }; +/* Only xb's param name varies between rows; xa and the root are shared. + * `xb_param` is the parameter spelling: "invalid" shadows the type leaf, + * "x" does not. */ +struct row { const char *label; const char *xb_param; int want_exit; }; static const struct row rows[] = { - { "shadow", - "package xb;\n" - "export type invalid = !i64;\n" - "export fn f(invalid: i32) i64 = { return size(invalid): i64; };\n" - "package xa;\n" - "export type invalid = !i8;\n" - "package main;\n" - "import xb;\n" - "import xa;\n" - "fn main() int = { return xb.f(0): int; };\n", - 8 }, - - { "noshadow", - "package xb;\n" - "export type invalid = !i64;\n" - "export fn f(x: i32) i64 = { return size(invalid): i64; };\n" - "package xa;\n" - "export type invalid = !i8;\n" - "package main;\n" - "import xb;\n" - "import xa;\n" - "fn main() int = { return xb.f(0): int; };\n", - 8 }, + { "shadow", "invalid", 8 }, + { "noshadow", "x", 8 }, }; +static int +write_file(const char *path, const char *content) +{ + FILE *f = fopen(path, "wb"); + if (!f) return -1; + fputs(content, f); + fclose(f); + return 0; +} + static int run_build(const char *driver, const struct row *r, int i) { - char src[64], tmpdir[64], cmd[1024]; - snprintf(src, sizeof src, "/tmp/sltp_%d_%d.ww", getpid(), i); - snprintf(tmpdir, sizeof tmpdir, "/tmp/sltp_%d_d_%d", getpid(), i); + char dir[64], sub[128], path[256], xbsrc[512], cmd[1024]; + snprintf(dir, sizeof dir, "/tmp/sltp_%d_d_%d", getpid(), i); - FILE *f = fopen(src, "wb"); - if (!f) return -2; - fputs(r->src, f); - fclose(f); + snprintf(cmd, sizeof cmd, "rm -rf %s && mkdir -p %s/xa %s/xb", + dir, dir, dir); + if (runwait(cmd) != 0) return -2; - mkdir(tmpdir, 0755); - snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>/dev/null", - tmpdir, driver, src); + snprintf(path, sizeof path, "%s/xa/xa.ww", dir); + if (write_file(path, "package xa;\nexport type invalid = !i8;\n")) + return -2; + + snprintf(sub, sizeof sub, "%s/xb/xb.ww", dir); + snprintf(xbsrc, sizeof xbsrc, + "package xb;\n" + "import xa;\n" + "export type invalid = !i64;\n" + "export fn f(%s: i32) i64 = { return size(invalid): i64; };\n", + r->xb_param); + if (write_file(sub, xbsrc)) return -2; + + snprintf(path, sizeof path, "%s/root.ww", dir); + if (write_file(path, + "package main;\n" + "import xb;\n" + "import xa;\n" + "fn main() int = { return xb.f(0): int; };\n")) + return -2; + + /* canonical dir-package build: cd into the package root, build the + * root unit; the binary lands in cwd (#22 dir=module). */ + snprintf(cmd, sizeof cmd, "cd %s && %s build root.ww 2>/dev/null", + dir, driver); int brc = runwait(cmd); - const char *base = strrchr(src, '/'); - base = base ? base + 1 : src; char outbin[128]; - snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); - char *dot = strrchr(outbin, '.'); - if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; - + snprintf(outbin, sizeof outbin, "%s/root", dir); int got = -1; if (brc == 0) got = runwait(outbin); - unlink(src); unlink(outbin); rmdir(tmpdir); + snprintf(cmd, sizeof cmd, "rm -rf %s", dir); + runwait(cmd); return brc == 0 ? got : -1; }