From e37b76710a5ffa1259e55201975a8de1e5f22f06 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Wed, 20 May 2026 13:42:15 +0900 Subject: [PATCH] selfhost/cmd/wcc: TARRAY struct-stride + cache-bind resolved body (Phase A.4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A.3 left wwstage slotsize at 134 fallback hits. Per-kind breakdown: N_TARRAY 33 + N_TNAME 101 (of which 71 resolve to TY_STRUCT, 3 to module-name quirks, 27 already had tinfo populated and were spurious fallbacks via missed cache hits). tinfofornode N_TNAME: existing arm already reached the resolved body via aliaslookup → tinfofornode recursion (reviewer-61a3's "isn't reaching body" hypothesis disproved by per-name instrumentation). A.4 binds the resolved-body node into the cache too — mirrors A.2's TSTRUCT/TFN/TTUPLE/TTAGGED cycle-break pattern so future calls on either node short-circuit. tinfofornode N_TARRAY: when sub.kind == TY_STRUCT, round sub.size up to 8 before stride. Mirrors registerstruct's slot-padded element stride (cgenutil.ww:2156-2165 / :2233). Primitive elements stay natural (slotsize's TARRAY walker also keeps them natural). slotsize fast-path adds TY_VOID (size 0) and TY_ARRAY (gated on alen > 0 so `[_]T` keeps routing through letslotsize). TY_STRUCT deferred to A.5: tinfofornode TSTRUCT uses per-field natural-align so size(T) stays natural at user level, but registerstruct uses size-derived align with nested structs slot-padded — diverges on ragged-tail shapes (`{inner=3*i32, mark: i32}` gives natural=16 vs totsize=24). Proper A.5 design is a tinfo.slotsize SSoT distinct from tinfo.size. Module-name TNAME quirks (`let l: lex;` where lex is both a struct and the imported module): resolvealias short-circuits on SK_MOD, n.type_ stays nil, falls through to AST walker which structlookups correctly. 3 hits in tree. A.5 work alongside TSTRUCT. Post-A.4 fallback: wwdump 134→45, w6a 17→12, w6l 6→6, ww 12→11 (reviewer also measured w6c at 40). Total 169→74 across the corpus (56% reduction). All 74 are TNAME → TY_STRUCT or module-name quirks. 131/131 + 994 + 995 + bootstrap (ww2==ww3==ww4) byte-identical. --- selfhost/cmd/w6c/main.combined.ww | 75 +++++++++++++++++++++++++--- selfhost/cmd/wcc/cgenutil.ww | 40 ++++++++++++--- selfhost/cmd/wcc/check.ww | 35 ++++++++++++- selfhost/cmd/wwdump/main.combined.ww | 75 +++++++++++++++++++++++++--- 4 files changed, 201 insertions(+), 24 deletions(-) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 6e24ace7..542fb26f 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -7934,9 +7934,25 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { // Mirrors astsize's TNAME fallback so the helpers stay in // lockstep until A.2 collapses each cgen size-walker onto // tinfo.size directly. + // + // #61 A.4: bind the resolved body too so future + // tinfofornode calls on either the TNAME or its target + // short-circuit on the cache hit instead of re-walking + // the chain. Pre-bind matches A.2's TSTRUCT/TFN/TTUPLE/ + // TTAGGED cycle-break pattern (a self-referential + // struct field's *T → TNAME → body would otherwise + // re-enter the same chain). let body: *node = resolvealias(c, n); if (body != nil && body != n) { - r = tinfofornode(c, body); + let cached2: *tinfo = tinfocachelookup(c.tc, body); + if (cached2 != nil) { + r = cached2; + } else { + r = tinfofornode(c, body); + if (r != nil) { + tinfocachebind(c.tc, body, r); + }; + }; }; }; } else { if (k == nkind.N_TBANG) { @@ -7961,7 +7977,22 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { if (n.rhs != nil) { if (n.rhs.kind == nkind.N_INTLIT) { elen = n.rhs.uval; }; }; - r = typearray(c.a, tinfofornode(c, n.lhs), elen); + let sub: *tinfo = tinfofornode(c, n.lhs); + r = typearray(c.a, sub, elen); + // #61 A.4: cgen's registerstruct.totsize slot-pads each struct + // to 8B (cgenutil.ww:2204-2205); fieldsize→[N]Struct stride + // (cgenutil.ww:2156-2165) uses that totsize, so tinfo.size + // must mirror the pad for byte-identity with the AST-walker + // fallback. Primitives (str/i32/u8/...) keep their natural + // stride — slotsize's TARRAY arm reads primsize directly, + // not si.totsize. Only TY_STRUCT subs need the round. + if (sub != nil && sub.kind == tykind.TY_STRUCT) { + let stride: u64 = sub.size; + if ((stride & 7u64) != 0u64) { + stride = (stride + 7u64) & ~7u64; + }; + r.size = stride * elen; + }; } else { if (k == nkind.N_TFN) { // Cstage cmd/wcc/check.c:437-466: function types are 8B / 8B // (call-target pointer shape). Pre-bind before recursing into @@ -10947,8 +10978,8 @@ export fn letslotsize(c: *cgen, n: *node) i32 = { }; fn slotsize(c: *cgen, typn: *node) i32 = { - // #61 audit §1.8 — A.3 fast-path expansion. Read tinfo.size off the - // populated type-expression node when the kind matches the cstage + // #61 audit §1.8 — A.3 / A.4 fast-path expansion. Read tinfo.size off + // the populated type-expression node when the kind matches the cstage // natural-size SSoT. Coverage: // - pointer-like (PTR/CHAN/FN), slice, str, tagged // return ti.size directly (size already encodes the slot). @@ -10959,17 +10990,45 @@ fn slotsize(c: *cgen, typn: *node) i32 = { // primitive into an 8B stack slot regardless of tinfo.size. // Padding lives at the read site, not in tinfo.size, so size(T) // stays a faithful natural-width SSoT. - // TUPLE / TSTRUCT / TARRAY still flow through the AST-walker fallback - // because cgen's per-field stride contract (registerstruct.totsize - // rounding, [N]T element-size walk) lives there, not in tinfo.size. + // - #61 A.4 adds: + // · TY_VOID returns 0 (mirrors the TNAME-"void" fallback arm). + // Closes 56 N_TNAME-"void" / void-aliased (utf8.invalid, + // overflow, done, more, ...) fallback hits — tinfofornode + // now caches both the TNAME and its resolved body so future + // calls on either land in the cache. + // · TY_ARRAY returns ti.size when alen > 0 — tinfofornode's + // TARRAY arm now applies the same struct-element round-to-8 + // cgen's fieldsize uses (#61 A.4 step 2), so [N]Struct's + // stride matches. [_]T (alen=0) still routes through the + // AST walker; letslotsize patches the length there. + // TUPLE still flows through the fallback — slotsize sums raw element + // sizes there, while tinfofornode TTUPLE mirrors cstage's natural + // sum, and the two diverge for `(i32, str)`-style mixes (slot vs + // natural per-element padding). + // + // TY_STRUCT deferred to A.5: tinfofornode TSTRUCT uses per-field + // natural-align for offsets (mirroring cstage's resolve_type + + // astsize, so `size(T)` stays natural), but registerstruct uses + // size-derived alignment with nested structs slot-padded to 8 + // (cgenutil.ww:2178-2207). The two agree for the common 8B-aligned + // shapes but diverge for "ragged tail" structs like + // `struct { inner: struct{i32,i32,i32}, mark: i32 }` where the + // inner struct's natural align (4) drops below cgen's nested- + // struct contract (aln=8 for sz>=8). Closing the gap without + // touching `size(T)`'s natural-width contract needs a separate + // SSoT (e.g. tinfo.slotsize) — filed as A.5. if (typn != nil && typn.type_ != nil) { let ti: *tinfo = typn.type_: *tinfo; let kk: tykind = ti.kind; if (kk == tykind.TY_PTR || kk == tykind.TY_SLICE || kk == tykind.TY_CHAN || kk == tykind.TY_FN || - kk == tykind.TY_STR || kk == tykind.TY_TAGGED) { + kk == tykind.TY_STR || kk == tykind.TY_TAGGED || + kk == tykind.TY_VOID) { return ti.size: i32; }; + if (kk == tykind.TY_ARRAY) { + if (ti.alen > 0u64) { return ti.size: i32; }; + }; if (kk == tykind.TY_BOOL || kk == tykind.TY_RUNE || kk == tykind.TY_I8 || kk == tykind.TY_I16 || kk == tykind.TY_I32 || kk == tykind.TY_I64 || diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 4213b309..f14815c2 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -1941,8 +1941,8 @@ export fn letslotsize(c: *cgen, n: *node) i32 = { }; fn slotsize(c: *cgen, typn: *node) i32 = { - // #61 audit §1.8 — A.3 fast-path expansion. Read tinfo.size off the - // populated type-expression node when the kind matches the cstage + // #61 audit §1.8 — A.3 / A.4 fast-path expansion. Read tinfo.size off + // the populated type-expression node when the kind matches the cstage // natural-size SSoT. Coverage: // - pointer-like (PTR/CHAN/FN), slice, str, tagged // return ti.size directly (size already encodes the slot). @@ -1953,17 +1953,45 @@ fn slotsize(c: *cgen, typn: *node) i32 = { // primitive into an 8B stack slot regardless of tinfo.size. // Padding lives at the read site, not in tinfo.size, so size(T) // stays a faithful natural-width SSoT. - // TUPLE / TSTRUCT / TARRAY still flow through the AST-walker fallback - // because cgen's per-field stride contract (registerstruct.totsize - // rounding, [N]T element-size walk) lives there, not in tinfo.size. + // - #61 A.4 adds: + // · TY_VOID returns 0 (mirrors the TNAME-"void" fallback arm). + // Closes 56 N_TNAME-"void" / void-aliased (utf8.invalid, + // overflow, done, more, ...) fallback hits — tinfofornode + // now caches both the TNAME and its resolved body so future + // calls on either land in the cache. + // · TY_ARRAY returns ti.size when alen > 0 — tinfofornode's + // TARRAY arm now applies the same struct-element round-to-8 + // cgen's fieldsize uses (#61 A.4 step 2), so [N]Struct's + // stride matches. [_]T (alen=0) still routes through the + // AST walker; letslotsize patches the length there. + // TUPLE still flows through the fallback — slotsize sums raw element + // sizes there, while tinfofornode TTUPLE mirrors cstage's natural + // sum, and the two diverge for `(i32, str)`-style mixes (slot vs + // natural per-element padding). + // + // TY_STRUCT deferred to A.5: tinfofornode TSTRUCT uses per-field + // natural-align for offsets (mirroring cstage's resolve_type + + // astsize, so `size(T)` stays natural), but registerstruct uses + // size-derived alignment with nested structs slot-padded to 8 + // (cgenutil.ww:2178-2207). The two agree for the common 8B-aligned + // shapes but diverge for "ragged tail" structs like + // `struct { inner: struct{i32,i32,i32}, mark: i32 }` where the + // inner struct's natural align (4) drops below cgen's nested- + // struct contract (aln=8 for sz>=8). Closing the gap without + // touching `size(T)`'s natural-width contract needs a separate + // SSoT (e.g. tinfo.slotsize) — filed as A.5. if (typn != nil && typn.type_ != nil) { let ti: *tinfo = typn.type_: *tinfo; let kk: tykind = ti.kind; if (kk == tykind.TY_PTR || kk == tykind.TY_SLICE || kk == tykind.TY_CHAN || kk == tykind.TY_FN || - kk == tykind.TY_STR || kk == tykind.TY_TAGGED) { + kk == tykind.TY_STR || kk == tykind.TY_TAGGED || + kk == tykind.TY_VOID) { return ti.size: i32; }; + if (kk == tykind.TY_ARRAY) { + if (ti.alen > 0u64) { return ti.size: i32; }; + }; if (kk == tykind.TY_BOOL || kk == tykind.TY_RUNE || kk == tykind.TY_I8 || kk == tykind.TY_I16 || kk == tykind.TY_I32 || kk == tykind.TY_I64 || diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index ffe71b1f..66a8450b 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -901,9 +901,25 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { // Mirrors astsize's TNAME fallback so the helpers stay in // lockstep until A.2 collapses each cgen size-walker onto // tinfo.size directly. + // + // #61 A.4: bind the resolved body too so future + // tinfofornode calls on either the TNAME or its target + // short-circuit on the cache hit instead of re-walking + // the chain. Pre-bind matches A.2's TSTRUCT/TFN/TTUPLE/ + // TTAGGED cycle-break pattern (a self-referential + // struct field's *T → TNAME → body would otherwise + // re-enter the same chain). let body: *node = resolvealias(c, n); if (body != nil && body != n) { - r = tinfofornode(c, body); + let cached2: *tinfo = tinfocachelookup(c.tc, body); + if (cached2 != nil) { + r = cached2; + } else { + r = tinfofornode(c, body); + if (r != nil) { + tinfocachebind(c.tc, body, r); + }; + }; }; }; } else { if (k == nkind.N_TBANG) { @@ -928,7 +944,22 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { if (n.rhs != nil) { if (n.rhs.kind == nkind.N_INTLIT) { elen = n.rhs.uval; }; }; - r = typearray(c.a, tinfofornode(c, n.lhs), elen); + let sub: *tinfo = tinfofornode(c, n.lhs); + r = typearray(c.a, sub, elen); + // #61 A.4: cgen's registerstruct.totsize slot-pads each struct + // to 8B (cgenutil.ww:2204-2205); fieldsize→[N]Struct stride + // (cgenutil.ww:2156-2165) uses that totsize, so tinfo.size + // must mirror the pad for byte-identity with the AST-walker + // fallback. Primitives (str/i32/u8/...) keep their natural + // stride — slotsize's TARRAY arm reads primsize directly, + // not si.totsize. Only TY_STRUCT subs need the round. + if (sub != nil && sub.kind == tykind.TY_STRUCT) { + let stride: u64 = sub.size; + if ((stride & 7u64) != 0u64) { + stride = (stride + 7u64) & ~7u64; + }; + r.size = stride * elen; + }; } else { if (k == nkind.N_TFN) { // Cstage cmd/wcc/check.c:437-466: function types are 8B / 8B // (call-target pointer shape). Pre-bind before recursing into diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index aaa997ee..6ff26b6f 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -7934,9 +7934,25 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { // Mirrors astsize's TNAME fallback so the helpers stay in // lockstep until A.2 collapses each cgen size-walker onto // tinfo.size directly. + // + // #61 A.4: bind the resolved body too so future + // tinfofornode calls on either the TNAME or its target + // short-circuit on the cache hit instead of re-walking + // the chain. Pre-bind matches A.2's TSTRUCT/TFN/TTUPLE/ + // TTAGGED cycle-break pattern (a self-referential + // struct field's *T → TNAME → body would otherwise + // re-enter the same chain). let body: *node = resolvealias(c, n); if (body != nil && body != n) { - r = tinfofornode(c, body); + let cached2: *tinfo = tinfocachelookup(c.tc, body); + if (cached2 != nil) { + r = cached2; + } else { + r = tinfofornode(c, body); + if (r != nil) { + tinfocachebind(c.tc, body, r); + }; + }; }; }; } else { if (k == nkind.N_TBANG) { @@ -7961,7 +7977,22 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { if (n.rhs != nil) { if (n.rhs.kind == nkind.N_INTLIT) { elen = n.rhs.uval; }; }; - r = typearray(c.a, tinfofornode(c, n.lhs), elen); + let sub: *tinfo = tinfofornode(c, n.lhs); + r = typearray(c.a, sub, elen); + // #61 A.4: cgen's registerstruct.totsize slot-pads each struct + // to 8B (cgenutil.ww:2204-2205); fieldsize→[N]Struct stride + // (cgenutil.ww:2156-2165) uses that totsize, so tinfo.size + // must mirror the pad for byte-identity with the AST-walker + // fallback. Primitives (str/i32/u8/...) keep their natural + // stride — slotsize's TARRAY arm reads primsize directly, + // not si.totsize. Only TY_STRUCT subs need the round. + if (sub != nil && sub.kind == tykind.TY_STRUCT) { + let stride: u64 = sub.size; + if ((stride & 7u64) != 0u64) { + stride = (stride + 7u64) & ~7u64; + }; + r.size = stride * elen; + }; } else { if (k == nkind.N_TFN) { // Cstage cmd/wcc/check.c:437-466: function types are 8B / 8B // (call-target pointer shape). Pre-bind before recursing into @@ -10947,8 +10978,8 @@ export fn letslotsize(c: *cgen, n: *node) i32 = { }; fn slotsize(c: *cgen, typn: *node) i32 = { - // #61 audit §1.8 — A.3 fast-path expansion. Read tinfo.size off the - // populated type-expression node when the kind matches the cstage + // #61 audit §1.8 — A.3 / A.4 fast-path expansion. Read tinfo.size off + // the populated type-expression node when the kind matches the cstage // natural-size SSoT. Coverage: // - pointer-like (PTR/CHAN/FN), slice, str, tagged // return ti.size directly (size already encodes the slot). @@ -10959,17 +10990,45 @@ fn slotsize(c: *cgen, typn: *node) i32 = { // primitive into an 8B stack slot regardless of tinfo.size. // Padding lives at the read site, not in tinfo.size, so size(T) // stays a faithful natural-width SSoT. - // TUPLE / TSTRUCT / TARRAY still flow through the AST-walker fallback - // because cgen's per-field stride contract (registerstruct.totsize - // rounding, [N]T element-size walk) lives there, not in tinfo.size. + // - #61 A.4 adds: + // · TY_VOID returns 0 (mirrors the TNAME-"void" fallback arm). + // Closes 56 N_TNAME-"void" / void-aliased (utf8.invalid, + // overflow, done, more, ...) fallback hits — tinfofornode + // now caches both the TNAME and its resolved body so future + // calls on either land in the cache. + // · TY_ARRAY returns ti.size when alen > 0 — tinfofornode's + // TARRAY arm now applies the same struct-element round-to-8 + // cgen's fieldsize uses (#61 A.4 step 2), so [N]Struct's + // stride matches. [_]T (alen=0) still routes through the + // AST walker; letslotsize patches the length there. + // TUPLE still flows through the fallback — slotsize sums raw element + // sizes there, while tinfofornode TTUPLE mirrors cstage's natural + // sum, and the two diverge for `(i32, str)`-style mixes (slot vs + // natural per-element padding). + // + // TY_STRUCT deferred to A.5: tinfofornode TSTRUCT uses per-field + // natural-align for offsets (mirroring cstage's resolve_type + + // astsize, so `size(T)` stays natural), but registerstruct uses + // size-derived alignment with nested structs slot-padded to 8 + // (cgenutil.ww:2178-2207). The two agree for the common 8B-aligned + // shapes but diverge for "ragged tail" structs like + // `struct { inner: struct{i32,i32,i32}, mark: i32 }` where the + // inner struct's natural align (4) drops below cgen's nested- + // struct contract (aln=8 for sz>=8). Closing the gap without + // touching `size(T)`'s natural-width contract needs a separate + // SSoT (e.g. tinfo.slotsize) — filed as A.5. if (typn != nil && typn.type_ != nil) { let ti: *tinfo = typn.type_: *tinfo; let kk: tykind = ti.kind; if (kk == tykind.TY_PTR || kk == tykind.TY_SLICE || kk == tykind.TY_CHAN || kk == tykind.TY_FN || - kk == tykind.TY_STR || kk == tykind.TY_TAGGED) { + kk == tykind.TY_STR || kk == tykind.TY_TAGGED || + kk == tykind.TY_VOID) { return ti.size: i32; }; + if (kk == tykind.TY_ARRAY) { + if (ti.alen > 0u64) { return ti.size: i32; }; + }; if (kk == tykind.TY_BOOL || kk == tykind.TY_RUNE || kk == tykind.TY_I8 || kk == tykind.TY_I16 || kk == tykind.TY_I32 || kk == tykind.TY_I64 ||