selfhost/cmd/wcc: extend tinfo coverage + graduate slotsize fast-path (Phase A.2)

tinfofornode (check.ww) covers six more kinds:
- N_TARRAY: typearray on recursed element, size = esz * elen.
- N_TFN: 8B/8B; recurse on ret.
- N_TENUM: storage size/align (default i32 → 4B). Mirrors cstage
  check.c:531-542.
- N_TTUPLE: raw element sum + max-align. Mirrors check.c:329-345.
- N_TSTRUCT: per-field align, round total to maxalign. Mirrors
  check.c:280-340 / :468-527.
- N_TTAGGED: 8B tag + (max(variant)+7)&~7, al ≥ 8. Mirrors
  check.c:347-435.

Cycle-prone arms (TFN/TTUPLE/TSTRUCT/TTAGGED) pre-bind the in-progress
tinfo into the cache BEFORE recursing on subfields so self-referential
shapes (`type node = struct { next: *node, … }`) terminate. Pre-fix
wwdump_ww segfaulted on its own combined source.

More population sites in exprtype: every primitive literal arm
(N_FLOATLIT/N_STRLIT/N_RUNELIT/N_TRUE/N_FALSE/N_VOIDLIT/N_NIL —
A.1 only had N_INTLIT), N_IDENT (propagate from sym.decl.lhs.type_,
eagerly tinfofornode + cache if not yet visited), resolvewalk type-expr
stamping, and resolvefnbody now recurses into N_PARAM.lhs (pre-#61 the
param type-exprs were never walked — every param had nil type_).

slotsize (cgenutil.ww) gains a fast-path: when n.type_ is set AND the
kind is PTR / SLICE / CHAN / FN / STR, return ti.size: i32 directly.
The fallback walker stays alive for primitive scalars, enums, named
structs, inline composites, TARRAY — those need cstage's let_emit_size
slot-pad-to-8 contract (cmd/w6c/cgen.c:691-720) which tinfo doesn't
carry. A.3+ moves padding into the fast-path.

TY_TAGGED *not* in the fast-path (reviewer-61a2 caught this) —
tinfofornode's TTAGGED arm doesn't implement cstage's nullable-pointer
fold (check.c:412-426: `(*T | void) → 8B`). Self-host code happens not
to use that shape today, but the divergence would land latent. Pull
TAGGED until A.3 folds nullable into tinfofornode.

A.2 fallback-hit count under wwdump build: 1554 fast vs 2187 fallback —
partial graduation; expected. 131/131 + 994 + 995 + bootstrap
byte-identical (ww2==ww3==ww4).
This commit is contained in:
2026-05-20 12:17:16 +09:00
parent 93ac65ba0a
commit a78b26c2d3
4 changed files with 708 additions and 81 deletions

View File

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

View File

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

View File

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

View File

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