selfhost/cmd/wcc: TARRAY struct-stride + cache-bind resolved body (Phase A.4)

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.
This commit is contained in:
2026-05-20 13:42:15 +09:00
parent 82c1948239
commit e37b76710a
4 changed files with 201 additions and 24 deletions

View File

@@ -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 ||

View File

@@ -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 ||

View File

@@ -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

View File

@@ -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 ||