diff --git a/Makefile b/Makefile index cb28e26c..533e91d7 100644 --- a/Makefile +++ b/Makefile @@ -502,6 +502,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_dyn_ww $(BIN)/test_selfcheck \ $(BIN)/test_attest_record $(BIN)/test_attest_drop \ $(BIN)/test_declns_sep $(BIN)/test_modresetadj_run \ + $(BIN)/test_wwileaf_run \ $(BIN)/test_wwispread_sep \ $(BIN)/test_fmt_run $(BIN)/test_log_run $(BIN)/test_fnmatch_run \ $(BIN)/test_shlex_run $(BIN)/test_getenv_run $(BIN)/test_dirs_run \ @@ -2668,6 +2669,15 @@ $(BIN)/test_modresetadj_run: test/wcc/989_modresetadj_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# #11 (BUG-C) — a decl-less primary module's `.wwi` `package` line must carry +# the real leaf, not the default "main", else its importer rejects the dep. +# Table-driven over empty/comment-only/nested-path shapes; feeds the composed +# producer + importer units straight to w6c/w6c_ww (.wwi leaf + byte-id + +# import resolves cs==ww). Frontends only — no asm/link/run leg. +$(BIN)/test_wwileaf_run: test/wcc/989_wwileaf_run.c \ + $(BIN)/w6c $(BIN)/w6c_ww | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + # #95 — .wwi producer must preserve the `...` union-spread marker; dir-package # `ww build --sep` + run, both driver stages, 2-arm + 3-arm shapes + cs==ww. $(BIN)/test_wwispread_sep: test/wcc/989_wwispread_sep.c $(BIN)/ww $(BIN)/ww_ww \ diff --git a/cmd/w6c/wwi.c b/cmd/w6c/wwi.c index 16f3627b..2d3877e6 100644 --- a/cmd/w6c/wwi.c +++ b/cmd/w6c/wwi.c @@ -1,8 +1,9 @@ /* - * wwi.c — `.wwi` export-data producer (w6c -I). M2 DEAD-CODE: writes a - * re-parseable ww-prototype rendering of a package's EXPORTED surface. - * Nothing consumes `.wwi` yet (combined.ww stays the live path); the only - * caller is the new -I flag, off on every existing invocation. + * wwi.c — `.wwi` export-data producer (w6c -I): a re-parseable ww-prototype + * rendering of a package's EXPORTED surface. Since the sep-compile flip + * (epic #22) this is the LIVE import path — the driver runs one `w6c -c -I` + * per package and feeds each dep's `.wwi` to its importers as import scope; + * the combined.ww amalgamator is gone. * * Specs: .ai/rob-M2-spec.md (producer) + .ai/drew-M2-checkexported.md * (check_exported_type). Two load-bearing choices follow them: @@ -492,13 +493,27 @@ wwi_emit(Checker *c, FILE *of, Node *file) /* package line: leaf of the first primary decl's module tag. */ const char *pkg = "main"; + int found = 0; for (Node *d = file->list; d; d = d->next) { if (wwi_primary(d) && d->module && d->module[0]) { const char *dot = strrchr(d->module, '.'); pkg = dot ? dot + 1 : d->module; + found = 1; break; } } + /* #11: a decl-less / export-less primary body carries no + * module-tagged decl, so the scan above finds nothing; fall back to + * the primary module identity stamped on the N_FILE node at parse + * time. A real root `package main` arrives via a bare module-reset + * and leaves file->module NULL, so it stays "main". The detector is + * scan-miss (`!found`), NOT pkg=="main": a body whose first tagged + * decl legitimately leafs to "main" must keep that, and must match + * selfhost's `found` flag byte-for-byte (rule 10). */ + if (!found && file->module && file->module[0]) { + const char *dot = strrchr(file->module, '.'); + pkg = dot ? dot + 1 : file->module; + } fprintf(of, "package %s;\n", pkg); /* imports — primary N_USE, byte-sorted by import path. */ diff --git a/cmd/wcc/parse.c b/cmd/wcc/parse.c index c7fb9dfd..9afa7fc0 100644 --- a/cmd/wcc/parse.c +++ b/cmd/wcc/parse.c @@ -1442,6 +1442,13 @@ parsefile(Parser *p) } } else { p->curmod = name; + /* #11: stamp the primary module identity on the + * N_FILE node so wwi_emit can derive the + * `package` leaf even when the body carries zero + * module-tagged decls. Primary identity only; + * never the imported boundary (TK_MODPATH). */ + if (file->module == NULL) + file->module = name; } continue; } @@ -1478,6 +1485,13 @@ parsefile(Parser *p) if (rp != NULL) { p->curmod = rp; p->resetmod = rp; + /* #11: path-carrying reset is a primary body + * identity (sep); stamp it for the wwi leaf + * fallback. A bare reset (rp==NULL) is the + * root/package-less boundary and MUST keep the + * "main" default — so do NOT stamp there. */ + if (file->module == NULL) + file->module = rp; } else { p->curmod = NULL; p->resetmod = NULL; diff --git a/lib/ww/syntax/parse.ww b/lib/ww/syntax/parse.ww index 45c51db8..b8b8ad48 100644 --- a/lib/ww/syntax/parse.ww +++ b/lib/ww/syntax/parse.ww @@ -448,6 +448,12 @@ export fn parsefile(p: *parser) *node = { }; } else { p.curmod = name; + // #11: stamp the primary module identity on the + // N_FILE node so wwi_emit can derive the `package` + // leaf even when the body carries zero module-tagged + // decls. Primary identity only; never the imported + // boundary (TK_MODPATH). + if (f.nmod.len == 0) { f.nmod = name; }; }; continue; }; @@ -483,6 +489,11 @@ export fn parsefile(p: *parser) *node = { if (rp.len != 0) { p.curmod = rp; p.resetmod = rp; + // #11: path-carrying reset is a primary body identity + // (sep); stamp it for the wwi leaf fallback. A bare + // reset (rp empty) is the root/package-less boundary + // and MUST keep the "main" default — so don't stamp. + if (f.nmod.len == 0) { f.nmod = rp; }; } else { let empty: str; empty.ptr = nil; diff --git a/selfhost/cmd/wcc/wwi.ww b/selfhost/cmd/wcc/wwi.ww index 4bab611c..4703ae64 100644 --- a/selfhost/cmd/wcc/wwi.ww +++ b/selfhost/cmd/wcc/wwi.ww @@ -587,6 +587,7 @@ export fn wwiemit(c: *checker, file: *syntax.node, path: str) i32 = { // package line: leaf of the first primary decl's module tag. let pkg: str = "main"; + let found: i32 = 0; let pd: *syntax.node = file.list; for (pd != nil) { if (wwiprimary(pd) && pd.nmod.len > 0) { @@ -604,11 +605,35 @@ export fn wwiemit(c: *checker, file: *syntax.node, path: str) i32 = { } else { pkg = pd.nmod; }; + found = 1; pd = nil; } else { pd = pd.next; }; }; + // #11: a decl-less / export-less primary body carries no + // module-tagged decl, so the scan above finds nothing; fall back to + // the primary module identity stamped on the N_FILE node at parse + // time. A real root `package main` arrives via a bare module-reset + // and leaves file.nmod empty, so it stays "main". The detector is + // scan-miss (found==0), NOT pkg=="main", to match cstage byte-for- + // byte (rule 10) when a tagged decl legitimately leafs to "main". + if (found == 0 && file.nmod.len > 0) { + let dotidx: i32 = -1; + let i: i32 = 0; + for (i < file.nmod.len) { + if (file.nmod[i] == 46u8) { dotidx = i; }; + i += 1; + }; + if (dotidx >= 0) { + let leaf: str; + leaf.ptr = file.nmod.ptr + ((dotidx + 1): u64); + leaf.len = file.nmod.len - dotidx - 1; + pkg = leaf; + } else { + pkg = file.nmod; + }; + }; wputs(fd, "package "); wputs(fd, pkg); wputs(fd, ";\n"); diff --git a/test/wcc/989_wwileaf_run.c b/test/wcc/989_wwileaf_run.c new file mode 100644 index 00000000..893cb365 --- /dev/null +++ b/test/wcc/989_wwileaf_run.c @@ -0,0 +1,273 @@ +/* + * 989_wwileaf_run — BUG-C (#11) regression pin: a decl-less / export-less + * primary module's `.wwi` `package` line must carry the module's real leaf, + * NOT the literal default "main". + * + * THE BUG: wwi_emit derived the `package` leaf by scanning for the first + * PRIMARY decl bearing a module tag. A fully empty (or comment-only, or + * export-less) body has NO such decl, so the scan fell through and the leaf + * stayed the seed default "main". The dep's `.wwi` then read `package main;` + * → when its importer composed the dep under `//ww:module `, the + * in-`.wwi` `package main` clause was validated against the import path and + * REJECTED: "package main does not match import path " + * (cmd/wcc/parse.c, both stages). A decl-less module was simply unimportable. + * + * THE FIX (cmd/w6c/wwi.c + selfhost/cmd/wcc/wwi.ww + the two parse sites): + * parse stamps the primary module identity on the N_FILE node (module/nmod, + * "only if empty", never on the imported TK_MODPATH boundary nor the bare + * root reset); wwi_emit falls back to that identity's leaf when the decl-scan + * finds nothing (`!found` — NOT pkg=="main", so a body whose first tagged + * decl legitimately leafs to "main" is unaffected and both stages decide + * identically). A real root `package main` arrives via a bare reset, leaves + * the stamp empty, and stays "main". + * + * Table-driven over the three decl-less shapes that all route the fallback: + * an empty body, a comment-only body, and a NESTED dotted path (a.b.c → leaf + * c, exercising the leaf-slice). Each row drives the REAL producer→importer + * flow on BOTH stages: + * (a) the dep `.wwi` `package` line == the real leaf (pre-fix: "main"); + * (b) the dep `.wwi` is byte-identical w6c vs w6c_ww (rule 10); + * (c) a root that `import`s the dep RESOLVES on both stages (pre-fix: + * hard REJECT) and the two importer `.s` are byte-identical (rule 10). + * + * The composed units are fed straight to w6c / w6c_ww (not `ww build`): the + * producer-unit shape `//ww:module-reset ` + body is exactly what the + * driver's sep_emit_body emits, so this pins the same path without the + * driver's filesystem layout. Non-vacuity: neutering the wwi_emit fallback + * reddens row (a) ("package main;") and row (c) (the import REJECT). + * + * Light wwstage-driver test (CLAUDE.md rule 14): every intermediate lands in + * a private mkdtemp dir, rm -rf'd at exit — no /tmp scratch leak (task #8). + * 989 prefix per the sep-gate precedent; twin of 989_modresetadj_run (#9). + */ +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return -1; +} + +static const char * +absbin(void) +{ + const char *b = getenv("BIN"); + if (!b) b = "out/bin"; + if (b[0] == '/') return b; + static char buf[2048]; + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return NULL; + snprintf(buf, sizeof buf, "%s/%s", cwd, b); + return buf; +} + +static int +slurp(const char *path, char **outbuf, size_t *outlen) +{ + FILE *f = fopen(path, "rb"); + if (!f) return -1; + fseek(f, 0, SEEK_END); + long n = ftell(f); + fseek(f, 0, SEEK_SET); + if (n < 0) { fclose(f); return -1; } + char *b = malloc((size_t)n + 1); + if (!b) { fclose(f); return -1; } + if (fread(b, 1, (size_t)n, f) != (size_t)n) { free(b); fclose(f); return -1; } + b[n] = '\0'; + fclose(f); + *outbuf = b; + *outlen = (size_t)n; + return 0; +} + +static int +files_eq(const char *a, const char *b) +{ + char *ba = NULL, *bb = NULL; + size_t na = 0, nb = 0; + if (slurp(a, &ba, &na) < 0 || slurp(b, &bb, &nb) < 0) { + free(ba); free(bb); + return -1; + } + int eq = (na == nb && memcmp(ba, bb, na) == 0); + free(ba); free(bb); + return eq ? 0 : 1; +} + +static int +write_file(const char *path, const char *content) +{ + FILE *f = fopen(path, "wb"); + if (!f) return -1; + fputs(content, f); + fclose(f); + return 0; +} + +/* First physical line of `path` (newline stripped) into `out`. */ +static int +first_line(const char *path, char *out, size_t outsz) +{ + FILE *f = fopen(path, "rb"); + if (!f) return -1; + if (fgets(out, (int)outsz, f) == NULL) { fclose(f); out[0] = '\0'; } + fclose(f); + size_t n = strlen(out); + if (n && out[n - 1] == '\n') out[n - 1] = '\0'; + return 0; +} + +struct row { + const char *tag; /* scratch-file stem */ + const char *path; /* dotted module path on the //ww:module-reset */ + const char *leaf; /* the in-file `package ;` clause */ + const char *body; /* body after the clause (zero module-tagged decls) */ + const char *want; /* expected emitted `package ;` leaf */ +}; + +/* Each shape lacks any module-tagged decl, so wwi_emit's decl-scan misses + * and the leaf must come from the parse-stamped N_FILE identity. */ +static const struct row rows[] = { + { "empty", "emptymod", "emptymod", "", "emptymod" }, + { "comment", "cmod", "cmod", "// only a comment\n", "cmod" }, + { "nested", "a.b.c", "c", "", "c" }, +}; +#define NROW ((int)(sizeof rows / sizeof rows[0])) +#define RET 42 + +static int +run_row(const char *bin, const char *dir, const struct row *r) +{ + char prod[1024], cswwi[1024], wwwwi[1024], css[1024], wws[1024]; + char root[1024], rcss[1024], rwws[1024], unit[8192], cmd[16384]; + + snprintf(prod, sizeof prod, "%s/%s.prod.ww", dir, r->tag); + snprintf(cswwi, sizeof cswwi, "%s/%s.cs.wwi", dir, r->tag); + snprintf(wwwwi, sizeof wwwwi, "%s/%s.ww.wwi", dir, r->tag); + snprintf(css, sizeof css, "%s/%s.cs.s", dir, r->tag); + snprintf(wws, sizeof wws, "%s/%s.ww.s", dir, r->tag); + snprintf(root, sizeof root, "%s/%s.root.ww", dir, r->tag); + snprintf(rcss, sizeof rcss, "%s/%s.rcs.s", dir, r->tag); + snprintf(rwws, sizeof rwws, "%s/%s.rww.s", dir, r->tag); + + /* the producer unit — exactly the driver's sep_emit_body shape. */ + snprintf(unit, sizeof unit, + "//ww:module-reset %s\n" + "package %s;\n" + "%s", + r->path, r->leaf, r->body); + if (write_file(prod, unit) != 0) { + fprintf(stderr, "wwileaf[%s]: cannot write producer unit\n", r->tag); + return 1; + } + + /* Leg a — both stages emit the dep `.wwi`; the `package` leaf is the + * module's real leaf, not the default "main". */ + snprintf(cmd, sizeof cmd, + "%s/w6c -c -I %s -o %s %s 2>/dev/null", bin, cswwi, css, prod); + if (runwait(cmd) != 0) { + fprintf(stderr, "wwileaf[%s] FAIL: w6c producer errored\n", r->tag); + return 1; + } + snprintf(cmd, sizeof cmd, + "%s/w6c_ww -c -I %s -o %s %s 2>/dev/null", bin, wwwwi, wws, prod); + if (runwait(cmd) != 0) { + fprintf(stderr, "wwileaf[%s] FAIL: w6c_ww producer errored\n", r->tag); + return 1; + } + char want[256], line[512]; + snprintf(want, sizeof want, "package %s;", r->want); + if (first_line(cswwi, line, sizeof line) != 0 || strcmp(line, want) != 0) { + fprintf(stderr, + "wwileaf[%s] FAIL: .wwi package line '%s' want '%s' (BUG-C)\n", + r->tag, line, want); + return 1; + } + + /* Leg b — the dep `.wwi` is byte-identical across stages (rule 10). */ + if (files_eq(cswwi, wwwwi) != 0) { + fprintf(stderr, + "wwileaf[%s] FAIL: w6c vs w6c_ww .wwi differ (rule 10)\n", r->tag); + return 1; + } + + /* Leg c — a root importing the dep RESOLVES on both stages (pre-fix: + * "package main does not match import path %s" REJECT). The dep `.wwi` + * is prepended under //ww:module , exactly as the driver composes + * an importer unit. */ + { + char *wwi = NULL; + size_t wlen = 0; + if (slurp(cswwi, &wwi, &wlen) < 0) { + fprintf(stderr, "wwileaf[%s]: cannot read dep .wwi\n", r->tag); + return 1; + } + snprintf(unit, sizeof unit, + "//ww:module %s\n" + "%s\n" + "//ww:module-reset\n" + "package main;\n" + "import %s;\n" + "export fn main() i32 = { return %d; };\n", + r->path, wwi, r->path, RET); + free(wwi); + } + if (write_file(root, unit) != 0) { + fprintf(stderr, "wwileaf[%s]: cannot write importer unit\n", r->tag); + return 1; + } + snprintf(cmd, sizeof cmd, + "%s/w6c -c -o %s %s 2>/dev/null", bin, rcss, root); + if (runwait(cmd) != 0) { + fprintf(stderr, + "wwileaf[%s] FAIL: w6c rejected the import (BUG-C)\n", r->tag); + return 1; + } + snprintf(cmd, sizeof cmd, + "%s/w6c_ww -c -o %s %s 2>/dev/null", bin, rwws, root); + if (runwait(cmd) != 0) { + fprintf(stderr, + "wwileaf[%s] FAIL: w6c_ww rejected the import (BUG-C)\n", r->tag); + return 1; + } + if (files_eq(rcss, rwws) != 0) { + fprintf(stderr, + "wwileaf[%s] FAIL: importer .s differ cs vs ww (rule 10)\n", + r->tag); + return 1; + } + return 0; +} + +int +main(void) +{ + const char *bin = absbin(); + if (!bin) return 1; + + char dir[] = "/tmp/wwileaf_XXXXXX"; + if (mkdtemp(dir) == NULL) { + fprintf(stderr, "wwileaf: mkdtemp failed\n"); + return 1; + } + + int fail = 0; + for (int i = 0; i < NROW; i++) + fail |= run_row(bin, dir, &rows[i]); + + char rm[2048]; + snprintf(rm, sizeof rm, "rm -rf %s", dir); + (void)runwait(rm); + + if (fail) return 1; + printf("wwileaf: %d decl-less shapes — .wwi leaf derived, byte-id, " + "import resolves cs==ww (#11)\n", NROW); + return 0; +}