From 5bbb81222fa64983b44662971b8f41e7dbdbbfd1 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Wed, 3 Jun 2026 20:55:52 +0900 Subject: [PATCH] w6c_ww: emit a bare str/slice global zero-header once, keyed on type kind (fix #10 part b) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Post-#1, size(str) == size(slice) == 24. emitletdataw's str arm (~cgen.ww:2074) and slice arm (~cgen.ww:2139) were sequential `if`s gated on SIZE alone, so a bare 24-byte global matched BOTH and BOTH fired the no-rhs zero fallback — two `DATAW main.g` rows. cstage discriminates on type kind (let_isstr/let_isslice, cgen.c:1026/1036) and emits one; the link+run is correct either way, so the divergence was byte-id-visible only. Gate the two arms on the declared type kind via the new letdeclkind helper (d.lhs.type_, TY_NAMED-peeled — the resolvewalk-stamped type-expression node), mutually exclusive: a 24B global now hits one arm. Falls to the str arm when unstamped, where the zero-init bytes are identical, so byte-id holds for that case too. cstage already correct — no change. New test 686 (5 runtime rows + 5 byte-id rows) pins single-emit + cs==ww. --- Makefile | 7 + selfhost/cmd/w6c/main.combined.ww | 22 ++- selfhost/cmd/wcc/cgen.ww | 22 ++- selfhost/cmd/wwdump/main.combined.ww | 22 ++- test/wcc/686_slice_str_global_zero.c | 233 +++++++++++++++++++++++++++ 5 files changed, 300 insertions(+), 6 deletions(-) create mode 100644 test/wcc/686_slice_str_global_zero.c diff --git a/Makefile b/Makefile index e62be690..d92ab015 100644 --- a/Makefile +++ b/Makefile @@ -243,6 +243,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_arr_strslice_elem \ $(BIN)/test_arr_tagged_elem \ $(BIN)/test_arr_infer_len \ + $(BIN)/test_slice_str_global_zero \ $(BIN)/test_dot_str_chained_arg \ $(BIN)/test_dot_slice_arg \ $(BIN)/test_dot_tagged_source \ @@ -575,6 +576,12 @@ $(BIN)/test_arr_tagged_elem: test/wcc/685_arr_tagged_elem.c $(BIN)/ww \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_slice_str_global_zero: test/wcc/686_slice_str_global_zero.c $(BIN)/ww \ + $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_dot_str_chained_arg: test/wcc/692_dot_str_chained_arg.c $(BIN)/ww \ $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 7091597e..1cc8dd81 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -32923,6 +32923,24 @@ fn letvarisslice(c: *cgen, name: str) bool = { return false; }; +// letdeclkind — unwrapped (TY_NAMED-peeled) kind of the let decl's +// declared type, read off the checker-stamped type-expression node +// (d.lhs.type_; resolvewalk stamps N_TNAME/N_TSLICE/… at check.ww:566). +// Mirrors cstage type_unwrap(d->type)->kind, the basis of let_isstr/ +// let_isslice (cgen.c:1026/1036). Post-#1 str and slice share a 24B +// header, so emitletdataw's size-only str and slice arms BOTH fired for +// a bare 24B global and double-emitted its DATAW (#10 part b); these two +// arms now branch on kind, not size. nil-safe: returns TY_VOID when +// unstamped so a 24B global still falls to the str arm (zero-init bytes +// are identical either way, so byte-id holds for the unstamped case). +fn letdeclkind(d: *node) tykind = { + if (d.lhs == nil) { return tykind.TY_VOID; }; + let ti: *tinfo = d.lhs.type_: *tinfo; + for (ti != nil && ti.kind == tykind.TY_NAMED) { ti = ti.under; }; + if (ti == nil) { return tykind.TY_VOID; }; + return ti.kind; +}; + // letvarisfloat — slot size for a named float global, or 0 if not // a float-typed let. Walks aliases so the byte-identity contract // matches C cgen's `let_isfloat` (which resolves Type kinds). @@ -33935,7 +33953,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { emitline("\"\n"); }; }; - if (sz == primtypesize("str"): i32 && !issg) { + if (sz == primtypesize("str"): i32 && !issg && letdeclkind(d) != tykind.TY_SLICE) { let r: *node = d.rhs; for (r != nil) { if (r.kind != nkind.N_CAST) { break; }; @@ -34000,7 +34018,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { }; }; }; - if (sz == tyslicesize(): i32 && !issg) { + if (sz == tyslicesize(): i32 && !issg && letdeclkind(d) == tykind.TY_SLICE) { // Slice: zero-init only (no slice-literal // syntax to honour). Any rhs other than // `nil` is skipped → undefined symbol at diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index da9b466b..cc1d79a9 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -1059,6 +1059,24 @@ fn letvarisslice(c: *cgen, name: str) bool = { return false; }; +// letdeclkind — unwrapped (TY_NAMED-peeled) kind of the let decl's +// declared type, read off the checker-stamped type-expression node +// (d.lhs.type_; resolvewalk stamps N_TNAME/N_TSLICE/… at check.ww:566). +// Mirrors cstage type_unwrap(d->type)->kind, the basis of let_isstr/ +// let_isslice (cgen.c:1026/1036). Post-#1 str and slice share a 24B +// header, so emitletdataw's size-only str and slice arms BOTH fired for +// a bare 24B global and double-emitted its DATAW (#10 part b); these two +// arms now branch on kind, not size. nil-safe: returns TY_VOID when +// unstamped so a 24B global still falls to the str arm (zero-init bytes +// are identical either way, so byte-id holds for the unstamped case). +fn letdeclkind(d: *node) tykind = { + if (d.lhs == nil) { return tykind.TY_VOID; }; + let ti: *tinfo = d.lhs.type_: *tinfo; + for (ti != nil && ti.kind == tykind.TY_NAMED) { ti = ti.under; }; + if (ti == nil) { return tykind.TY_VOID; }; + return ti.kind; +}; + // letvarisfloat — slot size for a named float global, or 0 if not // a float-typed let. Walks aliases so the byte-identity contract // matches C cgen's `let_isfloat` (which resolves Type kinds). @@ -2071,7 +2089,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { emitline("\"\n"); }; }; - if (sz == primtypesize("str"): i32 && !issg) { + if (sz == primtypesize("str"): i32 && !issg && letdeclkind(d) != tykind.TY_SLICE) { let r: *node = d.rhs; for (r != nil) { if (r.kind != nkind.N_CAST) { break; }; @@ -2136,7 +2154,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { }; }; }; - if (sz == tyslicesize(): i32 && !issg) { + if (sz == tyslicesize(): i32 && !issg && letdeclkind(d) == tykind.TY_SLICE) { // Slice: zero-init only (no slice-literal // syntax to honour). Any rhs other than // `nil` is skipped → undefined symbol at diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index f5daa0ed..a861f708 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -32923,6 +32923,24 @@ fn letvarisslice(c: *cgen, name: str) bool = { return false; }; +// letdeclkind — unwrapped (TY_NAMED-peeled) kind of the let decl's +// declared type, read off the checker-stamped type-expression node +// (d.lhs.type_; resolvewalk stamps N_TNAME/N_TSLICE/… at check.ww:566). +// Mirrors cstage type_unwrap(d->type)->kind, the basis of let_isstr/ +// let_isslice (cgen.c:1026/1036). Post-#1 str and slice share a 24B +// header, so emitletdataw's size-only str and slice arms BOTH fired for +// a bare 24B global and double-emitted its DATAW (#10 part b); these two +// arms now branch on kind, not size. nil-safe: returns TY_VOID when +// unstamped so a 24B global still falls to the str arm (zero-init bytes +// are identical either way, so byte-id holds for the unstamped case). +fn letdeclkind(d: *node) tykind = { + if (d.lhs == nil) { return tykind.TY_VOID; }; + let ti: *tinfo = d.lhs.type_: *tinfo; + for (ti != nil && ti.kind == tykind.TY_NAMED) { ti = ti.under; }; + if (ti == nil) { return tykind.TY_VOID; }; + return ti.kind; +}; + // letvarisfloat — slot size for a named float global, or 0 if not // a float-typed let. Walks aliases so the byte-identity contract // matches C cgen's `let_isfloat` (which resolves Type kinds). @@ -33935,7 +33953,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { emitline("\"\n"); }; }; - if (sz == primtypesize("str"): i32 && !issg) { + if (sz == primtypesize("str"): i32 && !issg && letdeclkind(d) != tykind.TY_SLICE) { let r: *node = d.rhs; for (r != nil) { if (r.kind != nkind.N_CAST) { break; }; @@ -34000,7 +34018,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { }; }; }; - if (sz == tyslicesize(): i32 && !issg) { + if (sz == tyslicesize(): i32 && !issg && letdeclkind(d) == tykind.TY_SLICE) { // Slice: zero-init only (no slice-literal // syntax to honour). Any rhs other than // `nil` is skipped → undefined symbol at diff --git a/test/wcc/686_slice_str_global_zero.c b/test/wcc/686_slice_str_global_zero.c new file mode 100644 index 00000000..8cef21ba --- /dev/null +++ b/test/wcc/686_slice_str_global_zero.c @@ -0,0 +1,233 @@ +/* + * 686_slice_str_global_zero — a bare module-level `str` global and a + * bare `[]u8` slice global emit their 24-byte zero header EXACTLY ONCE, + * and cstage / wwstage agree byte-for-byte (task #10 part b). + * + * The bug (wwstage only): post-#1, size(str) == size(slice) == 24. The + * emitletdataw str arm and slice arm were sequential `if`s gated on + * SIZE alone, so both matched a 24-byte bare global and BOTH fired the + * no-rhs zero fallback — two `DATAW main.g` rows. cstage discriminates + * on type kind (let_isstr / let_isslice) and emits one. Runtime reads + * the null header {0,0,0} correctly either way, so the divergence is + * byte-id-visible only — the asm_byte_identical gate is the real + * discriminator; the exit-code rows assert correctness is preserved. + * + * The fix: emitletdataw's str and slice arms now branch on the declared + * type kind (letdeclkind, TY_NAMED-peeled, mirroring let_isstr/ + * let_isslice), mutually exclusive, so a 24B global hits one arm. + * + * row | shape | want + * -----------------+----------------------------------------+------ + * slice_len | global let g:[]u8; g.len | 0 + * slice_cap | global let g:[]u8; g.cap | 0 + * slice_ptr_null | global let g:[]u8; g.ptr==nil ? 7 : 0 | 7 + * str_len | global let s:str; s.len | 0 + * str_ptr_null | global let s:str; s.ptr==nil ? 9 : 0 | 9 + */ +#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; +} + +struct row { const char *label; const char *src; int want; }; + +static const struct row rows[] = { + { "slice_len", + "package main;\n" + "let g: []u8;\n" + "export fn main() i32 = { return g.len: i32; };\n", + 0 }, + + { "slice_cap", + "package main;\n" + "let g: []u8;\n" + "export fn main() i32 = { return g.cap: i32; };\n", + 0 }, + + { "slice_ptr_null", + "package main;\n" + "let g: []u8;\n" + "export fn main() i32 = {\n" + "\tif (g.ptr == nil) { return 7; };\n" + "\treturn 0;\n" + "};\n", + 7 }, + + { "str_len", + "package main;\n" + "let s: str;\n" + "export fn main() i32 = { return s.len: i32; };\n", + 0 }, + + { "str_ptr_null", + "package main;\n" + "let s: str;\n" + "export fn main() i32 = {\n" + "\tif (s.ptr == nil) { return 9; };\n" + "\treturn 0;\n" + "};\n", + 9 }, +}; + +static int +run_driver(const char *driver, const struct row *r, int i) +{ + char src[64], tmpdir[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/ssgz_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/ssgz_%d_d_%d", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>/dev/null", + tmpdir, driver, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: build via %s failed\n", + r->label, driver); + unlink(src); rmdir(tmpdir); + return -1; + } + + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + char outbin[128]; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + int got = runwait(outbin); + + unlink(src); unlink(outbin); rmdir(tmpdir); + return got; +} + +/* asm_byte_identical — w6c vs w6c_ww .s for the same source must match. + * This is the #10-part-b discriminator: the old size-keyed double-emit + * produced a second DATAW row in the wwstage .s. */ +static int +asm_byte_identical(const char *bin, const struct row *r, int i) +{ + char src[64], cs[64], ws[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/ssgz_asm_%d_%d.ww", getpid(), i); + snprintf(cs, sizeof cs, "/tmp/ssgz_asm_%d_%d_c.s", getpid(), i); + snprintf(ws, sizeof ws, "/tmp/ssgz_asm_%d_%d_w.s", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s 2>/dev/null", bin, cs, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c errored\n", r->label); + unlink(src); + return -1; + } + snprintf(cmd, sizeof cmd, "%s/w6c_ww -o %s %s 2>/dev/null", + bin, ws, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c_ww errored\n", r->label); + unlink(src); unlink(cs); + return -1; + } + + FILE *fc = fopen(cs, "rb"); + FILE *fw = fopen(ws, "rb"); + int rc = 0; + if (!fc || !fw) { + rc = -1; + } else { + for (;;) { + int a = fgetc(fc); + int b = fgetc(fw); + if (a != b) { rc = -1; break; } + if (a == EOF) break; + } + } + if (fc) fclose(fc); + if (fw) fclose(fw); + if (rc != 0) + fprintf(stderr, "row[%s]: cstage vs wwstage asm differs\n", + r->label); + unlink(src); unlink(cs); unlink(ws); + return rc; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char cdrv[1024]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + char wdrv[1024]; + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + struct { const char *name; const char *path; int gated_on_existence; } + drivers[] = { + { "cstage", cdrv, 0 }, + { "wwstage", wdrv, 1 }, + { NULL, NULL, 0 }, + }; + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + + for (int d = 0; drivers[d].name; d++) { + if (drivers[d].gated_on_existence + && access(drivers[d].path, X_OK) != 0) { + fprintf(stderr, "slice_str_global_zero: skip %s (no %s)\n", + drivers[d].name, drivers[d].path); + continue; + } + for (int i = 0; i < n; i++) { + int got = run_driver(drivers[d].path, &rows[i], i); + total++; + if (got != rows[i].want) { + fprintf(stderr, + "slice_str_global_zero[%s][%s]: exit=%d want=%d\n", + drivers[d].name, rows[i].label, + got, rows[i].want); + fail++; + } + } + } + + if (access(wdrv, X_OK) == 0) { + for (int i = 0; i < n; i++) { + total++; + if (asm_byte_identical(bin, &rows[i], i) != 0) + fail++; + } + } + + if (fail) { + fprintf(stderr, + "slice_str_global_zero: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("slice_str_global_zero: %d/%d ok\n", total, total); + return 0; +}