From 13e5e35f8162411f45762e6a269fbf7effda185a Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 15 Jun 2026 22:00:13 +0900 Subject: [PATCH] =?UTF-8?q?wcc/ww:=20.wwi=20separate-compile=20consumer=20?= =?UTF-8?q?=E2=80=94=20w6c=20-c=20codegen=20filter=20(#22=20M3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New `w6c -c` (both stages): separate-compile / primary-only codegen. Emit code+DATA ONLY for a package's own (imported==0) decls; treat every `.wwi`-sourced (imported==1) dep decl as an external. Pure addition behind the flag — combined.ww stays the LIVE path, `-c` is off on every existing invocation, so the 990-997 byte-id gates + all prior tests are unperturbed. The keystone (rob): a `.wwi` is body-less/init-less prototype source, and cgen already skips body-less fns as externs, so dep fns/types/defs emit NOTHING for free. The single genuinely-new guard is an imported value- global (`export let`): its DATAW would DUPLICATE the dep's own definition (link collision), so it is skipped. The `imported==0` gate is applied at all top-level emit sites for uniformity (close-by-construction): the fn loop, emit_lets/emitletdataw, emit_defs/emitdefconstants, and let_pre_intern/letpreintern — that last one because an imported dep's body initializer interns strlits while its rhs-stripped `.wwi` does not, which would shift the _S_ sequence; gating it keeps the strlit table a pure function of the package's own decls. EXACTLY symmetric with M2's producer `imported==0` filter — same predicate both directions. Driver `--sep` build_one_sep + per-package archives + multi-.a link + cache + BROAD real-target dual-path soak are M3-tail (#46, rob ruling B): M3-core ships the codegen spine + a self-contained gate that proves all codegen correctness without a production driver. Gate 989_m3sep_run: a synth leaf->mid->root fixture carrying all four cross-boundary fact-classes (fn signature, struct LAYOUT, `def` const VALUE, `export let` value-global). Per package, holding `-c` constant: `w6c -c` of (deps-as-bodies) == (deps-as-.wwi) byte-for-byte (the .wwi conveys exactly the dep facts P's codegen needs); cs==ww at the .s AND final-exe level (rule 10); sep-path determinism; the value-global guard (imported origin_tag never re-emits DATAW); and behavioral identity (the linked program's exit code is the real cross-boundary computation). COLD: .wwi materialized fresh every run (no warm cache). combined.ww regen'd for wwdump + w6c (both embed cgen.ww); diff is exactly the four guards + the flag wiring, nothing spurious. --- Makefile | 10 + cmd/w6c/cgen.c | 29 ++ cmd/w6c/gc.h | 9 + cmd/w6c/main.c | 8 +- selfhost/cmd/w6c/main.combined.ww | 48 ++- selfhost/cmd/w6c/main.ww | 8 +- selfhost/cmd/wcc/cgen.ww | 29 ++ selfhost/cmd/wcc/cgendecl.ww | 11 +- selfhost/cmd/wwdump/main.combined.ww | 40 ++- test/wcc/989_m3sep_run.c | 425 +++++++++++++++++++++++++++ 10 files changed, 608 insertions(+), 9 deletions(-) create mode 100644 test/wcc/989_m3sep_run.c diff --git a/Makefile b/Makefile index 3d5cd149..d8d0c008 100644 --- a/Makefile +++ b/Makefile @@ -562,6 +562,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_regex_run \ $(BIN)/test_lib_byteid \ $(BIN)/test_m2wwi_run \ + $(BIN)/test_m3sep_run \ $(BIN)/test_floatlit_run \ $(BIN)/test_checked_run \ $(BIN)/test_floatarr_run \ @@ -3087,6 +3088,15 @@ $(BIN)/test_m2wwi_run: test/wcc/989_m2wwi_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6c_ww $(BIN)/wwdump $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# M3 `.wwi` separate-compile CONSUMER gate (task #22 arc). Drives both +# toolchains: w6c/w6c_ww (-c codegen filter), w6a/w6a_ww, w6l/w6l_ww for +# the per-package .s byte-id + cs==ww .s/exe + determinism + behavioral +# soak on the synth leaf->mid->root fixture. COLD: .wwi materialized fresh. +$(BIN)/test_m3sep_run: test/wcc/989_m3sep_run.c $(BIN)/ww $(BIN)/w6c \ + $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6a_ww $(BIN)/w6l $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_floatlit_run: test/wcc/989_floatlit_run.c $(BIN)/ww $(BIN)/w6c \ $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 5aa6c4f7..101f544b 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -16348,6 +16348,13 @@ emit_lets(Cg *c, FILE *out, Node *file) for (Node *d = file->list; d; d = d->next) { if (d->kind != N_LET) continue; if (d->str == NULL || d->str[0] == '\0') continue; + /* #22 M3 THE ONE REAL GUARD: a `.wwi` dep value-global is + * initializer-less; emitting a DATAW for it would DUPLICATE the + * definition that lives in the dep's own .o → link collision. + * Gate on the explicit imported flag (NOT rhs==NULL: a package's + * OWN init-less let must still zero-init). Symmetric with M2's + * producer `imported==0` filter — same predicate both ways. */ + if (c->sep_mode && d->imported) continue; int sz = let_emit_size(d->type); if (sz == 0) continue; if (let_isfloat(d->type)) { @@ -16527,6 +16534,13 @@ emit_defs(Cg *c, FILE *out, Node *file) { for (Node *d = file->list; d; d = d->next) { if (d->kind != N_DEF || d->rhs == NULL) continue; + /* #22 M3: a `.wwi` dep def with DATA storage (int-fold / float / + * struct / array) must NOT re-emit — the dep's own .o owns the + * symbol. Str defs are inline-spliced (sdef_collect), never + * emitted here, so they need no gate; the registries + * (defstructs/defarrays/defall) stay populated for imported + * decls so the target's LOAD paths still resolve the extern. */ + if (c->sep_mode && d->imported) continue; u64 v; if (fold_int_literal(d->rhs, &v)) { fprintf(out, "DATA %s(SB),\"", @@ -16657,6 +16671,15 @@ let_pre_intern(Cg *c, Node *file) { if (file == NULL) return; for (Node *d = file->list; d; d = d->next) { + /* #22 M3: skip imported deps so the strlit table (and its _S_ + * sequence) is a pure function of THIS package's own decls. A + * dep's body initializer would intern here, but its `.wwi` + * (init stripped) would not — gating on imported keeps the + * bodies-vs-.wwi `.s` byte-identical for P's own symbols. The + * dep's own strlits live in the dep's .o; cross-module str-def + * splicing rides sdef_collect (interned at the use site, not + * here), so it is unaffected. */ + if (c->sep_mode && d->imported) continue; /* #8/GAP-B: a `def [N]str` needs the SAME element-strlit * pre-interning as the let [N]str arm below (the #18 ordering * contract) so emit_strarray_data's DATAR rows find their _S_ @@ -16780,6 +16803,12 @@ cg_file(Cg *c, FILE *out, Node *file) strlit_seq = 0; for (Node *d = file->list; d; d = d->next) { if (d->kind != N_FNDECL) continue; + /* #22 M3: emit code ONLY for this package's own decls. A `.wwi` + * dep fn is a body-less prototype that already skips (cgfn's + * extern-decl early return); the explicit gate also covers a + * dep fn that still carries a body in a transitional unit and + * keeps the "emit iff imported==0" rule exception-free. */ + if (c->sep_mode && d->imported) continue; cgfn(c, out, d); } let_pre_intern(c, file); diff --git a/cmd/w6c/gc.h b/cmd/w6c/gc.h index 1c442b28..1221b8e3 100644 --- a/cmd/w6c/gc.h +++ b/cmd/w6c/gc.h @@ -45,6 +45,15 @@ struct Cg { int curoff; /* current top of locals */ Scope *locals; /* (name → offset) tracked via Sym */ int labelseq; + int sep_mode; /* #22 M3 `-c`: separate-compile / primary-only + * codegen. Emit code+DATA ONLY for this + * package's own (imported==0) decls; treat every + * `.wwi`-sourced (imported==1) dep decl as an + * external — body-less fns already skip, value + * globals/defs are gated so they don't duplicate + * the dep's real definition (link collision). + * Off on the combined path (every existing + * invocation) so M3 is a pure addition. */ }; /* cgen.c */ diff --git a/cmd/w6c/main.c b/cmd/w6c/main.c index ec489a3f..01161068 100644 --- a/cmd/w6c/main.c +++ b/cmd/w6c/main.c @@ -35,6 +35,9 @@ main(int argc, char **argv) const char *out = NULL; const char *wwiout = NULL; /* -I : M2 export-data producer */ int testmode = 0; + int sepmode = 0; /* -c: #22 M3 separate-compile / primary- + * only codegen (emit imported==0 decls + * only; treat `.wwi` deps as external) */ for (int i = 1; i < argc; i++) { const char *a = argv[i]; if (strcmp(a, "-o") == 0 && i + 1 < argc) { @@ -43,6 +46,8 @@ main(int argc, char **argv) wwiout = argv[++i]; } else if (strcmp(a, "-T") == 0) { testmode = 1; + } else if (strcmp(a, "-c") == 0) { + sepmode = 1; } else if (a[0] == '-') { fprintf(stderr, "w6c: unknown flag %s\n", a); return 2; @@ -54,7 +59,7 @@ main(int argc, char **argv) } } if (src == NULL) { - fputs("usage: w6c [-T] [-o out.s] file.ww\n", stderr); + fputs("usage: w6c [-T] [-c] [-I out.wwi] [-o out.s] file.ww\n", stderr); return 2; } @@ -104,6 +109,7 @@ main(int argc, char **argv) } cg_init(&cg, a); + cg.sep_mode = sepmode; cg_file(&cg, of, file); if (of != stdout) fclose(of); diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 4d9a66d9..7673f38d 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -41184,8 +41184,15 @@ export fn cgfile(c: *cgen, file: *node) void = { let d: *node = file.list; for (d != nil) { if (d.kind == nkind.N_FNDECL) { - if (d.body != nil) { - cgfn(c, d); + // #22 M3: emit code ONLY for this package's own decls. A + // `.wwi` dep fn is a body-less prototype that already skips + // (the body != nil gate); the explicit imported gate also + // covers a dep fn that still carries a body in a transitional + // unit, keeping the "emit iff imported==0" rule exception-free. + if (!(c.sepmode != 0 && d.imported != 0)) { + if (d.body != nil) { + cgfn(c, d); + }; }; }; d = d.next; @@ -41738,6 +41745,14 @@ type cgen = struct { // global receiver wired; mutually exclusive with sretdestoff. sretdestnode: *node, sretforward: i32, + // #22 M3 `-c`: separate-compile / primary-only codegen. Emit code+ + // DATA ONLY for this package's own (imported==0) decls; treat every + // `.wwi`-sourced (imported==1) dep decl as an external. Off on the + // combined path (every existing invocation) so M3 is a pure addition. + // NOT reset by cgeninit (which runs per-fn) — set once in main and + // must survive to the post-loop emitletdataw/emitdefconstants pass, + // like strlits/ffis. Symmetric with cstage Cg.sep_mode. + sepmode: i32, }; // Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar. @@ -42659,6 +42674,14 @@ export fn letpreintern(c: *cgen, file: *node) void = { if (file == nil) { return; }; let d: *node = file.list; for (d != nil) { + // #22 M3: skip imported deps so the strlit table (and its _S_ + // sequence) is a pure function of THIS package's own decls. A + // dep's body initializer would intern here, but its `.wwi` (init + // stripped) would not — gating on imported keeps the + // bodies-vs-.wwi `.s` byte-identical for P's own symbols. Cross- + // module str-def splicing rides the def registry (interned at the + // use site, not here), so it is unaffected. + if (c.sepmode != 0 && d.imported != 0) { d = d.next; continue; }; // #8/GAP-B: a `def [N]str` needs the SAME element-strlit // pre-interning as the let [N]str arm (the #18 ordering // contract) so emitstrarraydata's DATAR rows find their _S_ @@ -44123,6 +44146,13 @@ fn emittupledata(c: *cgen, name: str, module: str, tt: *node, rhs: *node) bool = fn emitletdataw(c: *cgen, file: *node) void = { let d: *node = file.list; for (d != nil) { + // #22 M3 THE ONE REAL GUARD: a `.wwi` dep value-global is + // initializer-less; emitting a DATAW for it would DUPLICATE the + // definition that lives in the dep's own .o → link collision. + // Gate on the explicit imported flag (NOT no-rhs: a package's OWN + // init-less let must still zero-init). Symmetric with M2's + // producer imported==0 filter — same predicate both ways. + if (c.sepmode != 0 && d.imported != 0) { d = d.next; continue; }; if (d.kind == nkind.N_LET) { let nm: str = d.str; if (nm.len > 0) { @@ -44461,6 +44491,12 @@ fn emitletdataw(c: *cgen, file: *node) void = { fn emitdefconstants(c: *cgen, file: *node) void = { let d: *node = file.list; for (d != nil) { + // #22 M3: a `.wwi` dep def with DATA storage (int-fold / float / + // struct / array) must NOT re-emit — the dep's own .o owns the + // symbol. Str defs are inline-spliced (never emitted here), so + // they need no gate; the def registry stays populated for imported + // decls so the target's LOAD paths still resolve the extern. + if (c.sepmode != 0 && d.imported != 0) { d = d.next; continue; }; if (d.kind == nkind.N_DEF) { let r: *node = d.rhs; let v: u64 = 0u64; @@ -45931,6 +45967,9 @@ export fn main(argc: i32, argv: **u8) i32 = { let out: *u8 = nil; let wwiout: *u8 = nil; // -I : M2 export-data producer let testmode: i32 = 0i32; // #15: `-T` test-mode + let sepmode: i32 = 0i32; // -c: #22 M3 separate-compile / primary- + // only codegen (emit imported==0 decls + // only; treat `.wwi` deps as external) let i: i32 = 1; for (i < argc) { @@ -45953,6 +45992,8 @@ export fn main(argc: i32, argv: **u8) i32 = { wwiout = argv[i]; } else { if (cstreq(a, "-T")) { testmode = 1i32; + } else { if (cstreq(a, "-c")) { + sepmode = 1i32; } else { if (a[0u64] == 45u8) { let m: str = "w6c: unknown flag\n"; os.write(2, m.ptr, m.len: u64); @@ -45964,7 +46005,7 @@ export fn main(argc: i32, argv: **u8) i32 = { return 2; }; src = a; - }; }; }; }; + }; }; }; }; }; i += 1; }; @@ -46043,6 +46084,7 @@ export fn main(argc: i32, argv: **u8) i32 = { let cg: cgen; cgeninit(&cg); + cg.sepmode = sepmode; cgfile(&cg, f); return 0; }; diff --git a/selfhost/cmd/w6c/main.ww b/selfhost/cmd/w6c/main.ww index c7c474a9..a93cb401 100644 --- a/selfhost/cmd/w6c/main.ww +++ b/selfhost/cmd/w6c/main.ww @@ -82,6 +82,9 @@ export fn main(argc: i32, argv: **u8) i32 = { let out: *u8 = nil; let wwiout: *u8 = nil; // -I : M2 export-data producer let testmode: i32 = 0i32; // #15: `-T` test-mode + let sepmode: i32 = 0i32; // -c: #22 M3 separate-compile / primary- + // only codegen (emit imported==0 decls + // only; treat `.wwi` deps as external) let i: i32 = 1; for (i < argc) { @@ -104,6 +107,8 @@ export fn main(argc: i32, argv: **u8) i32 = { wwiout = argv[i]; } else { if (cstreq(a, "-T")) { testmode = 1i32; + } else { if (cstreq(a, "-c")) { + sepmode = 1i32; } else { if (a[0u64] == 45u8) { let m: str = "w6c: unknown flag\n"; os.write(2, m.ptr, m.len: u64); @@ -115,7 +120,7 @@ export fn main(argc: i32, argv: **u8) i32 = { return 2; }; src = a; - }; }; }; }; + }; }; }; }; }; i += 1; }; @@ -194,6 +199,7 @@ export fn main(argc: i32, argv: **u8) i32 = { let cg: cgen; cgeninit(&cg); + cg.sepmode = sepmode; cgfile(&cg, f); return 0; }; diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index 0867f2ec..f622ca2a 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -539,6 +539,14 @@ type cgen = struct { // global receiver wired; mutually exclusive with sretdestoff. sretdestnode: *node, sretforward: i32, + // #22 M3 `-c`: separate-compile / primary-only codegen. Emit code+ + // DATA ONLY for this package's own (imported==0) decls; treat every + // `.wwi`-sourced (imported==1) dep decl as an external. Off on the + // combined path (every existing invocation) so M3 is a pure addition. + // NOT reset by cgeninit (which runs per-fn) — set once in main and + // must survive to the post-loop emitletdataw/emitdefconstants pass, + // like strlits/ffis. Symmetric with cstage Cg.sep_mode. + sepmode: i32, }; // Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar. @@ -1460,6 +1468,14 @@ export fn letpreintern(c: *cgen, file: *node) void = { if (file == nil) { return; }; let d: *node = file.list; for (d != nil) { + // #22 M3: skip imported deps so the strlit table (and its _S_ + // sequence) is a pure function of THIS package's own decls. A + // dep's body initializer would intern here, but its `.wwi` (init + // stripped) would not — gating on imported keeps the + // bodies-vs-.wwi `.s` byte-identical for P's own symbols. Cross- + // module str-def splicing rides the def registry (interned at the + // use site, not here), so it is unaffected. + if (c.sepmode != 0 && d.imported != 0) { d = d.next; continue; }; // #8/GAP-B: a `def [N]str` needs the SAME element-strlit // pre-interning as the let [N]str arm (the #18 ordering // contract) so emitstrarraydata's DATAR rows find their _S_ @@ -2924,6 +2940,13 @@ fn emittupledata(c: *cgen, name: str, module: str, tt: *node, rhs: *node) bool = fn emitletdataw(c: *cgen, file: *node) void = { let d: *node = file.list; for (d != nil) { + // #22 M3 THE ONE REAL GUARD: a `.wwi` dep value-global is + // initializer-less; emitting a DATAW for it would DUPLICATE the + // definition that lives in the dep's own .o → link collision. + // Gate on the explicit imported flag (NOT no-rhs: a package's OWN + // init-less let must still zero-init). Symmetric with M2's + // producer imported==0 filter — same predicate both ways. + if (c.sepmode != 0 && d.imported != 0) { d = d.next; continue; }; if (d.kind == nkind.N_LET) { let nm: str = d.str; if (nm.len > 0) { @@ -3262,6 +3285,12 @@ fn emitletdataw(c: *cgen, file: *node) void = { fn emitdefconstants(c: *cgen, file: *node) void = { let d: *node = file.list; for (d != nil) { + // #22 M3: a `.wwi` dep def with DATA storage (int-fold / float / + // struct / array) must NOT re-emit — the dep's own .o owns the + // symbol. Str defs are inline-spliced (never emitted here), so + // they need no gate; the def registry stays populated for imported + // decls so the target's LOAD paths still resolve the extern. + if (c.sepmode != 0 && d.imported != 0) { d = d.next; continue; }; if (d.kind == nkind.N_DEF) { let r: *node = d.rhs; let v: u64 = 0u64; diff --git a/selfhost/cmd/wcc/cgendecl.ww b/selfhost/cmd/wcc/cgendecl.ww index 72b5f7c8..585d8df4 100644 --- a/selfhost/cmd/wcc/cgendecl.ww +++ b/selfhost/cmd/wcc/cgendecl.ww @@ -647,8 +647,15 @@ export fn cgfile(c: *cgen, file: *node) void = { let d: *node = file.list; for (d != nil) { if (d.kind == nkind.N_FNDECL) { - if (d.body != nil) { - cgfn(c, d); + // #22 M3: emit code ONLY for this package's own decls. A + // `.wwi` dep fn is a body-less prototype that already skips + // (the body != nil gate); the explicit imported gate also + // covers a dep fn that still carries a body in a transitional + // unit, keeping the "emit iff imported==0" rule exception-free. + if (!(c.sepmode != 0 && d.imported != 0)) { + if (d.body != nil) { + cgfn(c, d); + }; }; }; d = d.next; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 3233a0ed..7440ab75 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -41184,8 +41184,15 @@ export fn cgfile(c: *cgen, file: *node) void = { let d: *node = file.list; for (d != nil) { if (d.kind == nkind.N_FNDECL) { - if (d.body != nil) { - cgfn(c, d); + // #22 M3: emit code ONLY for this package's own decls. A + // `.wwi` dep fn is a body-less prototype that already skips + // (the body != nil gate); the explicit imported gate also + // covers a dep fn that still carries a body in a transitional + // unit, keeping the "emit iff imported==0" rule exception-free. + if (!(c.sepmode != 0 && d.imported != 0)) { + if (d.body != nil) { + cgfn(c, d); + }; }; }; d = d.next; @@ -41738,6 +41745,14 @@ type cgen = struct { // global receiver wired; mutually exclusive with sretdestoff. sretdestnode: *node, sretforward: i32, + // #22 M3 `-c`: separate-compile / primary-only codegen. Emit code+ + // DATA ONLY for this package's own (imported==0) decls; treat every + // `.wwi`-sourced (imported==1) dep decl as an external. Off on the + // combined path (every existing invocation) so M3 is a pure addition. + // NOT reset by cgeninit (which runs per-fn) — set once in main and + // must survive to the post-loop emitletdataw/emitdefconstants pass, + // like strlits/ffis. Symmetric with cstage Cg.sep_mode. + sepmode: i32, }; // Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar. @@ -42659,6 +42674,14 @@ export fn letpreintern(c: *cgen, file: *node) void = { if (file == nil) { return; }; let d: *node = file.list; for (d != nil) { + // #22 M3: skip imported deps so the strlit table (and its _S_ + // sequence) is a pure function of THIS package's own decls. A + // dep's body initializer would intern here, but its `.wwi` (init + // stripped) would not — gating on imported keeps the + // bodies-vs-.wwi `.s` byte-identical for P's own symbols. Cross- + // module str-def splicing rides the def registry (interned at the + // use site, not here), so it is unaffected. + if (c.sepmode != 0 && d.imported != 0) { d = d.next; continue; }; // #8/GAP-B: a `def [N]str` needs the SAME element-strlit // pre-interning as the let [N]str arm (the #18 ordering // contract) so emitstrarraydata's DATAR rows find their _S_ @@ -44123,6 +44146,13 @@ fn emittupledata(c: *cgen, name: str, module: str, tt: *node, rhs: *node) bool = fn emitletdataw(c: *cgen, file: *node) void = { let d: *node = file.list; for (d != nil) { + // #22 M3 THE ONE REAL GUARD: a `.wwi` dep value-global is + // initializer-less; emitting a DATAW for it would DUPLICATE the + // definition that lives in the dep's own .o → link collision. + // Gate on the explicit imported flag (NOT no-rhs: a package's OWN + // init-less let must still zero-init). Symmetric with M2's + // producer imported==0 filter — same predicate both ways. + if (c.sepmode != 0 && d.imported != 0) { d = d.next; continue; }; if (d.kind == nkind.N_LET) { let nm: str = d.str; if (nm.len > 0) { @@ -44461,6 +44491,12 @@ fn emitletdataw(c: *cgen, file: *node) void = { fn emitdefconstants(c: *cgen, file: *node) void = { let d: *node = file.list; for (d != nil) { + // #22 M3: a `.wwi` dep def with DATA storage (int-fold / float / + // struct / array) must NOT re-emit — the dep's own .o owns the + // symbol. Str defs are inline-spliced (never emitted here), so + // they need no gate; the def registry stays populated for imported + // decls so the target's LOAD paths still resolve the extern. + if (c.sepmode != 0 && d.imported != 0) { d = d.next; continue; }; if (d.kind == nkind.N_DEF) { let r: *node = d.rhs; let v: u64 = 0u64; diff --git a/test/wcc/989_m3sep_run.c b/test/wcc/989_m3sep_run.c new file mode 100644 index 00000000..49365251 --- /dev/null +++ b/test/wcc/989_m3sep_run.c @@ -0,0 +1,425 @@ +/* + * 989_m3sep_run — M3 `.wwi` separate-compilation CONSUMER gate (task #22 arc). + * + * M3 = w6c `-c` (separate-compile / primary-only codegen) reads each imported + * package's `.wwi` prototypes (M2's producer output) as import scope INSTEAD of + * re-checking dep bodies. Pure addition: combined.ww stays the LIVE path, `-c` + * is off on every existing invocation. This gate certifies the consumer. + * + * Fixture: a leaf -> mid -> root chain carrying ALL FOUR cross-boundary fact- + * classes — fn SIGNATURE (leaf.add), struct/type LAYOUT (leaf.Point, used as a + * by-value param + returned), `def` const VALUE (leaf.SCALE, const-folded into + * mid), and `export let` value-global (leaf.origin_tag, read across the + * boundary). mid imports leaf; root imports both. + * + * THE LOAD-BEARING CLAIM (rob §4.1): compiling package P in sep mode (deps as + * `.wwi`) produces the SAME codegen for P's own symbols as compiling P with + * deps as full BODIES — proven by holding the `-c` codegen FILTER constant and + * varying ONLY the dep-source form: + * Pbodies.s = w6c -c (deps as //ww:module body sections + P) + * Psep.s = w6c -c (deps as //ww:module .wwi sections + P) + * GATE: cmp Pbodies.s Psep.s → byte-identical (per-package, sharp blame). + * Both emit ONLY P's symbols (the `-c` filter), so the cmp proves the `.wwi` + * conveys exactly the type/layout/const facts the dep bodies did, for P's code. + * + * Plus, per package: the keystone on BOTH stages (cstage bodies==sep and + * wwstage bodies==sep — so a wwstage-only guard regression can't hide behind + * the `.wwi`-sourced sep unit), cs==ww at the `.s` level (rule 10 — w6c vs + * w6c_ww on the identical sep-unit), sep-path determinism (compile twice -> + * identical), and the value-global guard (an imported `export let` must NOT + * re-emit DATA — that would duplicate the dep's own definition -> link + * collision). A keystone-only str sub-fixture (sleaf -> smid, never linked) + * makes the strlit-intern guard non-vacuous — see its note below. End-to-end: + * compile each package's .o under `-c`, link with w6l, run -> exit code is the + * real cross-boundary computation; the SAME with the wwstage tools (w6c_ww/ + * w6a_ww/w6l_ww) -> cs==ww at the final-exe level + behavioral identity. + * + * COLD: a fresh getpid-keyed /tmp dir, `.wwi` materialized from scratch every + * run (no warm cache — a stale `.wwi` is a #110-class freshness hazard). No + * source-tree writes -> phase-1 parallel-safe. 9xx is full; shares the 989 + * prefix per the 989_lib_byteid / 989_m2wwi precedent (the short name keys the + * binary). Models 989_m2wwi_run.c conventions. + */ +#include +#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 *body) +{ + FILE *f = fopen(path, "wb"); + if (!f) return -1; + fputs(body, f); + fclose(f); + return 0; +} + +/* concat src files (verbatim bodies, each under a //ww:module directive or the + * //ww:module-reset root boundary) into one sep-unit. `pre` is the directive + * line (without trailing newline) to emit before `src`'s bytes; chained calls + * build the full unit. Returns 0 on success. */ +static int +append_section(FILE *out, const char *directive, const char *srcpath) +{ + char *b = NULL; + size_t n = 0; + if (slurp(srcpath, &b, &n) < 0) return -1; + fprintf(out, "%s\n", directive); + fwrite(b, 1, n, out); + fputc('\n', out); + free(b); + return 0; +} + +/* The fixture. leaf is a leaf package (no imports); mid imports leaf; root + * imports both. All FOUR fact-classes cross the leaf boundary. root's exit code + * is the real cross-boundary computation: + * combine(p) = add(p.x,p.y) + SCALE + origin_tag = add(3,4)+7+42 = 7+7+42 = 56 + * main = combine(mkpoint(3,4)) + add(1,2) = 56 + 3 = 59 + */ +#define EXPECT_EXIT 59 +static const char *leaf_src = + "package leaf;\n" + "export type Point = struct { x: i32, y: i32 };\n" + "export def SCALE: i32 = 7;\n" + "export let origin_tag: i32 = 42;\n" + "export fn add(a: i32, b: i32) i32 = { return a + b; };\n" + "export fn mkpoint(x: i32, y: i32) Point = { return Point { x = x, y = y }; };\n" + "fn leafpriv(z: i32) i32 = { return z * SCALE; };\n"; +static const char *mid_src = + "package mid;\n" + "import leaf;\n" + "export fn combine(p: leaf.Point) i32 = {\n" + " return leaf.add(p.x, p.y) + leaf.SCALE + leaf.origin_tag;\n" + "};\n"; +static const char *root_src = + "package main;\n" + "import leaf;\n" + "import mid;\n" + "fn main() i32 = {\n" + " let p = leaf.mkpoint(3, 4);\n" + " let r = mid.combine(p);\n" + " r = r + leaf.add(1, 2);\n" + " return r;\n" + "};\n"; + +/* Str-literal sub-fixture (sleaf -> smid), KEYSTONE-ONLY (per-package + * bodies-vs-.wwi cmp; never linked, so it sidesteps the global-counter `_S_` + * label collision that blocks cross-unit linking of str-bearing packages — + * filed for M3-tail). Its job is to make the strlit-intern guard (let_pre_ + * intern / letpreintern) non-vacuous: sleaf carries a top-level str `export + * let` whose initializer interns a strlit in sleaf's OWN compile but is + * stripped from sleaf's `.wwi`. With the guard live, a dependent must NOT re- + * intern it, so smid's own strlit stays `_S_0` in both the bodies and `.wwi` + * units -> keystone byte-id. Drop the guard and sleaf.banner interns ahead of + * smid.label in the bodies-unit, shifting it to `_S_1` -> keystone goes RED. + * The i32-only leaf->mid->root fixture has no strlit and is blind to this. */ +static const char *sleaf_src = + "package sleaf;\n" + "export let banner: str = \"sleaf-banner\";\n" + "export fn tag(a: i32) i32 = { return a + 1; };\n"; +static const char *smid_src = + "package smid;\n" + "import sleaf;\n" + "export let label: str = \"smid-label\";\n" + "export fn use(a: i32) i32 = { return sleaf.tag(a); };\n"; + +/* A package's sep-unit + bodies-unit composition. `name` is the dotted package + * path; deps are spliced ahead under //ww:module , then the target's + * own source under //ww:module-reset. */ +struct pkg { + const char *name; /* dotted path / //ww:module directive */ + const char *src; /* the package's own source */ + const char *file; /* on-disk .ww basename */ + const char *wwi; /* produced .wwi basename (NULL if not produced) */ +}; + +int +main(void) +{ + const char *bin = absbin(); + if (!bin) return 1; + char td[64], cmd[8192], p[1024]; + int fail = 0; + + snprintf(td, sizeof td, "/tmp/wwm3_%d", getpid()); + snprintf(cmd, sizeof cmd, "rm -rf %s", td); + runwait(cmd); + mkdir(td, 0755); + + /* materialize the fixture cold */ + snprintf(p, sizeof p, "%s/leaf.ww", td); if (write_file(p, leaf_src)) { fail++; goto out; } + snprintf(p, sizeof p, "%s/mid.ww", td); if (write_file(p, mid_src)) { fail++; goto out; } + snprintf(p, sizeof p, "%s/root.ww", td); if (write_file(p, root_src)) { fail++; goto out; } + snprintf(p, sizeof p, "%s/sleaf.ww", td); if (write_file(p, sleaf_src)) { fail++; goto out; } + snprintf(p, sizeof p, "%s/smid.ww", td); if (write_file(p, smid_src)) { fail++; goto out; } + + char leafww[1024], midww[1024], rootww[1024]; + char leafwwi[1024], midwwi[1024]; + char sleafww[1024], smidww[1024], sleafwwi[1024]; + snprintf(leafww, sizeof leafww, "%s/leaf.ww", td); + snprintf(midww, sizeof midww, "%s/mid.ww", td); + snprintf(rootww, sizeof rootww, "%s/root.ww", td); + snprintf(leafwwi, sizeof leafwwi, "%s/leaf.wwi", td); + snprintf(midwwi, sizeof midwwi, "%s/mid.wwi", td); + snprintf(sleafww, sizeof sleafww, "%s/sleaf.ww", td); + snprintf(smidww, sizeof smidww, "%s/smid.ww", td); + snprintf(sleafwwi, sizeof sleafwwi, "%s/sleaf.wwi", td); + + /* ---- produce dep .wwi COLD, in dependency order -------------------- */ + /* leaf has no deps: produce directly. */ + snprintf(cmd, sizeof cmd, + "timeout 180 %s/w6c -I %s -o /dev/null %s >/dev/null 2>&1", + bin, leafwwi, leafww); + if (runwait(cmd) != 0) { fprintf(stderr, "m3sep FAIL: leaf.wwi produce\n"); fail++; goto out; } + /* mid deps leaf: produce against leaf.wwi (mid primary). */ + { + char unit[1024]; snprintf(unit, sizeof unit, "%s/mid.prod.ww", td); + FILE *u = fopen(unit, "wb"); + if (!u) { fail++; goto out; } + if (append_section(u, "//ww:module leaf", leafwwi) || + append_section(u, "//ww:module-reset", midww)) { fclose(u); fail++; goto out; } + fclose(u); + snprintf(cmd, sizeof cmd, + "timeout 180 %s/w6c -I %s -o /dev/null %s >/dev/null 2>&1", + bin, midwwi, unit); + if (runwait(cmd) != 0) { fprintf(stderr, "m3sep FAIL: mid.wwi produce\n"); fail++; goto out; } + } + /* sleaf has no deps: produce directly (str sub-fixture). */ + snprintf(cmd, sizeof cmd, + "timeout 180 %s/w6c -I %s -o /dev/null %s >/dev/null 2>&1", + bin, sleafwwi, sleafww); + if (runwait(cmd) != 0) { fprintf(stderr, "m3sep FAIL: sleaf.wwi produce\n"); fail++; goto out; } + + /* ---- per-package .s gate: keystone + cs==ww + determinism ---------- */ + /* compose mid's bodies-unit and sep-unit, then root's. */ + /* [8] (not [6]) so a 6-entry initializer leaves a NULL terminator in the + * trailing slots — the k+=2 loop stops at the first NULL. */ + struct { const char *label; const char *combine_bodies[8]; const char *combine_sep[8]; } + units[] = { + { "mid", + /* bodies: leaf body, then mid */ + { "//ww:module leaf", leafww, "//ww:module-reset", midww, NULL, NULL }, + /* sep: leaf.wwi, then mid */ + { "//ww:module leaf", leafwwi, "//ww:module-reset", midww, NULL, NULL } }, + { "root", + { "//ww:module leaf", leafww, "//ww:module mid", midww, "//ww:module-reset", rootww }, + { "//ww:module leaf", leafwwi, "//ww:module mid", midwwi, "//ww:module-reset", rootww } }, + /* str sub-fixture: keystone-only (this loop never links), so the + * cross-unit `_S_` collision is moot here. Exercises the strlit- + * intern guard — neutralize it and sleaf.banner interns ahead of + * smid.label in the bodies-unit, desyncing `_S_` -> keystone RED. */ + { "smid", + { "//ww:module sleaf", sleafww, "//ww:module-reset", smidww, NULL, NULL }, + { "//ww:module sleaf", sleafwwi, "//ww:module-reset", smidww, NULL, NULL } }, + }; + + for (int i = 0; i < (int)(sizeof units / sizeof units[0]); i++) { + char bodies[1024], sep[1024]; + snprintf(bodies, sizeof bodies, "%s/%s.bodies.ww", td, units[i].label); + snprintf(sep, sizeof sep, "%s/%s.sep.ww", td, units[i].label); + FILE *bf = fopen(bodies, "wb"), *sf = fopen(sep, "wb"); + if (!bf || !sf) { if (bf) fclose(bf); if (sf) fclose(sf); fail++; goto out; } + int berr = 0, serr = 0; + for (int k = 0; units[i].combine_bodies[k]; k += 2) + berr |= append_section(bf, units[i].combine_bodies[k], units[i].combine_bodies[k+1]); + for (int k = 0; units[i].combine_sep[k]; k += 2) + serr |= append_section(sf, units[i].combine_sep[k], units[i].combine_sep[k+1]); + fclose(bf); fclose(sf); + if (berr || serr) { fail++; goto out; } + + char csb[1024], css[1024], wws[1024], css2[1024], wwb[1024]; + snprintf(csb, sizeof csb, "%s/%s.bodies.cs.s", td, units[i].label); + snprintf(css, sizeof css, "%s/%s.sep.cs.s", td, units[i].label); + snprintf(wws, sizeof wws, "%s/%s.sep.ww.s", td, units[i].label); + snprintf(css2, sizeof css2, "%s/%s.sep.cs2.s", td, units[i].label); + snprintf(wwb, sizeof wwb, "%s/%s.bodies.ww.s", td, units[i].label); + + snprintf(cmd, sizeof cmd, "timeout 180 %s/w6c -c -o %s %s >/dev/null 2>&1", bin, csb, bodies); + if (runwait(cmd) != 0) { fprintf(stderr, "m3sep FAIL: %s bodies -c\n", units[i].label); fail++; continue; } + snprintf(cmd, sizeof cmd, "timeout 180 %s/w6c -c -o %s %s >/dev/null 2>&1", bin, css, sep); + if (runwait(cmd) != 0) { fprintf(stderr, "m3sep FAIL: %s sep -c (cstage)\n", units[i].label); fail++; continue; } + snprintf(cmd, sizeof cmd, "timeout 180 %s/w6c_ww -c -o %s %s >/dev/null 2>&1", bin, wws, sep); + if (runwait(cmd) != 0) { fprintf(stderr, "m3sep FAIL: %s sep -c (wwstage)\n", units[i].label); fail++; continue; } + snprintf(cmd, sizeof cmd, "timeout 180 %s/w6c -c -o %s %s >/dev/null 2>&1", bin, css2, sep); + if (runwait(cmd) != 0) { fprintf(stderr, "m3sep FAIL: %s sep -c (determinism re-run)\n", units[i].label); fail++; continue; } + snprintf(cmd, sizeof cmd, "timeout 180 %s/w6c_ww -c -o %s %s >/dev/null 2>&1", bin, wwb, bodies); + if (runwait(cmd) != 0) { fprintf(stderr, "m3sep FAIL: %s bodies -c (wwstage)\n", units[i].label); fail++; continue; } + + /* KEYSTONE: deps-as-bodies == deps-as-.wwi (holding -c). */ + if (files_eq(csb, css) != 0) { + fprintf(stderr, "m3sep FAIL: %s — bodies.s != sep.s (the .wwi does " + "not convey the dep facts P's codegen needs)\n", units[i].label); + fail++; + } + /* wwstage keystone: the cstage cmp above compiles BODIES with the C + * stage, so a wwstage-only guard regression (e.g. letpreintern) would + * not shift the .wwi-sourced sep unit and would slip. Compile the + * bodies unit with w6c_ww too and hold the same bodies==sep invariant + * on the wwstage side. */ + if (files_eq(wwb, wws) != 0) { + fprintf(stderr, "m3sep FAIL: %s — ww bodies.s != ww sep.s " + "(wwstage keystone)\n", units[i].label); + fail++; + } + /* rule 10: cstage sep.s == wwstage sep.s. */ + if (files_eq(css, wws) != 0) { + fprintf(stderr, "m3sep FAIL: %s — cs sep.s != ww sep.s (rule 10)\n", units[i].label); + fail++; + } + /* determinism: same input twice -> identical. */ + if (files_eq(css, css2) != 0) { + fprintf(stderr, "m3sep FAIL: %s — sep codegen non-deterministic\n", units[i].label); + fail++; + } + /* value-global guard: an imported `export let` must NOT re-emit. The + * `mid`/`root` sep-units import leaf.origin_tag; its DATAW must appear + * ONLY in leaf's own compile, never in a dependent's. */ + { + char *s = NULL; size_t n = 0; + if (slurp(css, &s, &n) == 0) { + /* the DEFINITION is `DATAW origin_tag(SB),...`; a READ is + * `LEAQ origin_tag(SB),...` (legitimately present — the + * dependent loads the imported global). Gate on the DATA[W] + * definition only. */ + if (strstr(s, "DATAW origin_tag(SB)") != NULL + || strstr(s, "DATA origin_tag(SB)") != NULL) { + fprintf(stderr, "m3sep FAIL: %s — imported value-global " + "origin_tag re-emitted (link collision)\n", units[i].label); + fail++; + } + free(s); + } + } + } + + /* ---- behavioral + final-exe cs==ww --------------------------------- */ + /* Build each package's .o under -c with BOTH toolchains, link, run. + * leaf is its own primary (no deps); mid/root use their sep-units. */ + { + char leafsep_cs[1024], midsep[1024], rootsep[1024]; + snprintf(midsep, sizeof midsep, "%s/mid.sep.ww", td); + snprintf(rootsep, sizeof rootsep, "%s/root.sep.ww", td); + (void)leafsep_cs; + + struct { const char *tool_c, *tool_a, *tool_l; const char *tag; } tc[] = { + { "w6c", "w6a", "w6l", "cs" }, + { "w6c_ww", "w6a_ww", "w6l_ww", "ww" }, + }; + char exe[2][1024]; + for (int t = 0; t < 2; t++) { + char ls[1024], lo[1024], ms[1024], mo[1024], rs[1024], ro[1024]; + snprintf(ls, sizeof ls, "%s/leaf.%s.s", td, tc[t].tag); + snprintf(lo, sizeof lo, "%s/leaf.%s.o", td, tc[t].tag); + snprintf(ms, sizeof ms, "%s/mid.%s.s", td, tc[t].tag); + snprintf(mo, sizeof mo, "%s/mid.%s.o", td, tc[t].tag); + snprintf(rs, sizeof rs, "%s/root.%s.s", td, tc[t].tag); + snprintf(ro, sizeof ro, "%s/root.%s.o", td, tc[t].tag); + snprintf(exe[t], sizeof exe[t], "%s/prog.%s", td, tc[t].tag); + + int ok = 1; + snprintf(cmd, sizeof cmd, "timeout 180 %s/%s -c -o %s %s >/dev/null 2>&1 && timeout 180 %s/%s -o %s %s >/dev/null 2>&1", + bin, tc[t].tool_c, ls, leafww, bin, tc[t].tool_a, lo, ls); + if (runwait(cmd) != 0) ok = 0; + snprintf(cmd, sizeof cmd, "timeout 180 %s/%s -c -o %s %s >/dev/null 2>&1 && timeout 180 %s/%s -o %s %s >/dev/null 2>&1", + bin, tc[t].tool_c, ms, midsep, bin, tc[t].tool_a, mo, ms); + if (runwait(cmd) != 0) ok = 0; + snprintf(cmd, sizeof cmd, "timeout 180 %s/%s -c -o %s %s >/dev/null 2>&1 && timeout 180 %s/%s -o %s %s >/dev/null 2>&1", + bin, tc[t].tool_c, rs, rootsep, bin, tc[t].tool_a, ro, rs); + if (runwait(cmd) != 0) ok = 0; + snprintf(cmd, sizeof cmd, "timeout 180 %s/%s -o %s %s %s %s %s/../lib/libwwrt.a >/dev/null 2>&1", + bin, tc[t].tool_l, exe[t], ro, mo, lo, bin); + if (runwait(cmd) != 0) ok = 0; + if (!ok) { + fprintf(stderr, "m3sep FAIL: %s toolchain sep build/link failed\n", tc[t].tag); + fail++; + exe[t][0] = '\0'; + continue; + } + int rc = runwait(exe[t]); + if (rc != EXPECT_EXIT) { + fprintf(stderr, "m3sep FAIL: %s sep program exit=%d, expected %d " + "(cross-boundary behavior wrong)\n", tc[t].tag, rc, EXPECT_EXIT); + fail++; + } + } + /* cs==ww at the final-exe level (rule 10 end-to-end). Symbol order + * is identical because both toolchains link the SAME object set in + * the SAME order; only the compiler differs, and it is byte-id at + * the .s level (proven above), so the linked images match too. */ + if (exe[0][0] && exe[1][0] && files_eq(exe[0], exe[1]) != 0) { + fprintf(stderr, "m3sep FAIL: cs exe != ww exe (rule 10, final image)\n"); + fail++; + } + } + +out: + snprintf(cmd, sizeof cmd, "rm -rf %s", td); + runwait(cmd); + if (fail) { + fprintf(stderr, "m3sep: %d check(s) failed\n", fail); + return 1; + } + printf("m3sep: synth leaf->mid->root — per-pkg bodies==.wwi (-c) + cs==ww " + ".s/exe + determinism + value-global guard + behavioral (exit %d)\n", + EXPECT_EXIT); + return 0; +}