From 9fd79cdc332ab19e3b07dc4d7a33ef91653ed85f Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Wed, 20 May 2026 14:30:55 +0900 Subject: [PATCH] selfhost/cmd/wcc + lib/ww: tinfo.slotsize SSoT + module-name TNAME fallback (Phase A.5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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] 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. --- lib/ww/sym.ww | 29 +++ lib/ww/typ.ww | 26 ++- selfhost/cmd/w6c/main.combined.ww | 263 +++++++++++++++++++++------ selfhost/cmd/wcc/cgenutil.ww | 65 +++---- selfhost/cmd/wcc/check.ww | 143 +++++++++++++-- selfhost/cmd/wwdump/main.combined.ww | 263 +++++++++++++++++++++------ 6 files changed, 621 insertions(+), 168 deletions(-) diff --git a/lib/ww/sym.ww b/lib/ww/sym.ww index c905a096..d625bcad 100644 --- a/lib/ww/sym.ww +++ b/lib/ww/sym.ww @@ -105,6 +105,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 diff --git a/lib/ww/typ.ww b/lib/ww/typ.ww index 97603333..6b434330 100644 --- a/lib/ww/typ.ww +++ b/lib/ww/typ.ww @@ -92,6 +92,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 @@ -140,7 +152,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; }; @@ -150,6 +164,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; }; @@ -188,6 +203,7 @@ export fn typeptr(a: *arena, sub: *tinfo) *tinfo = { t.sub = sub; t.size = 8u64; t.align = 8u64; + t.slotsize = 8u64; return t; }; @@ -196,6 +212,7 @@ export fn typeslice(a: *arena, sub: *tinfo) *tinfo = { t.sub = sub; t.size = 24u64; t.align = 8u64; + t.slotsize = 24u64; return t; }; @@ -206,6 +223,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; }; @@ -217,6 +239,7 @@ export fn typechan(a: *arena, sub: *tinfo) *tinfo = { t.sub = sub; t.size = 8u64; t.align = 8u64; + t.slotsize = 8u64; return t; }; @@ -227,6 +250,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; }; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 542fb26f..78e34d26 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -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 || diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index f14815c2..2ed8fa93 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -1941,45 +1941,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; @@ -1989,8 +1971,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 || diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 66a8450b..30ab6028 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -518,6 +518,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; }; @@ -849,6 +869,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*. @@ -940,26 +1018,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 @@ -969,6 +1039,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) { @@ -983,14 +1054,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) { @@ -998,11 +1077,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. @@ -1016,10 +1097,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) { @@ -1030,6 +1119,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; @@ -1038,6 +1138,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 @@ -1067,6 +1171,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; }; @@ -1086,8 +1191,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; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 6ff26b6f..de3e2d7c 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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 ||