From c893c4bc379126c64991346d3abb21c23aeb40df Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Mon, 18 May 2026 02:49:06 +0900 Subject: [PATCH] selfhost+test: zero-init !void / void-alias let-decl slots (#22) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wwstage's cglet skipped MOVQ $0 for sz=8 slots that cstage zero-inits unconditionally — !void error types (utf8.invalid) and void-alias variant tags (utf8.done / utf8.more) drifted byte-id post-utf8 + lib/strings; promotes STATUS-3 #22 from latent to bootstrap-blocking. Cstage emits MOVQ $0, -K(BP) in the prologue for any sz=8 let-decl slot via the natural type-fallthrough; wwstage's `typeis8byteprimitive` helper returned false on N_TBANG and on N_TNAME pointing to an alias that resolves to void, so the gate never fired and the slot stayed uninitialised. Polarity catalog: wwstage UNDER — `typeis8byteprimitive` classifier too narrow at N_TBANG and void-alias N_TNAME. Convergence wwstage → cstage's natural sz=8 fallthrough (rule 10). N_TBANG arm recurses on inner type (cmd/wcc/check.c:290 resolve_type copies T's kind, only sets iserror — so !T is 8B iff T is 8B); void-alias N_TNAME resolves through alias- recursion the same way. Tests: - 724_letdecl_zeroinit pins MOVQ $0, -K(BP) presence between function prologue and body on canonical !void and void- alias rows, plus cmp -s byte-id between stages per row. Filed follow-up (NOT in scope here): #25 wwstage 8B struct without rhs still under-emits (structlookup != nil short- circuits the classifier). Same family as STATUS-4 #36 primsize composite-aware sizing. No in-tree consumer. 96/96 ok. 995_self_rebuild stays green (ww2==ww3==ww4 byte-id). --- Makefile | 5 + selfhost/cmd/w6c/main.combined.ww | 22 +++ selfhost/cmd/wcc/cgenutil.ww | 22 +++ selfhost/cmd/wwdump/main.combined.ww | 22 +++ test/wcc/724_letdecl_zeroinit.c | 196 +++++++++++++++++++++++++++ 5 files changed, 267 insertions(+) create mode 100644 test/wcc/724_letdecl_zeroinit.c diff --git a/Makefile b/Makefile index 10028480..cf5a5d2e 100644 --- a/Makefile +++ b/Makefile @@ -252,6 +252,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_match_slice_variant_run \ $(BIN)/test_composite_call_arg \ $(BIN)/test_composite_call_arg_run \ + $(BIN)/test_letdecl_zeroinit \ $(BIN)/test_param_shadow_mod \ $(BIN)/test_localoff_scope \ $(BIN)/test_cast_enum_movl \ @@ -567,6 +568,10 @@ $(BIN)/test_composite_call_arg_run: test/wcc/927_composite_call_arg_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_letdecl_zeroinit: test/wcc/724_letdecl_zeroinit.c \ + $(BIN)/w6c $(BIN)/w6c_ww | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_use_promote_alias: test/wcc/699_use_promote_alias.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 809f0cff..5aad623d 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -7238,14 +7238,36 @@ fn typeis8byteprimitive(c: *cgen, t: *node) bool = { }; if (k == nkind.N_TTUPLE) { return false; }; if (k == nkind.N_TTAGGED){ return false; }; + // STATUS-3 #22: `!T` carries the error flag on T's underlying + // shape (cmd/wcc/check.c:290 resolve_type N_TBANG copies T's + // kind, just sets iserror). cstage's N_LET sizes off lu->kind, + // so `!void`/`!i32` land in the sz=8 default and `!str`/`!slice` + // keep their composite slot. Defer to the inner type so + // `let e: !void;` mirrors cstage's MOVQ $0 while `!str` falls + // through to the multi-word fill. + if (k == nkind.N_TBANG) { return typeis8byteprimitive(c, t.lhs); }; if (k == nkind.N_TNAME) { let nm: str = t.str; if (streq(nm, "str")) { return false; }; + // Plain `void` slot: cstage sz=8 default → MOVQ $0. The let- + // decl is a phantom (a tagged-union variant tag carrier), but + // the slot is still 8B and zero-inits like any other prim. + if (streq(nm, "void")) { return true; }; // Struct alias: not a primitive even if the slot is 8B. if (structlookup(c, nm) != nil) { return false; }; // Primitive (i8/u8/.../i64/u64/bool/rune/f32/f64/int/...). // All of these get slot-padded to 8 and zero-init in C. if (primsize(nm) > 0) { return true; }; + // STATUS-3 #22: alias to `!T` or to `void` (Hare-style error + // type / phantom variant). cstage resolves the alias and + // lands on sz=8 default. Follow through aliaslookup so + // `type invalid = !void;` and `type done = void;` zero-init. + if (c != nil) { + let aliased: *node = aliaslookup(c, nm); + if (aliased != nil) { + return typeis8byteprimitive(c, aliased); + }; + }; return false; }; return false; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 946a61d1..0cf21d17 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -854,14 +854,36 @@ fn typeis8byteprimitive(c: *cgen, t: *node) bool = { }; if (k == nkind.N_TTUPLE) { return false; }; if (k == nkind.N_TTAGGED){ return false; }; + // STATUS-3 #22: `!T` carries the error flag on T's underlying + // shape (cmd/wcc/check.c:290 resolve_type N_TBANG copies T's + // kind, just sets iserror). cstage's N_LET sizes off lu->kind, + // so `!void`/`!i32` land in the sz=8 default and `!str`/`!slice` + // keep their composite slot. Defer to the inner type so + // `let e: !void;` mirrors cstage's MOVQ $0 while `!str` falls + // through to the multi-word fill. + if (k == nkind.N_TBANG) { return typeis8byteprimitive(c, t.lhs); }; if (k == nkind.N_TNAME) { let nm: str = t.str; if (streq(nm, "str")) { return false; }; + // Plain `void` slot: cstage sz=8 default → MOVQ $0. The let- + // decl is a phantom (a tagged-union variant tag carrier), but + // the slot is still 8B and zero-inits like any other prim. + if (streq(nm, "void")) { return true; }; // Struct alias: not a primitive even if the slot is 8B. if (structlookup(c, nm) != nil) { return false; }; // Primitive (i8/u8/.../i64/u64/bool/rune/f32/f64/int/...). // All of these get slot-padded to 8 and zero-init in C. if (primsize(nm) > 0) { return true; }; + // STATUS-3 #22: alias to `!T` or to `void` (Hare-style error + // type / phantom variant). cstage resolves the alias and + // lands on sz=8 default. Follow through aliaslookup so + // `type invalid = !void;` and `type done = void;` zero-init. + if (c != nil) { + let aliased: *node = aliaslookup(c, nm); + if (aliased != nil) { + return typeis8byteprimitive(c, aliased); + }; + }; return false; }; return false; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index c7c476d3..82614b22 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -7238,14 +7238,36 @@ fn typeis8byteprimitive(c: *cgen, t: *node) bool = { }; if (k == nkind.N_TTUPLE) { return false; }; if (k == nkind.N_TTAGGED){ return false; }; + // STATUS-3 #22: `!T` carries the error flag on T's underlying + // shape (cmd/wcc/check.c:290 resolve_type N_TBANG copies T's + // kind, just sets iserror). cstage's N_LET sizes off lu->kind, + // so `!void`/`!i32` land in the sz=8 default and `!str`/`!slice` + // keep their composite slot. Defer to the inner type so + // `let e: !void;` mirrors cstage's MOVQ $0 while `!str` falls + // through to the multi-word fill. + if (k == nkind.N_TBANG) { return typeis8byteprimitive(c, t.lhs); }; if (k == nkind.N_TNAME) { let nm: str = t.str; if (streq(nm, "str")) { return false; }; + // Plain `void` slot: cstage sz=8 default → MOVQ $0. The let- + // decl is a phantom (a tagged-union variant tag carrier), but + // the slot is still 8B and zero-inits like any other prim. + if (streq(nm, "void")) { return true; }; // Struct alias: not a primitive even if the slot is 8B. if (structlookup(c, nm) != nil) { return false; }; // Primitive (i8/u8/.../i64/u64/bool/rune/f32/f64/int/...). // All of these get slot-padded to 8 and zero-init in C. if (primsize(nm) > 0) { return true; }; + // STATUS-3 #22: alias to `!T` or to `void` (Hare-style error + // type / phantom variant). cstage resolves the alias and + // lands on sz=8 default. Follow through aliaslookup so + // `type invalid = !void;` and `type done = void;` zero-init. + if (c != nil) { + let aliased: *node = aliaslookup(c, nm); + if (aliased != nil) { + return typeis8byteprimitive(c, aliased); + }; + }; return false; }; return false; diff --git a/test/wcc/724_letdecl_zeroinit.c b/test/wcc/724_letdecl_zeroinit.c new file mode 100644 index 00000000..514a1f61 --- /dev/null +++ b/test/wcc/724_letdecl_zeroinit.c @@ -0,0 +1,196 @@ +/* + * 724_letdecl_zeroinit — sentinel for STATUS-3 #22. Bare `let x: T;` + * (no rhs) where T is `!void`, `void`, or a name-aliased type that + * resolves to either — sz==8 but `typeis8byteprimitive` returns false + * because there's no N_TBANG arm. Cstage at cmd/w6c/cgen.c:6381 emits + * `MOVQ $0, -K(BP)` for any 8B slot unconditionally; wwstage skipped + * the emit, leaving the slot uninit (stack residue). + * + * Filed STATUS-3 #22, previously byte-id drift only: 995_self_rebuild + * masked the divergence because the bootstrap main.combined.ww had no + * `let x: !void;` sites. utf8.next / utf8.utf8sz / utf8.validate + * introduced them with lib/encoding/utf8 landing; lib/strings's + * nested-if shapes (task #15) compound the drift past the per-rebuild + * threshold — promotes #22 from latent to bootstrap-blocking. + * + * Fix aligns wwstage DOWN to cstage (rule 10) by extending + * `typeis8byteprimitive` to recognise the same sz=8 default arms + * cstage's N_LET falls through (N_TBANG recurses on inner, plain + * `void`, alias chains via aliaslookup). Gating purely on sz==8 + * would over-zero sub-8B structs whose wwstage slot pads to 8 — the + * classifier-level fix is the narrow match. + * + * Rows pin asm-presence in both stages on the canonical shape and + * cmp -s byte-id between them. + */ +#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; }; + +static const struct row rows[] = { + /* `!void`-aliased let-decl — the utf8.invalid shape. */ + { "bang_void_letdecl", + "type invalid = !void;\n" + "export fn produce() (i32 | invalid) = {\n" + " let e: invalid;\n" + " return e;\n" + "};\n" }, + /* Plain-`void`-aliased let-decl — the utf8.done / utf8.more shape. */ + { "void_alias_letdecl", + "type done = void;\n" + "export fn produce() (i32 | done) = {\n" + " let d: done;\n" + " return d;\n" + "};\n" }, +}; + +static int +slurp(const char *path, char *buf, size_t cap) +{ + FILE *f = fopen(path, "rb"); + if (!f) return -1; + size_t n = fread(buf, 1, cap - 1, f); + fclose(f); + buf[n] = '\0'; + return (int)n; +} + +/* The let-decl's slot is the first local off BP; cstage emits + * `MOVQ $0, -8(BP)` (or the appropriate slot offset). We just look + * for the literal `MOVQ\t$0, -` substring inside the produce body + * (between the function label and the RET). */ +static int +check_movq_zero_present(const char *spath, const struct row *r) +{ + char buf[1 << 16]; + if (slurp(spath, buf, sizeof buf) < 0) return -1; + + const char *body = strstr(buf, "TEXT produce"); + if (!body) { + fprintf(stderr, "row[%s]: no TEXT produce label\n", r->label); + return -1; + } + const char *end = strstr(body, "RET"); + if (!end) end = buf + strlen(buf); + long bodylen = end - body; + if (bodylen <= 0 || (size_t)bodylen >= sizeof buf) return -1; + + char windowed[1 << 16]; + memcpy(windowed, body, bodylen); + windowed[bodylen] = '\0'; + + if (strstr(windowed, "MOVQ\t$0, -") == NULL) { + fprintf(stderr, + "row[%s]: no `MOVQ $0, -K(BP)` zero-init in produce body\n", + r->label); + return -1; + } + return 0; +} + +static int +emit_s(const char *w6c, const struct row *r, int i, char *out_s, size_t cap) +{ + char src[64], cmd[1024]; + snprintf(src, sizeof src, "/tmp/lzi_asm_%d_%d.ww", getpid(), i); + snprintf(out_s, cap, "/tmp/lzi_asm_%d_%d_%s.s", + getpid(), i, w6c[strlen(w6c) - 1] == 'w' ? "ww" : "c"); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, out_s, src); + int rc = runwait(cmd); + unlink(src); + return rc; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[512]; + if (bin[0] != '/') { + char cwd[256]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char w6c[640], w6c_ww[640]; + snprintf(w6c, sizeof w6c, "%s/w6c", bin); + snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); + + int have_ww = (access(w6c_ww, X_OK) == 0); + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + + for (int i = 0; i < n; i++) { + char cs_path[128], ws_path[128]; + + if (emit_s(w6c, &rows[i], i, cs_path, sizeof cs_path) != 0) { + fprintf(stderr, + "letdecl_zeroinit[cstage][%s]: w6c failed\n", + rows[i].label); + fail++; total++; continue; + } + total++; + if (check_movq_zero_present(cs_path, &rows[i]) != 0) { + fail++; + } + + if (!have_ww) { unlink(cs_path); continue; } + + if (emit_s(w6c_ww, &rows[i], i, ws_path, sizeof ws_path) != 0) { + fprintf(stderr, + "letdecl_zeroinit[wwstage][%s]: w6c_ww failed\n", + rows[i].label); + fail++; total++; + unlink(cs_path); + continue; + } + total++; + if (check_movq_zero_present(ws_path, &rows[i]) != 0) { + fail++; + } + + /* Byte-id between stages — the actual fix-pin. */ + total++; + char cmd[512]; + snprintf(cmd, sizeof cmd, "cmp -s %s %s", cs_path, ws_path); + if (runwait(cmd) != 0) { + fprintf(stderr, + "letdecl_zeroinit[%s]: cstage vs wwstage asm differs\n", + rows[i].label); + fail++; + } + + unlink(cs_path); unlink(ws_path); + } + + if (fail) { + fprintf(stderr, + "letdecl_zeroinit: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("letdecl_zeroinit: %d/%d ok\n", total, total); + return 0; +}