From d8e2a0692cdd727c2e51bffe9fb884febb63546d Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 9 Jun 2026 01:57:46 +0900 Subject: [PATCH] =?UTF-8?q?wcc/cgen:=20#15=20empty=20zero-length=20array?= =?UTF-8?q?=20emit=20=E2=80=94=20no=20spurious=20DATAW,=20cstage=20frame?= =?UTF-8?q?=20formula=20(wwstage)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An empty zero-length array diverged cs!=ww in asm (both ran correct=7): a [0]int global emitted a spurious DATAW main.X(SB),"", and a [0]int local reserved a $16 frame slot. cstage emits neither. wwstage-only, byte-id-only. The global DATAW emit is now gated on sz > 0 (skips the empty array). The local frame: localreserve dropped its sub-8 floor (if asz<8 asz=8) to mirror cstage's localslot formula (frame+sz+7)&~7 -- but that floor was MASKING slotsize(TY_VOID)=0 (a void local), which cstage defaults to 8B; removing the floor alone collided the zero-size void slot with a spilled param (a real miscompile -- 1132 self-compile hunks). So letslotsize now returns 8 for a void local, while empty-struct / [0]-array stay genuine 0. The frame formula is byte-id-neutral for every sz>=1 local (round8 already >= 8); only true zero-size cases change. cstage unchanged (w6c md5 unchanged). byte-id 990-997 8/8 (the full self-compile is what caught the void-local class); test/wcc/820 un-carves the #9 empty-[0] byte-id exclusion + adds void-local/local-[0] rows. --- selfhost/cmd/w6c/main.combined.ww | 51 +++++++++++++++++++++------- selfhost/cmd/wcc/cgen.ww | 24 +++++++++---- selfhost/cmd/wcc/cgenutil.ww | 27 +++++++++++---- selfhost/cmd/wwdump/main.combined.ww | 51 +++++++++++++++++++++------- test/wcc/820_arr_zero_vs_infer.c | 48 +++++++++++++++++++------- 5 files changed, 150 insertions(+), 51 deletions(-) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 0af0f1f8..abd1cf8f 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -18772,13 +18772,26 @@ export fn letslotsize(c: *cgen, n: *node) i32 = { // intercept here: the checker (inferarraylen, check.ww) stamps the // real element count onto the array type's length child before cgen // runs, so slotsize reads it like any explicit `[N]T` (#7). - if (n.lhs != nil) { return slotsize(c, n.lhs); }; - // Annotation-less init: defer to the call's return type if we - // can infer it. Tagged-union returns need 24B; everything else - // matches slotsize on the inferred type. - let inferred: *node = inferletcalltype(c, n.rhs); - if (inferred != nil) { return slotsize(c, inferred); }; - return 8; + // Annotation-less init: defer to the call's return type if we can + // infer it. Tagged-union returns need 24B; everything else matches + // slotsize on the inferred type. + let tn: *node = n.lhs; + if (tn == nil) { tn = inferletcalltype(c, n.rhs); }; + if (tn == nil) { return 8; }; + let s: i32 = slotsize(c, tn); + // #15: cstage cglet (cmd/w6c/cgen.c:12321) defaults a local's slot + // to 8 — only ARRAY/SLICE/STR/STRUCT/TUPLE/TAGGED take the real + // type size. slotsize returns 0 for a void/`done`-aliased scalar + // local; localreserve no longer applies a sub-8 floor (it mirrors + // cstage's no-floor localslot), so a void slot must be floored here + // instead, else its zero width collides with the next local. A + // genuine empty struct or [0]T array (also slotsize 0) keeps its 0. + if (s == 0) { + let ti: *tinfo = tn.type_: *tinfo; + if (ti != nil) { ti = tichase(ti); }; + if (ti != nil && ti.kind == tykind.TY_VOID) { return 8; }; + }; + return s; }; // #48 A.6.3d: AST walker retired. resolvewalk (check.ww:426-436) stamps @@ -39161,12 +39174,17 @@ fn localalloc(c: *cgen, name: str, sz: i32, tnode: *node) i32 = { // (`let x = f(x)`) resolves x in the OUTER scope (Hare evals the init in // the outer scope: harec check.c clet runs cexpr before scope_define). fn localreserve(c: *cgen, name: str, sz: i32, tnode: *node) *local = { - let asz: i32 = sz; - if (asz < 8) { asz = 8; }; - if ((asz & 7) != 0) { asz = (asz + 7) & ~7; }; - c.frame += asz; + // #15: mirror cstage localslot (cmd/w6c/cgen.c:1900) — + // `frame = (frame + size + 7) & ~7`, NO sub-8 floor. Identical to + // the old `max(8, round8(sz))` accumulation for every sz>0 (frame + // stays 8-aligned, so a 1..8B slot still costs 8); the only change + // is a zero-size slot (`[0]T`, void) adds 0, matching cstage's $0 + // frame instead of over-reserving 8. local.sz is read only by the + // @-prefix grow-check in localadd, never for user lets, so storing + // the raw sz here is inert. + c.frame = (c.frame + sz + 7) & ~7; let off: i32 = 0 - c.frame; - let l: *local = alloc(local{name=name, off=off, sz=asz, tnode=tnode, lnext=nil})!; + let l: *local = alloc(local{name=name, off=off, sz=sz, tnode=tnode, lnext=nil})!; return l; }; @@ -41626,7 +41644,14 @@ fn emitletdataw(c: *cgen, file: *node) void = { route = true; }; }; - if (route) { + // #15: a zero-length array (`[0]T`) + // has no bytes — cstage emits no DATA + // row; wwstage's unguarded emit produced + // a spurious `DATAW name(SB),""`. sz + // (letemitsize, cgen.ww:2758) is 0 for + // [0]T → skip. (Non-empty [N>0] arrays + // keep sz>0.) + if (route && sz > 0) { emitarraydata(c, "DATAW", nm, d.nmod, dti, rh); }; diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index fdbd20cf..09c65a19 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -590,12 +590,17 @@ fn localalloc(c: *cgen, name: str, sz: i32, tnode: *node) i32 = { // (`let x = f(x)`) resolves x in the OUTER scope (Hare evals the init in // the outer scope: harec check.c clet runs cexpr before scope_define). fn localreserve(c: *cgen, name: str, sz: i32, tnode: *node) *local = { - let asz: i32 = sz; - if (asz < 8) { asz = 8; }; - if ((asz & 7) != 0) { asz = (asz + 7) & ~7; }; - c.frame += asz; + // #15: mirror cstage localslot (cmd/w6c/cgen.c:1900) — + // `frame = (frame + size + 7) & ~7`, NO sub-8 floor. Identical to + // the old `max(8, round8(sz))` accumulation for every sz>0 (frame + // stays 8-aligned, so a 1..8B slot still costs 8); the only change + // is a zero-size slot (`[0]T`, void) adds 0, matching cstage's $0 + // frame instead of over-reserving 8. local.sz is read only by the + // @-prefix grow-check in localadd, never for user lets, so storing + // the raw sz here is inert. + c.frame = (c.frame + sz + 7) & ~7; let off: i32 = 0 - c.frame; - let l: *local = alloc(local{name=name, off=off, sz=asz, tnode=tnode, lnext=nil})!; + let l: *local = alloc(local{name=name, off=off, sz=sz, tnode=tnode, lnext=nil})!; return l; }; @@ -3055,7 +3060,14 @@ fn emitletdataw(c: *cgen, file: *node) void = { route = true; }; }; - if (route) { + // #15: a zero-length array (`[0]T`) + // has no bytes — cstage emits no DATA + // row; wwstage's unguarded emit produced + // a spurious `DATAW name(SB),""`. sz + // (letemitsize, cgen.ww:2758) is 0 for + // [0]T → skip. (Non-empty [N>0] arrays + // keep sz>0.) + if (route && sz > 0) { emitarraydata(c, "DATAW", nm, d.nmod, dti, rh); }; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index dc2b0ac9..c9ce4d34 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -2417,13 +2417,26 @@ export fn letslotsize(c: *cgen, n: *node) i32 = { // intercept here: the checker (inferarraylen, check.ww) stamps the // real element count onto the array type's length child before cgen // runs, so slotsize reads it like any explicit `[N]T` (#7). - if (n.lhs != nil) { return slotsize(c, n.lhs); }; - // Annotation-less init: defer to the call's return type if we - // can infer it. Tagged-union returns need 24B; everything else - // matches slotsize on the inferred type. - let inferred: *node = inferletcalltype(c, n.rhs); - if (inferred != nil) { return slotsize(c, inferred); }; - return 8; + // Annotation-less init: defer to the call's return type if we can + // infer it. Tagged-union returns need 24B; everything else matches + // slotsize on the inferred type. + let tn: *node = n.lhs; + if (tn == nil) { tn = inferletcalltype(c, n.rhs); }; + if (tn == nil) { return 8; }; + let s: i32 = slotsize(c, tn); + // #15: cstage cglet (cmd/w6c/cgen.c:12321) defaults a local's slot + // to 8 — only ARRAY/SLICE/STR/STRUCT/TUPLE/TAGGED take the real + // type size. slotsize returns 0 for a void/`done`-aliased scalar + // local; localreserve no longer applies a sub-8 floor (it mirrors + // cstage's no-floor localslot), so a void slot must be floored here + // instead, else its zero width collides with the next local. A + // genuine empty struct or [0]T array (also slotsize 0) keeps its 0. + if (s == 0) { + let ti: *tinfo = tn.type_: *tinfo; + if (ti != nil) { ti = tichase(ti); }; + if (ti != nil && ti.kind == tykind.TY_VOID) { return 8; }; + }; + return s; }; // #48 A.6.3d: AST walker retired. resolvewalk (check.ww:426-436) stamps diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index a14b2cbd..cd2737e2 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -18772,13 +18772,26 @@ export fn letslotsize(c: *cgen, n: *node) i32 = { // intercept here: the checker (inferarraylen, check.ww) stamps the // real element count onto the array type's length child before cgen // runs, so slotsize reads it like any explicit `[N]T` (#7). - if (n.lhs != nil) { return slotsize(c, n.lhs); }; - // Annotation-less init: defer to the call's return type if we - // can infer it. Tagged-union returns need 24B; everything else - // matches slotsize on the inferred type. - let inferred: *node = inferletcalltype(c, n.rhs); - if (inferred != nil) { return slotsize(c, inferred); }; - return 8; + // Annotation-less init: defer to the call's return type if we can + // infer it. Tagged-union returns need 24B; everything else matches + // slotsize on the inferred type. + let tn: *node = n.lhs; + if (tn == nil) { tn = inferletcalltype(c, n.rhs); }; + if (tn == nil) { return 8; }; + let s: i32 = slotsize(c, tn); + // #15: cstage cglet (cmd/w6c/cgen.c:12321) defaults a local's slot + // to 8 — only ARRAY/SLICE/STR/STRUCT/TUPLE/TAGGED take the real + // type size. slotsize returns 0 for a void/`done`-aliased scalar + // local; localreserve no longer applies a sub-8 floor (it mirrors + // cstage's no-floor localslot), so a void slot must be floored here + // instead, else its zero width collides with the next local. A + // genuine empty struct or [0]T array (also slotsize 0) keeps its 0. + if (s == 0) { + let ti: *tinfo = tn.type_: *tinfo; + if (ti != nil) { ti = tichase(ti); }; + if (ti != nil && ti.kind == tykind.TY_VOID) { return 8; }; + }; + return s; }; // #48 A.6.3d: AST walker retired. resolvewalk (check.ww:426-436) stamps @@ -39161,12 +39174,17 @@ fn localalloc(c: *cgen, name: str, sz: i32, tnode: *node) i32 = { // (`let x = f(x)`) resolves x in the OUTER scope (Hare evals the init in // the outer scope: harec check.c clet runs cexpr before scope_define). fn localreserve(c: *cgen, name: str, sz: i32, tnode: *node) *local = { - let asz: i32 = sz; - if (asz < 8) { asz = 8; }; - if ((asz & 7) != 0) { asz = (asz + 7) & ~7; }; - c.frame += asz; + // #15: mirror cstage localslot (cmd/w6c/cgen.c:1900) — + // `frame = (frame + size + 7) & ~7`, NO sub-8 floor. Identical to + // the old `max(8, round8(sz))` accumulation for every sz>0 (frame + // stays 8-aligned, so a 1..8B slot still costs 8); the only change + // is a zero-size slot (`[0]T`, void) adds 0, matching cstage's $0 + // frame instead of over-reserving 8. local.sz is read only by the + // @-prefix grow-check in localadd, never for user lets, so storing + // the raw sz here is inert. + c.frame = (c.frame + sz + 7) & ~7; let off: i32 = 0 - c.frame; - let l: *local = alloc(local{name=name, off=off, sz=asz, tnode=tnode, lnext=nil})!; + let l: *local = alloc(local{name=name, off=off, sz=sz, tnode=tnode, lnext=nil})!; return l; }; @@ -41626,7 +41644,14 @@ fn emitletdataw(c: *cgen, file: *node) void = { route = true; }; }; - if (route) { + // #15: a zero-length array (`[0]T`) + // has no bytes — cstage emits no DATA + // row; wwstage's unguarded emit produced + // a spurious `DATAW name(SB),""`. sz + // (letemitsize, cgen.ww:2758) is 0 for + // [0]T → skip. (Non-empty [N>0] arrays + // keep sz>0.) + if (route && sz > 0) { emitarraydata(c, "DATAW", nm, d.nmod, dti, rh); }; diff --git a/test/wcc/820_arr_zero_vs_infer.c b/test/wcc/820_arr_zero_vs_infer.c index ac872d35..ba0aafd1 100644 --- a/test/wcc/820_arr_zero_vs_infer.c +++ b/test/wcc/820_arr_zero_vs_infer.c @@ -56,16 +56,15 @@ runwait(const char *cmd) return -1; } -/* beid — include this row in the cstage-vs-wwstage byte-id sweep. The - * empty_zero row is EXCLUDED (beid=0): an empty `[0]int = []` module global - * has a PRE-EXISTING cgen divergence unrelated to #9 (a checker-only fix) — - * wwstage emits an extra zero-width `DATAW main.X(SB),""` row that cstage - * omits; both run identically (return 7). Filed as task #15 (empty-array- - * global DATA emission, NOT folded into #9). A LOCAL `[0]int = []` is no - * escape — it diverges differently (wwstage allocates a $16 frame, cstage - * $0), so there is no byte-id-clean spelling of an empty zero-length array - * to restructure toward. The infer rows DO converge and pin that the - * accept-path asm stays byte-identical. */ +/* beid — include this row in the cstage-vs-wwstage byte-id sweep. Both the + * empty_zero global (`let X:[0]int=[]`) and the empty_zero_local row now + * converge byte-identically (beid=1): #15 closed the empty-`[0]T` cgen + * divergence — wwstage's spurious zero-width `DATAW main.X(SB),""` (cstage + * omits a zero-byte global) and its over-allocated `$16` local frame (cstage + * `$0` — a zero-length array reserves no slot) are both gated on the array + * being non-empty. See .ai/rob-15-spec.md: cgen.ww emitletdataw gates the + * array DATAW on `sz > 0`; cgenutil.ww slotsize returns 0 for a TY_ARRAY of + * alen==0. Both forms still run 7. */ struct row { const char *label; const char *src; int want; int beid; }; static const struct row rows[] = { @@ -79,14 +78,39 @@ static const struct row rows[] = { 20, 1 }, /* empty_zero — a real zero-length array (`[0]int = []`) stays VALID. - * beid=0: pre-existing empty-array-global cgen divergence (see beid). */ + * beid=1: #15 closed the empty-array-global DATAW divergence. */ { "empty_zero", "package main;\n" "let X: [0]int = [];\n" "export fn main() i32 = {\n" "\treturn 7;\n" "};\n", - 7, 0 }, + 7, 1 }, + + /* empty_zero_local — a LOCAL zero-length array (`[0]int = []`) reserves + * no frame slot (#15: cstage `$0`, wwstage was `$16`). beid=1. */ + { "empty_zero_local", + "package main;\n" + "export fn main() i32 = {\n" + "\tlet x: [0]int = [];\n" + "\treturn 7;\n" + "};\n", + 7, 1 }, + + /* void_local — a zero-SIZE (not zero-length) local: `done = void` sizes + * 0 via slotsize, but cstage cglet defaults a non-composite local to an + * 8B slot. #15 dropped localreserve's sub-8 floor, which had masked this + * — letslotsize now floors a void local to 8 (cstage parity). beid=1: a + * regression here (void slot 0) collides with the spilled param. */ + { "void_local", + "package main;\n" + "type done = void;\n" + "export fn main() i32 = {\n" + "\tlet d: done;\n" + "\tlet a: i32 = 7;\n" + "\treturn a;\n" + "};\n", + 7, 1 }, /* infer_len — `[_]` infer is unaffected, `.len` reads the real count. */ { "infer_len",