diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index f628f84e..c9fd3e3d 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -2647,19 +2647,27 @@ fn slotsize(c: *cgen, typn: *syntax.node) i32 = { kk == syntax.tykind.TY_STR || kk == syntax.tykind.TY_TAGGED) { return ti.size: i32; }; - // #75: a struct LOCAL's stack slot is the struct's NATURAL size - // rounded up to the 8B slot grain — NOT ti.slotsize, which sums the - // slot-padded field widths and over-reserves on sub-8 nested - // composites (e.g. outer{a:u8, p:inner{x:u8,y:u8}, z:i64} → ww $32 vs - // cstage $16). cstage reserves the local at f->type->size (cmd/w6c/ - // cgen.c), i.e. the checker's natural r.size (check.ww:2256). Same - // dual-SSoT leak as #44 (field-offset) / #55, one notion over. The - // TUPLE arm (8B/elem slot, user ruling #60) and ARRAY arm (element - // stride, #48 [N]Alias 24B) keep ti.slotsize — deliberate divergences. - if (kk == syntax.tykind.TY_STRUCT) { + // #75/#9: a struct OR array LOCAL's stack slot is the type's NATURAL + // size rounded to the 8B slot grain — NOT ti.slotsize, which sums the + // slot-PADDED element/field widths and over-reserves when a nested + // element is a sub-8-tail composite (e.g. [2]outer with + // outer{a:u8, p:inner{x:u8,y:u8}, z:i64}: outer.slotsize 24 != size 16 + // → ww frame $48 vs cstage natural $32). cstage's localslot reserves at + // round8(f->type->size) (cmd/w6c/cgen.c, frame=(frame+size+7)&~7 over + // the checker's natural size); ww's localreserve mirrors that round. + // ti.size and ti.slotsize round to the SAME 8-multiple for every array + // of prims/arrays (element slotsize==size) and for an 8-multiple tagged + // element (#48 [N]Alias), so this arm MOVES only the nested-sub-8-struct + // case — the #9 divergence. #75 fixed the TY_STRUCT arm; #9 carries the + // identical transform to its TY_ARRAY sibling (same dual-SSoT slotsize + // leak as #44/#55, one notion over). The per-element STRIDE is + // unaffected: it reads slotsize on the ELEMENT node (struct arm) / + // elemsizeofc's natural sub.size, never this array-total arm. TUPLE + // keeps ti.slotsize (8B/elem slot, USER ruling #60 — untouched). + if (kk == syntax.tykind.TY_STRUCT || kk == syntax.tykind.TY_ARRAY) { return ((ti.size + 7u64) & ~7u64): i32; }; - if (kk == syntax.tykind.TY_TUPLE || kk == syntax.tykind.TY_ARRAY) { + if (kk == syntax.tykind.TY_TUPLE) { return ti.slotsize: i32; }; return 8; diff --git a/test/lang/arr_struct_subtail_frame_test.ww b/test/lang/arr_struct_subtail_frame_test.ww new file mode 100644 index 00000000..de111821 --- /dev/null +++ b/test/lang/arr_struct_subtail_frame_test.ww @@ -0,0 +1,65 @@ +// arr_struct_subtail_frame_test — #9: a local `[N]S` where S is a struct with +// a sub-8-tail field over-sized its stack FRAME (cs≠ww, gate-visible). The +// wwstage cgen frame reader (slotsize, cgenutil.ww TY_ARRAY arm) summed the +// element's slot-PADDED width (outer.slotsize 24) × N instead of the array's +// NATURAL size (outer.size 16) × N rounded to 8 — so `[2]outer` reserved $48 +// where cstage reserves natural $32, and every later local stacked at a shifted +// BP offset. ROOT was the SAME dual-SSoT slotsize leak #75 fixed for the +// TY_STRUCT arm, never carried to its TY_ARRAY sibling; #9 applies the identical +// round8(ti.size) transform. ti.size == ti.slotsize for every array of prims / +// arrays / 8-multiple tagged elements, so the fix MOVES only this nested-sub-8- +// struct case (#48 [N]Alias and 1D/2D prim arrays are unchanged). +// +// The LOAD-BEARING tooth is the test-lang-byteid .s comparison: reverting the +// cgenutil.ww slotsize arm reddens this file (ww frame $48 vs cstage $32). The +// value asserts are a SECONDARY net — standalone ww is internally consistent at +// the inflated stride (it reserves AND addresses at the same over-sized frame), +// so a pure value run can pass while broken; they guard a future stride/offset +// regression, not the frame size. element_layout pins the natural per-element +// stride (16) and field offsets (a@0, p.x@1, p.y@2, z@8); cross_element pins +// that element 0 and element 1 don't alias after the fix shrinks the frame. +// +// Inline @test fns, not a row-table: the cases vary in the WRITE/READ place +// shape over a fixed struct layout, not in data over one operation, so a +// row-array `[](in,exp){}` can't express them (and that form is blocked by cgen +// #111). Mirrors dotbase_arr_test's #135 pins. + +package arr_struct_subtail_frame_test; + +type inner = struct { x: u8, y: u8 }; // natural 2, align 1 +type outer = struct { a: u8, p: inner, z: i64 }; // natural 16, align 8 (sub-8 tail at p) + +@test fn element_layout() void = { + let arr: [2]outer = [ + outer { a = 1u8, p = inner { x = 2u8, y = 3u8 }, z = 100i64 }, + outer { a = 4u8, p = inner { x = 5u8, y = 6u8 }, z = 200i64 }]; + assert(arr[0].a: i32 == 1); + assert(arr[0].p.x: i32 == 2); + assert(arr[0].p.y: i32 == 3); + assert(arr[0].z == 100i64); + assert(arr[1].a: i32 == 4); + assert(arr[1].p.x: i32 == 5); + assert(arr[1].p.y: i32 == 6); + assert(arr[1].z == 200i64); +}; + +@test fn write_tail() void = { // write the sub-8 tail field then read back + let arr: [2]outer = [ + outer { a = 1u8, p = inner { x = 2u8, y = 3u8 }, z = 100i64 }, + outer { a = 4u8, p = inner { x = 5u8, y = 6u8 }, z = 200i64 }]; + arr[1].z = 999i64; + arr[0].a = 7u8; + assert(arr[1].z == 999i64); + assert(arr[0].a: i32 == 7); + assert(arr[0].z == 100i64); // neighbour untouched +}; + +@test fn cross_element() void = { // element 0 and 1 are 16 apart, no alias + let arr: [2]outer = [ + outer { a = 1u8, p = inner { x = 2u8, y = 3u8 }, z = 100i64 }, + outer { a = 4u8, p = inner { x = 5u8, y = 6u8 }, z = 200i64 }]; + arr[0].p.y = 77u8; + assert(arr[0].p.y: i32 == 77); + assert(arr[1].p.x: i32 == 5); // not clobbered by the elem-0 write + assert(arr[1].p.y: i32 == 6); +};