selfhost/cmd/wcc + lib/ww: tinfo.slotsize SSoT + module-name TNAME fallback (Phase A.5)

A.4 left 74 fallback hits, all TNAME-flavored — 71 TNAME → TY_STRUCT
(natural-align vs slot-padded mismatch) + 3 module-name TNAME quirks
(`let l: lex;` where lex is both struct and imported module).

tinfo gains a slotsize: u64 field (96 → 104 bytes; amalloc bumped
to 112B per rule-7). size(T) stays Hare-natural at the user level;
cgen's slot storage now reads ti.slotsize for kinds where the two
differ. tinfofornode populates both:

- TSTRUCT: existing natural-align walk for r.size; new size-derived
  align walk (sz≥8→8, ≥4→4, ≥2→2) for r.slotsize, rounded to 8.
  Mirrors cgenutil.ww:2192-2218 registerstruct exactly.
- TTUPLE: parallel via tupleelemslot helper (primitives→8, str=16,
  slice=24, ptr/fn/chan/i64/u64/int/uint/uintptr/f64=8, composite
  →pt.slotsize, void=0).
- TARRAY: typearray sets slotsize = sub.slotsize * n. [N]i32 stays
  4N (natural); [N]Triplet lifts to 16N (slot-padded). Reverts
  A.4's r.size override since slot-pad now lives in slotsize.
- TFN/TENUM/TTAGGED/nullable: explicit slotsize. Default trail
  `if r.slotsize == 0 then r.slotsize = r.size` catches TBANG.
- New fieldslotsize(ft) helper mirrors registerstruct's per-field
  rule (struct→ft.slotsize, array→ft.slotsize, primitive→ft.size,
  tagged→ft.size).

slotsize fast-path (cgenutil.ww) reads ti.slotsize for TY_STRUCT,
TY_TUPLE, TY_ARRAY; ti.size stays correct for PTR/SLICE/CHAN/FN/
STR/TAGGED/VOID (size == slotsize for those). Narrow scalars still
pad-to-8 at the read site (moving into slotsize would break
[N]i32 stride).

lib/ww/sym.ww adds scopelookuptype(s, name) — same FNV bucket+parent
walk as scopelookup but filtered on skind==SK_TYPE. resolvealias
calls it when bare-leaf scopelookup returns non-TYPE (e.g., the
SK_USE/SK_MOD short-circuit case). Fixes `let l: lex;` (mod=leaf)
AND `let t: tok;` (mod≠leaf, tok lives in package lex).

Post-A.5 fallback: 0 across full bootstrap. Reviewer's stricter
metric (zero fast-path MISSES when tinfo IS stamped) also 0;
remaining FB_NIL hits are value-expression nodes the checker
doesn't yet stamp — A.6 candidate.

Ragged-tail probe `struct{inner=3*i32, mark:i32}`: ti.size=16
(natural), ti.slotsize=24 (slot-padded). Cstage emits [N]<ragged>
stride=16 on the same source — latent divergence filed as #63.
Not exercised by selfhost, so bootstrap byte-identity holds today.

131/131 + 994 + 995 + bootstrap (ww2==ww3==ww4) all green.
This commit is contained in:
2026-05-20 14:30:55 +09:00
parent e37b76710a
commit 9fd79cdc33
6 changed files with 621 additions and 168 deletions

View File

@@ -6518,6 +6518,18 @@ type tinfo = struct {
// is unchanged.
name: str,
under: *tinfo,
slotsize: u64, // #61 A.5: stack-slot SSoT split from `size`.
// `size` stays natural (Hare-faithful);
// `slotsize` carries the slot-padded width
// cgen's let/struct-field layout demands.
// For primitives/ptr/slice/chan/fn/str/tagged
// `slotsize == size`; struct + tuple + array
// of struct diverge — see check.ww tinfo-
// fornode + cgenutil.ww registerstruct.
// Pad-to-8 of narrow primitives in let slots
// still lives at slotsize()'s read site;
// graduating it here would break `[N]i32`
// stride (4*N stays natural).
};
// #61 audit §1.8 / Rob+Drew convergence 2026-05-20: memoizes
@@ -6566,7 +6578,9 @@ type tctx = struct {
// ---- constructors -----------------------------------------------------
export fn newtype(a: *arena, k: tykind) *tinfo = {
let t: *tinfo = amalloc(a, 96u64): *tinfo;
// #61 A.5: grew tinfo by slotsize: u64 (96 → 104). Over-size to 112
// per the bootstrap amalloc-undersize trap (selfhost/CLAUDE.md §1).
let t: *tinfo = amalloc(a, 112u64): *tinfo;
t.kind = k;
return t;
};
@@ -6576,6 +6590,7 @@ fn prim(a: *arena, k: tykind, nm: str, sz: u64, al: u64) *tinfo = {
t.name = nm;
t.size = sz;
if (al > 0u64) { t.align = al; } else { t.align = sz; };
t.slotsize = sz;
return t;
};
@@ -6614,6 +6629,7 @@ export fn typeptr(a: *arena, sub: *tinfo) *tinfo = {
t.sub = sub;
t.size = 8u64;
t.align = 8u64;
t.slotsize = 8u64;
return t;
};
@@ -6622,6 +6638,7 @@ export fn typeslice(a: *arena, sub: *tinfo) *tinfo = {
t.sub = sub;
t.size = 24u64;
t.align = 8u64;
t.slotsize = 24u64;
return t;
};
@@ -6632,6 +6649,11 @@ export fn typearray(a: *arena, sub: *tinfo, n: u64) *tinfo = {
if (sub != nil) {
t.size = sub.size * n;
t.align = sub.align;
// #61 A.5: ti.slotsize = stride * elen using the element's
// slot-padded width. Primitives have slotsize == size so
// `[N]i32` stride stays 4 (natural); structs have padded
// slotsize so `[N]Triplet` stride lifts to 16.
t.slotsize = sub.slotsize * n;
} else {
t.align = 1u64;
};
@@ -6643,6 +6665,7 @@ export fn typechan(a: *arena, sub: *tinfo) *tinfo = {
t.sub = sub;
t.size = 8u64;
t.align = 8u64;
t.slotsize = 8u64;
return t;
};
@@ -6653,6 +6676,7 @@ export fn typenamed(a: *arena, name: str, under: *tinfo) *tinfo = {
if (under != nil) {
t.size = under.size;
t.align = under.align;
t.slotsize = under.slotsize;
};
return t;
};
@@ -6914,6 +6938,35 @@ export fn scopelookup(s: *scope, name: str) *sym = {
return nil;
};
// scopelookuptype — find an SK_TYPE entry by name regardless of mod.
//
// Same FNV bucket + hashnext chain + parent walk as scopelookup, with
// an `skind == SK_TYPE` filter. Used to disambiguate the bare-TNAME
// vs imported-module-bareword collision: when scopelookup returns the
// SK_USE sym for a leaf that ALSO names a type (e.g. `tok` struct
// declared in lib/ww/lex/tok.ww with `package lex;` while
// `import tok;` registers a same-name SK_USE), the resolver needs
// the type entry regardless of its declared package — the struct's
// mod may differ from the leaf (lex/tok pair) so
// scopelookupinmodule(c, leaf, leaf) won't find it.
//
// Mirrors the bare-vs-qualified disambiguation pattern from task #57.
export fn scopelookuptype(s: *scope, name: str) *sym = {
for (s != nil) {
let h: u64 = hashstr(name);
let bi: i32 = (h % (s.nbuckets: u64)): i32;
let b: *sym = s.buckets[bi];
for (b != nil) {
if (streq(b.name, name)) {
if (b.skind == skind.SK_TYPE) { return b; };
};
b = b.hashnext;
};
s = s.parent;
};
return nil;
};
// scopelookupinmodule — module-filtered chain walk.
//
// Same FNV bucket + hashnext chain + parent walk as scopelookup, plus
@@ -7551,6 +7604,26 @@ fn resolvealias(c: *checker, n: *node) *node = {
s = scopelookupinmodule(c.cur, head, leaf);
} else {
s = scopelookup(c.cur, nm);
// #61 A.5: bare TNAME that collides with an imported
// module bareword. Two shapes hit this:
// - `let l: lex;` where `lex` struct lives in
// `package lex;` (mod matches leaf).
// - `let t: tok;` where `tok` struct lives in
// `package lex;` (mod differs from leaf — tok.ww
// declares `package lex;`).
// scopelookup bucket-walks the flat scope and can land
// on the SK_USE entry first; without the fallback we'd
// return the unresolved TNAME and tinfofornode aborts on
// body == n. scopelookuptype walks the same bucket but
// filters on SK_TYPE so the struct entry surfaces
// regardless of its declaring package. Mirrors the
// bare-vs-qualified pattern from task #57.
if (s != nil) {
if (s.skind != skind.SK_TYPE) {
let sm: *sym = scopelookuptype(c.cur, nm);
if (sm != nil) { s = sm; };
};
};
};
if (s == nil) { return cur; };
if (s.skind != skind.SK_TYPE) { return cur; };
@@ -7882,6 +7955,64 @@ fn foldtointlit(c: *checker, n: *node, v: i64) void = {
n.tsuffix = empty;
};
// #61 A.5 helper: per-element slot size when `pt` appears inside a
// tuple. Mirrors cgenutil.ww slotsize TTUPLE — cstage's tuple ABI
// spills each element into its own register / 8B eightbyte, so narrow
// scalars pad to 8 (cgen's let_emit_size + AX:DX:CX positional layout).
// str stays 16 (composite primitive), slice 24, pointer/fn/chan 8;
// composites contribute their own slot-padded width. void contributes
// 0 (never appears in tuples emitted by user code, but kept for SSoT
// symmetry with cgen's N_TNAME-"void" fallback arm).
fn tupleelemslot(pt: *tinfo) u64 = {
if (pt == nil) { return 8u64; };
let pk: tykind = pt.kind;
if (pk == tykind.TY_VOID) { return 0u64; };
if (pk == tykind.TY_STR) { return 16u64; };
if (pk == tykind.TY_SLICE) { return 24u64; };
if (pk == tykind.TY_PTR || pk == tykind.TY_FN ||
pk == tykind.TY_CHAN || pk == tykind.TY_I64 ||
pk == tykind.TY_U64 || pk == tykind.TY_INT ||
pk == tykind.TY_UINT || pk == tykind.TY_UINTPTR ||
pk == tykind.TY_F64) { return 8u64; };
if (pk == tykind.TY_BOOL || pk == tykind.TY_RUNE ||
pk == tykind.TY_I8 || pk == tykind.TY_I16 ||
pk == tykind.TY_I32 || pk == tykind.TY_U8 ||
pk == tykind.TY_U16 || pk == tykind.TY_U32 ||
pk == tykind.TY_F32 || pk == tykind.TY_ENUM) { return 8u64; };
// Composite — struct/tuple/array/tagged carry their own slot total.
return pt.slotsize;
};
// #61 A.5 helper: per-field slot size mirroring cgenutil.ww
// registerstruct/fieldsize. Nested struct fields contribute their
// slot-padded total (si.totsize equivalent); primitives keep their
// natural width (struct interior packing is unaffected by stack-slot
// pad-to-8); arrays use their slot-padded element-stride * elen.
fn fieldslotsize(ft: *tinfo) u64 = {
if (ft == nil) { return 8u64; };
let fk: tykind = ft.kind;
if (fk == tykind.TY_STRUCT) { return ft.slotsize; };
if (fk == tykind.TY_ARRAY) { return ft.slotsize; };
if (fk == tykind.TY_TAGGED) { return ft.size; };
if (fk == tykind.TY_SLICE) { return 24u64; };
if (fk == tykind.TY_PTR || fk == tykind.TY_FN ||
fk == tykind.TY_CHAN) { return 8u64; };
if (fk == tykind.TY_STR) { return 16u64; };
// Primitives keep natural width inside structs (matches
// cgenutil fieldsize: primsize, not pad-to-8). TY_TUPLE inside a
// struct currently defaults to 8 in cgenutil — preserve that
// shape until a future graduation aligns the two.
if (fk == tykind.TY_BOOL || fk == tykind.TY_RUNE ||
fk == tykind.TY_I8 || fk == tykind.TY_I16 ||
fk == tykind.TY_I32 || fk == tykind.TY_I64 ||
fk == tykind.TY_U8 || fk == tykind.TY_U16 ||
fk == tykind.TY_U32 || fk == tykind.TY_U64 ||
fk == tykind.TY_INT || fk == tykind.TY_UINT ||
fk == tykind.TY_UINTPTR || fk == tykind.TY_F32 ||
fk == tykind.TY_F64 || fk == tykind.TY_ENUM) { return ft.size; };
return 8u64;
};
// #61 audit §1.8 — resolve a type-expression AST node to its *tinfo.
// Mirrors cstage's resolve_type (cmd/wcc/check.c:286-565) which
// produces ty_* singletons / arena-allocated composites from a Node*.
@@ -7973,26 +8104,18 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = {
// Cstage cmd/wcc/check.c:314-326: length must be an integer
// literal (`[_]T` keeps alen=0 as the inferred-length sentinel
// patched at letslotsize-time).
//
// #61 A.5: ti.size = natural (sub.size * elen), ti.slotsize =
// slot-padded (sub.slotsize * elen) — typearray handles both.
// Reverts A.4's r.size override (which conflated stride with
// natural size); the slot-padded stride now lives in slotsize
// where cgenutil's fast-path reads it.
let elen: u64 = 0u64;
if (n.rhs != nil) {
if (n.rhs.kind == nkind.N_INTLIT) { elen = n.rhs.uval; };
};
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
@@ -8002,6 +8125,7 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = {
r = newtype(c.a, tykind.TY_FN);
r.size = 8u64;
r.align = 8u64;
r.slotsize = 8u64;
tinfocachebind(c.tc, n, r);
r.ret = tinfofornode(c, n.lhs);
} else { if (k == nkind.N_TENUM) {
@@ -8016,14 +8140,22 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = {
r.sub = storage;
r.size = storage.size;
r.align = storage.align;
r.slotsize = storage.size;
} else { if (k == nkind.N_TTUPLE) {
// Cstage cmd/wcc/check.c:329-345: sum of element sizes with
// per-element alignment NOT padded — cstage uses raw sums for
// tuples and 8B-rounding lives at the call/return ABI layer.
// Pre-bind for cycle protection (recursive tuple shapes).
//
// #61 A.5: ti.size = natural sum (cstage parity); ti.slotsize
// = per-element slot sum mirroring cgenutil.ww:2018-2029
// slotsize TTUPLE — narrow scalars pad to 8 (cgen spills each
// tuple element into its own register / stack-slot eightbyte),
// composites contribute their own ti.slotsize.
r = newtype(c.a, tykind.TY_TUPLE);
tinfocachebind(c.tc, n, r);
let total: u64 = 0u64;
let slottotal: u64 = 0u64;
let maxal: u64 = 1u64;
let p: *node = n.list;
for (p != nil) {
@@ -8031,11 +8163,13 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = {
if (pt != nil) {
if (pt.align > maxal) { maxal = pt.align; };
total += pt.size;
slottotal += tupleelemslot(pt);
};
p = p.next;
};
r.size = total;
r.align = maxal;
r.slotsize = slottotal;
} else { if (k == nkind.N_TSTRUCT) {
// Cstage cmd/wcc/check.c:468-527: per-field alignment, max
// align for the whole record, total rounded up to alignment.
@@ -8049,10 +8183,18 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = {
// the in-progress stub. r.size is filled in below; the stub's
// only consumer during the recursion is typeptr (8B/8B
// regardless of pointee size), so partial-fill is safe.
//
// #61 A.5: alongside the natural layout (cstage parity), walk
// the same fields with the slot-padded sizing cgenutil.ww
// registerstruct uses (fieldsize → si.totsize for nested
// struct; size-derived alignment; final round to 8). That
// slot total lands in ti.slotsize so the cgen fast-path can
// graduate TY_STRUCT off the AST walker.
r = newtype(c.a, tykind.TY_STRUCT);
tinfocachebind(c.tc, n, r);
let off: u64 = 0u64;
let maxalign: u64 = 1u64;
let soff: u64 = 0u64;
let f: *node = n.list;
for (f != nil) {
if (f.kind == nkind.N_TFIELD) {
@@ -8063,6 +8205,17 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = {
off = (off + ft.align - 1u64) & ~(ft.align - 1u64);
};
off += ft.size;
// Slot-padded layout (mirror of cgenutil
// fieldsize + registerstruct align rules).
let fsz: u64 = fieldslotsize(ft);
let faln: u64 = 1u64;
if (fsz >= 8u64) { faln = 8u64; }
else { if (fsz >= 4u64) { faln = 4u64; }
else { if (fsz >= 2u64) { faln = 2u64; }; }; };
if ((soff & (faln - 1u64)) != 0u64) {
soff = (soff + faln - 1u64) & ~(faln - 1u64);
};
soff += fsz;
};
};
f = f.next;
@@ -8071,6 +8224,10 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = {
r.size = (off + maxalign - 1u64) & ~(maxalign - 1u64);
};
r.align = maxalign;
if ((soff & 7u64) != 0u64) {
soff = (soff + 7u64) & ~7u64;
};
r.slotsize = soff;
} else { if (k == nkind.N_TTAGGED) {
// Cstage cmd/wcc/check.c:347-435: 8B tag + max(variant)
// rounded up to 8. Pre-bind for cycle protection (recursive
@@ -8100,6 +8257,7 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = {
r.size = 8u64;
r.align = 8u64;
r.nullable = 1;
r.slotsize = 8u64;
tinfocachebind(c.tc, n, r);
return r;
};
@@ -8119,8 +8277,16 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = {
let pad: u64 = (maxsz + 7u64) & ~7u64;
r.size = 8u64 + pad;
r.align = al;
r.slotsize = 8u64 + pad;
};};};};};};};};};};};
if (r != nil) { tinfocachebind(c.tc, n, r); };
if (r != nil) {
// #61 A.5: any arm that didn't set slotsize gets ti.size as
// the default (covers primitives via prim() + the ptr/slice/
// chan paths which already populate slotsize, plus TBANG which
// inherits the inner's tinfo unchanged).
if (r.slotsize == 0u64) { r.slotsize = r.size; };
tinfocachebind(c.tc, n, r);
};
return r;
};
@@ -10978,45 +11144,27 @@ export fn letslotsize(c: *cgen, n: *node) i32 = {
};
fn slotsize(c: *cgen, typn: *node) i32 = {
// #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).
// 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) 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.
// Padding lives at the read site, not in tinfo.size, so size(T)
// stays a faithful natural-width SSoT.
// - #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.
// #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) {
let ti: *tinfo = typn.type_: *tinfo;
let kk: tykind = ti.kind;
@@ -11026,8 +11174,11 @@ fn slotsize(c: *cgen, typn: *node) i32 = {
kk == tykind.TY_VOID) {
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.size: i32; };
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 ||