selfhost/cmd/wcc/cgenutil: collapse slotsize onto tinfo (#48, A.6.3d)

slotsize selected the slot-padded width for a frame slot via an AST
walker over TBANG / TPTR / TFN / TCHAN / TSLICE / TTUPLE / TTAGGED /
TNAME / TARRAY / TSTRUCT — duplicating the size + slot-pad math
tinfofornode already runs at check time. With A.6.2 stamping every
N_T* kind's n.type_, plus #61 A.5 splitting tinfo.slotsize from
tinfo.size, the dispatch collapses to a kind switch over the
populated tinfo:
  - TY_VOID → 0
  - TY_PTR/SLICE/CHAN/FN/STR/TAGGED → ti.size (size == slotsize for
    these kinds)
  - TY_STRUCT/TUPLE/ARRAY → ti.slotsize (slot-padded by check.ww's
    TSTRUCT/TTUPLE/TARRAY arms with the same field-pad / stride
    rules cgenutil's registerstruct uses)
  - primitives → catch-all 8 (pad-to-8 lives at the read site, not
    in ti.slotsize, so [N]i32 stride stays 4)

Ww-to-Hare divergence (deliberate, team consensus): Hare's struct
type carries only `size` (ref/harec/include/types.h:134-137); QBE
handles slot padding downstream. ww emits Plan 9 amd64 asm directly,
so slot-padding is part of the ww calling convention and belongs on
the type table. Cstage scatters pad-to-8 inline at ~30+ localoff
sites + the `(su->kind == TY_TAGGED) ? su->size : 16` tagged-spill
pattern at cmd/w6c/cgen.c:4850 / :5193 — that scattering would be a
rule-13 (no hardcoded size literals) violation in ww. Centralizing
on tinfo.slotsize IS the rule-13-compliant shape; #61 A.5 put it
there, and #48 just consumes it. Rule 9 is lib/-scoped; tinfo is
compiler-internal (already diverges via node.type_ as a checker
invariant Hare/harec lacks). Rule 10 covers inference power, not
data shape — byte-identity (994/995) holds at 133/133.

Body went from ~150 LOC AST walk to ~15 LOC tinfo dispatch. Two
nil guards (typn == nil, ti == nil) fall through to 8 — same outcome
as cstage's `(t && t->size > 0) ? sz : 8` defensive shape.
`c: *cgen` retained unused for callsite stability (localloadop
precedent, 68219a1).

Net -453 LOC across cgenutil.ww + two .combined.ww bundler regens.
Follow-up filed: grow cstage Type.slot_size symmetrically and
sizelint-ok-annotate the surviving inline 8s until ported.
This commit is contained in:
2026-05-23 00:55:44 +09:00
parent 68219a119c
commit a828c036d0
3 changed files with 90 additions and 543 deletions

View File

@@ -12155,190 +12155,39 @@ export fn letslotsize(c: *cgen, n: *node) i32 = {
return 8;
};
// #48 A.6.3d: AST walker retired. resolvewalk (check.ww:426-436) stamps
// `n.type_` on every N_T* kind via tinfofornode, which folds TBANG
// (inner unchanged, check.ww:1154-1161), TNAME alias chains
// (resolvealias, check.ww:1128-1153), TARRAY/TPTR/TSLICE/TCHAN/TFN/
// TENUM/TTUPLE/TSTRUCT/TTAGGED with size + slot-padded slotsize.
//
// cstage SSoT is distributed — there is no single slot_size(Type*).
// Tagged slot follows cstage cmd/wcc/check.c:348 + :998 (tag (8) +
// max variant payload rounded to 8); the wwstage TTAGGED arm at
// check.ww:1296-1346 mirrors that layout. cgen.c:4850 / :5193 use
// the same `(su->kind == TY_TAGGED) ? su->size : 16` pattern for the
// match-spill slot. Pointer-and-narrow → 8 is the local-frame
// convention encoded at every cstage `localoff(…, 8, …)` callsite
// (cmd/w6c/cgen.c throughout); wwstage encodes the same pad-to-8 at
// this read site so `[N]i32` stride stays 4 (natural) — moving it
// into ti.slotsize would lift array stride to 8/elem.
//
// `c: *cgen` retained unused for callsite stability (localloadop
// precedent, 68219a1).
fn slotsize(c: *cgen, typn: *node) i32 = {
// #61 audit §1.8 — A.3 / A.4 / A.5 fast-path expansion. Read the
// slot-padded width off the populated type-expression node when the
// kind matches the cstage natural-size SSoT. Coverage:
// - pointer-like (PTR/CHAN/FN), slice, str, tagged: ti.size ===
// ti.slotsize (slot already equals natural). TY_TAGGED is safe
// now that tinfofornode folds `(*T | void)` to 8B (#61 A.3 step
// 1, mirrors cmd/wcc/check.c:412-426).
// - narrow scalars (BOOL/RUNE/I8..I32/U8..U32/ENUM/F32) pad UP to
// 8 — cstage's let_emit_size (cmd/w6c/cgen.c:691-720) spills
// every primitive into an 8B stack slot regardless of
// tinfo.size. Pad-to-8 lives at the read site, not in
// tinfo.slotsize, so `[N]i32` stride stays 4 (natural) — moving
// the pad into ti.slotsize would lift array stride to 8/elem.
// - TY_VOID returns 0 (mirrors the TNAME-"void" fallback arm,
// same as #61 A.4).
// - #61 A.5 adds: TY_STRUCT / TY_TUPLE / TY_ARRAY read ti.slotsize
// (slot-padded). tinfofornode populates the slot total mirroring
// cgenutil.ww registerstruct (size-derived align, nested struct
// fields → si.totsize, final round to 8), and TARRAY threads
// stride through sub.slotsize so `[N]Triplet` lifts to padded *
// N. `size(T)` stays natural — split SSoT in tinfo.
if (typn != nil && typn.type_ != nil) {
if (typn == nil) { return 8; };
let ti: *tinfo = typn.type_: *tinfo;
if (ti == nil) { return 8; };
let kk: tykind = ti.kind;
if (kk == tykind.TY_VOID) { return 0; };
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_VOID) {
kk == tykind.TY_STR || kk == tykind.TY_TAGGED) {
return ti.size: i32;
};
if (kk == tykind.TY_STRUCT || kk == tykind.TY_TUPLE) {
if (ti.slotsize > 0u64) { return ti.slotsize: i32; };
};
if (kk == tykind.TY_ARRAY) {
if (ti.alen > 0u64) { return ti.slotsize: 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 ||
kk == tykind.TY_U8 || kk == tykind.TY_U16 ||
kk == tykind.TY_U32 || kk == tykind.TY_U64 ||
kk == tykind.TY_INT || kk == tykind.TY_UINT ||
kk == tykind.TY_UINTPTR || kk == tykind.TY_ENUM ||
kk == tykind.TY_F32 || kk == tykind.TY_F64) {
return 8;
};
};
if (typn == nil) { return 8; };
let k: nkind = typn.kind;
// `!T` carries T's memory layout; the error-tag bit lives in the
// enclosing union's discriminant, not the variant payload. cstage
// resolve_type N_TBANG copies the inner size (cmd/wcc/check.c:303-
// 306); wwstage previously fell through to the catch-all 8, so a
// bare `!void` variant sized to 8 instead of 0. (#48 part-A.)
if (k == nkind.N_TBANG) { return slotsize(c, typn.lhs); };
if (k == nkind.N_TPTR) { return 8; };
if (k == nkind.N_TFN) { return 8; };
if (k == nkind.N_TCHAN) { return 8; };
if (k == nkind.N_TSLICE) { return tyslicesize(): i32; };
if (k == nkind.N_TTUPLE) {
// Sum element sizes. Mirrors C cgen which uses raw type
// sizes; padding to 8 happens inside slotsize for primitives,
// so a `(i64, str)` resolves to 8 + 16 = 24 (matches the C
// cgen 24B init / positional-access layout).
let total: i32 = 0;
let p: *node = typn.list;
for (p != nil) {
total += slotsize(c, p.lhs);
p = p.next;
};
return total;
};
if (k == nkind.N_TTAGGED){
// Nullable `(*T | void)` collapses to a single 8B pointer.
if (isnullabletype(typn)) { return 8; };
// Slot = 8 (tag) + max(variant payload sizes), rounded up
// to an 8-byte multiple so the reg-passing ABI (size/8
// words) doesn't drop the last value register. Mirrors C
// cgen's resolve_type for nkind.N_TTAGGED.
let v: *node = typn.list;
let maxsz: i32 = 0;
for (v != nil) {
let sz: i32 = slotsize(c, v);
if (sz > maxsz) { maxsz = sz; };
v = v.next;
};
let pad: i32 = (maxsz + 7) & ~7;
return 8 + pad;
};
if (k == nkind.N_TNAME) {
let nm: str = typn.str;
// `void` is zero-sized per Hare design; cstage's ty_void.size = 0
// at cmd/wcc/type.c:46. Pre-#48 wwstage fell through to the
// catch-all 8, so (void | !void) sized as tag + 8 = 16 and let-
// init through cgwidentaggedstore emitted a phantom DX spill at
// slot+8 picking up the callee's stale-DX. Bypassing primsize's
// `> 0` guard keeps the rest of the prim-pad-to-8 contract intact.
if (streq(nm, "void")) { return 0; };
if (streq(nm, "str")) { return primtypesize("str"): i32; };
let ps: i32 = primsize(nm);
if (ps > 0) {
// Pad to 8 for stack slots — matches C cgen which spills
// every primitive into an 8-byte slot.
return 8;
};
// Named struct lookup.
let si: *structinfo = structlookup(c, nm);
if (si != nil) { return si.totsize; };
// Type alias (`type foo = !str;` / `type foo = bar;`):
// follow it so a tagged-union variant of a !str-aliased
// error type contributes 16 bytes to the max payload
// rather than 8 (the default).
if (c != nil) {
let aliased: *node = aliaslookup(c, nm);
if (aliased != nil) {
if (aliased.kind == nkind.N_TBANG) {
return slotsize(c, aliased.lhs);
};
return slotsize(c, aliased);
};
};
return 8;
};
if (k == nkind.N_TARRAY) {
let lenn: *node = typn.rhs;
let elemn: *node = typn.lhs;
let elen: i64 = 1i64;
if (lenn != nil) {
if (lenn.kind == nkind.N_INTLIT) { elen = lenn.uval: i64; };
};
let esz: i32 = 8;
if (elemn != nil) {
if (elemn.kind == nkind.N_TNAME) {
let en: str = elemn.str;
// `str` is a composite primitive (ptr+len, 16B);
// primsize returns 0 for it, so without this
// explicit case a `[N]str` would slot 8B/elem,
// collapsing the per-element stride and losing
// every .len half.
if (streq(en, "str")) { esz = primtypesize("str"): i32; };
let ps: i32 = primsize(en);
if (esz == 8) { if (ps > 0) { esz = ps; }
else {
// Named struct / aliased type: size off
// the structinfo if present, else follow
// the alias via aliaslookup so
// `[N]formattable` reads the resolved
// tagged slot (e.g. 24B for
// `(i64|str|bool)`), not the fall-
// through 8B.
let si: *structinfo = structlookup(c, en);
if (si != nil) { esz = si.totsize; }
else { if (c != nil) {
let al: *node = aliaslookup(c, en);
if (al != nil) {
esz = slotsize(c, al);
};
}; };
}; };
} else { if (elemn.kind == nkind.N_TTAGGED) {
// Tagged-union element: full slot (8 tag +
// padded max payload). Matches C cgen's
// resolve_type for `[N]TAGGED`.
esz = slotsize(c, elemn);
} else { if (elemn.kind == nkind.N_TPTR) {
esz = 8;
} else { if (elemn.kind == nkind.N_TSTRUCT) {
esz = slotsize(c, elemn);
}; }; }; };
};
return (esz: i64 * elen): i32;
};
if (k == nkind.N_TSTRUCT) {
// Inline anonymous struct — sum of field sizes.
let f: *node = typn.list;
let total: i32 = 0;
for (f != nil) {
if (f.kind == nkind.N_TFIELD) {
total += slotsize(c, f.lhs);
};
f = f.next;
};
return total;
if (kk == tykind.TY_STRUCT || kk == tykind.TY_TUPLE ||
kk == tykind.TY_ARRAY) {
return ti.slotsize: i32;
};
return 8;
};

View File

@@ -1783,190 +1783,39 @@ export fn letslotsize(c: *cgen, n: *node) i32 = {
return 8;
};
// #48 A.6.3d: AST walker retired. resolvewalk (check.ww:426-436) stamps
// `n.type_` on every N_T* kind via tinfofornode, which folds TBANG
// (inner unchanged, check.ww:1154-1161), TNAME alias chains
// (resolvealias, check.ww:1128-1153), TARRAY/TPTR/TSLICE/TCHAN/TFN/
// TENUM/TTUPLE/TSTRUCT/TTAGGED with size + slot-padded slotsize.
//
// cstage SSoT is distributed — there is no single slot_size(Type*).
// Tagged slot follows cstage cmd/wcc/check.c:348 + :998 (tag (8) +
// max variant payload rounded to 8); the wwstage TTAGGED arm at
// check.ww:1296-1346 mirrors that layout. cgen.c:4850 / :5193 use
// the same `(su->kind == TY_TAGGED) ? su->size : 16` pattern for the
// match-spill slot. Pointer-and-narrow → 8 is the local-frame
// convention encoded at every cstage `localoff(…, 8, …)` callsite
// (cmd/w6c/cgen.c throughout); wwstage encodes the same pad-to-8 at
// this read site so `[N]i32` stride stays 4 (natural) — moving it
// into ti.slotsize would lift array stride to 8/elem.
//
// `c: *cgen` retained unused for callsite stability (localloadop
// precedent, 68219a1).
fn slotsize(c: *cgen, typn: *node) i32 = {
// #61 audit §1.8 — A.3 / A.4 / A.5 fast-path expansion. Read the
// slot-padded width off the populated type-expression node when the
// kind matches the cstage natural-size SSoT. Coverage:
// - pointer-like (PTR/CHAN/FN), slice, str, tagged: ti.size ===
// ti.slotsize (slot already equals natural). TY_TAGGED is safe
// now that tinfofornode folds `(*T | void)` to 8B (#61 A.3 step
// 1, mirrors cmd/wcc/check.c:412-426).
// - narrow scalars (BOOL/RUNE/I8..I32/U8..U32/ENUM/F32) pad UP to
// 8 — cstage's let_emit_size (cmd/w6c/cgen.c:691-720) spills
// every primitive into an 8B stack slot regardless of
// tinfo.size. Pad-to-8 lives at the read site, not in
// tinfo.slotsize, so `[N]i32` stride stays 4 (natural) — moving
// the pad into ti.slotsize would lift array stride to 8/elem.
// - TY_VOID returns 0 (mirrors the TNAME-"void" fallback arm,
// same as #61 A.4).
// - #61 A.5 adds: TY_STRUCT / TY_TUPLE / TY_ARRAY read ti.slotsize
// (slot-padded). tinfofornode populates the slot total mirroring
// cgenutil.ww registerstruct (size-derived align, nested struct
// fields → si.totsize, final round to 8), and TARRAY threads
// stride through sub.slotsize so `[N]Triplet` lifts to padded *
// N. `size(T)` stays natural — split SSoT in tinfo.
if (typn != nil && typn.type_ != nil) {
if (typn == nil) { return 8; };
let ti: *tinfo = typn.type_: *tinfo;
if (ti == nil) { return 8; };
let kk: tykind = ti.kind;
if (kk == tykind.TY_VOID) { return 0; };
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_VOID) {
kk == tykind.TY_STR || kk == tykind.TY_TAGGED) {
return ti.size: i32;
};
if (kk == tykind.TY_STRUCT || kk == tykind.TY_TUPLE) {
if (ti.slotsize > 0u64) { return ti.slotsize: i32; };
};
if (kk == tykind.TY_ARRAY) {
if (ti.alen > 0u64) { return ti.slotsize: 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 ||
kk == tykind.TY_U8 || kk == tykind.TY_U16 ||
kk == tykind.TY_U32 || kk == tykind.TY_U64 ||
kk == tykind.TY_INT || kk == tykind.TY_UINT ||
kk == tykind.TY_UINTPTR || kk == tykind.TY_ENUM ||
kk == tykind.TY_F32 || kk == tykind.TY_F64) {
return 8;
};
};
if (typn == nil) { return 8; };
let k: nkind = typn.kind;
// `!T` carries T's memory layout; the error-tag bit lives in the
// enclosing union's discriminant, not the variant payload. cstage
// resolve_type N_TBANG copies the inner size (cmd/wcc/check.c:303-
// 306); wwstage previously fell through to the catch-all 8, so a
// bare `!void` variant sized to 8 instead of 0. (#48 part-A.)
if (k == nkind.N_TBANG) { return slotsize(c, typn.lhs); };
if (k == nkind.N_TPTR) { return 8; };
if (k == nkind.N_TFN) { return 8; };
if (k == nkind.N_TCHAN) { return 8; };
if (k == nkind.N_TSLICE) { return tyslicesize(): i32; };
if (k == nkind.N_TTUPLE) {
// Sum element sizes. Mirrors C cgen which uses raw type
// sizes; padding to 8 happens inside slotsize for primitives,
// so a `(i64, str)` resolves to 8 + 16 = 24 (matches the C
// cgen 24B init / positional-access layout).
let total: i32 = 0;
let p: *node = typn.list;
for (p != nil) {
total += slotsize(c, p.lhs);
p = p.next;
};
return total;
};
if (k == nkind.N_TTAGGED){
// Nullable `(*T | void)` collapses to a single 8B pointer.
if (isnullabletype(typn)) { return 8; };
// Slot = 8 (tag) + max(variant payload sizes), rounded up
// to an 8-byte multiple so the reg-passing ABI (size/8
// words) doesn't drop the last value register. Mirrors C
// cgen's resolve_type for nkind.N_TTAGGED.
let v: *node = typn.list;
let maxsz: i32 = 0;
for (v != nil) {
let sz: i32 = slotsize(c, v);
if (sz > maxsz) { maxsz = sz; };
v = v.next;
};
let pad: i32 = (maxsz + 7) & ~7;
return 8 + pad;
};
if (k == nkind.N_TNAME) {
let nm: str = typn.str;
// `void` is zero-sized per Hare design; cstage's ty_void.size = 0
// at cmd/wcc/type.c:46. Pre-#48 wwstage fell through to the
// catch-all 8, so (void | !void) sized as tag + 8 = 16 and let-
// init through cgwidentaggedstore emitted a phantom DX spill at
// slot+8 picking up the callee's stale-DX. Bypassing primsize's
// `> 0` guard keeps the rest of the prim-pad-to-8 contract intact.
if (streq(nm, "void")) { return 0; };
if (streq(nm, "str")) { return primtypesize("str"): i32; };
let ps: i32 = primsize(nm);
if (ps > 0) {
// Pad to 8 for stack slots — matches C cgen which spills
// every primitive into an 8-byte slot.
return 8;
};
// Named struct lookup.
let si: *structinfo = structlookup(c, nm);
if (si != nil) { return si.totsize; };
// Type alias (`type foo = !str;` / `type foo = bar;`):
// follow it so a tagged-union variant of a !str-aliased
// error type contributes 16 bytes to the max payload
// rather than 8 (the default).
if (c != nil) {
let aliased: *node = aliaslookup(c, nm);
if (aliased != nil) {
if (aliased.kind == nkind.N_TBANG) {
return slotsize(c, aliased.lhs);
};
return slotsize(c, aliased);
};
};
return 8;
};
if (k == nkind.N_TARRAY) {
let lenn: *node = typn.rhs;
let elemn: *node = typn.lhs;
let elen: i64 = 1i64;
if (lenn != nil) {
if (lenn.kind == nkind.N_INTLIT) { elen = lenn.uval: i64; };
};
let esz: i32 = 8;
if (elemn != nil) {
if (elemn.kind == nkind.N_TNAME) {
let en: str = elemn.str;
// `str` is a composite primitive (ptr+len, 16B);
// primsize returns 0 for it, so without this
// explicit case a `[N]str` would slot 8B/elem,
// collapsing the per-element stride and losing
// every .len half.
if (streq(en, "str")) { esz = primtypesize("str"): i32; };
let ps: i32 = primsize(en);
if (esz == 8) { if (ps > 0) { esz = ps; }
else {
// Named struct / aliased type: size off
// the structinfo if present, else follow
// the alias via aliaslookup so
// `[N]formattable` reads the resolved
// tagged slot (e.g. 24B for
// `(i64|str|bool)`), not the fall-
// through 8B.
let si: *structinfo = structlookup(c, en);
if (si != nil) { esz = si.totsize; }
else { if (c != nil) {
let al: *node = aliaslookup(c, en);
if (al != nil) {
esz = slotsize(c, al);
};
}; };
}; };
} else { if (elemn.kind == nkind.N_TTAGGED) {
// Tagged-union element: full slot (8 tag +
// padded max payload). Matches C cgen's
// resolve_type for `[N]TAGGED`.
esz = slotsize(c, elemn);
} else { if (elemn.kind == nkind.N_TPTR) {
esz = 8;
} else { if (elemn.kind == nkind.N_TSTRUCT) {
esz = slotsize(c, elemn);
}; }; }; };
};
return (esz: i64 * elen): i32;
};
if (k == nkind.N_TSTRUCT) {
// Inline anonymous struct — sum of field sizes.
let f: *node = typn.list;
let total: i32 = 0;
for (f != nil) {
if (f.kind == nkind.N_TFIELD) {
total += slotsize(c, f.lhs);
};
f = f.next;
};
return total;
if (kk == tykind.TY_STRUCT || kk == tykind.TY_TUPLE ||
kk == tykind.TY_ARRAY) {
return ti.slotsize: i32;
};
return 8;
};

View File

@@ -12155,190 +12155,39 @@ export fn letslotsize(c: *cgen, n: *node) i32 = {
return 8;
};
// #48 A.6.3d: AST walker retired. resolvewalk (check.ww:426-436) stamps
// `n.type_` on every N_T* kind via tinfofornode, which folds TBANG
// (inner unchanged, check.ww:1154-1161), TNAME alias chains
// (resolvealias, check.ww:1128-1153), TARRAY/TPTR/TSLICE/TCHAN/TFN/
// TENUM/TTUPLE/TSTRUCT/TTAGGED with size + slot-padded slotsize.
//
// cstage SSoT is distributed — there is no single slot_size(Type*).
// Tagged slot follows cstage cmd/wcc/check.c:348 + :998 (tag (8) +
// max variant payload rounded to 8); the wwstage TTAGGED arm at
// check.ww:1296-1346 mirrors that layout. cgen.c:4850 / :5193 use
// the same `(su->kind == TY_TAGGED) ? su->size : 16` pattern for the
// match-spill slot. Pointer-and-narrow → 8 is the local-frame
// convention encoded at every cstage `localoff(…, 8, …)` callsite
// (cmd/w6c/cgen.c throughout); wwstage encodes the same pad-to-8 at
// this read site so `[N]i32` stride stays 4 (natural) — moving it
// into ti.slotsize would lift array stride to 8/elem.
//
// `c: *cgen` retained unused for callsite stability (localloadop
// precedent, 68219a1).
fn slotsize(c: *cgen, typn: *node) i32 = {
// #61 audit §1.8 — A.3 / A.4 / A.5 fast-path expansion. Read the
// slot-padded width off the populated type-expression node when the
// kind matches the cstage natural-size SSoT. Coverage:
// - pointer-like (PTR/CHAN/FN), slice, str, tagged: ti.size ===
// ti.slotsize (slot already equals natural). TY_TAGGED is safe
// now that tinfofornode folds `(*T | void)` to 8B (#61 A.3 step
// 1, mirrors cmd/wcc/check.c:412-426).
// - narrow scalars (BOOL/RUNE/I8..I32/U8..U32/ENUM/F32) pad UP to
// 8 — cstage's let_emit_size (cmd/w6c/cgen.c:691-720) spills
// every primitive into an 8B stack slot regardless of
// tinfo.size. Pad-to-8 lives at the read site, not in
// tinfo.slotsize, so `[N]i32` stride stays 4 (natural) — moving
// the pad into ti.slotsize would lift array stride to 8/elem.
// - TY_VOID returns 0 (mirrors the TNAME-"void" fallback arm,
// same as #61 A.4).
// - #61 A.5 adds: TY_STRUCT / TY_TUPLE / TY_ARRAY read ti.slotsize
// (slot-padded). tinfofornode populates the slot total mirroring
// cgenutil.ww registerstruct (size-derived align, nested struct
// fields → si.totsize, final round to 8), and TARRAY threads
// stride through sub.slotsize so `[N]Triplet` lifts to padded *
// N. `size(T)` stays natural — split SSoT in tinfo.
if (typn != nil && typn.type_ != nil) {
if (typn == nil) { return 8; };
let ti: *tinfo = typn.type_: *tinfo;
if (ti == nil) { return 8; };
let kk: tykind = ti.kind;
if (kk == tykind.TY_VOID) { return 0; };
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_VOID) {
kk == tykind.TY_STR || kk == tykind.TY_TAGGED) {
return ti.size: i32;
};
if (kk == tykind.TY_STRUCT || kk == tykind.TY_TUPLE) {
if (ti.slotsize > 0u64) { return ti.slotsize: i32; };
};
if (kk == tykind.TY_ARRAY) {
if (ti.alen > 0u64) { return ti.slotsize: 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 ||
kk == tykind.TY_U8 || kk == tykind.TY_U16 ||
kk == tykind.TY_U32 || kk == tykind.TY_U64 ||
kk == tykind.TY_INT || kk == tykind.TY_UINT ||
kk == tykind.TY_UINTPTR || kk == tykind.TY_ENUM ||
kk == tykind.TY_F32 || kk == tykind.TY_F64) {
return 8;
};
};
if (typn == nil) { return 8; };
let k: nkind = typn.kind;
// `!T` carries T's memory layout; the error-tag bit lives in the
// enclosing union's discriminant, not the variant payload. cstage
// resolve_type N_TBANG copies the inner size (cmd/wcc/check.c:303-
// 306); wwstage previously fell through to the catch-all 8, so a
// bare `!void` variant sized to 8 instead of 0. (#48 part-A.)
if (k == nkind.N_TBANG) { return slotsize(c, typn.lhs); };
if (k == nkind.N_TPTR) { return 8; };
if (k == nkind.N_TFN) { return 8; };
if (k == nkind.N_TCHAN) { return 8; };
if (k == nkind.N_TSLICE) { return tyslicesize(): i32; };
if (k == nkind.N_TTUPLE) {
// Sum element sizes. Mirrors C cgen which uses raw type
// sizes; padding to 8 happens inside slotsize for primitives,
// so a `(i64, str)` resolves to 8 + 16 = 24 (matches the C
// cgen 24B init / positional-access layout).
let total: i32 = 0;
let p: *node = typn.list;
for (p != nil) {
total += slotsize(c, p.lhs);
p = p.next;
};
return total;
};
if (k == nkind.N_TTAGGED){
// Nullable `(*T | void)` collapses to a single 8B pointer.
if (isnullabletype(typn)) { return 8; };
// Slot = 8 (tag) + max(variant payload sizes), rounded up
// to an 8-byte multiple so the reg-passing ABI (size/8
// words) doesn't drop the last value register. Mirrors C
// cgen's resolve_type for nkind.N_TTAGGED.
let v: *node = typn.list;
let maxsz: i32 = 0;
for (v != nil) {
let sz: i32 = slotsize(c, v);
if (sz > maxsz) { maxsz = sz; };
v = v.next;
};
let pad: i32 = (maxsz + 7) & ~7;
return 8 + pad;
};
if (k == nkind.N_TNAME) {
let nm: str = typn.str;
// `void` is zero-sized per Hare design; cstage's ty_void.size = 0
// at cmd/wcc/type.c:46. Pre-#48 wwstage fell through to the
// catch-all 8, so (void | !void) sized as tag + 8 = 16 and let-
// init through cgwidentaggedstore emitted a phantom DX spill at
// slot+8 picking up the callee's stale-DX. Bypassing primsize's
// `> 0` guard keeps the rest of the prim-pad-to-8 contract intact.
if (streq(nm, "void")) { return 0; };
if (streq(nm, "str")) { return primtypesize("str"): i32; };
let ps: i32 = primsize(nm);
if (ps > 0) {
// Pad to 8 for stack slots — matches C cgen which spills
// every primitive into an 8-byte slot.
return 8;
};
// Named struct lookup.
let si: *structinfo = structlookup(c, nm);
if (si != nil) { return si.totsize; };
// Type alias (`type foo = !str;` / `type foo = bar;`):
// follow it so a tagged-union variant of a !str-aliased
// error type contributes 16 bytes to the max payload
// rather than 8 (the default).
if (c != nil) {
let aliased: *node = aliaslookup(c, nm);
if (aliased != nil) {
if (aliased.kind == nkind.N_TBANG) {
return slotsize(c, aliased.lhs);
};
return slotsize(c, aliased);
};
};
return 8;
};
if (k == nkind.N_TARRAY) {
let lenn: *node = typn.rhs;
let elemn: *node = typn.lhs;
let elen: i64 = 1i64;
if (lenn != nil) {
if (lenn.kind == nkind.N_INTLIT) { elen = lenn.uval: i64; };
};
let esz: i32 = 8;
if (elemn != nil) {
if (elemn.kind == nkind.N_TNAME) {
let en: str = elemn.str;
// `str` is a composite primitive (ptr+len, 16B);
// primsize returns 0 for it, so without this
// explicit case a `[N]str` would slot 8B/elem,
// collapsing the per-element stride and losing
// every .len half.
if (streq(en, "str")) { esz = primtypesize("str"): i32; };
let ps: i32 = primsize(en);
if (esz == 8) { if (ps > 0) { esz = ps; }
else {
// Named struct / aliased type: size off
// the structinfo if present, else follow
// the alias via aliaslookup so
// `[N]formattable` reads the resolved
// tagged slot (e.g. 24B for
// `(i64|str|bool)`), not the fall-
// through 8B.
let si: *structinfo = structlookup(c, en);
if (si != nil) { esz = si.totsize; }
else { if (c != nil) {
let al: *node = aliaslookup(c, en);
if (al != nil) {
esz = slotsize(c, al);
};
}; };
}; };
} else { if (elemn.kind == nkind.N_TTAGGED) {
// Tagged-union element: full slot (8 tag +
// padded max payload). Matches C cgen's
// resolve_type for `[N]TAGGED`.
esz = slotsize(c, elemn);
} else { if (elemn.kind == nkind.N_TPTR) {
esz = 8;
} else { if (elemn.kind == nkind.N_TSTRUCT) {
esz = slotsize(c, elemn);
}; }; }; };
};
return (esz: i64 * elen): i32;
};
if (k == nkind.N_TSTRUCT) {
// Inline anonymous struct — sum of field sizes.
let f: *node = typn.list;
let total: i32 = 0;
for (f != nil) {
if (f.kind == nkind.N_TFIELD) {
total += slotsize(c, f.lhs);
};
f = f.next;
};
return total;
if (kk == tykind.TY_STRUCT || kk == tykind.TY_TUPLE ||
kk == tykind.TY_ARRAY) {
return ti.slotsize: i32;
};
return 8;
};