lib/ww/typ + selfhost/cmd/wcc/check: tinfo-on-node infrastructure (Phase A.1)

Foundation for audit §1.8 — wwstage cgen recomputes type sizes at every
site instead of reading n.type_ like cstage does (cmd/wcc/check.c sets
n->type via cexpr; cgen reads n->type->size). The scattered literals
this session has been chasing (#43, #60, etc.) are the symptom; this
chain is the cure.

A.1 is infrastructure only — no cgen-site graduation yet. Subsequent
A.2+ sub-commits collapse each walker family (slotsize, elemsize,
fieldsize, isstrtype, istaggedtype, ...) onto n.type_ reads.

lib/ww/typ.ww:
- tinfocacheent struct (key, val, cnext) — sea-of-stars per rule 12.
- tinfocache: *tinfocacheent field on tctx (now 25 fields).
- tinfocachelookup / tinfocachebind — head-prepend linked-list ops.

selfhost/cmd/wcc/check.ww:
- tinfofornode(c, n) *tinfo — covers N_TNAME primitive (singleton
  lookup), N_TNAME alias (recurse via resolvealias), N_TBANG
  (unwrap+recurse, iserror dropped — graduate alongside the first
  cgen reader that needs it), N_TPTR/N_TSLICE/N_TCHAN (recurse on
  sub, call typeptr/typeslice/typechan).
- exprtype N_INTLIT arm now sets e.type_ = tinfofornode(c, tn). Only
  population site in this commit; every other arm unchanged.

Empirically verified via temp probe that tinfofornode is reached and
returns non-nil on `let x: i32 = 42;`. Strict scope: zero cgen reads
of n.type_; primtypesize/slotsize/etc. still drive size queries.

131/131 + 994 + 995 byte-identical to caa72f2.
This commit is contained in:
2026-05-20 10:40:12 +09:00
parent caa72f2365
commit 93ac65ba0a
4 changed files with 357 additions and 3 deletions

View File

@@ -89,6 +89,17 @@ type tinfo = struct {
under: *tinfo,
};
// #61 audit §1.8 / Rob+Drew convergence 2026-05-20: memoizes
// tinfofornode lookups keyed by AST pointer. Linked-list shape mirrors
// other wwstage-side caches (cgen.aliases, cgen.structs) — sea-of-stars
// over hash-table cleverness, and Sym/Scope already pay the FNV cost
// for the resolver pass.
type tinfocacheent = struct {
key: *node,
val: *tinfo,
cnext: *tinfocacheent,
};
// ---- tctx — the box of primitive types -------------------------------
type tctx = struct {
@@ -118,6 +129,7 @@ type tctx = struct {
tyuntypedrune: *tinfo,
tyuntypedbool: *tinfo,
tyuntypednil: *tinfo,
tinfocache: *tinfocacheent,
};
// ---- constructors -----------------------------------------------------
@@ -214,6 +226,28 @@ export fn typenamed(a: *arena, name: str, under: *tinfo) *tinfo = {
return t;
};
// #61 audit §1.8 — A.1 infrastructure: tinfocache lookup/bind. Keyed
// by AST node-pointer so two different N_TNAME("i32") nodes get
// independent entries that both resolve to c.tyi32. Used by
// tinfofornode in check.ww; cgen still reads sizes via primtypesize
// until A.2+ graduates each walker family.
export fn tinfocachelookup(c: *tctx, key: *node) *tinfo = {
let e: *tinfocacheent = c.tinfocache;
for (e != nil) {
if (e.key == key) { return e.val; };
e = e.cnext;
};
return nil;
};
export fn tinfocachebind(c: *tctx, key: *node, val: *tinfo) void = {
let e: *tinfocacheent = amalloc(c.a, 32u64): *tinfocacheent;
e.key = key;
e.val = val;
e.cnext = c.tinfocache;
c.tinfocache = e;
};
// ---- predicates -------------------------------------------------------
export fn typeisint(t: *tinfo) bool = {

View File

@@ -6515,6 +6515,17 @@ type tinfo = struct {
under: *tinfo,
};
// #61 audit §1.8 / Rob+Drew convergence 2026-05-20: memoizes
// tinfofornode lookups keyed by AST pointer. Linked-list shape mirrors
// other wwstage-side caches (cgen.aliases, cgen.structs) — sea-of-stars
// over hash-table cleverness, and Sym/Scope already pay the FNV cost
// for the resolver pass.
type tinfocacheent = struct {
key: *node,
val: *tinfo,
cnext: *tinfocacheent,
};
// ---- tctx — the box of primitive types -------------------------------
type tctx = struct {
@@ -6544,6 +6555,7 @@ type tctx = struct {
tyuntypedrune: *tinfo,
tyuntypedbool: *tinfo,
tyuntypednil: *tinfo,
tinfocache: *tinfocacheent,
};
// ---- constructors -----------------------------------------------------
@@ -6640,6 +6652,28 @@ export fn typenamed(a: *arena, name: str, under: *tinfo) *tinfo = {
return t;
};
// #61 audit §1.8 — A.1 infrastructure: tinfocache lookup/bind. Keyed
// by AST node-pointer so two different N_TNAME("i32") nodes get
// independent entries that both resolve to c.tyi32. Used by
// tinfofornode in check.ww; cgen still reads sizes via primtypesize
// until A.2+ graduates each walker family.
export fn tinfocachelookup(c: *tctx, key: *node) *tinfo = {
let e: *tinfocacheent = c.tinfocache;
for (e != nil) {
if (e.key == key) { return e.val; };
e = e.cnext;
};
return nil;
};
export fn tinfocachebind(c: *tctx, key: *node, val: *tinfo) void = {
let e: *tinfocacheent = amalloc(c.a, 32u64): *tinfocacheent;
e.key = key;
e.val = val;
e.cnext = c.tinfocache;
c.tinfocache = e;
};
// ---- predicates -------------------------------------------------------
export fn typeisint(t: *tinfo) bool = {
@@ -7825,6 +7859,81 @@ 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.
//
// 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.
fn tinfofornode(c: *checker, n: *node) *tinfo = {
if (n == nil) { return nil; };
let cached: *tinfo = tinfocachelookup(c.tc, n);
if (cached != nil) { return cached; };
let r: *tinfo = nil;
let k: nkind = n.kind;
if (k == nkind.N_TNAME) {
let nm: str = n.str;
if (streq(nm, "void")) { r = c.tc.tyvoid; };
if (streq(nm, "bool")) { r = c.tc.tybool; };
if (streq(nm, "rune")) { r = c.tc.tyrune; };
if (streq(nm, "i8")) { r = c.tc.tyi8; };
if (streq(nm, "i16")) { r = c.tc.tyi16; };
if (streq(nm, "i32")) { r = c.tc.tyi32; };
if (streq(nm, "i64")) { r = c.tc.tyi64; };
if (streq(nm, "u8")) { r = c.tc.tyu8; };
if (streq(nm, "u16")) { r = c.tc.tyu16; };
if (streq(nm, "u32")) { r = c.tc.tyu32; };
if (streq(nm, "u64")) { r = c.tc.tyu64; };
if (streq(nm, "int")) { r = c.tc.tyint; };
if (streq(nm, "uint")) { r = c.tc.tyuint; };
if (streq(nm, "uintptr")) { r = c.tc.tyuintptr; };
if (streq(nm, "f32")) { r = c.tc.tyf32; };
if (streq(nm, "f64")) { r = c.tc.tyf64; };
if (streq(nm, "str")) { r = c.tc.tystr; };
if (streq(nm, "never")) { r = c.tc.tynever; };
if (streq(nm, "untyped_int")) { r = c.tc.tyuntypedint; };
if (streq(nm, "untyped_float")) { r = c.tc.tyuntypedfloat; };
if (streq(nm, "untyped_str")) { r = c.tc.tyuntypedstr; };
if (streq(nm, "untyped_rune")) { r = c.tc.tyuntypedrune; };
if (streq(nm, "untyped_bool")) { r = c.tc.tyuntypedbool; };
if (streq(nm, "untyped_nil")) { r = c.tc.tyuntypednil; };
if (r == nil) {
// Alias / user-defined name: resolve via scope and recurse.
// Mirrors astsize's TNAME fallback so the helpers stay in
// lockstep until A.2 collapses each cgen size-walker onto
// tinfo.size directly.
let body: *node = resolvealias(c, n);
if (body != nil && body != n) {
r = tinfofornode(c, body);
};
};
} else { if (k == nkind.N_TBANG) {
// #61 audit §1.8: `!T` propagates the inner shape; cstage's
// resolve_type sets ty->iserror on the wrapper but no wwstage
// cgen reader consumes it yet, so A.1 drops the flag and
// returns the inner tinfo unchanged. Mirrors typeeqast's
// unwrapbang pre-walk; graduate alongside the first cgen
// site that needs iserror discrimination.
r = tinfofornode(c, n.lhs);
} else { if (k == nkind.N_TPTR) {
r = typeptr(c.a, tinfofornode(c, n.lhs));
} else { if (k == nkind.N_TSLICE) {
r = typeslice(c.a, tinfofornode(c, n.lhs));
} else { if (k == nkind.N_TCHAN) {
r = typechan(c.a, tinfofornode(c, n.lhs));
};};};};};
if (r != nil) { tinfocachebind(c.tc, n, r); };
return r;
};
// exprtype — best-effort type-AST inference for an expression
// node. Handles literals, identifiers, calls, and casts; returns
// nil for shapes we don't statically know (binary ops, struct
@@ -7832,7 +7941,16 @@ fn foldtointlit(c: *checker, n: *node, v: i64) void = {
fn exprtype(c: *checker, e: *node) *node = {
if (e == nil) { return nil; };
let k: nkind = e.kind;
if (k == nkind.N_INTLIT) { return mktname(c, "untyped_int"); };
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"); };

View File

@@ -831,6 +831,81 @@ 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.
//
// 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.
fn tinfofornode(c: *checker, n: *node) *tinfo = {
if (n == nil) { return nil; };
let cached: *tinfo = tinfocachelookup(c.tc, n);
if (cached != nil) { return cached; };
let r: *tinfo = nil;
let k: nkind = n.kind;
if (k == nkind.N_TNAME) {
let nm: str = n.str;
if (streq(nm, "void")) { r = c.tc.tyvoid; };
if (streq(nm, "bool")) { r = c.tc.tybool; };
if (streq(nm, "rune")) { r = c.tc.tyrune; };
if (streq(nm, "i8")) { r = c.tc.tyi8; };
if (streq(nm, "i16")) { r = c.tc.tyi16; };
if (streq(nm, "i32")) { r = c.tc.tyi32; };
if (streq(nm, "i64")) { r = c.tc.tyi64; };
if (streq(nm, "u8")) { r = c.tc.tyu8; };
if (streq(nm, "u16")) { r = c.tc.tyu16; };
if (streq(nm, "u32")) { r = c.tc.tyu32; };
if (streq(nm, "u64")) { r = c.tc.tyu64; };
if (streq(nm, "int")) { r = c.tc.tyint; };
if (streq(nm, "uint")) { r = c.tc.tyuint; };
if (streq(nm, "uintptr")) { r = c.tc.tyuintptr; };
if (streq(nm, "f32")) { r = c.tc.tyf32; };
if (streq(nm, "f64")) { r = c.tc.tyf64; };
if (streq(nm, "str")) { r = c.tc.tystr; };
if (streq(nm, "never")) { r = c.tc.tynever; };
if (streq(nm, "untyped_int")) { r = c.tc.tyuntypedint; };
if (streq(nm, "untyped_float")) { r = c.tc.tyuntypedfloat; };
if (streq(nm, "untyped_str")) { r = c.tc.tyuntypedstr; };
if (streq(nm, "untyped_rune")) { r = c.tc.tyuntypedrune; };
if (streq(nm, "untyped_bool")) { r = c.tc.tyuntypedbool; };
if (streq(nm, "untyped_nil")) { r = c.tc.tyuntypednil; };
if (r == nil) {
// Alias / user-defined name: resolve via scope and recurse.
// Mirrors astsize's TNAME fallback so the helpers stay in
// lockstep until A.2 collapses each cgen size-walker onto
// tinfo.size directly.
let body: *node = resolvealias(c, n);
if (body != nil && body != n) {
r = tinfofornode(c, body);
};
};
} else { if (k == nkind.N_TBANG) {
// #61 audit §1.8: `!T` propagates the inner shape; cstage's
// resolve_type sets ty->iserror on the wrapper but no wwstage
// cgen reader consumes it yet, so A.1 drops the flag and
// returns the inner tinfo unchanged. Mirrors typeeqast's
// unwrapbang pre-walk; graduate alongside the first cgen
// site that needs iserror discrimination.
r = tinfofornode(c, n.lhs);
} else { if (k == nkind.N_TPTR) {
r = typeptr(c.a, tinfofornode(c, n.lhs));
} else { if (k == nkind.N_TSLICE) {
r = typeslice(c.a, tinfofornode(c, n.lhs));
} else { if (k == nkind.N_TCHAN) {
r = typechan(c.a, tinfofornode(c, n.lhs));
};};};};};
if (r != nil) { tinfocachebind(c.tc, n, r); };
return r;
};
// exprtype — best-effort type-AST inference for an expression
// node. Handles literals, identifiers, calls, and casts; returns
// nil for shapes we don't statically know (binary ops, struct
@@ -838,7 +913,16 @@ fn foldtointlit(c: *checker, n: *node, v: i64) void = {
fn exprtype(c: *checker, e: *node) *node = {
if (e == nil) { return nil; };
let k: nkind = e.kind;
if (k == nkind.N_INTLIT) { return mktname(c, "untyped_int"); };
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"); };

View File

@@ -6515,6 +6515,17 @@ type tinfo = struct {
under: *tinfo,
};
// #61 audit §1.8 / Rob+Drew convergence 2026-05-20: memoizes
// tinfofornode lookups keyed by AST pointer. Linked-list shape mirrors
// other wwstage-side caches (cgen.aliases, cgen.structs) — sea-of-stars
// over hash-table cleverness, and Sym/Scope already pay the FNV cost
// for the resolver pass.
type tinfocacheent = struct {
key: *node,
val: *tinfo,
cnext: *tinfocacheent,
};
// ---- tctx — the box of primitive types -------------------------------
type tctx = struct {
@@ -6544,6 +6555,7 @@ type tctx = struct {
tyuntypedrune: *tinfo,
tyuntypedbool: *tinfo,
tyuntypednil: *tinfo,
tinfocache: *tinfocacheent,
};
// ---- constructors -----------------------------------------------------
@@ -6640,6 +6652,28 @@ export fn typenamed(a: *arena, name: str, under: *tinfo) *tinfo = {
return t;
};
// #61 audit §1.8 — A.1 infrastructure: tinfocache lookup/bind. Keyed
// by AST node-pointer so two different N_TNAME("i32") nodes get
// independent entries that both resolve to c.tyi32. Used by
// tinfofornode in check.ww; cgen still reads sizes via primtypesize
// until A.2+ graduates each walker family.
export fn tinfocachelookup(c: *tctx, key: *node) *tinfo = {
let e: *tinfocacheent = c.tinfocache;
for (e != nil) {
if (e.key == key) { return e.val; };
e = e.cnext;
};
return nil;
};
export fn tinfocachebind(c: *tctx, key: *node, val: *tinfo) void = {
let e: *tinfocacheent = amalloc(c.a, 32u64): *tinfocacheent;
e.key = key;
e.val = val;
e.cnext = c.tinfocache;
c.tinfocache = e;
};
// ---- predicates -------------------------------------------------------
export fn typeisint(t: *tinfo) bool = {
@@ -7825,6 +7859,81 @@ 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.
//
// 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.
fn tinfofornode(c: *checker, n: *node) *tinfo = {
if (n == nil) { return nil; };
let cached: *tinfo = tinfocachelookup(c.tc, n);
if (cached != nil) { return cached; };
let r: *tinfo = nil;
let k: nkind = n.kind;
if (k == nkind.N_TNAME) {
let nm: str = n.str;
if (streq(nm, "void")) { r = c.tc.tyvoid; };
if (streq(nm, "bool")) { r = c.tc.tybool; };
if (streq(nm, "rune")) { r = c.tc.tyrune; };
if (streq(nm, "i8")) { r = c.tc.tyi8; };
if (streq(nm, "i16")) { r = c.tc.tyi16; };
if (streq(nm, "i32")) { r = c.tc.tyi32; };
if (streq(nm, "i64")) { r = c.tc.tyi64; };
if (streq(nm, "u8")) { r = c.tc.tyu8; };
if (streq(nm, "u16")) { r = c.tc.tyu16; };
if (streq(nm, "u32")) { r = c.tc.tyu32; };
if (streq(nm, "u64")) { r = c.tc.tyu64; };
if (streq(nm, "int")) { r = c.tc.tyint; };
if (streq(nm, "uint")) { r = c.tc.tyuint; };
if (streq(nm, "uintptr")) { r = c.tc.tyuintptr; };
if (streq(nm, "f32")) { r = c.tc.tyf32; };
if (streq(nm, "f64")) { r = c.tc.tyf64; };
if (streq(nm, "str")) { r = c.tc.tystr; };
if (streq(nm, "never")) { r = c.tc.tynever; };
if (streq(nm, "untyped_int")) { r = c.tc.tyuntypedint; };
if (streq(nm, "untyped_float")) { r = c.tc.tyuntypedfloat; };
if (streq(nm, "untyped_str")) { r = c.tc.tyuntypedstr; };
if (streq(nm, "untyped_rune")) { r = c.tc.tyuntypedrune; };
if (streq(nm, "untyped_bool")) { r = c.tc.tyuntypedbool; };
if (streq(nm, "untyped_nil")) { r = c.tc.tyuntypednil; };
if (r == nil) {
// Alias / user-defined name: resolve via scope and recurse.
// Mirrors astsize's TNAME fallback so the helpers stay in
// lockstep until A.2 collapses each cgen size-walker onto
// tinfo.size directly.
let body: *node = resolvealias(c, n);
if (body != nil && body != n) {
r = tinfofornode(c, body);
};
};
} else { if (k == nkind.N_TBANG) {
// #61 audit §1.8: `!T` propagates the inner shape; cstage's
// resolve_type sets ty->iserror on the wrapper but no wwstage
// cgen reader consumes it yet, so A.1 drops the flag and
// returns the inner tinfo unchanged. Mirrors typeeqast's
// unwrapbang pre-walk; graduate alongside the first cgen
// site that needs iserror discrimination.
r = tinfofornode(c, n.lhs);
} else { if (k == nkind.N_TPTR) {
r = typeptr(c.a, tinfofornode(c, n.lhs));
} else { if (k == nkind.N_TSLICE) {
r = typeslice(c.a, tinfofornode(c, n.lhs));
} else { if (k == nkind.N_TCHAN) {
r = typechan(c.a, tinfofornode(c, n.lhs));
};};};};};
if (r != nil) { tinfocachebind(c.tc, n, r); };
return r;
};
// exprtype — best-effort type-AST inference for an expression
// node. Handles literals, identifiers, calls, and casts; returns
// nil for shapes we don't statically know (binary ops, struct
@@ -7832,7 +7941,16 @@ fn foldtointlit(c: *checker, n: *node, v: i64) void = {
fn exprtype(c: *checker, e: *node) *node = {
if (e == nil) { return nil; };
let k: nkind = e.kind;
if (k == nkind.N_INTLIT) { return mktname(c, "untyped_int"); };
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"); };