diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index c8ec1094..614dbe58 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -4158,8 +4158,31 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) } } else if (sz == 8) { ins2(c, A_MOVQ, aimm(0), amem(D_BP, off)); + } else if (!n->rhs && sz > 8 && lu && lu->kind != TY_ARRAY) { + /* `let x: T;` with no rhs for a multi-word composite + * (str/slice/tuple/struct/tagged). Zero the slot so + * reads after the bare let see {0...} rather than + * whatever the stack already held. Arrays keep the + * per-index-write contract — leave them uninit. */ + ins2(c, A_XORQ, areg(D_AX), areg(D_AX)); + int zi = 0; + while (zi + 8 <= sz) { + ins2(c, A_MOVQ, areg(D_AX), + amem(D_BP, off + zi)); + zi += 8; + } + while (zi + 4 <= sz) { + ins2(c, A_MOVL, areg(D_AX), + amem(D_BP, off + zi)); + zi += 4; + } + while (zi < sz) { + ins2(c, A_MOVB, areg(D_AX), + amem(D_BP, off + zi)); + zi += 1; + } } - /* arrays/slices left uninitialised — caller writes via index */ + /* arrays left uninitialised — caller writes via index */ break; } case N_RETURN: diff --git a/lib/ww/parse/expr.ww b/lib/ww/parse/expr.ww index a133952d..a474a213 100644 --- a/lib/ww/parse/expr.ww +++ b/lib/ww/parse/expr.ww @@ -68,14 +68,12 @@ fn parseprimary(p: *parser) *node = { return newnode(p.a, nkind.N_VOIDLIT, pf, pl, pc); }; if (p.curkind == tkind.TK_UNDER) { - // Bare `_` — valid only as a discard lvalue. Emit an nkind.N_IDENT - // with empty str; the checker rejects it outside lvalue + // Bare `_` — valid only as a discard lvalue. Emit an N_IDENT + // with empty str (newnode zeroes the node, so str.len is + // already 0); the checker rejects it outside lvalue // positions. advance(p); - let n: *node = newnode(p.a, nkind.N_IDENT, pf, pl, pc); - let empty: str; - n.str = empty; - return n; + return newnode(p.a, nkind.N_IDENT, pf, pl, pc); }; if (p.curkind == tkind.TK_LBRACK) { // Array literal `[a, b, c]` or `[v, w...]` (repeat suffix). diff --git a/lib/ww/parse/parse.ww b/lib/ww/parse/parse.ww index c09e23a1..4df0539c 100644 --- a/lib/ww/parse/parse.ww +++ b/lib/ww/parse/parse.ww @@ -92,8 +92,7 @@ fn expectident(p: *parser, into: *str) bool = { // scope_define for the binding. fn expectbindname(p: *parser, into: *str) bool = { if (p.curkind == tkind.TK_UNDER) { - let empty: str; - *into = empty; + *into = ""; advance(p); return true; }; diff --git a/selfhost/CLAUDE.md b/selfhost/CLAUDE.md index 5979949a..f4fc1b06 100644 --- a/selfhost/CLAUDE.md +++ b/selfhost/CLAUDE.md @@ -64,5 +64,13 @@ Fixed (no workaround needed): selfhost/cmd/wcc/cgenutil.ww slotsize N_TARRAY follows `aliaslookup` on a TNAME element, and `aliaslookup` strips a `pkg.` prefix so cross-module references resolve. +- Bare `let x: T;` (no rhs) of a multi-word composite — str (16B), + slice (24B), tuple, struct, tagged — now zero-inits the slot. + Previously only 8B-primitive slots were zeroed; larger slots + read whatever the stack held, so `let empty: str; *into = empty;` + copied stack garbage into the caller's slot. `[N]T` arrays still + follow the per-index-write contract and stay uninit. See + cmd/w6c/cgen.c N_LET (else branch, sz > 8 && !TY_ARRAY) and + selfhost/cmd/wcc/cgenstmt.ww cglet no-rhs branch. If a port "should work" but the binary is wrong, suspect these first. diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index d60ff2a8..334362a9 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -2730,14 +2730,12 @@ fn parseprimary(p: *parser) *node = { return newnode(p.a, nkind.N_VOIDLIT, pf, pl, pc); }; if (p.curkind == tkind.TK_UNDER) { - // Bare `_` — valid only as a discard lvalue. Emit an nkind.N_IDENT - // with empty str; the checker rejects it outside lvalue + // Bare `_` — valid only as a discard lvalue. Emit an N_IDENT + // with empty str (newnode zeroes the node, so str.len is + // already 0); the checker rejects it outside lvalue // positions. advance(p); - let n: *node = newnode(p.a, nkind.N_IDENT, pf, pl, pc); - let empty: str; - n.str = empty; - return n; + return newnode(p.a, nkind.N_IDENT, pf, pl, pc); }; if (p.curkind == tkind.TK_LBRACK) { // Array literal `[a, b, c]` or `[v, w...]` (repeat suffix). @@ -3799,8 +3797,7 @@ fn expectident(p: *parser, into: *str) bool = { // scope_define for the binding. fn expectbindname(p: *parser, into: *str) bool = { if (p.curkind == tkind.TK_UNDER) { - let empty: str; - *into = empty; + *into = ""; advance(p); return true; }; @@ -11788,16 +11785,46 @@ fn cglet(c: *cgen, n: *node) void = { }; } else { // Bare `let x: T;` with no initializer. C cgen - // (cmd/w6c/cgen.c:3317) zero-inits whenever the raw type size - // is 8: scalar primitives, pointers, fn/chan handles, plus 8B - // composites like `[8]bool`, `[2]i32`, `[4]i16`, `[1]i64`. - // Larger composites and `[N]T` with size != 8 are left for - // per-field writes. + // (cmd/w6c/cgen.c N_LET no-rhs branch) zero-inits in two + // shapes: + // - 8B primitives (scalar/ptr/fn/chan/`[8]bool` etc.): + // single `MOVQ $0, off(BP)`. + // - multi-word composites (str/slice/tuple/struct/tagged): + // `XORQ AX,AX` + a run of `MOVQ AX, ...` over the slot + // so reads after the bare let see {0...} rather than + // stack garbage. + // `[N]T` arrays of size != 8 keep the per-index-write + // contract — they're left uninit. + let isarr: bool = false; + if (n.lhs != nil) { + if (n.lhs.kind == nkind.N_TARRAY) { isarr = true; }; + }; if (typeis8byteprimitive(c, n.lhs)) { emitline("\tMOVQ\t$0, "); emitoff(off: i64); emitline("(BP)\n"); - }; + } else { if (!isarr) { if (sz > 8) { + emitline("\tXORQ\tAX, AX\n"); + let zi: i32 = 0; + for (zi + 8 <= sz) { + emitline("\tMOVQ\tAX, "); + emitoff((off + zi): i64); + emitline("(BP)\n"); + zi += 8; + }; + for (zi + 4 <= sz) { + emitline("\tMOVL\tAX, "); + emitoff((off + zi): i64); + emitline("(BP)\n"); + zi += 4; + }; + for (zi < sz) { + emitline("\tMOVB\tAX, "); + emitoff((off + zi): i64); + emitline("(BP)\n"); + zi += 1; + }; + }; }; }; }; c.lastwasreturn = 0; return; diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index 84db4d91..f7e4fb24 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -544,16 +544,46 @@ fn cglet(c: *cgen, n: *node) void = { }; } else { // Bare `let x: T;` with no initializer. C cgen - // (cmd/w6c/cgen.c:3317) zero-inits whenever the raw type size - // is 8: scalar primitives, pointers, fn/chan handles, plus 8B - // composites like `[8]bool`, `[2]i32`, `[4]i16`, `[1]i64`. - // Larger composites and `[N]T` with size != 8 are left for - // per-field writes. + // (cmd/w6c/cgen.c N_LET no-rhs branch) zero-inits in two + // shapes: + // - 8B primitives (scalar/ptr/fn/chan/`[8]bool` etc.): + // single `MOVQ $0, off(BP)`. + // - multi-word composites (str/slice/tuple/struct/tagged): + // `XORQ AX,AX` + a run of `MOVQ AX, ...` over the slot + // so reads after the bare let see {0...} rather than + // stack garbage. + // `[N]T` arrays of size != 8 keep the per-index-write + // contract — they're left uninit. + let isarr: bool = false; + if (n.lhs != nil) { + if (n.lhs.kind == nkind.N_TARRAY) { isarr = true; }; + }; if (typeis8byteprimitive(c, n.lhs)) { emitline("\tMOVQ\t$0, "); emitoff(off: i64); emitline("(BP)\n"); - }; + } else { if (!isarr) { if (sz > 8) { + emitline("\tXORQ\tAX, AX\n"); + let zi: i32 = 0; + for (zi + 8 <= sz) { + emitline("\tMOVQ\tAX, "); + emitoff((off + zi): i64); + emitline("(BP)\n"); + zi += 8; + }; + for (zi + 4 <= sz) { + emitline("\tMOVL\tAX, "); + emitoff((off + zi): i64); + emitline("(BP)\n"); + zi += 4; + }; + for (zi < sz) { + emitline("\tMOVB\tAX, "); + emitoff((off + zi): i64); + emitline("(BP)\n"); + zi += 1; + }; + }; }; }; }; c.lastwasreturn = 0; return; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index d2a742cb..465bbabc 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -2730,14 +2730,12 @@ fn parseprimary(p: *parser) *node = { return newnode(p.a, nkind.N_VOIDLIT, pf, pl, pc); }; if (p.curkind == tkind.TK_UNDER) { - // Bare `_` — valid only as a discard lvalue. Emit an nkind.N_IDENT - // with empty str; the checker rejects it outside lvalue + // Bare `_` — valid only as a discard lvalue. Emit an N_IDENT + // with empty str (newnode zeroes the node, so str.len is + // already 0); the checker rejects it outside lvalue // positions. advance(p); - let n: *node = newnode(p.a, nkind.N_IDENT, pf, pl, pc); - let empty: str; - n.str = empty; - return n; + return newnode(p.a, nkind.N_IDENT, pf, pl, pc); }; if (p.curkind == tkind.TK_LBRACK) { // Array literal `[a, b, c]` or `[v, w...]` (repeat suffix). @@ -3799,8 +3797,7 @@ fn expectident(p: *parser, into: *str) bool = { // scope_define for the binding. fn expectbindname(p: *parser, into: *str) bool = { if (p.curkind == tkind.TK_UNDER) { - let empty: str; - *into = empty; + *into = ""; advance(p); return true; }; @@ -11788,16 +11785,46 @@ fn cglet(c: *cgen, n: *node) void = { }; } else { // Bare `let x: T;` with no initializer. C cgen - // (cmd/w6c/cgen.c:3317) zero-inits whenever the raw type size - // is 8: scalar primitives, pointers, fn/chan handles, plus 8B - // composites like `[8]bool`, `[2]i32`, `[4]i16`, `[1]i64`. - // Larger composites and `[N]T` with size != 8 are left for - // per-field writes. + // (cmd/w6c/cgen.c N_LET no-rhs branch) zero-inits in two + // shapes: + // - 8B primitives (scalar/ptr/fn/chan/`[8]bool` etc.): + // single `MOVQ $0, off(BP)`. + // - multi-word composites (str/slice/tuple/struct/tagged): + // `XORQ AX,AX` + a run of `MOVQ AX, ...` over the slot + // so reads after the bare let see {0...} rather than + // stack garbage. + // `[N]T` arrays of size != 8 keep the per-index-write + // contract — they're left uninit. + let isarr: bool = false; + if (n.lhs != nil) { + if (n.lhs.kind == nkind.N_TARRAY) { isarr = true; }; + }; if (typeis8byteprimitive(c, n.lhs)) { emitline("\tMOVQ\t$0, "); emitoff(off: i64); emitline("(BP)\n"); - }; + } else { if (!isarr) { if (sz > 8) { + emitline("\tXORQ\tAX, AX\n"); + let zi: i32 = 0; + for (zi + 8 <= sz) { + emitline("\tMOVQ\tAX, "); + emitoff((off + zi): i64); + emitline("(BP)\n"); + zi += 8; + }; + for (zi + 4 <= sz) { + emitline("\tMOVL\tAX, "); + emitoff((off + zi): i64); + emitline("(BP)\n"); + zi += 4; + }; + for (zi < sz) { + emitline("\tMOVB\tAX, "); + emitoff((off + zi): i64); + emitline("(BP)\n"); + zi += 1; + }; + }; }; }; }; c.lastwasreturn = 0; return;