diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index f98736d7..a8664c3f 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -7442,6 +7442,24 @@ fn resolvewalk(c: *checker, n: *node) void = { }; }; + // #61 audit §1.8 — A.2 population: stamp tinfo onto type-expression + // nodes once their children have been walked (sub-element TNAMEs + // are now in scope so resolvealias inside tinfofornode can follow + // user-defined aliases). Cgen's slotsize fast-path reads off + // n.type_; uncovered shapes fall through to the cstage-mirror + // walker until the next sub-commit graduates them. + if (k == nkind.N_TNAME || k == nkind.N_TPTR || + k == nkind.N_TSLICE || k == nkind.N_TCHAN || + k == nkind.N_TBANG || k == nkind.N_TARRAY || + k == nkind.N_TFN || k == nkind.N_TSTRUCT || + k == nkind.N_TTUPLE || k == nkind.N_TTAGGED || + k == nkind.N_TENUM) { + if (n.type_ == nil) { + let ti: *tinfo = tinfofornode(c, n); + if (ti != nil) { n.type_ = ti: *void; }; + }; + }; + // #42: trigger the size/align/offset fold here so the mutation // fires regardless of context (if-conditions, expression statements, // etc.) — wwstage's exprtype is otherwise called only from @@ -7859,20 +7877,21 @@ fn foldtointlit(c: *checker, n: *node, v: i64) void = { n.tsuffix = empty; }; -// #61 audit §1.8 — A.1 infrastructure: resolve a type-expression AST -// node to its *tinfo. Mirrors cstage's resolve_type (cmd/wcc/check.c) -// which produces ty_* singletons / arena-allocated composites from a -// Node*. Cache lives in c.tc (typ.ww) so the same shape can be reused -// across modules within one check pass. Rob+Drew convergence -// 2026-05-20: cgen will graduate to read sizes from here in A.2+; A.1 -// just lays the helper and exercises it on N_INTLIT. +// #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*. +// Cache lives in c.tc (typ.ww) so the same shape can be reused across +// modules within one check pass. Rob+Drew convergence 2026-05-20: cgen +// reads sizes from here starting with slotsize in A.2; subsequent +// sub-commits graduate elemsize/fieldsize/letemitsize/etc. onto the +// same pivot. // -// Coverage today (A.1): -// - N_TNAME primitive (i32, str, untyped_int, ...) -> tctx singleton -// - N_TNAME alias -> recurse through resolvealias -// - N_TPTR / N_TSLICE / N_TCHAN / N_TBANG -> structural recurse -// Other shapes (TARRAY/TSTRUCT/TFN/TTAGGED/TTUPLE/TENUM) return nil -// for now; A.2+ extends as cgen-site graduation demands them. +// A.2 coverage: primitive TNAME singletons, TNAME aliases (via +// resolvealias), TBANG (inner unchanged — see iserror note), TPTR, +// TSLICE, TCHAN, TARRAY, TFN, TENUM, TTUPLE, TSTRUCT, TTAGGED. Size +// computation tracks cstage natural sizes; cgen's slot-padding +// contract (cmd/w6c/cgen.c let_emit_size:691-720 pads narrow scalars +// to 8B) stays in slotsize's fallback walker. fn tinfofornode(c: *checker, n: *node) *tinfo = { if (n == nil) { return nil; }; let cached: *tinfo = tinfocachelookup(c.tc, n); @@ -7929,7 +7948,119 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { r = typeslice(c.a, tinfofornode(c, n.lhs)); } else { if (k == nkind.N_TCHAN) { r = typechan(c.a, tinfofornode(c, n.lhs)); - };};};};}; + } else { if (k == nkind.N_TARRAY) { + // 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). + let elen: u64 = 0u64; + if (n.rhs != nil) { + if (n.rhs.kind == nkind.N_INTLIT) { elen = n.rhs.uval; }; + }; + r = typearray(c.a, tinfofornode(c, n.lhs), 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 + // the return type so a recursive `type F = fn() F` self-ref + // doesn't spin (cycle-break mirror of the TSTRUCT/TTAGGED + // pattern below). + r = newtype(c.a, tykind.TY_FN); + r.size = 8u64; + r.align = 8u64; + tinfocachebind(c.tc, n, r); + r.ret = tinfofornode(c, n.lhs); + } else { if (k == nkind.N_TENUM) { + // Cstage cmd/wcc/check.c:529-542: storage type's size/align + // (default i32 = 4B/4B). Cgen's slotsize-TENUM fallback pads + // to 8B per its stack-slot contract; tinfo.size carries the + // raw storage width so size(EnumT) folds to the correct value. + r = newtype(c.a, tykind.TY_ENUM); + let storage: *tinfo = nil; + if (n.lhs != nil) { storage = tinfofornode(c, n.lhs); }; + if (storage == nil) { storage = c.tc.tyi32; }; + r.sub = storage; + r.size = storage.size; + r.align = storage.align; + } 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). + r = newtype(c.a, tykind.TY_TUPLE); + tinfocachebind(c.tc, n, r); + let total: u64 = 0u64; + let maxal: u64 = 1u64; + let p: *node = n.list; + for (p != nil) { + let pt: *tinfo = tinfofornode(c, p); + if (pt != nil) { + if (pt.align > maxal) { maxal = pt.align; }; + total += pt.size; + }; + p = p.next; + }; + r.size = total; + r.align = maxal; + } 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. + // Anonymous-embed promotion is deferred (#13). + // + // Pre-bind into the cache BEFORE walking fields so a + // self-referential pointer field (e.g., `next: *node` inside + // `type node = struct {..., next: *node, ...}`) terminates: + // the inner tinfofornode(TNAME(node)) resolvealias-recurses + // back to this same body node, hits the cache, and returns + // 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. + r = newtype(c.a, tykind.TY_STRUCT); + tinfocachebind(c.tc, n, r); + let off: u64 = 0u64; + let maxalign: u64 = 1u64; + let f: *node = n.list; + for (f != nil) { + if (f.kind == nkind.N_TFIELD) { + let ft: *tinfo = tinfofornode(c, f.lhs); + if (ft != nil) { + if (ft.align > maxalign) { maxalign = ft.align; }; + if (ft.align > 0u64) { + off = (off + ft.align - 1u64) & ~(ft.align - 1u64); + }; + off += ft.size; + }; + }; + f = f.next; + }; + if (maxalign > 0u64) { + r.size = (off + maxalign - 1u64) & ~(maxalign - 1u64); + }; + r.align = maxalign; + } else { if (k == nkind.N_TTAGGED) { + // Cstage cmd/wcc/check.c:347-435: 8B tag + max(variant) + // rounded up to 8. Variant dedup / never-strip / nullable-fold + // stay in cstage's check.c for now — wwstage cgen only reads + // the size today, and the AST-level pre-fold (astsize's + // TTAGGED arm) already matches the cstage layout numerically + // for the shapes selfhost exercises. Pre-bind for cycle + // protection (recursive sum-type shapes through NAMED + // variants). + r = newtype(c.a, tykind.TY_TAGGED); + tinfocachebind(c.tc, n, r); + let maxsz: u64 = 0u64; + let al: u64 = 8u64; + let v: *node = n.list; + for (v != nil) { + let vt: *tinfo = tinfofornode(c, v); + if (vt != nil) { + if (vt.size > maxsz) { maxsz = vt.size; }; + if (vt.align > al) { al = vt.align; }; + }; + v = v.next; + }; + let pad: u64 = (maxsz + 7u64) & ~7u64; + r.size = 8u64 + pad; + r.align = al; + };};};};};};};};};};}; if (r != nil) { tinfocachebind(c.tc, n, r); }; return r; }; @@ -7941,28 +8072,70 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { fn exprtype(c: *checker, e: *node) *node = { if (e == nil) { return nil; }; let k: nkind = e.kind; + // #61 audit §1.8 — A.2 widens A.1's single N_INTLIT population to + // every primitive literal arm + N_IDENT. Cgen size walkers + // (slotsize first; elemsize/fieldsize/letemitsize follow) consult + // node.type_ as the SSoT; populating literals + idents closes the + // loop from the read side. if (k == nkind.N_INTLIT) { let tn: *node = mktname(c, "untyped_int"); - // #61 audit §1.8 — A.1 single population site. Cgen still - // reads sizes through primtypesize/slotsize today; A.2+ - // graduates each walker family to read e.type_ instead, - // ending the size-walker cascade (Rob+Drew convergence - // 2026-05-20). Don't extend population to other arms in A.1. e.type_ = tinfofornode(c, tn): *void; return tn; }; - if (k == nkind.N_FLOATLIT) { return mktname(c, "untyped_float"); }; - if (k == nkind.N_STRLIT) { return mktname(c, "str"); }; - if (k == nkind.N_RUNELIT) { return mktname(c, "rune"); }; - if (k == nkind.N_TRUE) { return mktname(c, "bool"); }; - if (k == nkind.N_FALSE) { return mktname(c, "bool"); }; - if (k == nkind.N_VOIDLIT) { return mktname(c, "void"); }; - if (k == nkind.N_NIL) { return mktname(c, "untyped_nil"); }; + if (k == nkind.N_FLOATLIT) { + let tn: *node = mktname(c, "untyped_float"); + e.type_ = tinfofornode(c, tn): *void; + return tn; + }; + if (k == nkind.N_STRLIT) { + let tn: *node = mktname(c, "str"); + e.type_ = tinfofornode(c, tn): *void; + return tn; + }; + if (k == nkind.N_RUNELIT) { + let tn: *node = mktname(c, "rune"); + e.type_ = tinfofornode(c, tn): *void; + return tn; + }; + if (k == nkind.N_TRUE) { + let tn: *node = mktname(c, "bool"); + e.type_ = tinfofornode(c, tn): *void; + return tn; + }; + if (k == nkind.N_FALSE) { + let tn: *node = mktname(c, "bool"); + e.type_ = tinfofornode(c, tn): *void; + return tn; + }; + if (k == nkind.N_VOIDLIT) { + let tn: *node = mktname(c, "void"); + e.type_ = tinfofornode(c, tn): *void; + return tn; + }; + if (k == nkind.N_NIL) { + let tn: *node = mktname(c, "untyped_nil"); + e.type_ = tinfofornode(c, tn): *void; + return tn; + }; if (k == nkind.N_IDENT) { let s: *sym = scopelookup(c.cur, e.str); if (s == nil) { return nil; }; if (s.decl == nil) { return nil; }; - return s.decl.lhs; + let t: *node = s.decl.lhs; + // Propagate the declared type's tinfo onto the use site so + // downstream cgen walkers can read n.type_ off an ident. + if (t != nil) { + if (t.type_ != nil) { + e.type_ = t.type_; + } else { + let ti: *tinfo = tinfofornode(c, t); + if (ti != nil) { + e.type_ = ti: *void; + t.type_ = ti: *void; + }; + }; + }; + return t; }; if (k == nkind.N_CAST) { // `expr: T` — explicit cast; the type expr is e.rhs. @@ -8728,6 +8901,17 @@ fn resolvefnbody(c: *checker, fnnode: *node) void = { let outer: *scope = c.cur; c.cur = newscope(c.a, c.cur); installparams(c, fnnode.list); + // #61 audit §1.8 — A.2: walk each param's declared type-expr so + // tinfofornode stamps n.type_ on it. installparams binds the name + // but never recurses into the type; without this, cgen's slotsize + // fast-path hits the fallback for every param load/store. + let p: *node = fnnode.list; + for (p != nil) { + if (p.kind == nkind.N_PARAM) { + if (p.lhs != nil) { resolvewalk(c, p.lhs); }; + }; + p = p.next; + }; let prevret: *node = c.fnret; c.fnret = fnnode.lhs; // return type AST, used by `?` check if (fnnode.body != nil) { @@ -10735,6 +10919,31 @@ export fn letslotsize(c: *cgen, n: *node) i32 = { }; fn slotsize(c: *cgen, typn: *node) i32 = { + // #61 audit §1.8 — A.2 graduation: read tinfo.size off the populated + // type-expression node when its kind matches the cstage natural-size + // SSoT. Filtered set covers shapes whose tinfo.size already encodes + // the cgen slot-size contract: pointer-like (PTR/CHAN/FN), slice + // (SLICE), and str (STR). Other kinds flow through the fallback + // walker. TAGGED stays out because cstage's resolve_type folds + // `(*T | void)` to a single 8B pointer (cmd/wcc/check.c:412-426) + // but tinfofornode's TTAGGED arm doesn't yet — graduating TAGGED + // would shrink that fold's slot from 16 to 8 on the wwstage side. + // Primitive scalars + enums + inline TUPLE / TSTRUCT also keep + // flowing through the fallback walker because cgen's slot-pad-to-8 + // contract (cmd/w6c/cgen.c let_emit_size:691-720 and the per-field + // slot rounding in registerstruct/letslotsize) lives there, not in + // tinfo.size. Subsequent sub-commits collapse the remaining shapes + // onto the same pivot once cstage parity catches up (#61 Phase A — + // see the audit doc for the staged plan). + if (typn != nil && typn.type_ != nil) { + let ti: *tinfo = typn.type_: *tinfo; + let kk: tykind = ti.kind; + if (kk == tykind.TY_PTR || kk == tykind.TY_SLICE || + kk == tykind.TY_CHAN || kk == tykind.TY_FN || + kk == tykind.TY_STR) { + return ti.size: i32; + }; + }; if (typn == nil) { return 8; }; let k: nkind = typn.kind; // `!T` carries T's memory layout; the error-tag bit lives in the diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 1e6005bf..e46b61da 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -1941,6 +1941,31 @@ export fn letslotsize(c: *cgen, n: *node) i32 = { }; fn slotsize(c: *cgen, typn: *node) i32 = { + // #61 audit §1.8 — A.2 graduation: read tinfo.size off the populated + // type-expression node when its kind matches the cstage natural-size + // SSoT. Filtered set covers shapes whose tinfo.size already encodes + // the cgen slot-size contract: pointer-like (PTR/CHAN/FN), slice + // (SLICE), and str (STR). Other kinds flow through the fallback + // walker. TAGGED stays out because cstage's resolve_type folds + // `(*T | void)` to a single 8B pointer (cmd/wcc/check.c:412-426) + // but tinfofornode's TTAGGED arm doesn't yet — graduating TAGGED + // would shrink that fold's slot from 16 to 8 on the wwstage side. + // Primitive scalars + enums + inline TUPLE / TSTRUCT also keep + // flowing through the fallback walker because cgen's slot-pad-to-8 + // contract (cmd/w6c/cgen.c let_emit_size:691-720 and the per-field + // slot rounding in registerstruct/letslotsize) lives there, not in + // tinfo.size. Subsequent sub-commits collapse the remaining shapes + // onto the same pivot once cstage parity catches up (#61 Phase A — + // see the audit doc for the staged plan). + if (typn != nil && typn.type_ != nil) { + let ti: *tinfo = typn.type_: *tinfo; + let kk: tykind = ti.kind; + if (kk == tykind.TY_PTR || kk == tykind.TY_SLICE || + kk == tykind.TY_CHAN || kk == tykind.TY_FN || + kk == tykind.TY_STR) { + return ti.size: i32; + }; + }; if (typn == nil) { return 8; }; let k: nkind = typn.kind; // `!T` carries T's memory layout; the error-tag bit lives in the diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 019b2fbc..f992e365 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -414,6 +414,24 @@ fn resolvewalk(c: *checker, n: *node) void = { }; }; + // #61 audit §1.8 — A.2 population: stamp tinfo onto type-expression + // nodes once their children have been walked (sub-element TNAMEs + // are now in scope so resolvealias inside tinfofornode can follow + // user-defined aliases). Cgen's slotsize fast-path reads off + // n.type_; uncovered shapes fall through to the cstage-mirror + // walker until the next sub-commit graduates them. + if (k == nkind.N_TNAME || k == nkind.N_TPTR || + k == nkind.N_TSLICE || k == nkind.N_TCHAN || + k == nkind.N_TBANG || k == nkind.N_TARRAY || + k == nkind.N_TFN || k == nkind.N_TSTRUCT || + k == nkind.N_TTUPLE || k == nkind.N_TTAGGED || + k == nkind.N_TENUM) { + if (n.type_ == nil) { + let ti: *tinfo = tinfofornode(c, n); + if (ti != nil) { n.type_ = ti: *void; }; + }; + }; + // #42: trigger the size/align/offset fold here so the mutation // fires regardless of context (if-conditions, expression statements, // etc.) — wwstage's exprtype is otherwise called only from @@ -831,20 +849,21 @@ fn foldtointlit(c: *checker, n: *node, v: i64) void = { n.tsuffix = empty; }; -// #61 audit §1.8 — A.1 infrastructure: resolve a type-expression AST -// node to its *tinfo. Mirrors cstage's resolve_type (cmd/wcc/check.c) -// which produces ty_* singletons / arena-allocated composites from a -// Node*. Cache lives in c.tc (typ.ww) so the same shape can be reused -// across modules within one check pass. Rob+Drew convergence -// 2026-05-20: cgen will graduate to read sizes from here in A.2+; A.1 -// just lays the helper and exercises it on N_INTLIT. +// #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*. +// Cache lives in c.tc (typ.ww) so the same shape can be reused across +// modules within one check pass. Rob+Drew convergence 2026-05-20: cgen +// reads sizes from here starting with slotsize in A.2; subsequent +// sub-commits graduate elemsize/fieldsize/letemitsize/etc. onto the +// same pivot. // -// Coverage today (A.1): -// - N_TNAME primitive (i32, str, untyped_int, ...) -> tctx singleton -// - N_TNAME alias -> recurse through resolvealias -// - N_TPTR / N_TSLICE / N_TCHAN / N_TBANG -> structural recurse -// Other shapes (TARRAY/TSTRUCT/TFN/TTAGGED/TTUPLE/TENUM) return nil -// for now; A.2+ extends as cgen-site graduation demands them. +// A.2 coverage: primitive TNAME singletons, TNAME aliases (via +// resolvealias), TBANG (inner unchanged — see iserror note), TPTR, +// TSLICE, TCHAN, TARRAY, TFN, TENUM, TTUPLE, TSTRUCT, TTAGGED. Size +// computation tracks cstage natural sizes; cgen's slot-padding +// contract (cmd/w6c/cgen.c let_emit_size:691-720 pads narrow scalars +// to 8B) stays in slotsize's fallback walker. fn tinfofornode(c: *checker, n: *node) *tinfo = { if (n == nil) { return nil; }; let cached: *tinfo = tinfocachelookup(c.tc, n); @@ -901,7 +920,119 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { r = typeslice(c.a, tinfofornode(c, n.lhs)); } else { if (k == nkind.N_TCHAN) { r = typechan(c.a, tinfofornode(c, n.lhs)); - };};};};}; + } else { if (k == nkind.N_TARRAY) { + // 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). + let elen: u64 = 0u64; + if (n.rhs != nil) { + if (n.rhs.kind == nkind.N_INTLIT) { elen = n.rhs.uval; }; + }; + r = typearray(c.a, tinfofornode(c, n.lhs), 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 + // the return type so a recursive `type F = fn() F` self-ref + // doesn't spin (cycle-break mirror of the TSTRUCT/TTAGGED + // pattern below). + r = newtype(c.a, tykind.TY_FN); + r.size = 8u64; + r.align = 8u64; + tinfocachebind(c.tc, n, r); + r.ret = tinfofornode(c, n.lhs); + } else { if (k == nkind.N_TENUM) { + // Cstage cmd/wcc/check.c:529-542: storage type's size/align + // (default i32 = 4B/4B). Cgen's slotsize-TENUM fallback pads + // to 8B per its stack-slot contract; tinfo.size carries the + // raw storage width so size(EnumT) folds to the correct value. + r = newtype(c.a, tykind.TY_ENUM); + let storage: *tinfo = nil; + if (n.lhs != nil) { storage = tinfofornode(c, n.lhs); }; + if (storage == nil) { storage = c.tc.tyi32; }; + r.sub = storage; + r.size = storage.size; + r.align = storage.align; + } 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). + r = newtype(c.a, tykind.TY_TUPLE); + tinfocachebind(c.tc, n, r); + let total: u64 = 0u64; + let maxal: u64 = 1u64; + let p: *node = n.list; + for (p != nil) { + let pt: *tinfo = tinfofornode(c, p); + if (pt != nil) { + if (pt.align > maxal) { maxal = pt.align; }; + total += pt.size; + }; + p = p.next; + }; + r.size = total; + r.align = maxal; + } 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. + // Anonymous-embed promotion is deferred (#13). + // + // Pre-bind into the cache BEFORE walking fields so a + // self-referential pointer field (e.g., `next: *node` inside + // `type node = struct {..., next: *node, ...}`) terminates: + // the inner tinfofornode(TNAME(node)) resolvealias-recurses + // back to this same body node, hits the cache, and returns + // 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. + r = newtype(c.a, tykind.TY_STRUCT); + tinfocachebind(c.tc, n, r); + let off: u64 = 0u64; + let maxalign: u64 = 1u64; + let f: *node = n.list; + for (f != nil) { + if (f.kind == nkind.N_TFIELD) { + let ft: *tinfo = tinfofornode(c, f.lhs); + if (ft != nil) { + if (ft.align > maxalign) { maxalign = ft.align; }; + if (ft.align > 0u64) { + off = (off + ft.align - 1u64) & ~(ft.align - 1u64); + }; + off += ft.size; + }; + }; + f = f.next; + }; + if (maxalign > 0u64) { + r.size = (off + maxalign - 1u64) & ~(maxalign - 1u64); + }; + r.align = maxalign; + } else { if (k == nkind.N_TTAGGED) { + // Cstage cmd/wcc/check.c:347-435: 8B tag + max(variant) + // rounded up to 8. Variant dedup / never-strip / nullable-fold + // stay in cstage's check.c for now — wwstage cgen only reads + // the size today, and the AST-level pre-fold (astsize's + // TTAGGED arm) already matches the cstage layout numerically + // for the shapes selfhost exercises. Pre-bind for cycle + // protection (recursive sum-type shapes through NAMED + // variants). + r = newtype(c.a, tykind.TY_TAGGED); + tinfocachebind(c.tc, n, r); + let maxsz: u64 = 0u64; + let al: u64 = 8u64; + let v: *node = n.list; + for (v != nil) { + let vt: *tinfo = tinfofornode(c, v); + if (vt != nil) { + if (vt.size > maxsz) { maxsz = vt.size; }; + if (vt.align > al) { al = vt.align; }; + }; + v = v.next; + }; + let pad: u64 = (maxsz + 7u64) & ~7u64; + r.size = 8u64 + pad; + r.align = al; + };};};};};};};};};};}; if (r != nil) { tinfocachebind(c.tc, n, r); }; return r; }; @@ -913,28 +1044,70 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { fn exprtype(c: *checker, e: *node) *node = { if (e == nil) { return nil; }; let k: nkind = e.kind; + // #61 audit §1.8 — A.2 widens A.1's single N_INTLIT population to + // every primitive literal arm + N_IDENT. Cgen size walkers + // (slotsize first; elemsize/fieldsize/letemitsize follow) consult + // node.type_ as the SSoT; populating literals + idents closes the + // loop from the read side. if (k == nkind.N_INTLIT) { let tn: *node = mktname(c, "untyped_int"); - // #61 audit §1.8 — A.1 single population site. Cgen still - // reads sizes through primtypesize/slotsize today; A.2+ - // graduates each walker family to read e.type_ instead, - // ending the size-walker cascade (Rob+Drew convergence - // 2026-05-20). Don't extend population to other arms in A.1. e.type_ = tinfofornode(c, tn): *void; return tn; }; - if (k == nkind.N_FLOATLIT) { return mktname(c, "untyped_float"); }; - if (k == nkind.N_STRLIT) { return mktname(c, "str"); }; - if (k == nkind.N_RUNELIT) { return mktname(c, "rune"); }; - if (k == nkind.N_TRUE) { return mktname(c, "bool"); }; - if (k == nkind.N_FALSE) { return mktname(c, "bool"); }; - if (k == nkind.N_VOIDLIT) { return mktname(c, "void"); }; - if (k == nkind.N_NIL) { return mktname(c, "untyped_nil"); }; + if (k == nkind.N_FLOATLIT) { + let tn: *node = mktname(c, "untyped_float"); + e.type_ = tinfofornode(c, tn): *void; + return tn; + }; + if (k == nkind.N_STRLIT) { + let tn: *node = mktname(c, "str"); + e.type_ = tinfofornode(c, tn): *void; + return tn; + }; + if (k == nkind.N_RUNELIT) { + let tn: *node = mktname(c, "rune"); + e.type_ = tinfofornode(c, tn): *void; + return tn; + }; + if (k == nkind.N_TRUE) { + let tn: *node = mktname(c, "bool"); + e.type_ = tinfofornode(c, tn): *void; + return tn; + }; + if (k == nkind.N_FALSE) { + let tn: *node = mktname(c, "bool"); + e.type_ = tinfofornode(c, tn): *void; + return tn; + }; + if (k == nkind.N_VOIDLIT) { + let tn: *node = mktname(c, "void"); + e.type_ = tinfofornode(c, tn): *void; + return tn; + }; + if (k == nkind.N_NIL) { + let tn: *node = mktname(c, "untyped_nil"); + e.type_ = tinfofornode(c, tn): *void; + return tn; + }; if (k == nkind.N_IDENT) { let s: *sym = scopelookup(c.cur, e.str); if (s == nil) { return nil; }; if (s.decl == nil) { return nil; }; - return s.decl.lhs; + let t: *node = s.decl.lhs; + // Propagate the declared type's tinfo onto the use site so + // downstream cgen walkers can read n.type_ off an ident. + if (t != nil) { + if (t.type_ != nil) { + e.type_ = t.type_; + } else { + let ti: *tinfo = tinfofornode(c, t); + if (ti != nil) { + e.type_ = ti: *void; + t.type_ = ti: *void; + }; + }; + }; + return t; }; if (k == nkind.N_CAST) { // `expr: T` — explicit cast; the type expr is e.rhs. @@ -1700,6 +1873,17 @@ fn resolvefnbody(c: *checker, fnnode: *node) void = { let outer: *scope = c.cur; c.cur = newscope(c.a, c.cur); installparams(c, fnnode.list); + // #61 audit §1.8 — A.2: walk each param's declared type-expr so + // tinfofornode stamps n.type_ on it. installparams binds the name + // but never recurses into the type; without this, cgen's slotsize + // fast-path hits the fallback for every param load/store. + let p: *node = fnnode.list; + for (p != nil) { + if (p.kind == nkind.N_PARAM) { + if (p.lhs != nil) { resolvewalk(c, p.lhs); }; + }; + p = p.next; + }; let prevret: *node = c.fnret; c.fnret = fnnode.lhs; // return type AST, used by `?` check if (fnnode.body != nil) { diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 926d31d8..a1c218a7 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -7442,6 +7442,24 @@ fn resolvewalk(c: *checker, n: *node) void = { }; }; + // #61 audit §1.8 — A.2 population: stamp tinfo onto type-expression + // nodes once their children have been walked (sub-element TNAMEs + // are now in scope so resolvealias inside tinfofornode can follow + // user-defined aliases). Cgen's slotsize fast-path reads off + // n.type_; uncovered shapes fall through to the cstage-mirror + // walker until the next sub-commit graduates them. + if (k == nkind.N_TNAME || k == nkind.N_TPTR || + k == nkind.N_TSLICE || k == nkind.N_TCHAN || + k == nkind.N_TBANG || k == nkind.N_TARRAY || + k == nkind.N_TFN || k == nkind.N_TSTRUCT || + k == nkind.N_TTUPLE || k == nkind.N_TTAGGED || + k == nkind.N_TENUM) { + if (n.type_ == nil) { + let ti: *tinfo = tinfofornode(c, n); + if (ti != nil) { n.type_ = ti: *void; }; + }; + }; + // #42: trigger the size/align/offset fold here so the mutation // fires regardless of context (if-conditions, expression statements, // etc.) — wwstage's exprtype is otherwise called only from @@ -7859,20 +7877,21 @@ fn foldtointlit(c: *checker, n: *node, v: i64) void = { n.tsuffix = empty; }; -// #61 audit §1.8 — A.1 infrastructure: resolve a type-expression AST -// node to its *tinfo. Mirrors cstage's resolve_type (cmd/wcc/check.c) -// which produces ty_* singletons / arena-allocated composites from a -// Node*. Cache lives in c.tc (typ.ww) so the same shape can be reused -// across modules within one check pass. Rob+Drew convergence -// 2026-05-20: cgen will graduate to read sizes from here in A.2+; A.1 -// just lays the helper and exercises it on N_INTLIT. +// #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*. +// Cache lives in c.tc (typ.ww) so the same shape can be reused across +// modules within one check pass. Rob+Drew convergence 2026-05-20: cgen +// reads sizes from here starting with slotsize in A.2; subsequent +// sub-commits graduate elemsize/fieldsize/letemitsize/etc. onto the +// same pivot. // -// Coverage today (A.1): -// - N_TNAME primitive (i32, str, untyped_int, ...) -> tctx singleton -// - N_TNAME alias -> recurse through resolvealias -// - N_TPTR / N_TSLICE / N_TCHAN / N_TBANG -> structural recurse -// Other shapes (TARRAY/TSTRUCT/TFN/TTAGGED/TTUPLE/TENUM) return nil -// for now; A.2+ extends as cgen-site graduation demands them. +// A.2 coverage: primitive TNAME singletons, TNAME aliases (via +// resolvealias), TBANG (inner unchanged — see iserror note), TPTR, +// TSLICE, TCHAN, TARRAY, TFN, TENUM, TTUPLE, TSTRUCT, TTAGGED. Size +// computation tracks cstage natural sizes; cgen's slot-padding +// contract (cmd/w6c/cgen.c let_emit_size:691-720 pads narrow scalars +// to 8B) stays in slotsize's fallback walker. fn tinfofornode(c: *checker, n: *node) *tinfo = { if (n == nil) { return nil; }; let cached: *tinfo = tinfocachelookup(c.tc, n); @@ -7929,7 +7948,119 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { r = typeslice(c.a, tinfofornode(c, n.lhs)); } else { if (k == nkind.N_TCHAN) { r = typechan(c.a, tinfofornode(c, n.lhs)); - };};};};}; + } else { if (k == nkind.N_TARRAY) { + // 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). + let elen: u64 = 0u64; + if (n.rhs != nil) { + if (n.rhs.kind == nkind.N_INTLIT) { elen = n.rhs.uval; }; + }; + r = typearray(c.a, tinfofornode(c, n.lhs), 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 + // the return type so a recursive `type F = fn() F` self-ref + // doesn't spin (cycle-break mirror of the TSTRUCT/TTAGGED + // pattern below). + r = newtype(c.a, tykind.TY_FN); + r.size = 8u64; + r.align = 8u64; + tinfocachebind(c.tc, n, r); + r.ret = tinfofornode(c, n.lhs); + } else { if (k == nkind.N_TENUM) { + // Cstage cmd/wcc/check.c:529-542: storage type's size/align + // (default i32 = 4B/4B). Cgen's slotsize-TENUM fallback pads + // to 8B per its stack-slot contract; tinfo.size carries the + // raw storage width so size(EnumT) folds to the correct value. + r = newtype(c.a, tykind.TY_ENUM); + let storage: *tinfo = nil; + if (n.lhs != nil) { storage = tinfofornode(c, n.lhs); }; + if (storage == nil) { storage = c.tc.tyi32; }; + r.sub = storage; + r.size = storage.size; + r.align = storage.align; + } 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). + r = newtype(c.a, tykind.TY_TUPLE); + tinfocachebind(c.tc, n, r); + let total: u64 = 0u64; + let maxal: u64 = 1u64; + let p: *node = n.list; + for (p != nil) { + let pt: *tinfo = tinfofornode(c, p); + if (pt != nil) { + if (pt.align > maxal) { maxal = pt.align; }; + total += pt.size; + }; + p = p.next; + }; + r.size = total; + r.align = maxal; + } 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. + // Anonymous-embed promotion is deferred (#13). + // + // Pre-bind into the cache BEFORE walking fields so a + // self-referential pointer field (e.g., `next: *node` inside + // `type node = struct {..., next: *node, ...}`) terminates: + // the inner tinfofornode(TNAME(node)) resolvealias-recurses + // back to this same body node, hits the cache, and returns + // 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. + r = newtype(c.a, tykind.TY_STRUCT); + tinfocachebind(c.tc, n, r); + let off: u64 = 0u64; + let maxalign: u64 = 1u64; + let f: *node = n.list; + for (f != nil) { + if (f.kind == nkind.N_TFIELD) { + let ft: *tinfo = tinfofornode(c, f.lhs); + if (ft != nil) { + if (ft.align > maxalign) { maxalign = ft.align; }; + if (ft.align > 0u64) { + off = (off + ft.align - 1u64) & ~(ft.align - 1u64); + }; + off += ft.size; + }; + }; + f = f.next; + }; + if (maxalign > 0u64) { + r.size = (off + maxalign - 1u64) & ~(maxalign - 1u64); + }; + r.align = maxalign; + } else { if (k == nkind.N_TTAGGED) { + // Cstage cmd/wcc/check.c:347-435: 8B tag + max(variant) + // rounded up to 8. Variant dedup / never-strip / nullable-fold + // stay in cstage's check.c for now — wwstage cgen only reads + // the size today, and the AST-level pre-fold (astsize's + // TTAGGED arm) already matches the cstage layout numerically + // for the shapes selfhost exercises. Pre-bind for cycle + // protection (recursive sum-type shapes through NAMED + // variants). + r = newtype(c.a, tykind.TY_TAGGED); + tinfocachebind(c.tc, n, r); + let maxsz: u64 = 0u64; + let al: u64 = 8u64; + let v: *node = n.list; + for (v != nil) { + let vt: *tinfo = tinfofornode(c, v); + if (vt != nil) { + if (vt.size > maxsz) { maxsz = vt.size; }; + if (vt.align > al) { al = vt.align; }; + }; + v = v.next; + }; + let pad: u64 = (maxsz + 7u64) & ~7u64; + r.size = 8u64 + pad; + r.align = al; + };};};};};};};};};};}; if (r != nil) { tinfocachebind(c.tc, n, r); }; return r; }; @@ -7941,28 +8072,70 @@ fn tinfofornode(c: *checker, n: *node) *tinfo = { fn exprtype(c: *checker, e: *node) *node = { if (e == nil) { return nil; }; let k: nkind = e.kind; + // #61 audit §1.8 — A.2 widens A.1's single N_INTLIT population to + // every primitive literal arm + N_IDENT. Cgen size walkers + // (slotsize first; elemsize/fieldsize/letemitsize follow) consult + // node.type_ as the SSoT; populating literals + idents closes the + // loop from the read side. if (k == nkind.N_INTLIT) { let tn: *node = mktname(c, "untyped_int"); - // #61 audit §1.8 — A.1 single population site. Cgen still - // reads sizes through primtypesize/slotsize today; A.2+ - // graduates each walker family to read e.type_ instead, - // ending the size-walker cascade (Rob+Drew convergence - // 2026-05-20). Don't extend population to other arms in A.1. e.type_ = tinfofornode(c, tn): *void; return tn; }; - if (k == nkind.N_FLOATLIT) { return mktname(c, "untyped_float"); }; - if (k == nkind.N_STRLIT) { return mktname(c, "str"); }; - if (k == nkind.N_RUNELIT) { return mktname(c, "rune"); }; - if (k == nkind.N_TRUE) { return mktname(c, "bool"); }; - if (k == nkind.N_FALSE) { return mktname(c, "bool"); }; - if (k == nkind.N_VOIDLIT) { return mktname(c, "void"); }; - if (k == nkind.N_NIL) { return mktname(c, "untyped_nil"); }; + if (k == nkind.N_FLOATLIT) { + let tn: *node = mktname(c, "untyped_float"); + e.type_ = tinfofornode(c, tn): *void; + return tn; + }; + if (k == nkind.N_STRLIT) { + let tn: *node = mktname(c, "str"); + e.type_ = tinfofornode(c, tn): *void; + return tn; + }; + if (k == nkind.N_RUNELIT) { + let tn: *node = mktname(c, "rune"); + e.type_ = tinfofornode(c, tn): *void; + return tn; + }; + if (k == nkind.N_TRUE) { + let tn: *node = mktname(c, "bool"); + e.type_ = tinfofornode(c, tn): *void; + return tn; + }; + if (k == nkind.N_FALSE) { + let tn: *node = mktname(c, "bool"); + e.type_ = tinfofornode(c, tn): *void; + return tn; + }; + if (k == nkind.N_VOIDLIT) { + let tn: *node = mktname(c, "void"); + e.type_ = tinfofornode(c, tn): *void; + return tn; + }; + if (k == nkind.N_NIL) { + let tn: *node = mktname(c, "untyped_nil"); + e.type_ = tinfofornode(c, tn): *void; + return tn; + }; if (k == nkind.N_IDENT) { let s: *sym = scopelookup(c.cur, e.str); if (s == nil) { return nil; }; if (s.decl == nil) { return nil; }; - return s.decl.lhs; + let t: *node = s.decl.lhs; + // Propagate the declared type's tinfo onto the use site so + // downstream cgen walkers can read n.type_ off an ident. + if (t != nil) { + if (t.type_ != nil) { + e.type_ = t.type_; + } else { + let ti: *tinfo = tinfofornode(c, t); + if (ti != nil) { + e.type_ = ti: *void; + t.type_ = ti: *void; + }; + }; + }; + return t; }; if (k == nkind.N_CAST) { // `expr: T` — explicit cast; the type expr is e.rhs. @@ -8728,6 +8901,17 @@ fn resolvefnbody(c: *checker, fnnode: *node) void = { let outer: *scope = c.cur; c.cur = newscope(c.a, c.cur); installparams(c, fnnode.list); + // #61 audit §1.8 — A.2: walk each param's declared type-expr so + // tinfofornode stamps n.type_ on it. installparams binds the name + // but never recurses into the type; without this, cgen's slotsize + // fast-path hits the fallback for every param load/store. + let p: *node = fnnode.list; + for (p != nil) { + if (p.kind == nkind.N_PARAM) { + if (p.lhs != nil) { resolvewalk(c, p.lhs); }; + }; + p = p.next; + }; let prevret: *node = c.fnret; c.fnret = fnnode.lhs; // return type AST, used by `?` check if (fnnode.body != nil) { @@ -10735,6 +10919,31 @@ export fn letslotsize(c: *cgen, n: *node) i32 = { }; fn slotsize(c: *cgen, typn: *node) i32 = { + // #61 audit §1.8 — A.2 graduation: read tinfo.size off the populated + // type-expression node when its kind matches the cstage natural-size + // SSoT. Filtered set covers shapes whose tinfo.size already encodes + // the cgen slot-size contract: pointer-like (PTR/CHAN/FN), slice + // (SLICE), and str (STR). Other kinds flow through the fallback + // walker. TAGGED stays out because cstage's resolve_type folds + // `(*T | void)` to a single 8B pointer (cmd/wcc/check.c:412-426) + // but tinfofornode's TTAGGED arm doesn't yet — graduating TAGGED + // would shrink that fold's slot from 16 to 8 on the wwstage side. + // Primitive scalars + enums + inline TUPLE / TSTRUCT also keep + // flowing through the fallback walker because cgen's slot-pad-to-8 + // contract (cmd/w6c/cgen.c let_emit_size:691-720 and the per-field + // slot rounding in registerstruct/letslotsize) lives there, not in + // tinfo.size. Subsequent sub-commits collapse the remaining shapes + // onto the same pivot once cstage parity catches up (#61 Phase A — + // see the audit doc for the staged plan). + if (typn != nil && typn.type_ != nil) { + let ti: *tinfo = typn.type_: *tinfo; + let kk: tykind = ti.kind; + if (kk == tykind.TY_PTR || kk == tykind.TY_SLICE || + kk == tykind.TY_CHAN || kk == tykind.TY_FN || + kk == tykind.TY_STR) { + return ti.size: i32; + }; + }; if (typn == nil) { return 8; }; let k: nkind = typn.kind; // `!T` carries T's memory layout; the error-tag bit lives in the