diff --git a/.gitignore b/.gitignore index 8712050d..25af67b3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,12 @@ # Build outputs. /out/ -# Separate-compilation scratch dirs. `ww build` / `ww --sep` (the sole -# build path post-#22 flip) drops .sepwork/ next to the OUTPUT -# stem; an in-place build (no -o) lands it next to the source. The -# Makefile byte-id gates read .sepwork/*.s deliberately (rule-10 -# #223 discriminator), so the dir is transient-not-tracked (CLAUDE.md -# rule-14), never relocated. +# Separate-compilation artifact dirs. `ww build` and explicit `ww test -o` +# leave caller-owned .sepwork/ at the selected artifact stem; they +# refuse to replace an existing tree. `ww run` and no-output single-file tests +# put the executable and scratch in one driver-owned temporary directory and +# remove both. Byte gates read selected files deliberately, then remove the +# exact tree, so it is transient-not-tracked. *.sepwork/ # Claude Code session artifacts. diff --git a/Makefile b/Makefile index 542c21b5..36650fb0 100644 --- a/Makefile +++ b/Makefile @@ -132,12 +132,16 @@ $(BIN)/wwdump_ww: selfhost/cmd/wwdump/main.ww \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) @mkdir -p $(BIN)/wwdump_ww.d - cd $(BIN)/wwdump_ww.d && $(CURDIR)/$(BIN)/ww build \ - -o main \ - -I $(CURDIR)/lib/ww \ - -I $(CURDIR)/selfhost/cmd/wcc \ - $(CURDIR)/selfhost/cmd/wwdump/main.ww - mv $(BIN)/wwdump_ww.d/main $@ + @d=$$(mktemp -d "$(CURDIR)/$(BIN)/wwdump_ww.d/build.XXXXXX") || exit 1; \ + rc=0; published=0; \ + (cd "$$d" && $(CURDIR)/$(BIN)/ww build -o main \ + -I $(CURDIR)/lib/ww -I $(CURDIR)/selfhost/cmd/wcc \ + $(CURDIR)/selfhost/cmd/wwdump/main.ww) || rc=$$?; \ + rm -rf -- "$$d/main.sepwork" || { echo "$@: scratch cleanup failed" >&2; if [ $$rc -eq 0 ]; then rc=1; fi; }; \ + if [ $$rc -eq 0 ]; then mv "$$d/main" "$(CURDIR)/$@" && published=1 || rc=$$?; fi; \ + if [ $$published -eq 0 ]; then rm -f -- "$$d/main" || { echo "$@: output cleanup failed" >&2; if [ $$rc -eq 0 ]; then rc=1; fi; }; fi; \ + if ! rmdir "$$d"; then echo "$@: workspace cleanup failed" >&2; if [ $$published -eq 1 ]; then rm -f -- "$(CURDIR)/$@" || echo "$@: published-output cleanup failed" >&2; fi; if [ $$rc -eq 0 ]; then rc=1; fi; fi; \ + exit $$rc # ---- ww-side w6c (compiler port, exercised by 994_w6c_ww) ------------- # Thin driver: parse + cgen. The frontend (lex/parse/ast/...) is the @@ -153,12 +157,16 @@ $(BIN)/w6c_ww: selfhost/cmd/w6c/main.ww \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) @mkdir -p $(BIN)/w6c_ww.d - cd $(BIN)/w6c_ww.d && $(CURDIR)/$(BIN)/ww build \ - -o main \ - -I $(CURDIR)/lib/ww \ - -I $(CURDIR)/selfhost/cmd/wcc \ - $(CURDIR)/selfhost/cmd/w6c/main.ww - mv $(BIN)/w6c_ww.d/main $@ + @d=$$(mktemp -d "$(CURDIR)/$(BIN)/w6c_ww.d/build.XXXXXX") || exit 1; \ + rc=0; published=0; \ + (cd "$$d" && $(CURDIR)/$(BIN)/ww build -o main \ + -I $(CURDIR)/lib/ww -I $(CURDIR)/selfhost/cmd/wcc \ + $(CURDIR)/selfhost/cmd/w6c/main.ww) || rc=$$?; \ + rm -rf -- "$$d/main.sepwork" || { echo "$@: scratch cleanup failed" >&2; if [ $$rc -eq 0 ]; then rc=1; fi; }; \ + if [ $$rc -eq 0 ]; then mv "$$d/main" "$(CURDIR)/$@" && published=1 || rc=$$?; fi; \ + if [ $$published -eq 0 ]; then rm -f -- "$$d/main" || { echo "$@: output cleanup failed" >&2; if [ $$rc -eq 0 ]; then rc=1; fi; }; fi; \ + if ! rmdir "$$d"; then echo "$@: workspace cleanup failed" >&2; if [ $$published -eq 1 ]; then rm -f -- "$(CURDIR)/$@" || echo "$@: published-output cleanup failed" >&2; fi; if [ $$rc -eq 0 ]; then rc=1; fi; fi; \ + exit $$rc # ---- ww-side w6a (assembler port, exercised by 991_w6a_ww) ------------ # Built like wwdump_ww. Needs -I selfhost/cmd/w6a for the local types/lex/ @@ -171,11 +179,16 @@ $(BIN)/w6a_ww: selfhost/cmd/w6a/main.ww selfhost/cmd/w6a/opcodes.ww \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) @mkdir -p $(BIN)/w6a_ww.d - cd $(BIN)/w6a_ww.d && $(CURDIR)/$(BIN)/ww build \ - -o main \ - -I $(CURDIR)/selfhost/cmd/w6a \ - $(CURDIR)/selfhost/cmd/w6a/main.ww - mv $(BIN)/w6a_ww.d/main $@ + @d=$$(mktemp -d "$(CURDIR)/$(BIN)/w6a_ww.d/build.XXXXXX") || exit 1; \ + rc=0; published=0; \ + (cd "$$d" && $(CURDIR)/$(BIN)/ww build -o main \ + -I $(CURDIR)/selfhost/cmd/w6a \ + $(CURDIR)/selfhost/cmd/w6a/main.ww) || rc=$$?; \ + rm -rf -- "$$d/main.sepwork" || { echo "$@: scratch cleanup failed" >&2; if [ $$rc -eq 0 ]; then rc=1; fi; }; \ + if [ $$rc -eq 0 ]; then mv "$$d/main" "$(CURDIR)/$@" && published=1 || rc=$$?; fi; \ + if [ $$published -eq 0 ]; then rm -f -- "$$d/main" || { echo "$@: output cleanup failed" >&2; if [ $$rc -eq 0 ]; then rc=1; fi; }; fi; \ + if ! rmdir "$$d"; then echo "$@: workspace cleanup failed" >&2; if [ $$published -eq 1 ]; then rm -f -- "$(CURDIR)/$@" || echo "$@: published-output cleanup failed" >&2; fi; if [ $$rc -eq 0 ]; then rc=1; fi; fi; \ + exit $$rc # ---- ww-side w6l (linker port, exercised by 992_w6l_ww) --------------- # Built like 6a_ww. Needs -I selfhost/cmd/w6l for the local sym/obj/pass/ @@ -189,25 +202,36 @@ $(BIN)/w6l_ww: selfhost/cmd/w6l/main.ww selfhost/cmd/w6l/sym.ww \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) @mkdir -p $(BIN)/w6l_ww.d - cd $(BIN)/w6l_ww.d && $(CURDIR)/$(BIN)/ww build \ - -o main \ - -I $(CURDIR)/selfhost/cmd/w6l \ - $(CURDIR)/selfhost/cmd/w6l/main.ww - mv $(BIN)/w6l_ww.d/main $@ + @d=$$(mktemp -d "$(CURDIR)/$(BIN)/w6l_ww.d/build.XXXXXX") || exit 1; \ + rc=0; published=0; \ + (cd "$$d" && $(CURDIR)/$(BIN)/ww build -o main \ + -I $(CURDIR)/selfhost/cmd/w6l \ + $(CURDIR)/selfhost/cmd/w6l/main.ww) || rc=$$?; \ + rm -rf -- "$$d/main.sepwork" || { echo "$@: scratch cleanup failed" >&2; if [ $$rc -eq 0 ]; then rc=1; fi; }; \ + if [ $$rc -eq 0 ]; then mv "$$d/main" "$(CURDIR)/$@" && published=1 || rc=$$?; fi; \ + if [ $$published -eq 0 ]; then rm -f -- "$$d/main" || { echo "$@: output cleanup failed" >&2; if [ $$rc -eq 0 ]; then rc=1; fi; }; fi; \ + if ! rmdir "$$d"; then echo "$@: workspace cleanup failed" >&2; if [ $$published -eq 1 ]; then rm -f -- "$(CURDIR)/$@" || echo "$@: published-output cleanup failed" >&2; fi; if [ $$rc -eq 0 ]; then rc=1; fi; fi; \ + exit $$rc # ---- ww-side ww driver (exercised by 993_ww_ww) ------------------------ # The driver pulls in lib/os (default search path) and orchestrates # w6c/w6a/w6l like the C driver. -$(BIN)/ww_ww: selfhost/cmd/ww/main.ww lib/os/os.ww lib/rt/malloc.ww \ +$(BIN)/ww_ww: selfhost/cmd/ww/main.ww lib/os/exec/exec.ww \ + lib/os/os.ww lib/rt/malloc.ww \ lib/time/time.ww lib/strconv/strconv.ww \ lib/strings/strings.ww lib/bytes/bytes.ww lib/encoding/utf8/utf8.ww \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) @mkdir -p $(BIN)/ww_ww.d - cd $(BIN)/ww_ww.d && $(CURDIR)/$(BIN)/ww build \ - -o main \ - $(CURDIR)/selfhost/cmd/ww/main.ww - mv $(BIN)/ww_ww.d/main $@ + @d=$$(mktemp -d "$(CURDIR)/$(BIN)/ww_ww.d/build.XXXXXX") || exit 1; \ + rc=0; published=0; \ + (cd "$$d" && $(CURDIR)/$(BIN)/ww build -o main \ + $(CURDIR)/selfhost/cmd/ww/main.ww) || rc=$$?; \ + rm -rf -- "$$d/main.sepwork" || { echo "$@: scratch cleanup failed" >&2; if [ $$rc -eq 0 ]; then rc=1; fi; }; \ + if [ $$rc -eq 0 ]; then mv "$$d/main" "$(CURDIR)/$@" && published=1 || rc=$$?; fi; \ + if [ $$published -eq 0 ]; then rm -f -- "$$d/main" || { echo "$@: output cleanup failed" >&2; if [ $$rc -eq 0 ]; then rc=1; fi; }; fi; \ + if ! rmdir "$$d"; then echo "$@: workspace cleanup failed" >&2; if [ $$published -eq 1 ]; then rm -f -- "$(CURDIR)/$@" || echo "$@: published-output cleanup failed" >&2; fi; if [ $$rc -eq 0 ]; then rc=1; fi; fi; \ + exit $$rc # ---- runtime (libwwrt.a, assembled by our own w6a) --------------------- $(OBJ)/rt/%.o: rt/%.s $(BIN)/w6a | $(OBJ)/rt diff --git a/cmd/ww/main.c b/cmd/ww/main.c index adb26b0e..cd7f1553 100644 --- a/cmd/ww/main.c +++ b/cmd/ww/main.c @@ -10,6 +10,7 @@ * out/bin/), and can be overridden with WW_W6C / WW_W6A / WW_W6L. */ #include "ww.h" +#include #include #include #include @@ -21,9 +22,9 @@ static const char *usage = "usage: ww [-V] [args...]\n" " -V print version and exit\n" -" build [path] compile module to a static binary (path defaults to cwd)\n" +" build [-S] [-o FILE] [path] compile module; -S stops after package asm\n" " run [path] ... build then exec, passing extra args to the program\n" -" test [path] build and run *_test.ww in the module (path defaults to cwd)\n" +" test [-S -o STEM] [options] [path] build/run tests; -S emits package asm\n" " fmt reformat ww source\n" " version print version and exit\n" "\n" @@ -58,8 +59,8 @@ run(const char *cmd) * pattern as argv[1] (lib/test run() reads it via os.args). fork+execv * (not system()) so glob metacharacters in the pattern reach the binary * verbatim instead of being expanded by the shell. Mirrors the wwstage - * twin (selfhost/cmd/ww/main.ww runsingletest, which always builds an - * execargv for procrun). #17 fnmatch filter. */ + * twin (selfhost/cmd/ww/main.ww runsingletest, which passes the same + * argv to os.exec.runstdio). #17 fnmatch filter. */ static int run_test_bin(const char *bin, const char *pattern) { @@ -80,6 +81,49 @@ run_test_bin(const char *bin, const char *pattern) return 1; } +/* Delegate package/directory testing to the native WW coordinator. Keep the + * old single-file path in this driver: wwtest itself builds each generated + * package root through `ww test -c ... package.ww`, so that file boundary also + * prevents delegation recursion. */ +static int +exec_package_tests(int argc, char **argv, const char *target, + const char *resolved, int add_dot) +{ + const char *override = getenv("WW_WWTEST"); + char fallback[1024]; + const char *prog = override && override[0] ? override : fallback; + if (prog == fallback) + snprintf(fallback, sizeof fallback, "%s/wwtest", self_dir); + char builder[1024]; + snprintf(builder, sizeof builder, "%s/ww", self_dir); + + char **xargv = calloc((size_t)argc + 6, sizeof *xargv); + if (xargv == NULL) { + fputs("ww test: cannot allocate package coordinator arguments\n", + stderr); + return 1; + } + int n = 0, dotted = 0; + xargv[n++] = (char *)prog; + xargv[n++] = "package"; + xargv[n++] = "--ww-driver"; + xargv[n++] = builder; + for (int i = 0; i < argc; i++) { + if (add_dot && !dotted && strcmp(argv[i], "--") == 0) { + xargv[n++] = "."; + dotted = 1; + } + xargv[n++] = (resolved && argv[i] == target) + ? (char *)resolved : argv[i]; + } + if (add_dot && !dotted) xargv[n++] = "."; + xargv[n] = NULL; + execv(prog, xargv); /* inherit the caller's environment */ + fputs("ww test: cannot exec package test coordinator\n", stderr); + free(xargv); + return 1; +} + /* Set of imported module paths, kept on the heap. Used to break * cycles in `use` resolution. Linear because typical imports are * a handful per build. */ @@ -153,7 +197,7 @@ locate_import_in(const char *dir, const char *path_form, char *out, * wins over a same-named sibling FILE on an EARLIER entry. The driver * builds the searchpath srcd-first; a co-located `lib//test.ww` * entry makes srcd = lib/, so a self-named `import ` would - * else file-hit the sibling lib//.ww and (under --sep) fold + * else file-hit the sibling lib//.ww and fold it * inline under the wrong module-reset → "package does not match * import path ". Two passes — directories first, files only * if no directory matches anywhere — let lib// resolve as the dir @@ -198,13 +242,38 @@ strs_cmp(const void *a, const void *b) return strcmp(sa, sb); } -/* enumerate_dir_ww — collect *.ww names in `dirpath` excluding - * *test.ww, sort byte-wise. Returns count; caller frees entries. */ +/* A temporary migration rule keeps the pre-underscore corpus working without + * reserving every filename ending in "test.ww": only a real line-leading + * @test declaration makes a noncanonical source test-only. */ +static int +file_has_line_test(const char *path) +{ + char line[4096]; + FILE *f = fopen(path, "rb"); + if (f == NULL) return 0; + int found = 0; + while (fgets(line, sizeof line, f) != NULL) { + char *p = line; + while (*p == ' ' || *p == '\t' || *p == '\r') p++; + if (strncmp(p, "@test", 5) == 0 + && (p[5] == ' ' || p[5] == '\t')) { + found = 1; + break; + } + } + fclose(f); + return found; +} + +/* enumerate_dir_ww — collect production *.ww paths in `dirpath`, excluding + * canonical *_test.ww plus the explicit line-leading-@test compatibility + * sources, then sort byte-wise. This is the sole directory-membership + * discovery path; the owning seppkg retains the returned list. */ static int enumerate_dir_ww(const char *dirpath, char ***out_files) { DIR *d = opendir(dirpath); - if (d == NULL) { *out_files = NULL; return 0; } + if (d == NULL) { *out_files = NULL; return -1; } char **arr = NULL; int n = 0, cap = 0; struct dirent *ent; @@ -213,21 +282,21 @@ enumerate_dir_ww(const char *dirpath, char ***out_files) size_t nl = strlen(nm); if (nl <= 3) continue; if (strcmp(nm + nl - 3, ".ww") != 0) continue; - /* skip "*test.ww" (no underscore — bytestest.ww - * ostest.ww utf8test.ww — Hare convention is _test.ha - * but ww corpus settled on the un-underscored shape). */ - if (nl >= 7 && strcmp(nm + nl - 7, "test.ww") == 0) - continue; /* skip "*.combined.ww" — legacy amalgamator artifacts * (pre-#87): they look like .ww but parse-error when * re-included, and the sep driver no longer writes them. */ if (nl >= 12 && strcmp(nm + nl - 12, ".combined.ww") == 0) continue; + char path[2048]; + snprintf(path, sizeof path, "%s/%s", dirpath, nm); + if ((nl >= 8 && strcmp(nm + nl - 8, "_test.ww") == 0) + || file_has_line_test(path)) + continue; if (n + 1 > cap) { cap = cap ? cap * 2 : 8; arr = realloc(arr, cap * sizeof *arr); } - arr[n++] = strdup(nm); + arr[n++] = strdup(path); } closedir(d); if (n > 1) qsort(arr, n, sizeof *arr, strs_cmp); @@ -268,6 +337,9 @@ enumerate_dir_ww(const char *dirpath, char ***out_files) struct seppkg { char path[256]; /* dotted import path; "" == root/primary */ char entry[1024]; /* resolved package dir (or file, for a file root) */ + char name[256]; /* validated declared name; directory packages only */ + char **sources; /* owned, byte-sorted production paths; dirs only */ + int nsources; int is_dir; int deps[SEP_MAXPKG]; /* direct-dep indices into sepgraph.pkg */ int ndeps; @@ -287,7 +359,7 @@ sep_find_or_add(struct sepgraph *g, const char *path, const char *entry, 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", + fprintf(stderr, "ww: too many packages (limit %d)\n", SEP_MAXPKG); return -1; } @@ -295,11 +367,28 @@ sep_find_or_add(struct sepgraph *g, const char *path, const char *entry, snprintf(p->path, sizeof p->path, "%s", path); snprintf(p->entry, sizeof p->entry, "%s", entry); p->is_dir = is_dir; + p->name[0] = '\0'; + p->sources = NULL; + p->nsources = 0; p->ndeps = 0; p->color = 0; return g->n++; } +/* Release the one package-owned directory-membership list. Every graph exit + * funnels through this function; regular-file nodes own no source list. */ +static void +sep_graph_free(struct sepgraph *g) +{ + if (g == NULL) return; + for (int i = 0; i < g->n; i++) { + for (int j = 0; j < g->pkg[i].nsources; j++) + free(g->pkg[i].sources[j]); + free(g->pkg[i].sources); + } + free(g); +} + /* 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 @@ -344,7 +433,78 @@ unit_has_package(const char *path, const char *leaf) return found; } -/* Scan one source file for top-level `import IDENT;`, resolving each. A +static int +sep_ident_start(int c) +{ + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; +} + +static int +sep_ident_continue(int c) +{ + return sep_ident_start(c) || (c >= '0' && c <= '9'); +} + +/* Skip the whitespace and comments accepted before and within the leading + * package clause. This is deliberately only the loader's small header + * grammar, not a second compiler lexer. */ +static int +sep_skip_space(const char *src, size_t n, size_t *off) +{ + size_t i = *off; + for (;;) { + while (i < n && (src[i] == ' ' || src[i] == '\t' + || src[i] == '\r' || src[i] == '\n')) + i++; + if (i + 1 < n && src[i] == '/' && src[i + 1] == '/') { + i += 2; + while (i < n && src[i] != '\n') i++; + continue; + } + if (i + 1 < n && src[i] == '/' && src[i + 1] == '*') { + i += 2; + while (i + 1 < n && !(src[i] == '*' && src[i + 1] == '/')) + i++; + if (i + 1 >= n) return -1; + i += 2; + continue; + } + break; + } + *off = i; + return 0; +} + +/* Parse exactly the leading loader grammar `package ident;`. */ +static int +sep_package_clause(const char *src, size_t n, char *name, size_t namesz) +{ + static const char kw[] = "package"; + size_t i = 0; + if (sep_skip_space(src, n, &i) < 0 + || i + sizeof kw - 1 >= n + || memcmp(src + i, kw, sizeof kw - 1) != 0) + return -1; + i += sizeof kw - 1; + if (i >= n || (src[i] != ' ' && src[i] != '\t' + && src[i] != '\r' && src[i] != '\n')) + return -1; + if (sep_skip_space(src, n, &i) < 0 || i >= n + || !sep_ident_start((unsigned char)src[i])) + return -1; + size_t begin = i++; + while (i < n && sep_ident_continue((unsigned char)src[i])) i++; + size_t len = i - begin; + if (len + 1 > namesz || sep_skip_space(src, n, &i) < 0 + || i >= n || src[i] != ';') + return -1; + memcpy(name, src + begin, len); + name[len] = '\0'; + return 0; +} + +/* Scan one source file's already-selected bytes for its leading package + * clause (when it is an owned directory source) and top-level imports. 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). Collects package PATHS @@ -352,30 +512,77 @@ unit_has_package(const char *path, const char *leaf) * (§1.1). */ static int sep_scan_file(struct sepgraph *g, int pi, const char *file, - const char *searchpath, struct ImportSet *filevisit) + const char *searchpath, struct ImportSet *filevisit, int owned_source) { 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); + fprintf(stderr, "ww: cannot read %s\n", file); return -1; } - char line[2048]; + if (fseek(in, 0, SEEK_END) != 0) { fclose(in); return -1; } + long flen = ftell(in); + if (flen < 0 || fseek(in, 0, SEEK_SET) != 0) { + fclose(in); + return -1; + } + char *buf = malloc((size_t)flen + 1); + if (buf == NULL) { fclose(in); return -1; } + if (fread(buf, 1, (size_t)flen, in) != (size_t)flen) { + free(buf); + fclose(in); + return -1; + } + buf[flen] = '\0'; + fclose(in); + + if (owned_source) { + char declared[256]; + if (sep_package_clause(buf, (size_t)flen, declared, + sizeof declared) < 0) { + fprintf(stderr, "ww: %s: invalid or missing package clause\n", + file); + free(buf); + return -1; + } + struct seppkg *pkg = &g->pkg[pi]; + if (pkg->name[0] == '\0') + snprintf(pkg->name, sizeof pkg->name, "%s", declared); + else if (strcmp(pkg->name, declared) != 0) { + fprintf(stderr, + "ww: %s: conflicting package names %s and %s\n", + pkg->entry, pkg->name, declared); + free(buf); + return -1; + } + } + 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) + for (size_t off = 0; off < (size_t)flen && rc == 0;) { + size_t end = off; + while (end < (size_t)flen && buf[end] != '\n') end++; + const char *p = buf + off; + const char *lineend = buf + end; + while (p < lineend && (*p == ' ' || *p == '\t')) p++; + if ((size_t)(lineend - p) < 7 + || (memcmp(p, "import ", 7) != 0 + && memcmp(p, "import\t", 7) != 0)) { + off = end < (size_t)flen ? end + 1 : end; continue; + } p += 7; - while (*p == ' ' || *p == '\t') p++; + while (p < lineend && (*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')) + while (p < lineend && ((*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; + if (j == 0) { + off = end < (size_t)flen ? end + 1 : end; + continue; + } char path_form[256]; import_path_form(name, path_form, sizeof path_form); char ipath[1024]; @@ -391,9 +598,10 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file, const char *dot = strrchr(name, '.'); const char *leaf = dot ? dot + 1 : name; if (unit_has_package(file, leaf)) - continue; /* inline-satisfied */ + goto next_line; /* inline-satisfied */ fprintf(stderr, "ww: cannot find package %s\n", name); - exit(1); + rc = -1; + goto next_line; } if (is_dir) { int di = sep_find_or_add(g, name, ipath, 1); @@ -405,37 +613,56 @@ sep_scan_file(struct sepgraph *g, int pi, const char *file, 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) { + } else if (sep_scan_file(g, pi, ipath, searchpath, filevisit, 0) < 0) { rc = -1; break; } + next_line: + off = end < (size_t)flen ? end + 1 : end; } - fclose(in); + free(buf); 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. */ +/* Load package pi once: a directory node takes ownership of its sorted + * production paths, then the same stored list supplies package-name + * validation and dependency scanning. Recurse over the resulting edges. + * `color` doubles as a loaded marker here (2 == loaded); it is reset to + * white before the topo pass. */ static int -sep_scan_pkg(struct sepgraph *g, int pi, const char *searchpath) +sep_load_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); + g->pkg[pi].nsources = enumerate_dir_ww(g->pkg[pi].entry, + &g->pkg[pi].sources); + if (g->pkg[pi].nsources < 0) { + fprintf(stderr, "ww: cannot read directory %s\n", + g->pkg[pi].entry); + rc = -1; + } else if (g->pkg[pi].nsources == 0) { + fprintf(stderr, + "ww: %s: directory contains no WW package sources\n", + g->pkg[pi].entry); + rc = -1; + } + for (int i = 0; i < g->pkg[pi].nsources && rc == 0; i++) + rc = sep_scan_file(g, pi, g->pkg[pi].sources[i], + searchpath, &fv, 1); + if (rc == 0 && g->pkg[pi].path[0] != '\0') { + const char *dot = strrchr(g->pkg[pi].path, '.'); + const char *leaf = dot ? dot + 1 : g->pkg[pi].path; + if (strcmp(g->pkg[pi].name, leaf) != 0) { + fprintf(stderr, + "ww: package %s does not match import path %s\n", + g->pkg[pi].name, g->pkg[pi].path); + rc = -1; + } } - 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); + rc = sep_scan_file(g, pi, g->pkg[pi].entry, searchpath, &fv, 0); } for (int i = 0; i < fv.n; i++) free(fv.paths[i]); free(fv.paths); @@ -443,7 +670,7 @@ sep_scan_pkg(struct sepgraph *g, int pi, const char *searchpath) /* 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) + if (sep_load_pkg(g, g->pkg[pi].deps[k], searchpath) < 0) return -1; return 0; } @@ -461,7 +688,7 @@ sep_topo_visit(struct sepgraph *g, int pi, int *order, int *no, if (g->pkg[pi].color == 1) { int j = 0; while (j < depth && stack[j] != pi) j++; - fprintf(stderr, "ww --sep: dependency cycle: "); + fprintf(stderr, "ww: dependency cycle: "); for (int s = j; s < depth; s++) fprintf(stderr, "%s -> ", g->pkg[stack[s]].path[0] ? g->pkg[stack[s]].path : "(root)"); @@ -502,7 +729,7 @@ sep_emit_body(FILE *out, const char *path, struct ImportSet *visited, import_add(visited, path); FILE *in = fopen(path, "rb"); if (in == NULL) { - fprintf(stderr, "ww --sep: cannot read %s\n", path); + fprintf(stderr, "ww: cannot read %s\n", path); return; } char line[2048]; @@ -543,31 +770,17 @@ sep_emit_body(FILE *out, const char *path, struct ImportSet *visited, fclose(in); } -static void -sep_emit_dir_body(FILE *out, const char *dir, struct ImportSet *visited, - const char *searchpath, const char *modpath) -{ - 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, modpath); - 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. */ + * body under //ww:module-reset. Directory membership comes only from the + * package node loaded before planning. */ 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); + fprintf(stderr, "ww: cannot open %s\n", unitf); return -1; } char inset[SEP_MAXPKG] = {0}; @@ -579,7 +792,7 @@ sep_compose_unit(struct sepgraph *g, int pi, const char *scratch, 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); + fprintf(stderr, "ww: missing %s\n", wwi); fclose(u); return -1; } @@ -590,12 +803,14 @@ sep_compose_unit(struct sepgraph *g, int pi, const char *scratch, fclose(wf); } struct ImportSet bodyvisit = {0}; - if (g->pkg[pi].is_dir) - sep_emit_dir_body(u, g->pkg[pi].entry, &bodyvisit, searchpath, - g->pkg[pi].path); - else + if (g->pkg[pi].is_dir) { + for (int i = 0; i < g->pkg[pi].nsources; i++) + sep_emit_body(u, g->pkg[pi].sources[i], &bodyvisit, + searchpath, g->pkg[pi].path); + } else { sep_emit_body(u, g->pkg[pi].entry, &bodyvisit, searchpath, g->pkg[pi].path); + } for (int i = 0; i < bodyvisit.n; i++) free(bodyvisit.paths[i]); free(bodyvisit.paths); fclose(u); @@ -609,14 +824,13 @@ sep_compose_unit(struct sepgraph *g, int pi, const char *scratch, * one 60-byte member header, and the `.o` bytes (newline-padded to even). * Zeroed mtime/uid/gid + fixed mode + a fixed member name make the bytes * a pure function of the `.o` content → cstage `.a` == wwstage `.a` - * (rule 10) and a stable md5 for the 5b cache key. The wwstage twin is - * archiveo (selfhost/cmd/ww/main.ww). */ + * (rule 10). The wwstage twin is archiveo (selfhost/cmd/ww/main.ww). */ static int archive_o(const char *objpath, const char *apath) { FILE *in = fopen(objpath, "rb"); if (in == NULL) { - fprintf(stderr, "ww --sep: cannot read %s\n", objpath); + fprintf(stderr, "ww: cannot read %s\n", objpath); return -1; } fseek(in, 0, SEEK_END); @@ -632,13 +846,13 @@ archive_o(const char *objpath, const char *apath) FILE *out = fopen(apath, "wb"); if (out == NULL) { - fprintf(stderr, "ww --sep: cannot open %s\n", apath); + fprintf(stderr, "ww: cannot open %s\n", apath); free(buf); return -1; } fwrite("!\n", 1, 8, out); - /* sizelint-ok: the 60-byte ar(5) member header and its field offsets - * are a FILE-FORMAT constant, not a type size (CLAUDE.md rule 13). */ + /* ar(5) fixes each member header at 60 bytes; the offsets below + * address fields in that serialized header. */ char hdr[60]; memset(hdr, ' ', sizeof hdr); memcpy(hdr + 0, "pkg.o/", 6); /* GNU short-name '/' terminator */ @@ -659,230 +873,16 @@ archive_o(const char *objpath, const char *apath) return 0; } -/* ---- 5b content-keyed package cache (#63) ---------------------------- - * - * A per-package cache under WW_PKGCACHE (default out/.pkgcache; gitignored, - * make clean wipes $(OUT)). Purely a dev-inner-loop convenience: every - * bootstrap/byte-id gate cold-compiles (--sep scratch is wiped each run), so - * the cache changes NO gate output. Mirrors out/.testcache + Hare get_cache. - * - * rule-10: md5 is NOT reimplemented per stage — both stages shell to the - * host md5sum and assemble an identical text manifest, so a HIT's reused - * P.wwi/P.o stay byte-identical cs==ww by construction. The compiler-binary - * line keys CODEGEN identity, so it is intentionally stage-specific (w6c vs - * w6c_ww) — each stage maintains its own cache namespace; the cacheable - * OUTPUTS it reuses remain byte-identical. */ - -static const char * -pkgcache_root(void) -{ - const char *p = getenv("WW_PKGCACHE"); - if (p && p[0]) return p; - return "out/.pkgcache"; -} - -/* Hex md5 digest of `path` via the host tool. The cache is gate-cold, so a - * host-tool dep is sanctioned (md5sum is already a build dependency). */ -static int -md5_file(const char *path, char *hex, size_t hexsz) -{ - char cmd[1200]; - snprintf(cmd, sizeof cmd, "md5sum '%s' 2>/dev/null", path); - FILE *p = popen(cmd, "r"); - if (p == NULL) return -1; - char line[256]; - char *got = fgets(line, sizeof line, p); - pclose(p); - if (got == NULL) return -1; - size_t i = 0; - while (i + 1 < hexsz && line[i] && line[i] != ' ' - && line[i] != '\t' && line[i] != '\n') i++; - if (i == 0) return -1; - memcpy(hex, line, i); - hex[i] = '\0'; - return 0; -} - -/* Assemble package pi's content-key manifest (D4) into `out` as deterministic - * text: P's own *.ww md5s (sorted by name) | each DIRECT dep's .wwi md5 - * (sorted by dep path) | the w6c/w6a md5s | the flag string. DIRECT deps - * only — reverse-topo folds transitivity bottom-up, since a dep's .wwi - * already folds ITS deps. Returns 0 on success, -1 on any md5/truncation - * failure (caller treats that as non-cacheable → cold compile). */ -static int -sep_manifest(struct sepgraph *g, int pi, const char *scratch, - const char *c6, const char *a6, char *out, size_t outsz) -{ - size_t off = 0; - char hex[64]; - - off += (size_t)snprintf(out + off, outsz - off, "src"); - if (g->pkg[pi].is_dir) { - char **files = NULL; - int n = enumerate_dir_ww(g->pkg[pi].entry, &files); - int rc = 0; - for (int i = 0; i < n; i++) { - char fp[1024]; - snprintf(fp, sizeof fp, "%s/%s", g->pkg[pi].entry, - files[i]); - if (rc == 0 && md5_file(fp, hex, sizeof hex) == 0) - off += (size_t)snprintf(out + off, outsz - off, - " %s", hex); - else - rc = -1; - free(files[i]); - } - free(files); - if (rc != 0) return -1; - } else { - if (md5_file(g->pkg[pi].entry, hex, sizeof hex) != 0) - return -1; - off += (size_t)snprintf(out + off, outsz - off, " %s", hex); - } - off += (size_t)snprintf(out + off, outsz - off, "\n"); - - /* dep lines, sorted by dep path (deps[] are in import-appearance - * order; insertion-sort the indices for a deterministic manifest). */ - int nd = g->pkg[pi].ndeps; - int idx[SEP_MAXPKG]; - for (int i = 0; i < nd; i++) idx[i] = g->pkg[pi].deps[i]; - for (int i = 1; i < nd; i++) { - int v = idx[i], j = i; - while (j > 0 && - strcmp(g->pkg[idx[j-1]].path, g->pkg[v].path) > 0) { - idx[j] = idx[j-1]; - j--; - } - idx[j] = v; - } - for (int i = 0; i < nd; i++) { - int di = idx[i]; - char wwip[1024]; - sep_fname(g, di, scratch, ".wwi", wwip, sizeof wwip); - if (md5_file(wwip, hex, sizeof hex) != 0) return -1; - off += (size_t)snprintf(out + off, outsz - off, - "dep %s %s\n", g->pkg[di].path, hex); - } - - if (md5_file(c6, hex, sizeof hex) != 0) return -1; - off += (size_t)snprintf(out + off, outsz - off, "w6c %s\n", hex); - if (md5_file(a6, hex, sizeof hex) != 0) return -1; - off += (size_t)snprintf(out + off, outsz - off, "w6a %s\n", hex); - off += (size_t)snprintf(out + off, outsz - off, "flags -c -I\n"); - if (off >= outsz) return -1; /* truncated → unusable key */ - return 0; -} - -static void -pkgcache_dir(struct sepgraph *g, int pi, char *out, size_t outsz) -{ - const char *base = g->pkg[pi].path[0] ? g->pkg[pi].path : "__root"; - snprintf(out, outsz, "%s/%s", pkgcache_root(), base); -} - -/* A torn producer write (e.g. disk-full mid-copy) can leave a 0-byte P.wwi/P.o - * under a self-consistent key; size==0 is unambiguous poison (wwi_emit always - * writes >=1 line, a valid .o is never empty), so both store and lookup reject - * it — self-healing, Go-build-cache style (#10). */ -static int -filenonempty(const char *p) -{ - struct stat st; - return stat(p, &st) == 0 && st.st_size > 0; -} - -/* HIT iff a freshly recomputed manifest equals the stored P.key byte-for-byte - * AND both cached artifacts exist; on HIT copy them into the scratch wwi/obj - * paths so the producer loop can skip compose+w6c+w6a. */ -static int -cache_lookup(struct sepgraph *g, int pi, const char *manifest, - const char *wwi, const char *obj) -{ - char dir[1024], keyp[1100], cwwi[1100], cobj[1100], cmd[4096]; - pkgcache_dir(g, pi, dir, sizeof dir); - snprintf(keyp, sizeof keyp, "%s/P.key", dir); - snprintf(cwwi, sizeof cwwi, "%s/P.wwi", dir); - snprintf(cobj, sizeof cobj, "%s/P.o", dir); - - FILE *f = fopen(keyp, "rb"); - if (f == NULL) return 0; - char stored[16384]; - size_t sn = fread(stored, 1, sizeof stored, f); - int more = (fgetc(f) != EOF); - fclose(f); - if (more || sn != strlen(manifest) || memcmp(stored, manifest, sn) != 0) - return 0; - if (access(cwwi, 0) != 0 || access(cobj, 0) != 0) return 0; - if (!filenonempty(cwwi) || !filenonempty(cobj)) return 0; - snprintf(cmd, sizeof cmd, "cp -f '%s' '%s'", cwwi, wwi); - if (run(cmd) != 0) return 0; - snprintf(cmd, sizeof cmd, "cp -f '%s' '%s'", cobj, obj); - if (run(cmd) != 0) return 0; - return 1; -} - -/* On MISS, persist the freshly compiled artifacts then the manifest. Each is - * copied/written to a per-pid same-dir temp then rename()d into place: rename - * is atomic within one filesystem (cross-fs is not), so a concurrent - * cache_lookup never observes a half-written P.wwi/P.o/P.key (#104). The - * per-pid temp name keeps two concurrent writers from clobbering each other - * mid-copy; content-keyed ⇒ last-writer-wins is byte-identical. The key is - * renamed LAST so a reader that sees the new key always finds complete - * artifacts, and a crash mid-store never leaves a key without them. - * - * The torn-read race is thus closed BY CONSTRUCTION. A deterministic - * behavioral regression-guard isn't feasible through the product build path: - * content-keying means concurrent COLD builds all MISS at lookup and STORE — - * none HIT-reads a mid-store entry — and a warm cache is never re-stored, so - * "store concurrent with a HIT-read of the same entry" can't be forced. The - * deferred white-box guard is TASK #105; 989_pkgcache_concurrent_run smokes - * that concurrent shared-cache builds stay correct. On any mid-store error the - * per-pid temps are unlinked so a failed store leaves no litter. */ -static void -cache_store(struct sepgraph *g, int pi, const char *manifest, - const char *wwi, const char *obj) -{ - char dir[1024], keyp[1100], cwwi[1100], cobj[1100], cmd[4096]; - char twwi[1200], tobj[1200], tkey[1200]; - int pid = (int)getpid(); - FILE *f; - pkgcache_dir(g, pi, dir, sizeof dir); - snprintf(cmd, sizeof cmd, "mkdir -p '%s'", dir); - if (run(cmd) != 0) return; - snprintf(cwwi, sizeof cwwi, "%s/P.wwi", dir); - snprintf(cobj, sizeof cobj, "%s/P.o", dir); - snprintf(keyp, sizeof keyp, "%s/P.key", dir); - snprintf(twwi, sizeof twwi, "%s/P.wwi.tmp.%d", dir, pid); - snprintf(tobj, sizeof tobj, "%s/P.o.tmp.%d", dir, pid); - snprintf(tkey, sizeof tkey, "%s/P.key.tmp.%d", dir, pid); - snprintf(cmd, sizeof cmd, "cp -f '%s' '%s'", wwi, twwi); - if (run(cmd) != 0) goto cleanup; - snprintf(cmd, sizeof cmd, "cp -f '%s' '%s'", obj, tobj); - if (run(cmd) != 0) goto cleanup; - if (!filenonempty(twwi) || !filenonempty(tobj)) goto cleanup; - f = fopen(tkey, "wb"); - if (f == NULL) goto cleanup; - fputs(manifest, f); - fclose(f); - if (rename(twwi, cwwi) != 0) goto cleanup; - if (rename(tobj, cobj) != 0) goto cleanup; - if (rename(tkey, keyp) != 0) goto cleanup; - return; -cleanup: - unlink(twwi); - unlink(tobj); - unlink(tkey); -} - -/* build_one_sep — the --sep orchestration: discover_deps, reverse_topo, +/* build_one_sep — discover_deps, reverse_topo, * the transitive producer loop (one `w6c -c -I` per package, dep-first, * each DEP `.o` wrapped in its own deterministic `.a`), then a * reverse-topo `w6l` of the root `.o` + dep `.a` set + libwwrt.a. Side - * files land in a cold `.sepwork` dir (content-keyed cache = 5b). */ + * files land in a cold `.sepwork` dir. */ static int build_one_sep_impl(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, char *scratchout, size_t scratchoutsz) + const char *extra_libdirs, int is_test, int emit_asm, char *scratchout, + size_t scratchoutsz, struct sepgraph **graphout) { const char *c6 = toolpath("WW_W6C", "w6c"); const char *a6 = toolpath("WW_W6A", "w6a"); @@ -937,23 +937,23 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *out, 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); + fprintf(stderr, "ww: cannot create scratch %s\n", scratch); return 1; } - /* #59: hand the scratch path back so the build_one_sep wrapper can - * rm it on run/test (keepscratch 0). Set AFTER mkdir succeeds so the - * wrapper only removes a dir we actually created. */ + /* Hand the scratch path back only after mkdir succeeds. The wrapper + * therefore never removes a pre-existing path that this build failed + * to acquire. */ if (scratchout) snprintf(scratchout, scratchoutsz, "%s", scratch); struct sepgraph *g = calloc(1, sizeof *g); if (g == NULL) return 1; + if (graphout) *graphout = g; int root = sep_find_or_add(g, "", src, entry_is_dir); - if (root < 0) { free(g); return 1; } + if (root < 0) return 1; /* #79 (-T): lib/test is the synth main's `test.run` callee but @test * files never `import test;`. Inject it as a direct dep of the root so - * sep_scan_pkg pulls test + its transitive deps; the producer adds -T + * sep_load_pkg pulls test + its transitive deps; the producer adds -T * to the root and `test.run` links against test's `.a` — via the * sep_scan_file dedup-guarded dep append. */ if (is_test) { @@ -961,7 +961,7 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *out, int tdir = 0; if (locate_import(srcdir, "test", tpath, sizeof tpath, &tdir)) { int ti = sep_find_or_add(g, "test", tpath, tdir); - if (ti < 0) { free(g); return 1; } + if (ti < 0) return 1; int seen = 0; for (int k = 0; k < g->pkg[root].ndeps; k++) if (g->pkg[root].deps[k] == ti) { seen = 1; break; } @@ -969,14 +969,14 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *out, g->pkg[root].deps[g->pkg[root].ndeps++] = ti; } } - if (sep_scan_pkg(g, root, srcdir) < 0) { free(g); return 1; } + if (sep_load_pkg(g, root, srcdir) < 0) 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 *stack = calloc((size_t)g->n, sizeof *stack); int norder = 0; if (order == NULL || stack == NULL || sep_topo_visit(g, root, order, &norder, stack, 0) < 0) { - free(stack); free(order); free(g); return 1; + free(stack); free(order); return 1; } free(stack); @@ -988,59 +988,53 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *out, 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); - /* 5b: skip compose+w6c+w6a on a content-key HIT. The root is - * never cached — it is the build target, always recompiled. */ - char manifest[16384]; - int cacheable = (pi != root) && sep_manifest(g, pi, scratch, - c6, a6, manifest, sizeof manifest) == 0; - int fresh = cacheable && cache_lookup(g, pi, manifest, wwi, obj); - if (!fresh) { - if (sep_compose_unit(g, pi, scratch, order, norder, srcdir, - unitf) < 0) { free(order); free(g); return 1; } - /* BUG-1 (#69): -I is purely the root's UNUSED - * `.wwi` output path, but it triggers wwi_emit → - * check_exported_type on the root. A terminal binary's - * root legitimately has `export fn` over an unexported - * LOCAL type (the root is never imported), which the - * export-check rejects. Skip -I for the root; its `.wwi` - * is never consumed. */ - if (pi == root) - /* #79: the root carries -T under `ww test --sep` - * so w6c synthesizes the test main. Deps never - * get -T. */ - snprintf(cmd, sizeof cmd, "%s %s-c -o %s %s", - c6, is_test ? "-T " : "", asmf, unitf); - else - 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; - } + if (sep_compose_unit(g, pi, scratch, order, norder, srcdir, + unitf) < 0) { free(order); return 1; } + /* BUG-1 (#69): -I is purely the root's UNUSED + * `.wwi` output path, but it triggers wwi_emit → + * check_exported_type on the root. A terminal binary's + * root legitimately has `export fn` over an unexported + * LOCAL type (the root is never imported), which the + * export-check rejects. Skip -I for the root; its `.wwi` + * is never consumed. */ + if (pi == root) + /* #79: the root carries -T under `ww test` + * so w6c synthesizes the test main. Deps never + * get -T. */ + snprintf(cmd, sizeof cmd, "%s %s-c -o %s %s", + c6, is_test ? "-T " : "", asmf, unitf); + else + snprintf(cmd, sizeof cmd, "%s -c -I %s -o %s %s", + c6, wwi, asmf, unitf); + if (run(cmd) != 0) { + fprintf(stderr, "ww: w6c failed for %s\n", + g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)"); + free(order); return 1; + } + if (!emit_asm) { snprintf(cmd, sizeof cmd, "%s -o %s %s", a6, obj, asmf); if (run(cmd) != 0) { - fprintf(stderr, "ww --sep: w6a failed for %s\n", + fprintf(stderr, "ww: w6a failed for %s\n", g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)"); - free(order); free(g); return 1; + free(order); return 1; } - if (cacheable) cache_store(g, pi, manifest, wwi, obj); } /* wrap each DEP package's `.o` in its own deterministic `.a` * (5a). The ROOT stays a positional `.o` (force-loaded — it's * the build target, always fully linked), so `main` is defined * before any archive is processed. The link consumes `.o`/`.a`, * never `.wwi`. */ - if (pi != root) { + if (!emit_asm && pi != root) { char apath[1024]; sep_fname(g, pi, scratch, ".a", apath, sizeof apath); if (archive_o(obj, apath) != 0) { - fprintf(stderr, "ww --sep: archive failed for %s\n", + fprintf(stderr, "ww: archive failed for %s\n", g->pkg[pi].path[0] ? g->pkg[pi].path : "(root)"); - free(order); free(g); return 1; + free(order); return 1; } } } + if (emit_asm) { free(order); return 0; } /* reverse-topo link: root `.o` first (order[norder-1], force-loaded), * then transitive dep `.a` in reverse-topo order, then libwwrt.a — @@ -1074,34 +1068,48 @@ build_one_sep_impl(const char *src, int entry_is_dir, const char *out, 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; } + free(order); + if (rc != 0) { fprintf(stderr, "ww: w6l failed\n"); return 1; } return 0; } -/* build_one_sep — thin wrapper over build_one_sep_impl that removes the - * per-build `.sepwork` scratch dir when keepscratch is 0 (ww run / - * ww test — the binary is the only artifact wanted). do_build passes - * keepscratch 1: the byte-id gates read `.sepwork/*.s` from the - * `ww build -o` path, so build scratch must persist. One cleanup site - * covers every impl return (success AND error). Guard: only rm a path the - * impl actually wrote that ends ".sepwork" — never an empty/foreign stem - * (#59). Reuses the existing shell rm -rf idiom (build_one_sep_impl:927). */ +/* build_one_sep — thin wrapper over build_one_sep_impl. `ww build` and an + * explicit `ww test -o` retain caller-visible `.sepwork` artifacts; their + * caller owns that exact tree. `ww run` and a no-output single-file test use + * internal scratch and remove it on success and failure. One cleanup site + * covers every internal-scratch impl return. The path is nonempty only after + * this invocation successfully created the exact `.sepwork` tree. */ 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, int is_test, int keepscratch) + const char *extra_libdirs, int is_test, int emit_asm, int keepscratch) { char scratch[1100] = {0}; + struct sepgraph *g = NULL; int r = build_one_sep_impl(src, entry_is_dir, out, objstem, - extra_includes, extra_libs, extra_libdirs, is_test, - scratch, sizeof scratch); + extra_includes, extra_libs, extra_libdirs, is_test, emit_asm, + scratch, sizeof scratch, &g); + sep_graph_free(g); if (!keepscratch && scratch[0]) { size_t sl = strlen(scratch); if (sl > 8 && strcmp(scratch + sl - 8, ".sepwork") == 0) { - char m[1200]; - snprintf(m, sizeof m, "rm -rf %s", scratch); - run(m); + int cleanrc = 1; + pid_t pid = fork(); + if (pid == 0) { + execl("/bin/rm", "rm", "-rf", "--", scratch, + (char *)NULL); + _exit(127); + } + if (pid > 0) { + int status = 0; + if (waitpid(pid, &status, 0) == pid && + WIFEXITED(status)) + cleanrc = WEXITSTATUS(status); + } + if (cleanrc != 0) { + fprintf(stderr, "ww: cannot remove scratch %s\n", scratch); + if (r == 0) r = 1; + } } } return r; @@ -1182,43 +1190,6 @@ resolve_module(const char *name, const char *incs, char *out, size_t outsz, return locate_import(sp, path_form, out, outsz, is_dir); } -/* Append `path` to a heap string-array. Caller frees each entry + the array. */ -static void -strarr_push(char ***arr, int *n, int *cap, const char *path) -{ - if (*n + 1 > *cap) { - *cap = *cap ? *cap * 2 : 8; - *arr = realloc(*arr, *cap * sizeof **arr); - } - (*arr)[(*n)++] = strdup(path); -} - -/* collect_tests: enumerate _test.ww files in (no recursion). - * Returns 0 on success and writes the file list + count, -1 on failure. */ -static int -collect_tests(const char *dir, char ***files, int *n) -{ - DIR *d = opendir(dir); - if (!d) return -1; - *files = NULL; - *n = 0; - int cap = 0; - struct dirent *ent; - while ((ent = readdir(d)) != NULL) { - const char *nm = ent->d_name; - size_t nl = strlen(nm); - const char *suf = "_test.ww"; - size_t sl = strlen(suf); - if (nl <= sl) continue; - if (strcmp(nm + nl - sl, suf) != 0) continue; - char path[1024]; - snprintf(path, sizeof path, "%s/%s", dir, nm); - strarr_push(files, n, &cap, path); - } - closedir(d); - return 0; -} - /* Parse the standard -I/-L/-l/-o flags into incs/libdirs/libs/outpath. The * first non-flag positional becomes *src_out. Returns the index past the last * arg consumed for positionals (so callers can pick up trailing args), or -1 @@ -1231,17 +1202,18 @@ 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 *emit_asm_out) { *src_out = NULL; + if (emit_asm_out) *emit_asm_out = 0; int i = 0; for (; i < argc; i++) { - if (strcmp(argv[i], "--sep") == 0) { - /* 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.) */ - ; + if (strcmp(argv[i], "-S") == 0) { + if (emit_asm_out == NULL) { + fprintf(stderr, "ww %s: unknown flag\n", cmd); + return -1; + } + *emit_asm_out = 1; } else if (strncmp(argv[i], "-l", 2) == 0 && argv[i][2]) { size_t n = strlen(libs); snprintf(libs + n, libsz - n, @@ -1290,6 +1262,9 @@ parse_build_flags(const char *cmd, int argc, char **argv, snprintf(outpath, outsz, "%s", argv[++i]); } else if (strncmp(argv[i], "-o", 2) == 0 && argv[i][2]) { snprintf(outpath, outsz, "%s", argv[i] + 2); + } else if (argv[i][0] == '-') { + fprintf(stderr, "ww %s: unknown flag\n", cmd); + return -1; } else if (*src_out == NULL) { *src_out = argv[i]; } else { @@ -1307,9 +1282,10 @@ do_build(int argc, char **argv) char libdirs[2048] = {0}; char incs[2048] = {0}; char outflag[1024] = {0}; + int emit_asm = 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, &emit_asm) < 0) return 2; if (src == NULL) src = "."; /* default: build cwd */ char resolved[1024]; @@ -1336,7 +1312,7 @@ do_build(int argc, char **argv) basename_no_ext(resolved, out, sizeof out); } return build_one_sep(resolved, is_dir, out, objstem, incs, libs, - libdirs, 0, 1 /* keepscratch: gates read build -o .sepwork */); + libdirs, 0, emit_asm, 1); } static int @@ -1349,7 +1325,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]; @@ -1358,16 +1334,33 @@ do_run(int argc, char **argv) fprintf(stderr, "ww run: cannot find module %s\n", src); return 1; } - char tmp[1024]; - snprintf(tmp, sizeof tmp, "/tmp/ww_run_%d", getpid()); - /* objstem = tmp → intermediates land under /tmp/ww_run_.sepwork/, - * never next to the source (T3). */ - if (build_one_sep(resolved, is_dir, tmp, tmp, incs, libs, libdirs, 0, - 0 /* keepscratch: throwaway run scratch */) != 0) + char tmpdir[1024], tmp[1024]; + snprintf(tmpdir, sizeof tmpdir, "/tmp/ww_run_%d", getpid()); + if (mkdir(tmpdir, 0700) != 0) { + fprintf(stderr, "ww: cannot create temporary directory %s\n", tmpdir); return 1; + } + snprintf(tmp, sizeof tmp, "%s/main", tmpdir); + /* The freshly acquired directory owns both the executable and the + * adjacent main.sepwork tree. Nothing outside it is adopted or removed. */ + if (build_one_sep(resolved, is_dir, tmp, tmp, incs, libs, libdirs, 0, 0, + 0) != 0) { + if (unlink(tmp) != 0 && errno != ENOENT) + fputs("ww: cannot remove temporary output\n", stderr); + if (rmdir(tmpdir) != 0) + fputs("ww: cannot remove temporary directory\n", stderr); + return 1; + } /* exec the built binary with any trailing argv as its argv. */ pid_t pid = fork(); - if (pid < 0) { perror("ww: fork"); unlink(tmp); return 1; } + if (pid < 0) { + perror("ww: fork"); + if (unlink(tmp) != 0 && errno != ENOENT) + fputs("ww: cannot remove temporary output\n", stderr); + if (rmdir(tmpdir) != 0) + fputs("ww: cannot remove temporary directory\n", stderr); + return 1; + } if (pid == 0) { int n_extra = argc - next; char **xargv = calloc((size_t)n_extra + 2, sizeof *xargv); @@ -1379,10 +1372,19 @@ do_run(int argc, char **argv) _exit(127); } int status = 0; - waitpid(pid, &status, 0); - unlink(tmp); - if (WIFEXITED(status)) return WEXITSTATUS(status); - return 1; + pid_t got; + do { got = waitpid(pid, &status, 0); } while (got < 0 && errno == EINTR); + int rc = got == pid && WIFEXITED(status) ? WEXITSTATUS(status) : 1; + if (got != pid) perror("ww: waitpid"); + if (unlink(tmp) != 0 && errno != ENOENT) { + fputs("ww: cannot remove temporary output\n", stderr); + if (rc == 0) rc = 1; + } + if (rmdir(tmpdir) != 0) { + fputs("ww: cannot remove temporary directory\n", stderr); + if (rc == 0) rc = 1; + } + return rc; } static int @@ -1390,26 +1392,29 @@ do_test(int argc, char **argv) { const char *src = NULL; char incs[2048] = {0}; - /* -c (compile-only, Go's `go test -c`) + -o build the test - * binary (and its lib/test-inclusive sep unit, via build_one_sep's - * is_test auto-bundle + the T3 objstem redirect) WITHOUT running it — - * the byte-id gates feed .sepwork/__root.unit.ww to raw - * w6c -T / w6c_ww -T. + /* -c (Go's `go test -c`) builds the test binary without running it. + * -S + -o stops after the lib/test-inclusive package `.s` + * outputs are emitted. Both routes use build_one_sep's is_test bundle + * and T3 objstem redirect. * -T stays internal to w6c; the driver never sees it. -l/-L carry no * meaning for a test build, so they (and any unknown flag) are rejected * rather than silently swallowed — byte-identical wording to the * wwstage twin (selfhost/cmd/ww/main.ww dotest). */ int compileonly = 0; + int emit_asm = 0; char outstem[1024] = {0}; + int packageopts = 0; + int afterdash = 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. */ const char *pattern = NULL; for (int i = 0; i < argc; i++) { + if (afterdash) continue; if (argv[i][0] == '-') { - if (strcmp(argv[i], "--sep") == 0) { - /* E3-C1 flip: sep is the sole path; --sep is an - * accepted no-op (task #87). */ + if (strcmp(argv[i], "--") == 0) { + packageopts = 1; + afterdash = 1; continue; } if (argv[i][1] == 'I') { @@ -1429,6 +1434,23 @@ do_test(int argc, char **argv) "%s%s", n ? ":" : "", dir); } else if (strcmp(argv[i], "-c") == 0) { compileonly = 1; + } else if (strcmp(argv[i], "-S") == 0) { + emit_asm = 1; + } else if (strcmp(argv[i], "-list") == 0) { + packageopts = 1; + } else if (strcmp(argv[i], "-j") == 0 || + strcmp(argv[i], "-run") == 0 || + strcmp(argv[i], "-filter") == 0) { + if (i + 1 >= argc) { + fprintf(stderr, "ww test: %s needs an argument\n", + argv[i]); + return 2; + } + packageopts = 1; + i++; + } else if (strncmp(argv[i], "-timeout-ms=", 12) == 0 && + argv[i][12] != '\0') { + packageopts = 1; } else if (strcmp(argv[i], "-o") == 0) { if (i + 1 >= argc) { fprintf(stderr, @@ -1449,6 +1471,10 @@ do_test(int argc, char **argv) } } const char *target = src ? src : "."; + if (emit_asm && !outstem[0]) { + fprintf(stderr, "ww test: -S needs -o\n"); + return 2; + } struct stat st; if (stat(target, &st) != 0) { /* not a literal path — try module resolution and run as @@ -1460,89 +1486,140 @@ do_test(int argc, char **argv) fprintf(stderr, "ww test: cannot find %s\n", target); return 1; } - char tmp[1024]; + if (is_dir) { + if (outstem[0]) { + fprintf(stderr, + "ww test: -c/-S/-o need a single test file\n"); + return 2; + } + if (pattern) { + fprintf(stderr, + "ww test: pattern needs a single test file\n"); + return 2; + } + return exec_package_tests(argc, argv, src, resolved, 0); + } + if (packageopts) { + fprintf(stderr, + "ww test: package options need a directory\n"); + return 2; + } + char tmpdir[1024] = {0}, tmp[1024]; const char *outp; if (outstem[0]) outp = outstem; - else { snprintf(tmp, sizeof tmp, "/tmp/ww_test_%d", getpid()); outp = tmp; } - /* #59: no-o → objstem=tmp so scratch lands in /tmp (cleaned), - * not next to the source; keepscratch 0 throws it away. With -o - * the user named an artifact home, so keep it (mirror do_build). */ + else { + snprintf(tmpdir, sizeof tmpdir, "/tmp/ww_test_%d", getpid()); + if (mkdir(tmpdir, 0700) != 0) { + fprintf(stderr, "ww: cannot create temporary directory %s\n", + tmpdir); + return 1; + } + snprintf(tmp, sizeof tmp, "%s/main", tmpdir); + outp = tmp; + } + /* No-o redirects internal scratch to /tmp rather than beside the + * source. An explicit -o names the caller-owned artifact stem. */ int br = build_one_sep(resolved, is_dir, outp, outstem[0] ? outstem : tmp, incs, "", "", 1, - outstem[0] ? 1 : 0); - if (br != 0) return 1; - if (compileonly) return 0; + emit_asm, outstem[0] ? 1 : 0); + if (br != 0) { + if (!outstem[0] && unlink(outp) != 0 && errno != ENOENT) + fputs("ww: cannot remove temporary output\n", stderr); + if (!outstem[0] && rmdir(tmpdir) != 0) + fputs("ww: cannot remove temporary directory\n", stderr); + return 1; + } + if (compileonly || emit_asm) { + int cleanfail = 0; + if (!outstem[0] && unlink(outp) != 0 && errno != ENOENT) { + fputs("ww: cannot remove temporary output\n", stderr); + cleanfail = 1; + } + if (!outstem[0] && rmdir(tmpdir) != 0) { + fputs("ww: cannot remove temporary directory\n", stderr); + cleanfail = 1; + } + return cleanfail ? 1 : 0; + } int rc = run_test_bin(outp, pattern); - if (!outstem[0]) unlink(outp); + if (!outstem[0] && unlink(outp) != 0 && errno != ENOENT) { + fputs("ww: cannot remove temporary output\n", stderr); + if (rc == 0) rc = 1; + } + if (!outstem[0] && rmdir(tmpdir) != 0) { + fputs("ww: cannot remove temporary directory\n", stderr); + if (rc == 0) rc = 1; + } return rc; } if (S_ISREG(st.st_mode)) { + if (packageopts) { + fprintf(stderr, + "ww test: package options need a directory\n"); + return 2; + } /* single .ww file — build, then run unless -c (compile-only). */ - char tmp[1024]; + char tmpdir[1024] = {0}, tmp[1024]; const char *outp; if (outstem[0]) outp = outstem; - else { snprintf(tmp, sizeof tmp, "/tmp/ww_test_%d", getpid()); outp = tmp; } - /* #59: see module-mode note — no-o scratch → /tmp, cleaned. */ + else { + snprintf(tmpdir, sizeof tmpdir, "/tmp/ww_test_%d", getpid()); + if (mkdir(tmpdir, 0700) != 0) { + fprintf(stderr, "ww: cannot create temporary directory %s\n", + tmpdir); + return 1; + } + snprintf(tmp, sizeof tmp, "%s/main", tmpdir); + outp = tmp; + } + /* See module-mode note: no-o scratch is redirected to /tmp. */ int br = build_one_sep(target, 0, outp, outstem[0] ? outstem : tmp, - incs, "", "", 1, outstem[0] ? 1 : 0); - if (br != 0) return 1; - if (compileonly) return 0; + incs, "", "", 1, emit_asm, outstem[0] ? 1 : 0); + if (br != 0) { + if (!outstem[0] && unlink(outp) != 0 && errno != ENOENT) + fputs("ww: cannot remove temporary output\n", stderr); + if (!outstem[0] && rmdir(tmpdir) != 0) + fputs("ww: cannot remove temporary directory\n", stderr); + return 1; + } + if (compileonly || emit_asm) { + int cleanfail = 0; + if (!outstem[0] && unlink(outp) != 0 && errno != ENOENT) { + fputs("ww: cannot remove temporary output\n", stderr); + cleanfail = 1; + } + if (!outstem[0] && rmdir(tmpdir) != 0) { + fputs("ww: cannot remove temporary directory\n", stderr); + cleanfail = 1; + } + return cleanfail ? 1 : 0; + } int rc = run_test_bin(outp, pattern); - if (!outstem[0]) unlink(outp); + if (!outstem[0] && unlink(outp) != 0 && errno != ENOENT) { + fputs("ww: cannot remove temporary output\n", stderr); + if (rc == 0) rc = 1; + } + if (!outstem[0] && rmdir(tmpdir) != 0) { + fputs("ww: cannot remove temporary directory\n", stderr); + if (rc == 0) rc = 1; + } return rc; } if (!S_ISDIR(st.st_mode)) { fprintf(stderr, "ww test: %s is neither file nor directory\n", target); return 1; } - if (compileonly || outstem[0]) { - fprintf(stderr, "ww test: -c/-o need a single test file\n"); + if (outstem[0]) { + fprintf(stderr, "ww test: -c/-S/-o need 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. */ + /* The second bare positional remains the legacy single-file filter form; + * package filtering uses explicit -run/-filter options. */ if (pattern) { fprintf(stderr, "ww test: pattern needs a single test file\n"); return 2; } - /* directory — run every *_test.ww inside. */ - char **files = NULL; - int n = 0; - if (collect_tests(target, &files, &n) < 0) { - fprintf(stderr, "ww test: cannot read directory %s\n", target); - return 1; - } - if (n == 0) { - fprintf(stderr, "ww test: no *_test.ww files in %s\n", target); - return 1; - } - int pass = 0, fail = 0; - for (int i = 0; i < n; i++) { - char tmp[1024]; - snprintf(tmp, sizeof tmp, "/tmp/ww_test_%d_%d", getpid(), i); - const char *label = strrchr(files[i], '/'); - label = label ? label + 1 : files[i]; - /* #59: objstem=tmp → scratch in /tmp (cleaned), keepscratch 0. */ - int rc = build_one_sep(files[i], 0, tmp, tmp, target, "", "", 1, 0); - if (rc != 0) { - fprintf(stderr, "FAIL %s (build)\n", label); - fail++; - } else { - int xrc = run(tmp); - if (xrc == 0) { - printf("ok %s\n", label); - pass++; - } else { - printf("FAIL %s (rc=%d)\n", label, xrc); - fail++; - } - } - unlink(tmp); - free(files[i]); - } - free(files); - printf("ww test: %d pass, %d fail\n", pass, fail); - return fail == 0 ? 0 : 1; + return exec_package_tests(argc, argv, src, NULL, src == NULL); } static int diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index b82c1070..53df8d15 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -14,6 +14,7 @@ package main; import os; +import os.exec; import rt; import strings; @@ -149,36 +150,62 @@ fn joinpathlit(dir: *u8, name: str) *u8 = { return buf.ptr; }; -// ---- Subprocess plumbing ---------------------------------------------- +fn owncstr(s: str) *u8 = { + let b: []u8 = alloc([], (s.len + 1): u64)!; + b.len = s.len + 1; + let i: i32 = 0; + for (i < s.len) { b[i] = s[i]; i += 1; }; + b[s.len] = 0u8; + return b.ptr; +}; -// procrun — fork, execve `path` with `argv` (NULL-terminated), wait. -// Returns the child's real exit code on clean exit, 1 on signal kill, -// -1 on fork/wait failure. Mirrors cmd/ww/main.c:do_run WEXITSTATUS: -// the build-step callers only test `!= 0`, so propagating the exact -// non-zero code leaves them unaffected while `dorun` reports the true -// program exit status (was collapsing every non-zero exit to 1; fix #16). -fn procrun(path: *u8, argv: **u8) i32 = { - let pid: i32 = os.fork(); - if (pid < 0) { - cerr("ww: fork failed\n"); - return -1; +// execpackagetests — replace the driver with the native WW package +// coordinator for directory/default invocations. Explicit generated +// package.ww roots stay on runsingletest, which is the recursion boundary when +// wwtest builds its already-aggregated package binaries. +fn execpackagetests(selfdir: *u8, argv: **u8, argc: i32, start: i32, + targetindex: i32, resolved: *u8, adddot: bool) i32 = { + let prog: *u8 = joinpathlit(selfdir, "wwtest"); + match (os.getenv("WW_WWTEST")) { + case let p: str => { + if (p.len != 0) { prog = owncstr(p); }; }; - if (pid == 0) { - os.execve(pathstr(path), argv, nil: **u8); - cerr("ww: execve failed\n"); - os.exit(127); + case void => void; }; - let status: i32 = 0; - let r: i32 = os.wait4(pid, &status, 0i32, nil: *void); - if (r < 0) { - cerr("ww: wait4 failed\n"); - return -1; + + let builder: *u8 = joinpathlit(selfdir, "ww_ww"); + let cap: i32 = argc - start + 6; + let execargv: []*u8 = alloc([], cap: u64)!; + execargv.len = cap; + let n: i32 = 0; + execargv[n] = prog; n += 1; + execargv[n] = "package".ptr; n += 1; + execargv[n] = "--ww-driver".ptr; n += 1; + execargv[n] = builder; n += 1; + let dotted: bool = false; + let i: i32 = start; + for (i < argc) { + if (adddot && !dotted && cstreqlit(argv[i], "--")) { + execargv[n] = ".".ptr; n += 1; + dotted = true; + }; + if (resolved != nil && i == targetindex) { execargv[n] = resolved; } + else { execargv[n] = argv[i]; }; + n += 1; + i += 1; }; - // Linux wait status: low byte = signal (0 if exited cleanly), - // next byte = exit code. - if ((status & 127i32) != 0) { return 1; }; - let code: i32 = (status >> 8i32) & 255i32; - return code; + if (adddot && !dotted) { execargv[n] = ".".ptr; n += 1; }; + execargv[n] = nil; + + let env: []str = os.getenvs(); + let envp: []*u8 = alloc([], (env.len + 1): u64)!; + envp.len = env.len + 1; + i = 0; + for (i < env.len) { envp[i] = owncstr(env[i]); i += 1; }; + envp[env.len] = nil; + os.execve(pathstr(prog), execargv.ptr, envp.ptr); + cerr("ww test: cannot exec package test coordinator\n"); + return 1; }; // ---- import resolution + visited-set ----------------------------------- @@ -290,7 +317,7 @@ fn locatein(dir: *u8, dirlen: u64, // wins over a same-named sibling FILE on an EARLIER entry. The driver // builds the searchpath srcd-first; a co-located `lib//test.ww` // entry makes srcd = lib/, so a self-named `import ` would -// else file-hit the sibling lib//.ww and (under --sep) fold +// else file-hit the sibling lib//.ww and fold it // inline under the wrong module-reset. Two passes — directories first, // files only if no directory matches anywhere — let lib// resolve // as the dir while a genuine leaf package with no directory (e.g. @@ -325,11 +352,51 @@ fn locateimport(dirs: *u8, name: *u8, namelen: u64, return nil; }; -// Filter for dir enumeration: keep `*.ww` minus `*test.ww` and the -// `*.combined.ww` legacy amalgamator artifacts (pre-#87): they -// parse-error when re-included, and the sep driver no longer writes -// them. Returns true to keep. -fn dirfilekeep(name: *u8, nlen: u64) bool = { +// Temporary compatibility for the repository's pre-underscore test sources: +// a noncanonical filename is test-only only when its body contains a real +// line-leading @test declaration. This is the wwstage twin of +// cmd/ww/main.c:file_has_line_test. +fn dirfileattest(dirpath: *u8, name: *u8) bool = { + let path: *u8 = joinpath(dirpath, name); + let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32); + if (fd < 0) { return false; }; + let sr: (i64 | os.oserror) = os.filesize(fd); + let n: i64 = -1i64; + match (sr) { + case let v: i64 => n = v; + case let e: os.oserror => { os.close(fd); return false; }; + }; + if (n <= 0i64) { os.close(fd); return false; }; + let b: []u8 = alloc([], n: u64)!; + b.len = n: i32; + let rr: (i64 | os.oserror) = os.readall(fd, b.ptr, n: u64); + os.close(fd); + let got: i64 = -1i64; + match (rr) { + case let v: i64 => got = v; + case let e: os.oserror => return false; + }; + if (got != n) { return false; }; + let i: i32 = 0; + for (i < b.len) { + for (i < b.len && (b[i] == ' ' || b[i] == '\t' + || b[i] == '\r')) { i += 1; }; + if (i + 5 < b.len && b[i] == '@' && b[i + 1] == 't' + && b[i + 2] == 'e' && b[i + 3] == 's' + && b[i + 4] == 't' + && (b[i + 5] == ' ' || b[i + 5] == '\t')) { + return true; + }; + for (i < b.len && b[i] != '\n') { i += 1; }; + if (i < b.len) { i += 1; }; + }; + return false; +}; + +// Filter for production enumeration: keep `*.ww` minus canonical +// `*_test.ww`, the explicit compatibility sources above, and generated +// `*.combined.ww` artifacts. Returns true to keep. +fn dirfilekeep(dirpath: *u8, name: *u8, nlen: u64) bool = { // nlen<=3 guard kept: a bare ".ww" (len 3) is rejected here but // would pass strings.hassuffix(".ww"); preserves cstage parity. if (nlen <= 3u64) { return false; }; @@ -337,10 +404,11 @@ fn dirfilekeep(name: *u8, nlen: u64) bool = { s.ptr = name; s.len = nlen: i32; if (!strings.hassuffix(s, ".ww")) { return false; }; - if (strings.hassuffix(s, "test.ww")) { return false; }; // ".combined.ww" — full 12-char match mirrors cstage // cmd/ww/main.c enumerate_dir_ww strcmp (rule 10). if (strings.hassuffix(s, ".combined.ww")) { return false; }; + if (strings.hassuffix(s, "_test.ww") + || dirfileattest(dirpath, name)) { return false; }; return true; }; @@ -364,12 +432,13 @@ fn bytecmp(a: *u8, alen: u64, b: *u8, blen: u64) i32 = { return 0; }; -// enumeratedir — list *.ww entries of `dirpath` (less *test.ww and -// *.combined.ww), byte-sort. Returns (names[], nnames) with each -// name a NUL-terminated heap copy. +// enumeratedir — list production *.ww paths of `dirpath` (less canonical +// and compatibility test sources plus *.combined.ww), byte-sort. Returns +// one exact pointer array of NUL-terminated full paths. This is the sole +// directory-membership discovery path; the owning seppkg retains the list. fn enumeratedir(dirpath: *u8) (**u8, i32) = { let fd: i32 = os.open(pathstr(dirpath), os.flag.RDONLY, 0i32); - if (fd < 0) { return nil: **u8, 0; }; + if (fd < 0) { return nil: **u8, -1; }; // #65: grow-dynamic (mirror cstage enumerate_dir_ww realloc-doubling, // cmd/ww/main.c:209). The old fixed 256-name cap silently dropped every // eligible file past it, diverging the package unit from cstage on a @@ -390,7 +459,7 @@ fn enumeratedir(dirpath: *u8) (**u8, i32) = { let reclen: u64 = blo + (bhi * 256u64); let nm: *u8 = buf.ptr + off + 19u64; let nl: u64 = cstrlen(nm); - if (dirfilekeep(nm, nl)) { + if (dirfilekeep(dirpath, nm, nl)) { if (n >= cap) { let ncap: i32 = cap * 2; let nn: []*u8 = alloc([], ncap: u64)!; @@ -405,12 +474,9 @@ fn enumeratedir(dirpath: *u8) (**u8, i32) = { nlens = nl2; cap = ncap; }; - let cp: []u8 = alloc([], nl + 1u64)!; - let i: u64 = 0u64; - for (i < nl) { cp[i] = nm[i]; i += 1u64; }; - cp[nl] = 0u8; - names[n] = cp.ptr; - nlens[n] = nl; + let full: *u8 = joinpath(dirpath, nm); + names[n] = full; + nlens[n] = cstrlen(full); n += 1; }; off += reclen; @@ -438,7 +504,20 @@ fn enumeratedir(dirpath: *u8) (**u8, i32) = { }; i += 1; }; - return names.ptr, n; + if (n == 0) { + os.free(names.ptr: *void, (cap: u64) * (size(*u8): u64)); + os.free(nlens.ptr: *void, (cap: u64) * (size(u64): u64)); + return nil: **u8, 0; + }; + let exact: []*u8 = alloc([], n: u64)!; + exact.len = n; + let k: i32 = 0; + for (k < n) { exact[k] = names[k]; k += 1; }; + // rt_free is currently a no-op, but keep the concrete owner/release + // shape correct for the driver's allocations. + os.free(names.ptr: *void, (cap: u64) * (size(*u8): u64)); + os.free(nlens.ptr: *void, (cap: u64) * (size(u64): u64)); + return exact.ptr, n; }; // ---- file slurp ------------------------------------------------------- @@ -553,9 +632,9 @@ type lflags = struct { nlibs: i32, }; -// ---- ww build --sep — M3-tail separate-compilation driver ------------ +// ---- separate-compilation driver ------------------------------------- // -// Port of cmd/ww/main.c build_one_sep (task #46/c3). The `--sep` path +// Port of cmd/ww/main.c build_one_sep (task #46/c3). The build path // materializes each imported package's `.wwi` interface and compiles // every package on its own (`w6c -c`), then flat-links the `.o` set. // Separate compilation is the SOLE build path (E3-C1 flip, task #87). @@ -578,6 +657,9 @@ 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 + name: *u8, // validated declared name; directory packages only + sources: **u8, // owned, byte-sorted production paths; dirs only + nsources: i32, isdir: i32, deps: []i32, // direct-dep indices into sepgraph.pkg ndeps: i32, @@ -597,13 +679,16 @@ fn sepfindoradd(g: *sepgraph, path: *u8, entry: *u8, isdir: i32) i32 = { i += 1; }; if (g.n >= SEP_MAXPKG) { - cerr("ww --sep: too many packages\n"); + cerr("ww: 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].name = nil; + g.pkg[g.n].sources = nil; + g.pkg[g.n].nsources = 0; g.pkg[g.n].isdir = isdir; let dslot: []i32 = alloc([], SEP_MAXPKG: u64)!; dslot.len = SEP_MAXPKG; @@ -615,6 +700,30 @@ fn sepfindoradd(g: *sepgraph, path: *u8, entry: *u8, isdir: i32) i32 = { return r; }; +// Release the package-owned directory-membership lists through one graph +// cleanup function. rt_free is a no-op in today's no-free runtime, but this +// records the same ownership boundary as the C bootstrap twin. +fn sepgraphfree(g: *sepgraph) void = { + if (g == nil) { return; }; + let i: i32 = 0; + for (i < g.n) { + let j: i32 = 0; + for (j < g.pkg[i].nsources) { + let p: *u8 = g.pkg[i].sources[j]; + os.free(p: *void, os.PATH_MAX: u64); + j += 1; + }; + if (g.pkg[i].sources != nil) { + os.free(g.pkg[i].sources: *void, + (g.pkg[i].nsources: u64) * (size(*u8): u64)); + }; + if (g.pkg[i].name != nil) { + os.free(g.pkg[i].name: *void, cstrlen(g.pkg[i].name) + 1u64); + }; + i += 1; + }; +}; + // Build "/" NUL-term; base = path, or "__root" // for the empty root path. fn sepfname(g: *sepgraph, pi: i32, scratch: *u8, suffix: str) *u8 = { @@ -694,12 +803,85 @@ fn unithaspackage(buf: *u8, buflen: u64, leafp: *u8, leafn: u64) bool = { 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 +fn sepidentstart(c: u8) bool = { + if (c >= 'a' && c <= 'z') { return true; }; + if (c >= 'A' && c <= 'Z') { return true; }; + return c == '_'; +}; + +fn sepidentcontinue(c: u8) bool = { + if (sepidentstart(c)) { return true; }; + return c >= '0' && c <= '9'; +}; + +// Skip the whitespace and comments accepted before and within the leading +// package clause. This is deliberately only the loader's small header +// grammar, not a second compiler lexer. +fn sepskipspace(src: *u8, n: u64, start: u64, ok: *bool) u64 = { + let i: u64 = start; + *ok = true; + for (true) { + for (i < n && (src[i] == ' ' || src[i] == '\t' + || src[i] == '\r' || src[i] == '\n')) { i += 1u64; }; + if (i + 1u64 < n && src[i] == '/' && src[i + 1u64] == '/') { + i += 2u64; + for (i < n && src[i] != '\n') { i += 1u64; }; + continue; + }; + if (i + 1u64 < n && src[i] == '/' && src[i + 1u64] == '*') { + i += 2u64; + let closed: bool = false; + for (i + 1u64 < n) { + if (src[i] == '*' && src[i + 1u64] == '/') { + i += 2u64; + closed = true; + break; + }; + i += 1u64; + }; + if (!closed) { *ok = false; return i; }; + continue; + }; + break; + }; + return i; +}; + +// Parse exactly the leading loader grammar `package ident;`. +fn seppackageclause(src: *u8, n: u64, outp: **u8, outn: *u64) bool = { + let ok: bool = true; + let i: u64 = sepskipspace(src, n, 0u64, &ok); + if (!ok || i + 7u64 >= n) { return false; }; + let word: str = "package"; + let j: i32 = 0; + for (j < word.len) { + let ju: u64 = j: u64; + if (src[i + ju] != word[j]) { return false; }; + j += 1; + }; + i += 7u64; + if (i >= n || !(src[i] == ' ' || src[i] == '\t' + || src[i] == '\r' || src[i] == '\n')) { return false; }; + i = sepskipspace(src, n, i, &ok); + if (!ok || i >= n || !sepidentstart(src[i])) { return false; }; + let begin: u64 = i; + i += 1u64; + for (i < n && sepidentcontinue(src[i])) { i += 1u64; }; + let end: u64 = i; + i = sepskipspace(src, n, i, &ok); + if (!ok || i >= n || src[i] != ';') { return false; }; + *outp = src + begin; + *outn = end - begin; + return true; +}; + +// Scan one already-selected source file for its leading package clause +// (when it is an owned directory source) and top-level imports. 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 = { + fv: *expctx, ownedsource: i32) i32 = { let fview: str; fview.ptr = file; fview.len = cstrlen(file): i32; @@ -710,9 +892,32 @@ fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8, let blen: u64; bufp, blen = slurp(file); if (bufp == nil) { - cerr("ww --sep: cannot read source\n"); + cerr("ww: cannot read source\n"); return -1; }; + if (ownedsource != 0) { + let declared: *u8 = nil; + let declaredn: u64 = 0u64; + if (!seppackageclause(bufp, blen, &declared, &declaredn)) { + cerr("ww: "); + cerr(pathstr(file)); + cerr(": invalid or missing package clause\n"); + return -1; + }; + if (g.pkg[pi].name == nil) { + g.pkg[pi].name = arenadupcstr(declared, declaredn); + } else { if (bytecmp(g.pkg[pi].name, cstrlen(g.pkg[pi].name), + declared, declaredn) != 0) { + cerr("ww: "); + cerr(pathstr(g.pkg[pi].entry)); + cerr(": conflicting package names "); + cerr(pathstr(g.pkg[pi].name)); + cerr(" and "); + os.write(2, declared, declaredn); + cerr("\n"); + return -1; + }; }; + }; let i: u64 = 0u64; for (i < blen) { let j: u64 = i; @@ -746,7 +951,7 @@ fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8, g.pkg[pi].ndeps += 1; }; } else { - if (sepscanfile(g, pi, ipath, searchpath, fv) < 0) { + if (sepscanfile(g, pi, ipath, searchpath, fv, 0) < 0) { return -1; }; }; @@ -771,7 +976,7 @@ fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8, cerr("ww: cannot find package "); os.write(2, idp, idn); cerr("\n"); - os.exit(1); + return -1; }; }; }; @@ -780,10 +985,11 @@ fn sepscanfile(g: *sepgraph, pi: i32, file: *u8, searchpath: *u8, 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 = { +// Load pi once: a directory node takes ownership of its sorted production +// paths, then the same stored list supplies package-name validation and +// dependency scanning. Recurse over the resulting edges. `color` doubles as +// a loaded marker (2); reset to white before topo. +fn seploadpkg(g: *sepgraph, pi: i32, searchpath: *u8) i32 = { if (g.pkg[pi].color == 2) { return 0; }; g.pkg[pi].color = 2; let fv: expctx; @@ -792,32 +998,54 @@ fn sepscanpkg(g: *sepgraph, pi: i32, searchpath: *u8) i32 = { 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 sources: **u8; + let nsources: i32; + sources, nsources = enumeratedir(g.pkg[pi].entry); + g.pkg[pi].sources = sources; + g.pkg[pi].nsources = nsources; + if (g.pkg[pi].nsources < 0) { + cerr("ww: cannot read directory "); + cerr(pathstr(g.pkg[pi].entry)); + cerr("\n"); + rc = -1; + } else { if (g.pkg[pi].nsources == 0) { + cerr("ww: "); + cerr(pathstr(g.pkg[pi].entry)); + cerr(": directory contains no WW package sources\n"); + rc = -1; + }; }; let i: i32 = 0; - for (i < n) { + for (i < g.pkg[pi].nsources) { 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); + rc = sepscanfile(g, pi, g.pkg[pi].sources[i], + searchpath, &fv, 1); }; i += 1; }; + if (rc == 0 && g.pkg[pi].path[0u64] != 0u8) { + let plen: u64 = cstrlen(g.pkg[pi].path); + let leaf: *u8 = g.pkg[pi].path; + let j: u64 = 0u64; + for (j < plen) { + if (g.pkg[pi].path[j] == '.') { leaf = g.pkg[pi].path + j + 1u64; }; + j += 1u64; + }; + if (!cstreq(g.pkg[pi].name, leaf)) { + cerr("ww: package "); + cerr(pathstr(g.pkg[pi].name)); + cerr(" does not match import path "); + cerr(pathstr(g.pkg[pi].path)); + cerr("\n"); + rc = -1; + }; + }; } else { - rc = sepscanfile(g, pi, g.pkg[pi].entry, searchpath, &fv); + rc = sepscanfile(g, pi, g.pkg[pi].entry, searchpath, &fv, 0); }; 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; }; + if (seploadpkg(g, g.pkg[pi].deps[k], searchpath) < 0) { return -1; }; k += 1; }; return 0; @@ -840,7 +1068,7 @@ fn septopovisit(g: *sepgraph, pi: i32, order: []i32, no: *i32, if (g.pkg[pi].color == 1) { let j: i32 = 0; for (j < depth && stack[j] != pi) { j += 1; }; - cerr("ww --sep: dependency cycle: "); + cerr("ww: dependency cycle: "); let s: i32 = j; for (s < depth) { sepcyclenode(g.pkg[stack[s]].path); @@ -894,7 +1122,7 @@ fn sepemitbody(fd: i32, path: *u8, visit: *expctx, searchpath: *u8, modpath: *u8 let blen: u64; bufp, blen = slurp(path); if (bufp == nil) { - cerr("ww --sep: cannot read source\n"); + cerr("ww: cannot read source\n"); return; }; let i: u64 = 0u64; @@ -934,34 +1162,15 @@ fn sepemitbody(fd: i32, path: *u8, visit: *expctx, searchpath: *u8, modpath: *u8 os.writeall(fd, "\n".ptr, 1u64); }; -fn sepemitdirbody(fd: i32, dir: *u8, visit: *expctx, searchpath: *u8, modpath: *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, modpath); - 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. +// body under //ww:module-reset. Directory membership comes only from the +// package node loaded before planning. 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"); + cerr("ww: cannot open unit\n"); return -1; }; let inset: []u8 = alloc([], g.n: u64)!; @@ -979,7 +1188,7 @@ fn sepcomposeunit(g: *sepgraph, pi: i32, scratch: *u8, order: []i32, let wn: u64; wb, wn = slurp(wwi); if (wb == nil) { - cerr("ww --sep: missing wwi\n"); + cerr("ww: missing wwi\n"); os.close(u); return -1; }; @@ -998,7 +1207,12 @@ fn sepcomposeunit(g: *sepgraph, pi: i32, scratch: *u8, order: []i32, bv.dirs = searchpath; bv.visit = nil; if (g.pkg[pi].isdir != 0) { - sepemitdirbody(u, g.pkg[pi].entry, &bv, searchpath, g.pkg[pi].path); + let i: i32 = 0; + for (i < g.pkg[pi].nsources) { + sepemitbody(u, g.pkg[pi].sources[i], &bv, searchpath, + g.pkg[pi].path); + i += 1; + }; } else { sepemitbody(u, g.pkg[pi].entry, &bv, searchpath, g.pkg[pi].path); }; @@ -1013,20 +1227,19 @@ fn sepcomposeunit(g: *sepgraph, pi: i32, scratch: *u8, order: []i32, // just the global magic + one 60-byte member header + the `.o` bytes // (newline-padded to even). Zeroed mtime/uid/gid + fixed mode + a fixed // member name make the bytes a pure function of the `.o` content → -// cstage `.a` == wwstage `.a` (rule 10) and a stable md5 for the 5b -// cache key. +// cstage `.a` == wwstage `.a` (rule 10). fn archiveo(objpath: *u8, apath: *u8) i32 = { let objp: *u8; let objn: u64; objp, objn = slurp(objpath); if (objp == nil) { - cerr("ww --sep: cannot read object for archive\n"); + cerr("ww: cannot read object for archive\n"); return -1; }; let pad: u64 = 0u64; if ((objn & 1u64) != 0u64) { pad = 1u64; }; - // sizelint-ok: 8B ar(5) magic + 60B member header are FILE-FORMAT - // constants, not type sizes (CLAUDE.md rule 13 carve-out). + // ar(5) fixes the archive magic at 8 bytes and each serialized + // member header at 60 bytes. let total: u64 = 8u64 + 60u64 + objn + pad; let outs: []u8 = alloc([], total)!; let out: *u8 = outs.ptr; @@ -1070,7 +1283,7 @@ fn archiveo(objpath: *u8, apath: *u8) i32 = { let fd: i32 = os.open(pathstr(apath), os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 if (fd < 0) { - cerr("ww --sep: cannot open archive\n"); + cerr("ww: cannot open archive\n"); return -1; }; os.writeall(fd, out, total); @@ -1078,311 +1291,15 @@ fn archiveo(objpath: *u8, apath: *u8) i32 = { return 0; }; -// ---- 5b content-keyed package cache (#63) ---------------------------- -// -// Twin of cstage's pkgcache_root/md5_file/sep_manifest/cache_lookup/ -// cache_store (cmd/ww/main.c). A per-package cache under WW_PKGCACHE -// (default out/.pkgcache; gitignored, make clean wipes $(OUT)). Dev-inner- -// loop convenience only: every gate cold-compiles, so the cache changes no -// gate output. rule-10: md5 is NOT reimplemented here — both stages shell to -// the host md5sum, so a HIT's reused P.wwi/P.o stay byte-identical cs==ww. - -fn pkgcacheroot() *u8 = { - match (os.getenv("WW_PKGCACHE")) { - case let s: str => - if (s.len > 0) { - let buf: []u8 = alloc([], (s.len: u64) + 1u64)!; - let i: i32 = 0; - for (i < s.len) { buf[i] = s[i]; i += 1; }; - buf[s.len] = 0u8; - return buf.ptr; - }; - case void => void; - }; - let d: []u8 = alloc([], 64u64)!; - d.len = 64; - let o: u64 = strinto(d.ptr, 0u64, "out/.pkgcache"); - cstrseal(d.ptr, o); - return d.ptr; -}; - -fn pkgcachedir(cacheroot: *u8, g: *sepgraph, pi: i32) *u8 = { - if (g.pkg[pi].path[0u64] == 0u8) { - return joinpathlit(cacheroot, "__root"); - }; - return joinpathlit(cacheroot, pathstr(g.pkg[pi].path)); -}; - -// md5appendhex — host md5sum of `path`, captured via a shell redirect to -// `tmp` (cache is gate-cold → host-tool dep sanctioned, rule-7). Appends the -// hex digest to `dst` at `off`; returns the new offset, or 0 on failure -// (caller treats 0 as a non-cacheable miss). Mirrors cstage md5_file's -// copy-until-whitespace parse so the digest text is byte-identical. -fn md5appendhex(dst: *u8, off: u64, path: *u8, tmp: *u8) u64 = { - let cmd: []u8 = alloc([], 8192u64)!; - cmd.len = 8192; - let co: u64 = strinto(cmd.ptr, 0u64, "md5sum '"); - co = cstrinto(cmd.ptr, co, path); - co = strinto(cmd.ptr, co, "' > '"); - co = cstrinto(cmd.ptr, co, tmp); - co = strinto(cmd.ptr, co, "'"); - cstrseal(cmd.ptr, co); - let argv: []*u8 = alloc([], 4u64)!; - argv.len = 4; - argv[0] = "sh\0".ptr; - argv[1] = "-c\0".ptr; - argv[2] = cmd.ptr; - argv[3] = nil; - if (procrun("/bin/sh\0".ptr, argv.ptr) != 0) { return 0u64; }; - let (buf, n) = slurp(tmp); - if (buf == nil) { return 0u64; }; - let i: u64 = 0u64; - for (i < n) { - let c: u8 = buf[i]; - if (c == 32u8 || c == 9u8 || c == 10u8) { break; }; - dst[off + i] = c; - i += 1u64; - }; - if (i == 0u64) { return 0u64; }; - return off + i; -}; - -// sepmanifest — assemble package pi's content-key manifest (D4) into `out` -// as deterministic text (see cstage sep_manifest). Returns the manifest -// length, or 0 on any md5 failure (caller → cold compile). The md5 capture -// tmp lives in `scratch` (which exists), not the cache dir (created lazily). -fn sepmanifest(g: *sepgraph, pi: i32, scratch: *u8, c6: *u8, a6: *u8, - out: *u8) u64 = { - let tmp: *u8 = sepfname(g, pi, scratch, ".md5tmp"); - let off: u64 = strinto(out, 0u64, "src"); - if (g.pkg[pi].isdir != 0) { - let (names, n) = enumeratedir(g.pkg[pi].entry); - let i: i32 = 0; - for (i < n) { - let fp: []u8 = alloc([], os.PATH_MAX: u64)!; - fp.len = os.PATH_MAX; - let fo: u64 = cstrinto(fp.ptr, 0u64, g.pkg[pi].entry); - fo = byteinto(fp.ptr, fo, 47u8); // '/' - fo = cstrinto(fp.ptr, fo, names[i]); - cstrseal(fp.ptr, fo); - off = byteinto(out, off, 32u8); // ' ' - let no: u64 = md5appendhex(out, off, fp.ptr, tmp); - if (no == 0u64) { return 0u64; }; - off = no; - i += 1; - }; - } else { - off = byteinto(out, off, 32u8); - let no: u64 = md5appendhex(out, off, g.pkg[pi].entry, tmp); - if (no == 0u64) { return 0u64; }; - off = no; - }; - off = byteinto(out, off, 10u8); // '\n' - - // dep lines, sorted by dep path (insertion sort, mirrors enumeratedir). - let nd: i32 = g.pkg[pi].ndeps; - let idx: []i32 = alloc([], SEP_MAXPKG: u64)!; - idx.len = SEP_MAXPKG; - let i: i32 = 0; - for (i < nd) { idx[i] = g.pkg[pi].deps[i]; i += 1; }; - i = 1; - for (i < nd) { - let j: i32 = i; - for (j > 0) { - let a: *u8 = g.pkg[idx[j - 1]].path; - let b: *u8 = g.pkg[idx[j]].path; - if (bytecmp(a, cstrlen(a), b, cstrlen(b)) <= 0) { j = 0; } - else { - let t: i32 = idx[j]; - idx[j] = idx[j - 1]; - idx[j - 1] = t; - j -= 1; - }; - }; - i += 1; - }; - i = 0; - for (i < nd) { - let di: i32 = idx[i]; - let wwip: *u8 = sepfname(g, di, scratch, ".wwi"); - off = strinto(out, off, "dep "); - off = cstrinto(out, off, g.pkg[di].path); - off = byteinto(out, off, 32u8); - let no: u64 = md5appendhex(out, off, wwip, tmp); - if (no == 0u64) { return 0u64; }; - off = no; - off = byteinto(out, off, 10u8); - i += 1; - }; - - off = strinto(out, off, "w6c "); - let no: u64 = md5appendhex(out, off, c6, tmp); - if (no == 0u64) { return 0u64; }; - off = no; - off = byteinto(out, off, 10u8); - off = strinto(out, off, "w6a "); - no = md5appendhex(out, off, a6, tmp); - if (no == 0u64) { return 0u64; }; - off = no; - off = byteinto(out, off, 10u8); - off = strinto(out, off, "flags -c -I\n"); - return off; -}; - -// copyfile — byte-copy src→dst (TRUNC). Returns 0 on success, -1 on failure. -fn copyfile(src: *u8, dst: *u8) i32 = { - let (buf, n) = slurp(src); - if (buf == nil) { return -1; }; - let fd: i32 = os.open(pathstr(dst), - os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 - if (fd < 0) { return -1; }; - os.writeall(fd, buf, n); - os.close(fd); - return 0; -}; - -// cachefilesize — open+filesize+close; -1 on any error. A torn producer write -// (e.g. disk-full mid-copy) can leave a 0-byte P.wwi/P.o under a self-consistent -// key; size==0 is unambiguous poison (wwi_emit always writes >=1 line, a valid -// .o is never empty), so both cachelookup and cachestore reject it — self-healing -// (#10). Twin of cstage filenonempty. -fn cachefilesize(path: *u8) i64 = { - let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32); - if (fd < 0) { return -1i64; }; - let szr: (i64 | os.oserror) = os.filesize(fd); - os.close(fd); - match (szr) { - case let v: i64 => return v; - case let e: os.oserror => return -1i64; - }; -}; - -// cachelookup — HIT iff the manifest equals the stored P.key byte-for-byte -// AND both cached artifacts exist; on HIT copy them into the scratch -// wwi/objf so the producer loop can skip compose+w6c+w6a. -fn cachelookup(cacheroot: *u8, g: *sepgraph, pi: i32, manifest: *u8, - mlen: u64, wwi: *u8, objf: *u8) i32 = { - let dir: *u8 = pkgcachedir(cacheroot, g, pi); - let keyp: *u8 = joinpathlit(dir, "P.key"); - let cwwi: *u8 = joinpathlit(dir, "P.wwi"); - let cobj: *u8 = joinpathlit(dir, "P.o"); - let (stored, sn) = slurp(keyp); - if (stored == nil) { return 0; }; - if (sn != mlen) { return 0; }; - let i: u64 = 0u64; - for (i < mlen) { - if (stored[i] != manifest[i]) { return 0; }; - i += 1u64; - }; - if (os.access(pathstr(cwwi), 0i32) != 0) { return 0; }; - if (os.access(pathstr(cobj), 0i32) != 0) { return 0; }; - if (cachefilesize(cwwi) <= 0i64) { return 0; }; - if (cachefilesize(cobj) <= 0i64) { return 0; }; - if (copyfile(cwwi, wwi) != 0) { return 0; }; - if (copyfile(cobj, objf) != 0) { return 0; }; - return 1; -}; - -// cachetmp — build "/.tmp." (NUL-terminated) into a fresh -// buffer. #104: a per-pid unique same-dir temp for the atomic cachestore -// rename. pid is folded in decimal manually (no strconv import here, twin -// of makeruntmp). -fn cachetmp(dir: *u8, name: str, pid: i32) *u8 = { - let buf: []u8 = alloc([], (os.PATH_MAX: u64))!; - buf.len = os.PATH_MAX; - let off: u64 = cstrinto(buf.ptr, 0u64, dir); - off = byteinto(buf.ptr, off, 47u8); // '/' - off = strinto(buf.ptr, off, name); - off = strinto(buf.ptr, off, ".tmp."); - let dig: [16]u8; - let n: i32 = 0; - if (pid <= 0) { - dig[n] = 48u8; // '0' - n += 1; - } else { - let v: i32 = pid; - for (v > 0) { - dig[n] = ((v % 10) + 48): u8; - n += 1; - v = v / 10; - }; - }; - let k: i32 = n - 1; - for (k >= 0) { - buf.ptr[off] = dig[k]; - off += 1u64; - k -= 1; - }; - cstrseal(buf.ptr, off); - return buf.ptr; -}; - -// cachermtmp — remove the per-pid cachestore temps on a mid-store error so a -// failed store leaves no litter (#104). Twin of cstage cache_store's cleanup. -fn cachermtmp(a: *u8, b: *u8, c: *u8) void = { - os.remove(pathstr(a)); - os.remove(pathstr(b)); - os.remove(pathstr(c)); -}; - -// cachestore — on MISS persist the artifacts then the key. Each is copied/ -// written to a per-pid same-dir temp then os.rename'd into place: rename is -// atomic within one filesystem (cross-fs is not), so a concurrent cachelookup -// never observes a half-written P.wwi/P.o/P.key (#104). The per-pid temp name -// keeps two concurrent writers from clobbering mid-copy; content-keyed ⇒ -// last-writer-wins is byte-identical. The key is renamed LAST so a reader that -// sees the new key always finds complete artifacts, and a crash mid-store -// never leaves a key without them. Twin of cstage cache_store. -// -// The torn-read race is thus closed BY CONSTRUCTION. A deterministic -// behavioral regression-guard isn't feasible through the product build path: -// content-keying means concurrent COLD builds all MISS at lookup and STORE — -// none HIT-reads a mid-store entry — and a warm cache is never re-stored, so -// "store concurrent with a HIT-read of the same entry" can't be forced. The -// deferred white-box guard is TASK #105; 989_pkgcache_concurrent_run smokes -// that concurrent shared-cache builds stay correct. On any mid-store error the -// per-pid temps are removed so a failed store leaves no litter. -fn cachestore(cacheroot: *u8, g: *sepgraph, pi: i32, manifest: *u8, - mlen: u64, wwi: *u8, objf: *u8) void = { - let dir: *u8 = pkgcachedir(cacheroot, g, pi); - match (os.mkdirs(pathstr(dir), 493i32)) { // 0o755 - case void => void; - case let e: os.oserror => void; // best-effort; copyfile surfaces a real failure - }; - let cwwi: *u8 = joinpathlit(dir, "P.wwi"); - let cobj: *u8 = joinpathlit(dir, "P.o"); - let keyp: *u8 = joinpathlit(dir, "P.key"); - let pid: i32 = os.getpid(); - let twwi: *u8 = cachetmp(dir, "P.wwi", pid); - let tobj: *u8 = cachetmp(dir, "P.o", pid); - let tkey: *u8 = cachetmp(dir, "P.key", pid); - if (copyfile(wwi, twwi) != 0) { cachermtmp(twwi, tobj, tkey); return; }; - if (copyfile(objf, tobj) != 0) { cachermtmp(twwi, tobj, tkey); return; }; - if (cachefilesize(twwi) <= 0i64) { cachermtmp(twwi, tobj, tkey); return; }; - if (cachefilesize(tobj) <= 0i64) { cachermtmp(twwi, tobj, tkey); return; }; - let fd: i32 = os.open(pathstr(tkey), - os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644 - if (fd < 0) { cachermtmp(twwi, tobj, tkey); return; }; - os.writeall(fd, manifest, mlen); - os.close(fd); - if (os.rename(pathstr(twwi), pathstr(cwwi)) != 0) { - cachermtmp(twwi, tobj, tkey); return; - }; - if (os.rename(pathstr(tobj), pathstr(cobj)) != 0) { - cachermtmp(twwi, tobj, tkey); return; - }; - if (os.rename(pathstr(tkey), pathstr(keyp)) != 0) { - cachermtmp(twwi, tobj, tkey); return; - }; -}; - -// buildonesep — the --sep orchestration: discover deps, reverse-topo, +// buildonesep — discover deps, reverse-topo, // the transitive producer loop (one `w6c -c -I` per package, dep-first, -// each `.o` wrapped in its own deterministic per-package `.a`), then a -// reverse-topo `w6l` of the `.a` set + libwwrt.a. Side files land in a -// cold `.sepwork` scratch dir. Twin of cstage build_one_sep. +// each dependency `.o` wrapped in its own deterministic per-package `.a`), then a +// reverse-topo `w6l` of the root `.o` + dependency `.a` set + libwwrt.a. +// Side files land in a cold `.sepwork` scratch dir. Twin of cstage +// build_one_sep. fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, - objstem: *u8, incs: *u8, lf: *lflags, istest: i32, scratchout: **u8) i32 = { + objstem: *u8, incs: *u8, lf: *lflags, istest: i32, emitasm: i32, + scratchout: **u8, graphout: **sepgraph) i32 = { let c6: *u8 = joinpathlit(selfdir, "w6c_ww"); let a6: *u8 = joinpathlit(selfdir, "w6a_ww"); let l6: *u8 = joinpathlit(selfdir, "w6l_ww"); @@ -1458,13 +1375,12 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, 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) - // #59: hand the scratch path back so the buildonesep wrapper can rm it - // on run/test (keepscratch 0). Set AFTER mkdir so we only remove a dir - // we created. + if (os.mkdir(pathstr(scratch), 493i32) != 0) { + cerr("ww: cannot create scratch\n"); + return 1; + }; + // Hand the path back only after mkdir succeeds, so the wrapper never + // removes a pre-existing path that this invocation failed to acquire. if (scratchout != nil) { *scratchout = scratch; }; // libwwrt.a path: /../lib/libwwrt.a @@ -1480,11 +1396,12 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, let pkgslot: []seppkg = alloc([], SEP_MAXPKG: u64)!; pkgslot.len = SEP_MAXPKG; let g: *sepgraph = alloc(sepgraph{pkg = pkgslot, n = 0})!; + if (graphout != nil) { *graphout = g; }; let root: i32 = sepfindoradd(g, "\0".ptr, src, entryisdir); if (root < 0) { return 1; }; // #79 (-T): lib/test is the synth main's `test.run` callee but @test // files never `import test;`. Inject it as a direct dep of the root so - // sepscanpkg pulls test + its transitive deps; the producer adds -T to + // seploadpkg pulls test + its transitive deps; the producer adds -T to // the root and `test.run` links against test's `.a` — via the // sepscanfile dedup-guarded dep append. if (istest != 0) { @@ -1507,7 +1424,7 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, }; }; }; - if (sepscanpkg(g, root, searchpath.ptr) < 0) { return 1; }; + if (seploadpkg(g, root, searchpath.ptr) < 0) { return 1; }; // Reset colors, reverse-topo. let ci: i32 = 0; @@ -1520,7 +1437,6 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, if (septopovisit(g, root, order, &norder, stack, 0) < 0) { return 1; }; // Producer loop — dep-first, one `w6c -c -I` per package. - let cacheroot: *u8 = pkgcacheroot(); let oi: i32 = 0; for (oi < norder) { let pi: i32 = order[oi]; @@ -1528,21 +1444,6 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, let wwi: *u8 = sepfname(g, pi, scratch, ".wwi"); let asmf: *u8 = sepfname(g, pi, scratch, ".s"); let objf: *u8 = sepfname(g, pi, scratch, ".o"); - // 5b: skip compose+w6c+w6a on a content-key HIT. The root is - // never cached — it is the build target, always recompiled. - let manbuf: []u8 = alloc([], 16384u64)!; - manbuf.len = 16384; - let mlen: u64 = 0u64; - let cacheable: i32 = 0; - if (pi != root) { - mlen = sepmanifest(g, pi, scratch, c6, a6, manbuf.ptr); - if (mlen > 0u64) { cacheable = 1; }; - }; - let fresh: i32 = 0; - if (cacheable != 0) { - fresh = cachelookup(cacheroot, g, pi, manbuf.ptr, mlen, wwi, objf); - }; - if (fresh == 0) { if (sepcomposeunit(g, pi, scratch, order, norder, searchpath.ptr, unitf) < 0) { return 1; }; @@ -1554,67 +1455,75 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, // LOCAL type (the root is never imported), which the // export-check rejects. Build a shorter root argv // without the -I/wwi pair; root's `.wwi` is unconsumed. - // #79: the root carries -T under `ww test --sep` so w6c + // #79: the root carries -T under `ww test` so w6c // synthesizes the test main; deps never get -T. let roott: bool = (pi == root) && (istest != 0); let alen: u64 = 8u64; if (pi == root) { alen = 6u64; if (roott) { alen = 7u64; }; }; - let argv: []*u8 = alloc([], alen)!; - argv.len = (alen: i32); - argv[0] = "w6c\0".ptr; - let k: u64 = 1u64; - if (roott) { argv[k] = "-T\0".ptr; k += 1u64; }; - argv[k] = "-c\0".ptr; k += 1u64; + let argv: []str = alloc([], alen)!; + append(argv, "w6c"); + if (roott) { append(argv, "-T"); }; + append(argv, "-c"); if (pi != root) { - argv[k] = "-I\0".ptr; k += 1u64; - argv[k] = wwi; k += 1u64; + append(argv, "-I"); + append(argv, pathstr(wwi)); }; - argv[k] = "-o\0".ptr; k += 1u64; - argv[k] = asmf; k += 1u64; - argv[k] = unitf; k += 1u64; - argv[k] = nil; - if (procrun(c6, argv.ptr) != 0) { - cerr("ww --sep: w6c failed\n"); + append(argv, "-o"); + append(argv, pathstr(asmf)); + append(argv, pathstr(unitf)); + let env: []str = os.getenvs(); + let result: exec.result; + exec.runstdio(pathstr(c6), argv, env, &result); + if (result.termination != exec.termination.EXIT + || result.code != 0) { + if (result.termination == exec.termination.ERROR + && result.code == 127) { + cerr("ww: execve failed\n"); + }; + cerr("ww: 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"); + if (emitasm == 0) { + let argv: []str = alloc([], 4u64)!; + append(argv, "w6a"); + append(argv, "-o"); + append(argv, pathstr(objf)); + append(argv, pathstr(asmf)); + let env: []str = os.getenvs(); + let result: exec.result; + exec.runstdio(pathstr(a6), argv, env, &result); + if (result.termination != exec.termination.EXIT + || result.code != 0) { + if (result.termination == exec.termination.ERROR + && result.code == 127) { + cerr("ww: execve failed\n"); + }; + cerr("ww: w6a failed\n"); return 1; }; }; - if (cacheable != 0) { - cachestore(cacheroot, g, pi, manbuf.ptr, mlen, wwi, objf); - }; - }; // Wrap each DEP package's `.o` in its own deterministic `.a` // (5a). The ROOT stays a positional `.o` (force-loaded — it's // the build target), so `main` is defined before any archive is // processed. The link // consumes `.o`/`.a`, never `.wwi`. - if (pi != root) { + if (emitasm == 0 && pi != root) { let apath: *u8 = sepfname(g, pi, scratch, ".a"); if (archiveo(objf, apath) != 0) { - cerr("ww --sep: archive failed\n"); + cerr("ww: archive failed\n"); return 1; }; }; oi += 1; }; + if (emitasm != 0) { return 0; }; - // Reverse-topo link of the per-package `.a` set: root.a first - // (order[norder-1]), deps after, then libwwrt.a (which still + // Reverse-topo link: root `.o` first (order[norder-1]), dependency `.a` + // files after, then libwwrt.a (which still // selectively pulls only the runtime members a live undef needs). - // argv: 3 fixed (w6l,-o,out) + 1 per .a + 1 libwwrt + 2*nlibdirs - // + 2*nlibs + 1 nil. + // argv: 3 fixed (w6l,-o,out) + one root object/archive per package + // + 1 libwwrt + 2*nlibdirs + 2*nlibs + 1 nil. let nldirs: i32 = 0; let nllibs: i32 = 0; let ldirs: **u8 = nil; @@ -1657,41 +1566,59 @@ fn buildonesepimpl(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, k += 1; }; largv[pos] = nil; - if (procrun(l6, largv.ptr) != 0) { - cerr("ww --sep: w6l failed\n"); + let linkargs: []str = alloc([], pos: u64)!; + let ai: i32 = 0; + for (ai < pos) { + append(linkargs, pathstr(largv[ai])); + ai += 1; + }; + let linkenv: []str = os.getenvs(); + let linkresult: exec.result; + exec.runstdio(pathstr(l6), linkargs, linkenv, &linkresult); + if (linkresult.termination != exec.termination.EXIT + || linkresult.code != 0) { + if (linkresult.termination == exec.termination.ERROR + && linkresult.code == 127) { + cerr("ww: execve failed\n"); + }; + cerr("ww: w6l failed\n"); return 1; }; return 0; }; -// buildonesep — wrapper over buildonesepimpl that rm -rf's the per-build -// `.sepwork` scratch when keepscratch==0 (ww run / ww test — the -// binary is the only wanted artifact). dobuild passes keepscratch 1: the -// byte-id gates read `.sepwork/*.s` from the `ww build -o` path, so -// build scratch must persist. One cleanup site covers every impl return. -// Guard: only rm a path the impl wrote that ends ".sepwork". Shells the -// existing /bin/sh idiom (md5appendhex precedent; no os.removeall yet — -// #109). Twin of cstage build_one_sep wrapper. +// buildonesep — `ww build` and an explicit `ww test -o` retain caller-visible +// `.sepwork` artifacts; their caller owns that exact tree. `ww run` and a +// no-output single-file test remove internal scratch on success and failure. +// The path is non-nil only after this invocation successfully created the +// exact tree. Twin of the cstage build_one_sep wrapper. fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32, out: *u8, - objstem: *u8, incs: *u8, lf: *lflags, istest: i32, keepscratch: i32) i32 = { + objstem: *u8, incs: *u8, lf: *lflags, istest: i32, emitasm: i32, + keepscratch: i32) i32 = { let scratch: *u8 = nil; + let g: *sepgraph = nil; let r: i32 = buildonesepimpl(selfdir, src, entryisdir, out, objstem, - incs, lf, istest, &scratch); + incs, lf, istest, emitasm, &scratch, &g); + sepgraphfree(g); if (keepscratch == 0 && scratch != nil) { if (cstrendswithlit(scratch, ".sepwork")) { - let cmd: []u8 = alloc([], 8192u64)!; - cmd.len = 8192; - let co: u64 = strinto(cmd.ptr, 0u64, "rm -rf '"); - co = cstrinto(cmd.ptr, co, scratch); - co = strinto(cmd.ptr, co, "'"); - cstrseal(cmd.ptr, co); - let argv: []*u8 = alloc([], 4u64)!; - argv.len = 4; - argv[0] = "sh\0".ptr; - argv[1] = "-c\0".ptr; - argv[2] = cmd.ptr; - argv[3] = nil; - procrun("/bin/sh\0".ptr, argv.ptr); + let argv: []str = alloc([], 4u64)!; + append(argv, "rm"); + append(argv, "-rf"); + append(argv, "--"); + append(argv, pathstr(scratch)); + let env: []str = os.getenvs(); + let result: exec.result; + exec.runstdio("/bin/rm", argv, env, &result); + if (result.termination != exec.termination.EXIT + || result.code != 0) { + if (result.termination == exec.termination.ERROR + && result.code == 127) { + cerr("ww: execve failed\n"); + }; + cerr("ww: cannot remove scratch\n"); + if (r == 0) { r = 1; }; + }; }; }; return r; @@ -1792,7 +1719,7 @@ fn resolvemodule(selfdir: *u8, name: *u8, incs: *u8, isdir: *i32) *u8 = { // ---- Subcommand handlers ---------------------------------------------- fn writeusage(fd: i32) void = { - let s: str = "usage: ww [-V] [args...]\n -V print version and exit\n build [path] compile module to a static binary (path defaults to cwd)\n run [path] ... build then exec, passing extra args to the program\n test [path] build and run *_test.ww in the module (path defaults to cwd)\n version print version and exit\n\n path forms:\n foo.ww literal file\n foo search cwd, -I dirs, then $WW_LIB-equiv for foo.ww or foo/foo.ww\n lib/foo directory: build lib/foo/foo.ww\n . build the cwd's .ww\n"; + let s: str = "usage: ww [-V] [args...]\n -V print version and exit\n build [-S] [-o FILE] [path] compile module; -S stops after package asm\n run [path] ... build then exec, passing extra args to the program\n test [-S -o STEM] [options] [path] build/run tests; -S emits package asm\n version print version and exit\n\n path forms:\n foo.ww literal file\n foo search cwd, -I dirs, then $WW_LIB-equiv for foo.ww or foo/foo.ww\n lib/foo directory: build lib/foo/foo.ww\n . build the cwd's .ww\n"; os.write(fd, s.ptr, s.len: u64); }; @@ -1837,6 +1764,7 @@ fn defaultoutpath(src: *u8) *u8 = { fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { let src: *u8 = nil; let outflag: *u8 = nil; // -o target (binary + intermediate stem); T3 + let emitasm: i32 = 0; let incs: []u8 = alloc([], (os.PATH_MAX: u64) * 2u64)!; incs.len = ((os.PATH_MAX: u64) * 2u64): i32; let incoff: u64 = 0u64; @@ -1854,7 +1782,8 @@ 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")) { // E3-C1: sep is sole path; accepted no-op (#87) + if (cstreqlit(p, "-S")) { + emitasm = 1; } else { if (p[1u64] == 73u8) { // '-I' let dir: *u8 = nil; if (p[2u64] != 0u8) { @@ -1970,16 +1899,21 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { lf.nlibdirs = nlibdirs; lf.libs = libs.ptr; lf.nlibs = nlibs; - return buildonesep(selfdir, resolved, isdir, out, objstem, incs.ptr, &lf, 0i32, - 1i32 /* keepscratch: gates read build -o .sepwork */); + return buildonesep(selfdir, resolved, isdir, out, objstem, incs.ptr, &lf, + 0i32, emitasm, 1i32); }; -// Format the scratch path /tmp/ww_run_ into buf. Returns NUL- -// terminated buf. Pid is folded in decimal manually since we don't -// import strconv. -fn makeruntmp(buf: *u8) void = { +// Format the owned driver workspace /tmp/ into buf. Pid is +// folded in decimal manually since this driver does not import strconv. +fn makedrivertmp(buf: *u8, prefix: str) void = { let off: u64 = 0u64; - off = strinto(buf, off, "/tmp/ww_run_"); + off = strinto(buf, off, "/tmp/"); + let pk: i32 = 0; + for (pk < prefix.len) { + buf[off] = prefix[pk]; + off += 1u64; + pk += 1; + }; let pid: i32 = os.getpid(); // itoa for non-negative pid let dig: [16]u8; @@ -2026,8 +1960,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")) { // E3-C1: sep is sole path; accepted no-op (#87) - } else { if (p[1u64] == 73u8) { + if (p[1u64] == 73u8) { let dir: *u8 = nil; if (p[2u64] != 0u8) { dir = p + 2u64; @@ -2094,7 +2027,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) { @@ -2120,61 +2053,92 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { let tmp: []u8 = alloc([], (os.PATH_MAX: u64))!; tmp.len = os.PATH_MAX; - makeruntmp(tmp.ptr); + makedrivertmp(tmp.ptr, "ww_run_"); + if (os.mkdir(pathstr(tmp.ptr), 448i32) != 0) { + cerr("ww: cannot create temporary directory\n"); + return 1; + }; + let outp: *u8 = joinpathlit(tmp.ptr, "main"); let lf: lflags; lf.libdirs = libdirs.ptr; lf.nlibdirs = nlibdirs; lf.libs = libs.ptr; lf.nlibs = nlibs; - // objstem = tmp → intermediates under /tmp/ww_run_.sepwork/, - // never next to the source (T3). - if (buildonesep(selfdir, resolved, isdir, tmp.ptr, tmp.ptr, incs.ptr, &lf, 0i32, - 0i32 /* keepscratch: throwaway run scratch */) != 0) { - os.remove(pathstr(tmp.ptr)); + // The freshly acquired directory owns both main and main.sepwork. + if (buildonesep(selfdir, resolved, isdir, outp, outp, incs.ptr, &lf, + 0i32, 0i32, 0i32) != 0) { + let cleanrc: i32 = os.remove(pathstr(outp)); + if (cleanrc != 0 && cleanrc != -2i32) { + cerr("ww: cannot remove temporary output\n"); + }; + if (os.rmdir(pathstr(tmp.ptr)) != 0) { + cerr("ww: cannot remove temporary directory\n"); + }; return 1; }; - // exec with [tmp, argv[passstart..argc), nil] + // Execute with [tmp, argv[passstart..argc)). The standard process + // facility inherits stdio and waits only for this user program. let nextra: i32 = 0; if (passstart >= 0) { nextra = argc - passstart; }; - let total: i32 = nextra + 2; - let execargv: []*u8 = alloc([], total: u64)!; - execargv.len = total; - execargv[0] = tmp.ptr; + let execargv: []str = alloc([], (nextra + 1): u64)!; + append(execargv, pathstr(outp)); let k: i32 = 0; for (k < nextra) { - execargv[k + 1] = argv[passstart + k]; + append(execargv, pathstr(argv[passstart + k])); k += 1; }; - execargv[nextra + 1] = nil; - let rc: i32 = procrun(tmp.ptr, execargv.ptr); - os.remove(pathstr(tmp.ptr)); + let env: []str = os.getenvs(); + let result: exec.result; + exec.runstdio(pathstr(outp), execargv, env, &result); + let rc: i32 = 1; + if (result.termination == exec.termination.EXIT) { + rc = result.code; + } else { if (result.termination == exec.termination.ERROR) { + if (result.code == 127) { + cerr("ww: execve failed\n"); + rc = 127; + } else { + cerr("ww: process launch/wait failed\n"); + rc = -1; + }; + }; }; + let cleanrc: i32 = os.remove(pathstr(outp)); + if (cleanrc != 0 && cleanrc != -2i32) { + cerr("ww: cannot remove temporary output\n"); + if (rc == 0) { rc = 1; }; + }; + if (os.rmdir(pathstr(tmp.ptr)) != 0) { + cerr("ww: cannot remove temporary directory\n"); + if (rc == 0) { rc = 1; }; + }; return rc; }; // ---- ww test ---------------------------------------------------------- // -// Mirrors cmd/ww/main.c:dotest. Two modes: -// single-file: build+run a literal *.ww file, return its exit code -// directory: open the dir, getdents64, build+run each *_test.ww, -// report ok/FAIL per file, return 0 iff all pass. +// Mirrors cmd/ww/main.c:dotest. Explicit regular files retain the bootstrap +// compatibility route; directory/default requests delegate to wwtest. -fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, outstem: *u8, pattern: *u8) i32 = { +fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, + emitasm: i32, outstem: *u8, pattern: *u8) i32 = { let tmp: []u8 = alloc([], (os.PATH_MAX: u64))!; tmp.len = os.PATH_MAX; - // -o redirects the binary + its sepwork intermediates (objstem, T3) to - // ; the default temp keeps them next to the source as before. + // -o redirects the binary + its caller-owned sepwork intermediates + // (objstem, T3) to ; without -o both are driver-owned /tmp paths. let outp: *u8 = nil; let objstem: *u8 = nil; if (outstem != nil) { outp = outstem; objstem = outstem; } else { - makeruntmp(tmp.ptr); - outp = tmp.ptr; - // #59: scratch under /tmp (cleaned by keepscratch 0), not next to - // the source. - objstem = tmp.ptr; + makedrivertmp(tmp.ptr, "ww_test_"); + if (os.mkdir(pathstr(tmp.ptr), 448i32) != 0) { + cerr("ww: cannot create temporary directory\n"); + return 1; + }; + outp = joinpathlit(tmp.ptr, "main"); + objstem = outp; }; // E3-C1: separate compilation is the sole build path (task #87). let lf: lflags; @@ -2182,120 +2146,72 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32, outstem: * lf.nlibdirs = 0; lf.libs = nil; lf.nlibs = 0; - // #59: -o → keep artifacts (mirror dobuild); no-o → throwaway, clean. let keep: i32 = 0; if (outstem != nil) { keep = 1; }; - let bres: i32 = buildonesep(selfdir, src, 0, outp, objstem, incs, &lf, 1i32, keep); + let bres: i32 = buildonesep(selfdir, src, 0, outp, objstem, incs, &lf, + 1i32, emitasm, keep); if (bres != 0) { - if (outstem == nil) { os.remove(pathstr(outp)); }; - return 1; - }; - if (compileonly != 0) { return 0; }; - // #17 fnmatch filter: forward `pattern` as argv[1] so lib/test run() - // reads it via os.args. procrun execve's a NUL-terminated argv, so the - // terminator (not .len) bounds the vector. cstage twin: run_test_bin. - let execargv: []*u8 = alloc([], 3u64)!; - execargv[0] = outp; - if (pattern != nil) { - execargv.len = 3; - execargv[1] = pattern; - execargv[2] = nil; - } else { - execargv.len = 2; - execargv[1] = nil; - }; - let rc: i32 = procrun(outp, execargv.ptr); - if (outstem == nil) { os.remove(pathstr(outp)); }; - return rc; -}; - -fn rundirtests(selfdir: *u8, dir: *u8) i32 = { - let fd: i32 = os.open(pathstr(dir), os.flag.RDONLY, 0i32); - if (fd < 0) { - cerr("ww test: cannot open directory\n"); - return 1; - }; - - let pass: i32 = 0; - let fail: i32 = 0; - let seen: i32 = 0; // #64: count *_test.ww matches for the zero-test gate - let buf: []u8 = alloc([], 8192u64)!; - buf.len = 8192; - let dirlen: u64 = cstrlen(dir); - - let n: i64 = os.getdents64(fd, buf.ptr, 8192u64); - for (n > 0i64) { - let off: u64 = 0u64; - let nu: u64 = n: u64; - for (off < nu) { - // d_reclen at offset+16 (u16 LE), d_name at offset+19 (cstr) - let blo: u64 = (buf[off + 16u64]): u64; - let bhi: u64 = (buf[off + 17u64]): u64; - let reclen: u64 = blo + (bhi * 256u64); - let name: *u8 = buf.ptr + off + 19u64; - if (cstrendswithlit(name, "_test.ww")) { - seen += 1; - let nlen: u64 = cstrlen(name); - // path = / - let path: []u8 = alloc([], (os.PATH_MAX: u64))!; - path.len = os.PATH_MAX; - let poff: u64 = cstrinto(path.ptr, 0u64, dir); - path[poff] = 47u8; poff += 1u64; - let i: u64 = 0u64; - for (i < nlen) { path[poff + i] = name[i]; i += 1u64; }; - poff += nlen; - cstrseal(path.ptr, poff); - // incs = so test files can `use ` siblings - let tincs: []u8 = alloc([], (os.PATH_MAX: u64))!; - tincs.len = os.PATH_MAX; - let ic: u64 = cstrinto(tincs.ptr, 0u64, dir); - cstrseal(tincs.ptr, ic); - let tmp: []u8 = alloc([], (os.PATH_MAX: u64))!; - tmp.len = os.PATH_MAX; - makeruntmp(tmp.ptr); - // #59: objstem=tmp → scratch in /tmp (cleaned), keepscratch 0. - let bres: i32 = buildonesep(selfdir, path.ptr, 0, tmp.ptr, tmp.ptr, tincs.ptr, nil, 1i32, 0i32); - if (bres != 0) { - fail += 1; - cerr("FAIL "); - os.write(2, name, nlen); - cerr(" (build)\n"); - } else { - let execargv: []*u8 = alloc([], 2u64)!; - execargv.len = 2; - execargv[0] = tmp.ptr; - execargv[1] = nil; - let rc: i32 = procrun(tmp.ptr, execargv.ptr); - if (rc == 0) { - pass += 1; - os.write(1, "ok ".ptr, 5u64); - os.write(1, name, nlen); - os.write(1, "\n".ptr, 1u64); - } else { - fail += 1; - cerr("FAIL "); - os.write(2, name, nlen); - cerr("\n"); - }; - }; - os.remove(pathstr(tmp.ptr)); + if (outstem == nil) { + let cleanrc: i32 = os.remove(pathstr(outp)); + if (cleanrc != 0 && cleanrc != -2i32) { + cerr("ww: cannot remove temporary output\n"); + }; + if (os.rmdir(pathstr(tmp.ptr)) != 0) { + cerr("ww: cannot remove temporary directory\n"); }; - off += reclen; }; - n = os.getdents64(fd, buf.ptr, 8192u64); - }; - os.close(fd); - // #64: a directory with no *_test.ww files is a loud failure, not a - // silent rc=0 false-green. Mirrors cstage do_test (cmd/ww/main.c:945) - // "ww test: no *_test.ww files in %s". - if (seen == 0) { - cerr("ww test: no *_test.ww files in "); - os.write(2, dir, dirlen); - cerr("\n"); return 1; }; - if (fail == 0) { return 0; }; - return 1; + if (compileonly != 0 || emitasm != 0) { + if (outstem == nil) { + let cleanbad: bool = false; + let cleanrc: i32 = os.remove(pathstr(outp)); + if (cleanrc != 0 && cleanrc != -2i32) { + cerr("ww: cannot remove temporary output\n"); + cleanbad = true; + }; + if (os.rmdir(pathstr(tmp.ptr)) != 0) { + cerr("ww: cannot remove temporary directory\n"); + cleanbad = true; + }; + if (cleanbad) { return 1; }; + }; + return 0; + }; + // #17 fnmatch filter: forward `pattern` as argv[1] so lib/test run() + // reads it via os.args. cstage twin: run_test_bin. + let execargv: []str = alloc([], 2u64)!; + append(execargv, pathstr(outp)); + if (pattern != nil) { + append(execargv, pathstr(pattern)); + }; + let env: []str = os.getenvs(); + let result: exec.result; + exec.runstdio(pathstr(outp), execargv, env, &result); + let rc: i32 = 1; + if (result.termination == exec.termination.EXIT) { + rc = result.code; + } else { if (result.termination == exec.termination.ERROR) { + if (result.code == 127) { + cerr("ww: execve failed\n"); + rc = 127; + } else { + cerr("ww: process launch/wait failed\n"); + rc = -1; + }; + }; }; + if (outstem == nil) { + let cleanrc: i32 = os.remove(pathstr(outp)); + if (cleanrc != 0 && cleanrc != -2i32) { + cerr("ww: cannot remove temporary output\n"); + if (rc == 0) { rc = 1; }; + }; + if (os.rmdir(pathstr(tmp.ptr)) != 0) { + cerr("ww: cannot remove temporary directory\n"); + if (rc == 0) { rc = 1; }; + }; + }; + return rc; }; fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { @@ -2303,6 +2219,7 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { // transitive imports (e.g. 905_nkname asttest → tok); coupled to the // -T flip (task #5/#10). let target: *u8 = nil; + let targetindex: i32 = -1; // #17: optional 2nd positional = fnmatch name-filter pattern, forwarded // to the test binary as argv[1] (single-file/module only; dir-mode // rejects). cstage twin: do_test `pattern`. @@ -2316,13 +2233,19 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { // inclusive sep unit WITHOUT running it, for the byte-id gates. cstage // twin: cmd/ww/main.c do_test (error wording identical). let compileonly: i32 = 0; + let emitasm: i32 = 0; let outstem: *u8 = nil; + let packageopts: bool = false; + let afterdash: bool = false; let i: i32 = start; for (i < argc) { let p: *u8 = argv[i]; + if (afterdash) { i += 1; continue; }; if (p[0u64] == 45u8) { // '-' - if (cstreqlit(p, "--sep")) { // E3-C1: sep is sole path; accepted no-op (#87) - } else { if (p[1u64] == 73u8) { // '-I' + if (cstreqlit(p, "--")) { + packageopts = true; afterdash = true; i += 1; continue; + }; + if (p[1u64] == 73u8) { // '-I' let dir: *u8 = nil; if (p[2u64] != 0u8) { dir = p + 2u64; @@ -2340,9 +2263,27 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { }; incoff = cstrinto(incs.ptr, incoff, dir); cstrseal(incs.ptr, incoff); - } else { if (p[1u64] == 99u8 && p[2u64] == 0u8) { // "-c" - compileonly = 1; - } else { if (p[1u64] == 111u8) { // '-o' + i += 1; continue; + }; + if (cstreqlit(p, "-c")) { compileonly = 1; i += 1; continue; }; + if (cstreqlit(p, "-S")) { emitasm = 1; i += 1; continue; }; + if (cstreqlit(p, "-list")) { + packageopts = true; i += 1; continue; + }; + if (cstreqlit(p, "-j") || cstreqlit(p, "-run") + || cstreqlit(p, "-filter")) { + if (i + 1 >= argc) { + cerr("ww test: "); os.write(2, p, cstrlen(p)); + cerr(" needs an argument\n"); return 2; + }; + packageopts = true; i += 2; continue; + }; + let ps: str = pathstr(p); + if (strings.hasprefix(ps, "-timeout-ms=") + && ps.len > 12) { + packageopts = true; i += 1; continue; + }; + if (p[1u64] == 111u8) { // '-o' if (p[2u64] != 0u8) { outstem = p + 2u64; } else { @@ -2353,12 +2294,11 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { i += 1; outstem = argv[i]; }; - } else { - cerr("ww test: unknown flag\n"); - return 2; - }; }; }; }; // #79: extra close for the --sep else + i += 1; continue; + }; + cerr("ww test: unknown flag\n"); return 2; } else { - if (target == nil) { target = p; } + if (target == nil) { target = p; targetindex = i; } else { if (patarg == nil) { patarg = p; }; }; }; i += 1; @@ -2367,26 +2307,51 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = { let dot: [2]u8 = ['.': u8, 0u8]; target = &dot[0]; }; - - // 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); - }; - }; - - if (compileonly != 0 || outstem != nil) { - cerr("ww test: -c/-o need a single test file\n"); + if (emitasm != 0 && outstem == nil) { + cerr("ww test: -S needs -o\n"); + return 2; + }; + + let resolved: *u8 = target; + let isdir: i32 = 0; + let found: bool = false; + let fi: os.filestat; + match (os.stat(&fi, pathstr(target))) { + case void => { + let t: u32 = (fi.mode: u32) & 61440u32; + if (t == os.mode.DIR: u32) { isdir = 1; found = true; } + else { if (t == os.mode.REG: u32) { found = true; }; }; + }; + case let e: os.oserror => void; + }; + if (!found) { + resolved = resolvemodule(selfdir, target, incs.ptr, &isdir); + if (resolved == nil) { + cerr("ww test: cannot find "); + os.write(2, target, cstrlen(target)); cerr("\n"); + return 1; + }; + }; + if (isdir == 0) { + if (packageopts) { + cerr("ww test: package options need a directory\n"); return 2; + }; + return runsingletest(selfdir, resolved, incs.ptr, compileonly, + emitasm, outstem, patarg); + }; + + if (outstem != nil) { + cerr("ww test: -c/-S/-o need 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) { cerr("ww test: pattern needs a single test file\n"); return 2; }; - // otherwise treat target as a directory; enumerate *_test.ww - return rundirtests(selfdir, target); + let replacement: *u8 = nil; + if (resolved != target) { replacement = resolved; }; + return execpackagetests(selfdir, argv, argc, start, targetindex, + replacement, targetindex < 0); }; // ---- Entry ------------------------------------------------------------- diff --git a/test/wcc/989_pkgcache_concurrent_run.c b/test/wcc/989_pkgcache_concurrent_run.c deleted file mode 100644 index 04df9bc1..00000000 --- a/test/wcc/989_pkgcache_concurrent_run.c +++ /dev/null @@ -1,227 +0,0 @@ -/* - * 989_pkgcache_concurrent_run — concurrent shared-cache build-correctness - * smoke (#104). N distinct root programs that all `import shared` are built - * CONCURRENTLY into ONE shared WW_PKGCACHE; every resulting binary must be - * byte-IDENTICAL to a reference built in ISOLATION (private cache) and run to - * its expected exit. This guards that concurrent `ww build --sep` sharing one - * out/.pkgcache produces correct, deterministic binaries — a regression guard - * for the cache subsystem under contention (store crash, lock bug, wrong-key - * copy, etc.). - * - * It does NOT prove the temp+rename store is atomic against torn reads: with - * content-keying every concurrent cold build MISSES at lookup and STORES (it - * never HIT-reads a mid-store entry), so the torn-read window is not forced - * here. That race is closed by construction at the cache_store fix site; the - * deferred white-box guard is TASK #105. - * - * Both driver stages (rule 10): the cs and ww references are byte-identical. - * Light wwstage-driver test (CLAUDE.md rule 14): all outputs + caches live - * under a private /tmp tree, so it is parallel-safe and off every byte-id / - * bootstrap gate. Models 989_pkgcache_run conventions. - */ -#include -#include -#include -#include -#include -#include - -#define NPROG 4 -#define BASE_EXIT 7 /* shared.v() == 7; rootI returns 7 + I */ - -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 -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 -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 const char *shared_src = - "package shared;\n" - "export fn v() i32 = { return 7; };\n"; - -int -main(void) -{ - const char *bin = absbin(); - if (!bin) return 1; - int fail = 0; - char td[64], cmd[8192], batch[32768]; - char sharedww[1024]; - - snprintf(td, sizeof td, "/tmp/wwconc_%d", getpid()); - snprintf(cmd, sizeof cmd, "rm -rf %s", td); - runwait(cmd); - snprintf(cmd, sizeof cmd, "mkdir -p %s/shared", td); - runwait(cmd); - - snprintf(sharedww, sizeof sharedww, "%s/shared/shared.ww", td); - if (write_file(sharedww, shared_src)) { fail++; goto out; } - - /* N distinct roots, each importing the one shared pkg. Distinct return - * (7 + I) so each program — and so each clean binary — is distinguishable, - * while the contended cache entry (shared) is common to all. */ - for (int i = 0; i < NPROG; i++) { - char rootww[1024], src[256]; - snprintf(rootww, sizeof rootww, "%s/root%d.ww", td, i); - snprintf(src, sizeof src, - "package main;\nimport shared;\n" - "fn main() i32 = { return shared.v() + %d; };\n", i); - if (write_file(rootww, src)) { fail++; goto out; } - } - - struct { const char *drv, *tag; } stg[] = { - { "ww", "cs" }, - { "ww_ww", "ww" }, - }; - - for (int s = 0; s < 2; s++) { - const char *drv = stg[s].drv, *tag = stg[s].tag; - char shcache[1024]; - snprintf(shcache, sizeof shcache, "%s/cache.%s", td, tag); - - /* References: each rootI built ISOLATED (private cache, no contention) - * = the clean baseline bytes a concurrent build must reproduce. */ - for (int i = 0; i < NPROG; i++) { - char refcache[1024], refprog[1024], refscr[1024], rootww[1024]; - snprintf(refcache, sizeof refcache, "%s/refc.%s.%d", td, tag, i); - snprintf(refprog, sizeof refprog, "%s/ref.%s.%d", td, tag, i); - snprintf(refscr, sizeof refscr, "%s/ref.%s.%d.sepwork", td, tag, i); - snprintf(rootww, sizeof rootww, "%s/root%d.ww", td, i); - snprintf(cmd, sizeof cmd, - "rm -rf %s %s; WW_PKGCACHE='%s' timeout 240 %s/%s build --sep " - "-o %s %s >/dev/null 2>&1", - refscr, refcache, refcache, bin, drv, refprog, rootww); - if (runwait(cmd) != 0) { - fprintf(stderr, "concur FAIL[%s]: reference build %d failed\n", - drv, i); - fail++; - } - if (runwait(refprog) != BASE_EXIT + i) { - fprintf(stderr, "concur FAIL[%s]: reference %d wrong exit\n", - drv, i); - fail++; - } - } - - /* Fresh shared cache so every build in the batch MISS-stores `shared` - * concurrently → write contention on shared/P.{wwi,o,key}. Distinct - * per-prog output so every binary can be verified, not just one. */ - snprintf(cmd, sizeof cmd, "rm -rf %s", shcache); - runwait(cmd); - - size_t off = 0; - off += (size_t)snprintf(batch + off, sizeof batch - off, - "export WW_PKGCACHE='%s'; ", shcache); - for (int i = 0; i < NPROG; i++) { - off += (size_t)snprintf(batch + off, sizeof batch - off, - "( rm -rf %s/c.%s.%d.sepwork; timeout 240 %s/%s build " - "--sep -o %s/c.%s.%d %s/root%d.ww >/dev/null 2>&1 ) & ", - td, tag, i, bin, drv, - td, tag, i, td, i); - } - off += (size_t)snprintf(batch + off, sizeof batch - off, "wait"); - if (off >= sizeof batch) { - fprintf(stderr, "concur FAIL: batch cmd truncated\n"); - fail++; goto out; - } - runwait(batch); - - for (int i = 0; i < NPROG; i++) { - char prog[1024], refprog[1024]; - snprintf(prog, sizeof prog, "%s/c.%s.%d", td, tag, i); - snprintf(refprog, sizeof refprog, "%s/ref.%s.%d", td, tag, i); - if (runwait(prog) != BASE_EXIT + i) { - fprintf(stderr, "concur FAIL[%s]: concurrent prog i=%d " - "wrong/failed exit\n", drv, i); - fail++; - } - if (files_eq(prog, refprog) != 0) { - fprintf(stderr, "concur FAIL[%s]: concurrent binary i=%d " - "!= isolated reference\n", drv, i); - fail++; - } - } - } - - /* rule 10: the cs and ww isolated references are byte-identical. */ - for (int i = 0; i < NPROG; i++) { - char a[1024], b[1024]; - snprintf(a, sizeof a, "%s/ref.cs.%d", td, i); - snprintf(b, sizeof b, "%s/ref.ww.%d", td, i); - if (files_eq(a, b) != 0) { - fprintf(stderr, "concur FAIL: cs != ww reference %d (rule 10)\n", i); - fail++; - } - } - -out: - snprintf(cmd, sizeof cmd, "rm -rf %s", td); - runwait(cmd); - if (fail) { - fprintf(stderr, "concur: %d check(s) failed\n", fail); - return 1; - } - printf("concur: %d concurrent --sep builds sharing one cache, both stages " - "— every binary byte-identical to its isolated reference + correct run; " - "cs==ww references (rule 10)\n", NPROG); - return 0; -} diff --git a/test/wcc/989_pkgcache_poison_run.c b/test/wcc/989_pkgcache_poison_run.c deleted file mode 100644 index cd3e8986..00000000 --- a/test/wcc/989_pkgcache_poison_run.c +++ /dev/null @@ -1,247 +0,0 @@ -/* - * 989_pkgcache_poison_run — out/.pkgcache 0-byte-artifact self-heal gate (#10, - * BUG-B). A torn producer write (e.g. disk-full mid-copy) can leave a 0-byte - * P.wwi/P.o under a SELF-CONSISTENT P.key. Pre-fix the cache served that empty - * artifact forever (the key matched and access() saw the file), so every later - * build link-failed or produced a wrong binary — a SILENT serve-wrong that - * never surfaces on its own. The fix rejects size==0 on BOTH the lookup (read) - * and the store (write) side: a poisoned entry self-heals (treated as MISS → - * re-derived), and a torn store never commits the key. The guard is symmetric - * cstage (filenonempty, stat) / wwstage (cachefilesize, open+filesize) — the - * hit/miss DECISION must match (rule 10). - * - * Target: a 2-level graph root(main) -> leaf in a private temp tree. ROOT is - * never cached (it is the build target); `leaf` is the cacheable package. We - * seed a warm cache, POISON the cached artifact to 0 bytes, rebuild, and assert - * the build self-heals: leaf re-compiles (MISS sentinel) AND the program runs - * to its correct value (not an empty/link-failed binary) AND the cache is - * re-stored non-empty. - * - * HIT/MISS sentinel (same as 989_pkgcache_run): a MISS runs w6c → writes - * /leaf.s; a HIT copies the cached .o/.wwi and skips compose+w6c+w6a, - * so no leaf.s. The scratch is wiped before each build so leaf.s presence - * reflects exactly the last build. - * - * Table-driven over WHICH artifact is poisoned: { P.wwi | P.o | both }. Each - * row, per stage (ww, ww_ww): - * - warm baseline: an unchanged rebuild HITs (leaf.s ABSENT, prog runs) — - * proves the cache is genuinely warm before we poison it (non-vacuity); - * - POISON the row's artifact(s) to 0 bytes; - * - heal rebuild: leaf re-compiles (leaf.s PRESENT = MISS, not served poison) - * AND prog runs == EXPECT_EXIT AND both cached artifacts are non-empty again - * (re-stored). Pre-fix this row served the 0-byte artifact and the assert - * on leaf.s (and on prog exit) fails. - * - * Light wwstage-driver test (CLAUDE.md rule 14): all build outputs and caches - * live under a private /tmp tree, wiped on exit, so it is parallel-safe and off - * every byte-id/bootstrap gate. Models 989_pkgcache_run conventions. - */ -#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 -write_file(const char *path, const char *body) -{ - FILE *f = fopen(path, "wb"); - if (!f) return -1; - fputs(body, f); - fclose(f); - return 0; -} - -/* Truncate `path` to 0 bytes (models a torn producer write under a valid key); - * "wb" opens-and-truncates, leaving the file present but empty. */ -static int -truncate0(const char *path) -{ - FILE *f = fopen(path, "wb"); - if (!f) return -1; - fclose(f); - return 0; -} - -static long -fsize(const char *path) -{ - struct stat st; - if (stat(path, &st) != 0) return -1; - return (long)st.st_size; -} - -/* /.s present ⇒ that package compiled this build (MISS). */ -static int -sdot(const char *scratch, const char *pkg) -{ - char p[1100]; - snprintf(p, sizeof p, "%s/%s.s", scratch, pkg); - return access(p, 0) == 0; -} - -static const char *leaf_src = - "package leaf;\n" - "export fn base() i32 = { return 7; };\n"; - -static const char *root_src = - "package main;\n" - "import leaf;\n" - "fn main() i32 = { return leaf.base(); };\n"; - -int -main(void) -{ - const char *bin = absbin(); - if (!bin) return 1; - int fail = 0; - char td[64], cmd[8192]; - char rootww[1024], leafww[1024]; - char scratch[1024], prog[1024]; - - snprintf(td, sizeof td, "/tmp/wwpkgpoison_%d", getpid()); - snprintf(cmd, sizeof cmd, "rm -rf %s", td); - runwait(cmd); - snprintf(cmd, sizeof cmd, "mkdir -p %s/leaf", td); - runwait(cmd); - - snprintf(rootww, sizeof rootww, "%s/root.ww", td); - snprintf(leafww, sizeof leafww, "%s/leaf/leaf.ww", td); - snprintf(prog, sizeof prog, "%s/p", td); - snprintf(scratch, sizeof scratch, "%s/p.sepwork", td); - - if (write_file(rootww, root_src) || write_file(leafww, leaf_src)) { - fail++; goto out; - } - - /* Which cached artifact(s) the row poisons. */ - struct { const char *tag; int wwi, obj; } rows[] = { - { "P.wwi", 1, 0 }, - { "P.o", 0, 1 }, - { "both", 1, 1 }, - }; - struct { const char *drv; } stg[] = { { "ww" }, { "ww_ww" } }; - - for (int s = 0; s < 2; s++) { - const char *drv = stg[s].drv; - char cache[1024], cwwi[1100], cobj[1100]; - snprintf(cache, sizeof cache, "%s/cache.%d", td, s); - snprintf(cwwi, sizeof cwwi, "%s/leaf/P.wwi", cache); - snprintf(cobj, sizeof cobj, "%s/leaf/P.o", cache); - - /* The cstage driver rm -rf's the scratch each build; the wwstage - * driver only mkdir's it (#58(a) asymmetry), so a stale leaf.s could - * persist and defeat the MISS/HIT sentinel. The gate owns its scratch: - * wipe it before every build so a fresh leaf.s reflects the last build. */ -#define BUILD() do { \ - snprintf(cmd, sizeof cmd, \ - "rm -rf %s; WW_PKGCACHE='%s' timeout 240 %s/%s build --sep " \ - "-o %s %s >/dev/null 2>&1", scratch, cache, bin, drv, prog, \ - rootww); \ - } while (0) - - /* fresh cache for this stage, cold-seed it (MISS) */ - snprintf(cmd, sizeof cmd, "rm -rf %s", cache); - runwait(cmd); - BUILD(); - if (runwait(cmd) != 0) { - fprintf(stderr, "poison FAIL[%s]: cold seed build failed\n", drv); - fail++; continue; - } - if (!sdot(scratch, "leaf")) { - fprintf(stderr, "poison FAIL[%s]: cold seed — leaf did not " - "compile (MISS expected)\n", drv); - fail++; - } - if (runwait(prog) != EXPECT_EXIT) { - fprintf(stderr, "poison FAIL[%s]: cold seed prog exit != %d\n", - drv, EXPECT_EXIT); - fail++; - } - - for (size_t r = 0; r < sizeof rows / sizeof rows[0]; r++) { - /* warm baseline — the cache is warm (heals re-store it), so an - * unchanged rebuild must HIT; proves we poison a LIVE entry. */ - BUILD(); runwait(cmd); - if (sdot(scratch, "leaf")) { - fprintf(stderr, "poison FAIL[%s/%s]: warm baseline — leaf " - "recompiled (cache not warm before poison)\n", - drv, rows[r].tag); - fail++; - } - - /* poison the row's artifact(s) to 0 bytes, key stays valid */ - if (rows[r].wwi && truncate0(cwwi) != 0) { - fprintf(stderr, "poison FAIL[%s/%s]: cannot truncate P.wwi\n", - drv, rows[r].tag); - fail++; - } - if (rows[r].obj && truncate0(cobj) != 0) { - fprintf(stderr, "poison FAIL[%s/%s]: cannot truncate P.o\n", - drv, rows[r].tag); - fail++; - } - - /* heal rebuild — a 0-byte artifact under a valid key must be - * treated as MISS (re-derived), NOT served. */ - BUILD(); runwait(cmd); - if (!sdot(scratch, "leaf")) { - fprintf(stderr, "poison FAIL[%s/%s]: served poison — leaf did " - "NOT re-derive after 0-byte artifact (silent serve-wrong)\n", - drv, rows[r].tag); - fail++; - } - if (runwait(prog) != EXPECT_EXIT) { - fprintf(stderr, "poison FAIL[%s/%s]: heal prog exit != %d " - "(empty/link-failed binary)\n", drv, rows[r].tag, EXPECT_EXIT); - fail++; - } - /* re-store must leave both artifacts non-empty (write guard let a - * good store through, AND a torn store never commits a 0-byte). */ - if (fsize(cwwi) <= 0 || fsize(cobj) <= 0) { - fprintf(stderr, "poison FAIL[%s/%s]: cached artifact still " - "0-byte after heal (re-store did not repair)\n", - drv, rows[r].tag); - fail++; - } - } -#undef BUILD - } - -out: - snprintf(cmd, sizeof cmd, "rm -rf %s", td); - runwait(cmd); - if (fail) { - fprintf(stderr, "poison: %d check(s) failed\n", fail); - return 1; - } - printf("poison: root->leaf via build_one_sep — a 0-byte cached P.wwi/P.o/both " - "under a valid key self-heals (treated as MISS, re-derived to correct " - "value, re-stored non-empty) — both stages\n"); - return 0; -} diff --git a/test/wcc/989_pkgcache_run.c b/test/wcc/989_pkgcache_run.c deleted file mode 100644 index 9d7a8843..00000000 --- a/test/wcc/989_pkgcache_run.c +++ /dev/null @@ -1,381 +0,0 @@ -/* - * 989_pkgcache_run — M3-tail commit-5b out/.pkgcache content-keyed package - * cache gate (#63). COLD, dev-only: the cache is keyed by content, never - * mtime, and every bootstrap/byte-id gate cold-compiles (the `--sep` scratch - * is wiped each run), so 5b changes NO gate output. This leg certifies the - * cache itself, through the REAL `ww` / `ww_ww` drivers. - * - * Target: a synthetic 3-level graph root(main) -> mid -> leaf in a private - * temp tree, so source bytes and the dep interface are fully under test - * control. The ROOT is never cached (it is the build target); `mid` and - * `leaf` are the cacheable packages. Each stage uses its OWN WW_PKGCACHE dir - * (the compiler-binary line of P.key is intentionally stage-specific — it - * keys CODEGEN identity — so cs and ww keep separate cache namespaces; the - * cacheable OUTPUTS they reuse stay byte-identical, asserted below). - * - * HIT/MISS sentinel: a cache MISS runs w6c, which writes /.s; - * a HIT copies the cached .o/.wwi and skips compose+w6c+w6a, so no .s - * is produced. The scratch is wiped each build, so .s presence reflects - * exactly the last build. - * - * Legs, per stage (ww, ww_ww): - * 1. MISS: a cold build compiles (leaf.s AND mid.s present; prog runs == 5). - * 2. HIT (+ non-vacuity baseline): an unchanged rebuild skips both - * (leaf.s AND mid.s ABSENT; prog still runs == 5 from cached artifacts). - * 3. BUST — source edit: perturb mid.ww -> mid MISS, leaf still HIT; - * restore + reseed -> HIT again. - * 4. BUST — dep .wwi change: change leaf's EXPORTED interface -> leaf's .wwi - * md5 changes -> the importer `mid` (own source unchanged) MISSES via its - * dep line; restore + reseed -> HIT again. - * 5. BUST — compiler binary md5: corrupt the stored mid/P.key `w6c` line -> - * the freshly recomputed manifest mismatches -> mid MISS; the rebuild - * rewrites a correct key -> HIT again. (Models a codegen-changed compiler; - * a real WW_W6C swap is cstage-only — the wwstage driver hardcodes its - * toolchain — so a white-box key-line edit proves the class on both.) - * 6. BUST — flag string: corrupt the stored mid/P.key `flags` line -> mid - * MISS; rebuild rewrites -> HIT again. - * - * Cross-stage (rule 10): the cached mid/leaf P.o AND P.wwi are byte-identical - * between the cs and ww caches. - * - * Light wwstage-driver test (CLAUDE.md rule 14): all build outputs and caches - * live under a private /tmp tree, so it is parallel-safe and off every - * byte-id/bootstrap gate. Models 989_sepbuild_run conventions. - */ -#include -#include -#include -#include -#include -#include - -#define EXPECT_EXIT 5 - -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 -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 -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; -} - -/* Flip one byte inside the line of `path` that starts with `prefix` (keeping - * the line length so the mutation isolates that one input class, not the - * blob length). Returns 0 on success. */ -static int -mutate_keyline(const char *path, const char *prefix) -{ - char *buf = NULL; - size_t n = 0; - if (slurp(path, &buf, &n) < 0) return -1; - size_t pl = strlen(prefix); - char *hit = NULL; - for (size_t i = 0; i < n; i++) { - if ((i == 0 || buf[i-1] == '\n') && i + pl <= n && - memcmp(buf + i, prefix, pl) == 0) { - hit = buf + i + pl; - break; - } - } - if (!hit || hit >= buf + n || *hit == '\n') { free(buf); return -1; } - *hit = (*hit == 'a') ? 'b' : 'a'; - FILE *f = fopen(path, "wb"); - if (!f) { free(buf); return -1; } - fwrite(buf, 1, n, f); - fclose(f); - free(buf); - return 0; -} - -/* /.s present ⇒ that package compiled this build (MISS). */ -static int -sdot(const char *scratch, const char *pkg) -{ - char p[1100]; - snprintf(p, sizeof p, "%s/%s.s", scratch, pkg); - return access(p, 0) == 0; -} - -static const char *leaf_v1 = - "package leaf;\n" - "export fn base() i32 = { return 3; };\n"; -/* a CHANGED EXPORTED interface (extra export) → leaf's .wwi md5 changes. */ -static const char *leaf_v2 = - "package leaf;\n" - "export fn base() i32 = { return 3; };\n" - "export fn extra() i32 = { return 9; };\n"; - -static const char *mid_v1 = - "package mid;\n" - "import leaf;\n" - "export fn val() i32 = { return leaf.base() + 2; };\n"; -/* mid source perturbed by a comment — busts mid's src md5 without changing - * codegen (so the program result is preserved). */ -static const char *mid_v2 = - "package mid;\n" - "import leaf;\n" - "// 5b src-edit bust probe\n" - "export fn val() i32 = { return leaf.base() + 2; };\n"; - -static const char *root_src = - "package main;\n" - "import mid;\n" - "fn main() i32 = { return mid.val(); };\n"; - -int -main(void) -{ - const char *bin = absbin(); - if (!bin) return 1; - int fail = 0; - char td[64], cmd[8192]; - char rootww[1024], midww[1024], leafww[1024]; - char scratch[1024], prog[1024]; - char cache_cs[1024], cache_ww[1024]; - - snprintf(td, sizeof td, "/tmp/wwpkgc_%d", getpid()); - snprintf(cmd, sizeof cmd, "rm -rf %s", td); - runwait(cmd); - snprintf(cmd, sizeof cmd, "mkdir -p %s/mid %s/leaf", td, td); - runwait(cmd); - - snprintf(rootww, sizeof rootww, "%s/root.ww", td); - snprintf(midww, sizeof midww, "%s/mid/mid.ww", td); - snprintf(leafww, sizeof leafww, "%s/leaf/leaf.ww", td); - snprintf(prog, sizeof prog, "%s/p", td); - snprintf(scratch, sizeof scratch, "%s/p.sepwork", td); - snprintf(cache_cs, sizeof cache_cs, "%s/cache.cs", td); - snprintf(cache_ww, sizeof cache_ww, "%s/cache.ww", td); - - if (write_file(rootww, root_src) || write_file(midww, mid_v1) || - write_file(leafww, leaf_v1)) { fail++; goto out; } - - struct { const char *drv, *cache; } stg[] = { - { "ww", cache_cs }, - { "ww_ww", cache_ww }, - }; - - for (int s = 0; s < 2; s++) { - const char *drv = stg[s].drv; - const char *cache = stg[s].cache; - char midkey[1100]; - snprintf(midkey, sizeof midkey, "%s/mid/P.key", cache); - /* The cstage driver rm -rf's the scratch each build; the wwstage - * driver only mkdir's it (pre-existing #58(a) asymmetry), so a - * stale .s could persist and defeat the MISS/HIT sentinel. - * The gate owns its scratch: wipe it before every build so a - * fresh .s reflects exactly the last compile. */ -#define BUILD() do { \ - snprintf(cmd, sizeof cmd, \ - "rm -rf %s; WW_PKGCACHE='%s' timeout 240 %s/%s build --sep " \ - "-o %s %s >/dev/null 2>&1", scratch, cache, bin, drv, prog, \ - rootww); \ - } while (0) -#define SDOT(pkg) sdot(scratch, (pkg)) - - /* fresh cache for this stage */ - snprintf(cmd, sizeof cmd, "rm -rf %s", cache); - runwait(cmd); - /* sources back to v1 (a prior stage's restore may have left them) */ - write_file(midww, mid_v1); - write_file(leafww, leaf_v1); - - /* leg 1 — MISS: cold build compiles both, prog runs */ - BUILD(); - if (runwait(cmd) != 0) { - fprintf(stderr, "pkgcache FAIL[%s]: cold build failed\n", drv); - fail++; continue; - } - if (!SDOT("leaf") || !SDOT("mid")) { - fprintf(stderr, "pkgcache FAIL[%s]: leg1 MISS — leaf/mid did " - "not compile\n", drv); - fail++; - } - if (runwait(prog) != EXPECT_EXIT) { - fprintf(stderr, "pkgcache FAIL[%s]: leg1 prog exit != %d\n", - drv, EXPECT_EXIT); - fail++; - } - - /* leg 2 — HIT + non-vacuity: unchanged rebuild skips both */ - BUILD(); - runwait(cmd); - if (SDOT("leaf") || SDOT("mid")) { - fprintf(stderr, "pkgcache FAIL[%s]: leg2 HIT — recompiled an " - "unchanged package (cache vacuous)\n", drv); - fail++; - } - if (runwait(prog) != EXPECT_EXIT) { - fprintf(stderr, "pkgcache FAIL[%s]: leg2 prog exit != %d " - "(cached artifact unusable)\n", drv, EXPECT_EXIT); - fail++; - } - - /* leg 3 — BUST source edit: mid MISS, leaf still HIT */ - write_file(midww, mid_v2); - BUILD(); - runwait(cmd); - if (!SDOT("mid") || SDOT("leaf")) { - fprintf(stderr, "pkgcache FAIL[%s]: leg3 src-edit — expected " - "mid MISS + leaf HIT\n", drv); - fail++; - } - /* restore + reseed, then prove HIT returns */ - write_file(midww, mid_v1); - BUILD(); runwait(cmd); /* reseeds the v1 key (MISS) */ - BUILD(); runwait(cmd); /* must HIT now */ - if (SDOT("mid")) { - fprintf(stderr, "pkgcache FAIL[%s]: leg3 non-vacuity — mid did " - "not HIT after restore\n", drv); - fail++; - } - - /* leg 4 — BUST dep .wwi: change leaf interface → importer mid MISSES */ - write_file(leafww, leaf_v2); - BUILD(); - runwait(cmd); - if (!SDOT("leaf") || !SDOT("mid")) { - fprintf(stderr, "pkgcache FAIL[%s]: leg4 dep-.wwi — leaf " - "interface change did not bust importer mid\n", drv); - fail++; - } - write_file(leafww, leaf_v1); - BUILD(); runwait(cmd); /* reseed v1 */ - BUILD(); runwait(cmd); /* must HIT now */ - if (SDOT("leaf") || SDOT("mid")) { - fprintf(stderr, "pkgcache FAIL[%s]: leg4 non-vacuity — did not " - "HIT after restore\n", drv); - fail++; - } - - /* leg 5 — BUST compiler md5: corrupt the stored mid w6c line */ - if (mutate_keyline(midkey, "w6c ") != 0) { - fprintf(stderr, "pkgcache FAIL[%s]: leg5 — no w6c line in " - "mid/P.key\n", drv); - fail++; - } - BUILD(); - runwait(cmd); - if (!SDOT("mid") || SDOT("leaf")) { - fprintf(stderr, "pkgcache FAIL[%s]: leg5 compiler-md5 — corrupt " - "w6c line did not bust mid (only)\n", drv); - fail++; - } - BUILD(); runwait(cmd); /* key rewritten → must HIT */ - if (SDOT("mid")) { - fprintf(stderr, "pkgcache FAIL[%s]: leg5 non-vacuity — mid did " - "not HIT after key rewrite\n", drv); - fail++; - } - - /* leg 6 — BUST flag string: corrupt the stored mid flags line */ - if (mutate_keyline(midkey, "flags ") != 0) { - fprintf(stderr, "pkgcache FAIL[%s]: leg6 — no flags line in " - "mid/P.key\n", drv); - fail++; - } - BUILD(); - runwait(cmd); - if (!SDOT("mid")) { - fprintf(stderr, "pkgcache FAIL[%s]: leg6 flags — corrupt flags " - "line did not bust mid\n", drv); - fail++; - } - BUILD(); runwait(cmd); - if (SDOT("mid")) { - fprintf(stderr, "pkgcache FAIL[%s]: leg6 non-vacuity — mid did " - "not HIT after key rewrite\n", drv); - fail++; - } -#undef BUILD -#undef SDOT - } - - /* cross-stage byte-id (rule 10): cached OUTPUTS identical cs==ww. The - * caches end in the v1/HIT state from each stage's leg 6 tail. */ - { - const char *pkgs[] = { "mid", "leaf" }; - const char *arts[] = { "P.o", "P.wwi" }; - for (int i = 0; i < 2; i++) - for (int k = 0; k < 2; k++) { - char a[1100], b[1100]; - snprintf(a, sizeof a, "%s/%s/%s", cache_cs, pkgs[i], arts[k]); - snprintf(b, sizeof b, "%s/%s/%s", cache_ww, pkgs[i], arts[k]); - if (files_eq(a, b) != 0) { - fprintf(stderr, "pkgcache FAIL: cs != ww for cached " - "%s/%s (rule 10)\n", pkgs[i], arts[k]); - fail++; - } - } - } - -out: - snprintf(cmd, sizeof cmd, "rm -rf %s", td); - runwait(cmd); - if (fail) { - fprintf(stderr, "pkgcache: %d check(s) failed\n", fail); - return 1; - } - printf("pkgcache: 3-level root->mid->leaf via build_one_sep — MISS " - "compiles, HIT skips (non-vacuous, prog runs), and key busts on each " - "input class (src / dep .wwi / compiler md5 / flags) — both stages; " - "cached P.o/P.wwi byte-identical cs==ww\n"); - return 0; -}