From 6525e137ae2c36979fab488c3d67a74725289e4b Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 22 Jun 2026 21:17:30 +0900 Subject: [PATCH] wwi: derive decl-less module's .wwi package leaf from parse-stamped path (#11) wwi_emit took the .wwi `package` leaf from the first primary decl's module tag; a fully empty primary module body (zero decls) had none, so the leaf stayed the literal default "main" and the importer rejected it ("package main does not match import path "). The module identity is only available at parse time (curmod is overwritten by imported //ww:module sections before emit), so stamp the primary path onto the N_FILE node (TK_MODULE and TK_MODRESET rp!=NULL sites, only-if-empty so a bare-reset `package main` root stays "main") and, when the decl-scan finds no leaf, fall back to that stamped path. Symmetric cstage+ selfhost; both detect scan-miss via the same found-flag so the emitted .wwi stays byte-identical. Regression: test/wcc/989_wwileaf_run.c, table-driven over {empty body, comment-only, nested a.b.c} decl-less shapes, non-vacuity proven. --- Makefile | 10 ++ cmd/w6c/wwi.c | 23 +++- cmd/wcc/parse.c | 14 ++ lib/ww/syntax/parse.ww | 11 ++ selfhost/cmd/wcc/wwi.ww | 25 ++++ test/wcc/989_wwileaf_run.c | 273 +++++++++++++++++++++++++++++++++++++ 6 files changed, 352 insertions(+), 4 deletions(-) create mode 100644 test/wcc/989_wwileaf_run.c 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; +}