Files
ww/selfhost/cmd/wcc/check.ww
Hojun-Cho d0a1e2a221 wcc/check: #133 const-expr scalar module-global — fold+stamp let-init like def, emit DATA (both stages)
A module-global let with a const-expr init (let s = 7*6) emitted NO DATA word: cstage LINK-FAILed (undefined main.s, loud), wwstage was SILENT (no DATA, MOVSXD on stale AX, exit 152). The DEF pass-2 arm already const-folds + stamps its rhs to N_INTLIT (the #88 eval_def_const/stamp_intlit machinery); the LET pass-2 arm omitted it. Mirror it: after the assignability check, fold the rhs and stamp N_INTLIT when the plain-literal fold missed AND the const-fold succeeded. The existing DATA-emit downstream then fires (DATAW 42 + load). Both stages, byte-identical. Closes the inferred const-expr global and the typed b-ii case (let s:i64=7*6, link-fail both stages) with one stamp.

Gated on genuine int-const success (the eval return value, not the out-param): str/struct/slice/call/runtime-operand rhs short-circuit before the stamp and are left untouched — never zeroed. Non-const rhs stays on its current loud route; div-by-zero stays loud. Latent in selfhost (no const-expr module globals → 990-997 byte-id unchanged).

Pin: 947 rows C1 inferred 7*6, C2 typed b-ii, C3 def-ref K*7, C4 unary-over-binop, C5 div-by-zero loud-guard; cs==ww byte-id.
2026-06-07 22:57:06 +09:00

5201 lines
204 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// selfhost/cmd/wcc/check.ww — minimal port of cmd/wcc/check.c.
//
// Status: name-resolution + primitive-type seeding only. Full type
// inference, conversion rules, tagged-union dispatch typing, return-
// type checking, etc. all live in cmd/wcc/check.c (937 lines) and
// will land here in subsequent commits.
//
// What this version does:
// 1. Creates a top scope and seeds it with primitive type names so
// `i32`, `str`, `*u8` etc. resolve.
// 2. Walks the file's top-level decls (use/def/type/fn/let) and
// installs Sym entries for each.
// 3. Recursively walks fn bodies; for every nkind.N_IDENT used as an
// expression or as a type name, looks it up and counts the
// resolved vs. unresolved.
// 4. Returns a summary the caller (wwdump -r) prints; the test
// asserts unresolved == 0 on every selfhost fixture, which is
// the floor signal that the frontend can name-resolve real ww.
package wcc;
import os;
import tok;
import strconv;
type checker = struct {
tc: *tctx,
top: *scope,
cur: *scope,
nresolved: i32,
nunresolved: i32,
errs: i32,
verbose: i32, // when non-zero, log each unresolved name
fnret: *node, // enclosing fn's return type AST (for `?`)
curmod: str, // importing-module bareword for the decl
// currently being walked; "" for primary
// compilation unit. Drives same-module
// preference in bare-leaf lookups.
file: *node, // N_FILE root; used by checkmoduleshadow
// to consult the declaring source's own
// `use` directives.
allococtx: *node, // #3/B': the one empty alloc([], n) call node
// with let-declared slice context this walk;
// any other empty alloc has no element hint
// and must fail to infer (harec
// check.c:1801). Set by checkletassign
// around its exprtype, nil elsewhere.
};
// cerr — bare stderr fragment writer for the checker's piecewise
// diagnostics. Tool-local (NOT a lib wrapper): messages are built from
// many fragments and we route through os.write to avoid libc stdio.
// .len replaces the error-prone hand-counted byte literals these sites
// carried. Lives here (not err.ww) because the selfhost build pulls
// check.ww but not err.ww (which ports err.c for the C-driven path).
fn cerr(m: str) void = {
os.write(2, m.ptr, m.len: u64);
};
// circularnamed — #62/#69 cycle guard, cstage circular_named twin
// (cmd/wcc/check.c): a VALUE-position read of a TY_NAMED whose body is
// still resolving is a true type cycle (infinite size) — loud, per
// harec's in_progress check (ref/harec/src/check.c:4767 "Circular
// dependency for '%s'"). Pointer/slice/chan/fn positions never read
// the target's size and legitimately receive the in-progress
// placeholder, so `type node = struct { next: *node }` stays legal.
// Pre-#62 a pure alias cycle left a CYCLIC under-chain in the table
// and every NAMED-chain chase loop downstream spun forever (the #69
// compiler hang); a struct-value cycle recursed the slot walkers to
// stack overflow.
fn circularnamed(c: *checker, t: *tinfo, n: *node) bool = {
if (t == nil) { return false; };
if (t.kind != tykind.TY_NAMED) { return false; };
if (t.resolving == 0) { return false; };
if (n != nil) { cerr(n.file); cerr(": "); };
cerr("error: circular type dependency: '");
cerr(t.name);
cerr("'\n");
c.errs += 1;
// Loud-STOP, not accumulate: wwstage's AST-level alias walkers
// (resolvealias, cgenutil aliaslookup chains) follow TNAME->TNAME
// by NAME, blind to the tinfo table — on a cyclic alias graph they
// spin forever even after the table edge is cut to tyerr (measured:
// error printed once, then hang). cstage accumulates instead — its
// single-peel ternaries can't loop. Asymmetry is deliberate; both
// stages reject with the same message + non-zero exit.
os.exit(1);
};
// seedprimitives — install the built-in type names so `i32`, `str`,
// etc. can be looked up like ordinary symbols.
fn seedprimitives(c: *checker) void = {
scopedefine(c.top, "void", skind.SK_TYPE, c.tc.tyvoid, nil);
scopedefine(c.top, "bool", skind.SK_TYPE, c.tc.tybool, nil);
scopedefine(c.top, "rune", skind.SK_TYPE, c.tc.tyrune, nil);
scopedefine(c.top, "i8", skind.SK_TYPE, c.tc.tyi8, nil);
scopedefine(c.top, "i16", skind.SK_TYPE, c.tc.tyi16, nil);
scopedefine(c.top, "i32", skind.SK_TYPE, c.tc.tyi32, nil);
scopedefine(c.top, "i64", skind.SK_TYPE, c.tc.tyi64, nil);
scopedefine(c.top, "u8", skind.SK_TYPE, c.tc.tyu8, nil);
scopedefine(c.top, "u16", skind.SK_TYPE, c.tc.tyu16, nil);
scopedefine(c.top, "u32", skind.SK_TYPE, c.tc.tyu32, nil);
scopedefine(c.top, "u64", skind.SK_TYPE, c.tc.tyu64, nil);
scopedefine(c.top, "int", skind.SK_TYPE, c.tc.tyint, nil);
scopedefine(c.top, "uint", skind.SK_TYPE, c.tc.tyuint, nil);
scopedefine(c.top, "uintptr", skind.SK_TYPE, c.tc.tyuintptr, nil);
scopedefine(c.top, "f32", skind.SK_TYPE, c.tc.tyf32, nil);
scopedefine(c.top, "f64", skind.SK_TYPE, c.tc.tyf64, nil);
scopedefine(c.top, "str", skind.SK_TYPE, c.tc.tystr, nil);
scopedefine(c.top, "never", skind.SK_TYPE, c.tc.tynever, nil);
// #29: predeclare `type nomem = !void;` so user code needn't
// declare it locally. Synthesize an nkind.N_TYPEDECL whose lhs is
// nkind.N_TBANG{nkind.N_TNAME("void")} so varianterr and other
// iserror-aware paths treat `nomem` identically to a user-written
// alias. Mirrors cmd/wcc/check.c lookup_builtin returning
// ty_nomem (NAMED, under=ty_void, iserror=1). Note: cgen owns a
// separate alias chain — see collectaliases in cgen.ww for the
// companion seed.
let empty: str;
let tnvoid: *node = newnode(nkind.N_TNAME, empty, 0, 0);
tnvoid.str = "void";
let bang: *node = newnode(nkind.N_TBANG, empty, 0, 0);
bang.lhs = tnvoid;
let nomemdecl: *node = newnode(nkind.N_TYPEDECL, empty, 0, 0);
nomemdecl.str = "nomem";
nomemdecl.lhs = bang;
scopedefine(c.top, "nomem", skind.SK_TYPE, nil, nomemdecl);
// `nil`, `true`, `false` are keywords — handled at the lex/parser
// level, no symbol needed.
// `len`, `alloc`, `free`, `append`, `delete`, `insert` are
// pseudo-builtins; scopedefine them so their use sites resolve.
// The actual semantics live in cgen.
scopedefine(c.top, "len", skind.SK_FN, nil, nil);
scopedefine(c.top, "alloc", skind.SK_FN, nil, nil);
scopedefine(c.top, "free", skind.SK_FN, nil, nil);
scopedefine(c.top, "append", skind.SK_FN, nil, nil);
scopedefine(c.top, "delete", skind.SK_FN, nil, nil);
scopedefine(c.top, "insert", skind.SK_FN, nil, nil);
// #42: typed builtins folded to integer literals at check time —
// `size(T)` / `align(T)` (arg is a type-expression planted by the
// parser at lib/ww/parse/expr.ww:254-267) and `offset(e.f)` (arg is
// an N_DOT). exprtype intercepts these and rewrites the N_CALL to
// N_INTLIT so cgen never sees an unresolved size/align/offset symbol.
// Mirrors cmd/wcc/check.c:907-955.
scopedefine(c.top, "size", skind.SK_FN, nil, nil);
scopedefine(c.top, "align", skind.SK_FN, nil, nil);
scopedefine(c.top, "offset", skind.SK_FN, nil, nil);
};
// declmod — module-tag stamp for a top-level decl.
//
// The driver concatenates imported sources before the primary file and
// emits `// MODULE: foo` directives the lexer pins onto each decl's
// `module` field. We treat a decl as "imported" iff its module
// directive matches some `use IDENT;` bareword in this compilation
// unit. Primary-file decls return "" so they coexist (mod="") with
// imported decls of the same leaf name in scopelookupinmodule.
fn declmod(file: *node, d: *node) str = {
let empty: str;
if (d == nil) { return empty; };
if (d.nmod.len == 0) { return empty; };
if (file == nil) { return empty; };
let u: *node = file.list;
for (u != nil) {
if (u.kind == nkind.N_USE) {
if (streq(u.str, d.nmod)) { return d.nmod; };
};
u = u.next;
};
return empty;
};
// srcimports — does the source file that contributed decl-module
// `modtag` carry `use <name>;`? Mirrors cstage's src_imports —
// `modtag.len == 0` means primary, matching declmod's empty-str
// return for primary-source decls.
fn srcimports(file: *node, modtag: str, name: str) bool = {
if (file == nil) { return false; };
if (name.len == 0) { return false; };
let u: *node = file.list;
for (u != nil) {
if (u.kind == nkind.N_USE) {
// Skip self-imports: lib/fmt/fmttest.ww carries
// `use fmt;` while its module tag is also "fmt".
// That directive doesn't introduce a foreign
// module bareword and lib/fmt's own
// `fn bsprintf(fmt: str, ...)` is not a shadow.
if (u.nmod.len > 0) {
if (streq(u.nmod, u.str)) {
u = u.next;
continue;
};
};
let um: str = declmod(file, u);
let m: bool = false;
if (modtag.len == 0) {
if (um.len == 0) { m = true; };
} else { if (streq(um, modtag)) { m = true; }; };
if (m) {
if (streq(u.str, name)) { return true; };
};
};
u = u.next;
};
return false;
};
// checkmoduleshadow — enforce "value names and module names are
// disjoint" at nested-scope binds. Mirrors cstage check_module_shadow
// (cmd/wcc/check.c). Fires for fn params / lets / forrange iters /
// mcase bindings whose name matches an in-scope `use foo;` import
// declared in the same source file. Top-level decls are exempt
// (their same-leaf-as-module pattern is the intentional coexistence
// shape — `use fnmatch; fn fnmatch(...)` etc.).
fn checkmoduleshadow(c: *checker, name: str, kindstr: str) void = {
if (name.len == 0) { return; };
if (c.cur == c.top) { return; };
let seen: bool = false;
let s: *scope = c.cur;
for (s != nil) {
let r: *sym = scopelookuplocal(s, name);
if (r != nil) {
if (r.skind == skind.SK_USE) {
seen = true;
s = nil;
};
};
if (s != nil) { s = s.parent; };
};
if (!seen) { return; };
if (!srcimports(c.file, c.curmod, name)) { return; };
cerr(kindstr);
cerr(" '");
cerr(name);
cerr("' shadows imported module '");
cerr(name);
cerr("'\n");
c.errs += 1;
};
// installdecl — install the top-level decl's name into the top scope.
// We don't compute its type yet (that's the resolve pass) — just bind
// the name so forward references resolve.
//
// Architectural note: wwstage uses COEXISTENCE rather than the cstage
// promote-SK_USE-in-place approach in cmd/wcc/check.c. SK_USE and any
// same-leaf SK_TYPE/SK_FN/SK_DEF/SK_VAR live as separate entries in
// the same scope-bucket, distinguished by `sym.mod`. This avoids the
// cstage use_alias FLAG (a field on the sym, which would grow its size
// and risk the wwstage cgen amalloc-undersize trap, rob-pike) — but the
// flag's RESOLUTION job still has to be done. When the colliding decl's
// package equals the importing unit's curmod (the `package fnmatch;` /
// `package random;` self-import: random.random, fnmatch.fnmatch), the
// mod-preferring scopelookupprefer returns the same-leaf SK_FN/SK_TYPE,
// not the coexisting SK_USE, so a dot-lhs `mod.x` would miss the
// module-qualified arm and the call nil-stamps (#6a-D). The dot-lhs
// resolvers in exprtype's N_CALL and N_DOT arms re-resolve through
// scopelookupuselocal (lib/ww/sym.ww) to the SK_USE that coexists in the
// landed scope — the coexistence-equivalent of cstage's use_alias bit.
// Cite: project memory module_type_name_collision (cstage fix
// 2026-05-13). #11 (wwstage checkfile pass) revisits the dup-decl errors
// below when wwstage grows a real check pass on the cgen path.
// TODO(#11): cstage check.c errors on duplicate top-level type/def/fn
// (see cmd/wcc/check.c L1800/L1839/L1860 "duplicate <kind>") and on
// duplicate top-level let (cmd/wcc/check.c L1880, "duplicate let %s")
// once #32 lands. Wwstage's installdecl just drops the second insert
// silently. Add `if (s == nil) err(...)` here once #11 wires checkfile
// into w6c_ww. Silent-accept matches the deferred-check design — see
// test/wcc/708 and test/wcc/696 for the same cstage-only neg-case
// precedent.
fn installdecl(c: *checker, file: *node, d: *node) void = {
if (d == nil) { return; };
let k: nkind = d.kind;
let nm: str = d.str;
let mod: str = declmod(file, d);
if (k == nkind.N_USE) { scopedefine(c.top, nm, skind.SK_USE, nil, d); return; };
if (k == nkind.N_DEF) { scopedefineinmodule(c.top, nm, mod, skind.SK_DEF, nil, d); return; };
if (k == nkind.N_TYPEDECL) { scopedefineinmodule(c.top, nm, mod, skind.SK_TYPE, nil, d); return; };
if (k == nkind.N_FNDECL) { scopedefineinmodule(c.top, nm, mod, skind.SK_FN, nil, d); return; };
if (k == nkind.N_LET) { scopedefineinmodule(c.top, nm, mod, skind.SK_VAR, nil, d); return; };
};
// stamptuplebinds — distribute a tuple's per-element types onto a
// destructure binding chain, walked in lockstep with the resolved
// N_TTUPLE element chain (each `elems` link carries its element type on
// .lhs). Mirror of harec's create_unpack_bindings
// (ref/harec/src/check.c:1354-1419), which harec shares between
// let-unpack (check_expr_binding) and the for-each loop header
// (ref/harec/src/check.c:2308-2317) — the one shape behind ww's
// `let (a,b) = f()`, `for (let (a,b) .. s)`, and the ww-extension
// multi-assign `a, _ = f()`.
//
// `define` (the binding contexts: let-unpack + for-range) installs each
// named binder as a fresh SK_VAR and back-fills its declared type onto
// .lhs so use sites resolve through the N_IDENT exprtype path. Multi-
// assign targets are pre-declared lvalues, so it passes false: .lhs is
// left untouched (an N_INDEX/N_DOT target carries a live operand there)
// and only the type_ stamp fires on the still-untyped slots.
//
// Each binder/target node's own type_ is stamped from its element type so
// the asserttyped gate sees a typed node. This covers the discard `_` (an
// empty-str N_IDENT with no decl to read a type back from): harec drops
// `_` yet still advances the tuple slot, so that slot's element type is
// the honest type to stamp — `_` is UNBOUND, not UNTYPED.
fn stamptuplebinds(c: *checker, binds: *node, elems: *node,
define: bool, what: str) void = {
let b: *node = binds;
let pt: *node = elems;
for (b != nil) {
let et: *node = nil;
if (pt != nil) { et = pt.lhs; };
if (define) {
if (b.lhs == nil) { b.lhs = et; };
let bnm: str = b.str;
if (bnm.len > 0) {
checkmoduleshadow(c, bnm, what);
scopedefine(c.cur, bnm, skind.SK_VAR, nil, b);
};
};
if (b.type_ == nil) {
let src: *node = b.lhs;
if (src == nil) { src = et; };
if (src != nil) {
let ti: *tinfo = tinfofornode(c, src);
if (ti != nil) { b.type_ = ti: *void; };
};
};
b = b.next;
if (pt != nil) { pt = pt.next; };
};
};
// resolvewalk — recursive AST walk that, for every nkind.N_IDENT and
// nkind.N_TNAME seen, looks up the name and bumps the resolved/unresolved
// counters. Local lets are installed in the current scope as soon as
// their init/type expressions have been walked (forward use of a let
// before its declaration would resolve to nothing — same semantics as
// the C checker's collect-then-resolve flow within a function).
// Also runs the typed checks (match exhaustiveness, ? subset) in
// the same pass — they need the same scope state.
fn resolvewalk(c: *checker, n: *node) void = {
if (n == nil) { return; };
let k: nkind = n.kind;
// Typed checks fire on the way down so the scrutinee/operand
// is examined before the arm bodies install new bindings.
if (k == nkind.N_MATCH) { checkmatchexhaust(c, n); };
if (k == nkind.N_TRYPROP) { checktryprop(c, n); };
if (k == nkind.N_TRYUNW) { checktryprop(c, n); };
if (k == nkind.N_TYPETEST) { checkisas(c, n); };
if (k == nkind.N_TYPEASSERT) { checkisas(c, n); };
if (k == nkind.N_LET) { checkletassign(c, n); };
if (k == nkind.N_RETURN) { checkretassign(c, n); };
// `use IDENT;` — name is a module label, not a free ident.
if (k == nkind.N_USE) { return; };
if (k == nkind.N_IDENT) {
let nm: str = n.str;
if (nm.len > 0) {
let s: *sym = scopelookupprefer(c.cur, c.curmod, nm);
if (s == nil) {
// Unshadowed abort/assert binds no sym BY
// DESIGN (the EXPR_ASSERT family has no callee
// object — see isassertfam); count it resolved
// so wwdump -r's zero-unresolved gate holds
// over builtin-using lib code (#58 respell).
if (isassertfam(c, n)) {
c.nresolved += 1;
} else {
c.nunresolved += 1;
if (c.verbose != 0) {
cerr(" unresolved id: ");
cerr(nm);
cerr("\n");
};
};
} else { c.nresolved += 1; };
};
};
if (k == nkind.N_TNAME) {
let nm: str = n.str;
if (nm.len > 0) {
let s: *sym = scopelookupprefer(c.cur, c.curmod, nm);
// `pkg.Type` — strip the last dot prefix and look up
// the leaf with a mod filter so same-leaf-name types
// from different imports (`bufio.stream` vs
// `io.stream`) disambiguate to the right one.
// Mirrors cmd/wcc/check.c resolve_typename.
if (s == nil) {
let dot: i32 = nm.len - 1;
for (dot >= 0) {
if (nm[dot] == 46u8) { break; };
dot -= 1;
};
if (dot > 0) {
let head: str;
head.ptr = nm.ptr;
head.len = dot;
let m: *sym = scopelookup(c.cur, head);
if (m != nil) {
let leaf: str;
leaf.ptr = nm.ptr + (dot + 1): u64;
leaf.len = nm.len - (dot + 1);
s = scopelookupinmodule(c.cur, head, leaf);
};
};
};
if (s == nil) {
c.nunresolved += 1;
if (c.verbose != 0) {
cerr(" unresolved tname: ");
cerr(nm);
cerr("\n");
};
} else { c.nresolved += 1; };
};
};
// `for (let x .. slice) body` / `for (let (a, b) .. slice) body` —
// each binding name becomes a fresh local. Walk the slice expr first
// so its idents resolve before the bindings shadow anything, then
// install bindings and walk the body/else.
//
// TODO(#11): cstage check.c (post-#32) errors `binding '%s'
// redeclared in same scope` when the tuple-pattern lists the same
// name twice (`for (let (a, a) .. xs)`). Wwstage's resolvewalk has
// no per-block scope (see resolvefnbody's docstring) and is used
// only by wwdump_ww as a diagnostic, so silent-accept here avoids
// false-positives on legal cross-block shadow until #11 adds the
// scoping infrastructure.
if (k == nkind.N_FORRANGE) {
if (n.lhs != nil) { resolvewalk(c, n.lhs); };
if (n.list != nil) {
// Tuple destructure `for (let (a,b) .. xs)`: peel the
// iterable's element type and distribute its tuple
// element types onto the binders, the same lockstep walk
// harec runs for the for-each header
// (ref/harec/src/check.c:2308-2317 → create_unpack_bindings).
let elems: *node = nil;
let it: *node = exprtype(c, n.lhs, nil);
if (it != nil) {
let et: *node = nil;
if (it.kind == nkind.N_TSLICE) { et = it.lhs; };
if (it.kind == nkind.N_TARRAY) { et = it.lhs; };
if (et != nil) { if (et.kind == nkind.N_TTUPLE) {
elems = et.list;
}; };
};
stamptuplebinds(c, n.list, elems, true, "binding");
} else {
let bnm: str = n.str;
if (bnm.len > 0) {
checkmoduleshadow(c, bnm, "binding");
// C4 (task #7): bind the ELEMENT type so field
// reads off a by-value aggregate binding
// (`for (let t .. threads) { t.pc }`) resolve —
// pre-C4 the binding's decl was the N_FORRANGE
// node itself, whose .lhs is the SCRUTINEE expr,
// so exprtype's decl.lhs read handed the dot a
// non-type node and asserttyped bailed (cstage
// types it: check.c N_FORRANGE scope_define(...,
// elem, ...)). Synthetic N_LET binder whose .lhs
// is the element tnode — the stamptuplebinds
// `b.lhs = et` idiom. A str scrutinee keeps the
// old decl: cgen synthesises the u8 elem there
// and no dot applies to a u8 binding.
let et: *node = nil;
let it: *node = exprtype(c, n.lhs, nil);
// #80 (F2a batch-4 c4): an alias-typed iterable
// arrives as N_TNAME — without the AST-level
// dealias the binder fell to the N_FORRANGE
// fallback decl, stayed untyped, and any binop
// over the rangevar asserttyped-bailed (cs
// accepts: its scope_define types the elem).
if (it != nil) { it = resolvealias(c, unwrapbang(it)); };
if (it != nil) {
if (it.kind == nkind.N_TSLICE) { et = it.lhs; };
if (it.kind == nkind.N_TARRAY) { et = it.lhs; };
};
if (et != nil) {
let bn: *node = newnode(nkind.N_LET, n.file, n.line, n.col);
bn.str = bnm;
bn.lhs = et;
scopedefine(c.cur, bnm, skind.SK_VAR, nil, bn);
} else {
scopedefine(c.cur, bnm, skind.SK_VAR, nil, n);
};
};
};
if (n.body != nil) { resolvewalk(c, n.body); };
if (n.els != nil) { resolvewalk(c, n.els); };
return;
};
// `match (e) { case let v: T => stmt; ... }` — the binding `v`
// is declared by the case arm and visible inside its body. Push a
// fresh scope so `case let e: str` doesn't collide with an outer
// `let e: *T` (scopedefine drops same-scope dupes silently and
// would leave references to `e` resolving to the outer type).
// Mirrors cmd/wcc/check.c's newscope/saved-restore around cstmt.
if (k == nkind.N_MCASE) {
if (n.lhs != nil) { resolvewalk(c, n.lhs); };
let outer: *scope = c.cur;
c.cur = newscope(outer);
let nm: str = n.str;
if (nm.len > 0) {
checkmoduleshadow(c, nm, "binding");
scopedefine(c.cur, nm, skind.SK_VAR, nil, n);
};
if (n.body != nil) { resolvewalk(c, n.body); };
c.cur = outer;
return;
};
// #53: lexical block. Push a child scope so locals introduced by
// inner-block lets (and the `let` install at the tail of this fn) go
// out of scope at block exit. Without this, a deeply nested
// `let i: u64 = 0u64;` survived to shadow a same-named outer
// `let i: i32 = 1;` for the whole fn body, and exprtype handed
// stale primitive types to checkletassign — silent miscompile
// becomes a false-positive on the next driver (`wwdump_ww -r`
// flagged the u64→i32 pair in selfhost/cmd/ww/enumeratedir).
// Mirrors cstage cstmt N_BLOCK at cmd/wcc/check.c:1559-1566.
if (k == nkind.N_BLOCK) {
let outer: *scope = c.cur;
c.cur = newscope(outer);
let m: *node = n.list;
for (m != nil) {
resolvewalk(c, m);
m = m.next;
};
c.cur = outer;
return;
};
// `let (a, b) = call();` / `let a, b = call();` — destructure
// bindings. #121 (Package B, A-narrow): distribute the callee's
// tuple return-type element types onto the un-annotated bindings so
// later references stamp n.type_, matching cgen's structural binding-
// type classifier (cgmlet's rettupleof→localadd path). This closes
// the unstamped-float-destructure gap that the exprfloatkind collapse
// (commit 2's bridge) needs: without it a `let (f,i)=mk()` f64 binding
// reads stamp nil → would disagree with the structural f64.
//
// #6a-A: backfill off ANY call rhs, not just a bare N_IDENT callee, so
// a module-qualified `let (res, ov) = checked.addi64(a, b)` (N_DOT
// callee) stamps its bindings too. This is now a SINGLE path: just call
// exprtype(rhs) and consume the resolved N_TTUPLE — no callee resolution
// here. Harec's create_unpack_bindings does the same: ZERO callee
// resolution, it walks an already-typed tuple result (ref/harec/src/
// check.c:1354-1419). The module-qualified resolution that makes this
// correct for an N_DOT callee lives at the ROOT, in exprtype's N_CALL
// arm (the SK_USE-gated scopelookupinmodule there), so the binding just
// consumes. A D-class module whose leaf collides with a type/fn name
// resolves to nil/wrong-kind at the exprtype root (the SK_USE gate
// fails) → no N_TTUPLE → those destructures stay unstamped, a separate
// nominal-collision fold (#6a-D), not this one. Annotated bindings keep
// their own type. Mirrors the N_FORRANGE binding-install shape above;
// the bindings would otherwise install (unstamped) via the generic
// N_LET walk, so this early return must register them itself.
if (k == nkind.N_MLET) {
if (n.rhs != nil) { resolvewalk(c, n.rhs); };
let pt: *node = nil;
// #242: consume the rhs's tuple type for ANY rhs, not just an
// N_CALL — `let (a,b) = t` over a plain tuple ident (e.g. a
// match-bound union payload) must stamp its bindings too, or
// the un-annotated binder stays untyped and asserttyped aborts.
// Mirrors cstage check.c:2017 (cexpr(rhs), unconditional).
if (n.rhs != nil) {
// `rt` would shadow the imported lib/rt module
// (checkmoduleshadow errors); `rty` avoids it.
let rty: *node = exprtype(c, n.rhs, nil);
if (rty != nil) { if (rty.kind == nkind.N_TTUPLE) {
pt = rty.list;
}; };
};
stamptuplebinds(c, n.list, pt, true, "let");
return;
};
// `a, _ = call();` — tuple multi-assign (a retained ww extension over
// Hare; harec has no statement-position unpack-assign). Targets are
// pre-declared lvalues, resolved by the per-target resolvewalk below
// before the distribution; stamptuplebinds(define=false) only stamps
// still-nil slots, which is exactly the discard `_` (no decl, so the
// N_IDENT exprtype path leaves it untyped). Distribution mirrors the
// N_MLET/N_FORRANGE binders; see stamptuplebinds.
if (k == nkind.N_MASSIGN) {
if (n.rhs != nil) { resolvewalk(c, n.rhs); };
let pt: *node = nil;
// #242: consume the rhs tuple type for ANY rhs (see N_MLET).
if (n.rhs != nil) {
let rty: *node = exprtype(c, n.rhs, nil);
if (rty != nil) { if (rty.kind == nkind.N_TTUPLE) {
pt = rty.list;
}; };
};
let l: *node = n.list;
for (l != nil) { resolvewalk(c, l); l = l.next; };
stamptuplebinds(c, n.list, pt, false, "");
return;
};
if (k == nkind.N_DOT) {
// Walk only the base; the .field name is a member, not a
// free identifier.
if (n.lhs != nil) { resolvewalk(c, n.lhs); };
// A.6.0: branch returns early; stamp here so the post-walk
// dispatch below sees N_DOT covered. exprtype N_DOT arm is
// added in A.6.1; for now this is a no-op nil return.
let _t: *node = exprtype(c, n, nil);
return;
};
if (k == nkind.N_FIELD) {
if (n.lhs != nil) { resolvewalk(c, n.lhs); };
return;
};
if (k == nkind.N_TFIELD) {
if (n.lhs != nil) { resolvewalk(c, n.lhs); };
return;
};
// Walk children (mirroring ast.ww's printer descent order).
if (n.attr != nil) { resolvewalk(c, n.attr); };
if (n.lhs != nil) { resolvewalk(c, n.lhs); };
if (n.rhs != nil) { resolvewalk(c, n.rhs); };
if (n.cond != nil) { resolvewalk(c, n.cond); };
if (n.body != nil) { resolvewalk(c, n.body); };
if (n.els != nil) { resolvewalk(c, n.els); };
if (n.list != nil) {
let m: *node = n.list;
for (m != nil) {
resolvewalk(c, m);
m = m.next;
};
};
// #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; };
};
};
if (k == nkind.N_TENUM) { stampenumvals(c, n); };
// #42's size/align/offset fold trigger lived here pre-A.6.0; the
// A.6.0 end-of-fn general dispatch (below) now fires exprtype on
// every N_CALL — same context-free coverage, one dispatch site.
// After walking children: a local `let X: T = init;` registers
// `X` so subsequent statements can resolve it. Top-level lets
// are installed in installdecl, so this duplicate install at
// the file scope just no-ops (scopedefine returns nil on dup).
//
// Cross-block `let a; { let a; };` no longer trips dup-silence
// since #53 added N_BLOCK push/pop above — the inner `a` lands in
// the inner block's scope. Same-scope dup `let a=1; let a=2;`
// still silent-accepts here; promoting that to an error stays
// queued behind #11 (test/wcc/708 + test/wcc/696 are the cstage-
// only neg-case precedent).
if (k == nkind.N_LET) {
let nm: str = n.str;
if (nm.len > 0) {
checkmoduleshadow(c, nm, "let");
scopedefine(c.cur, nm, skind.SK_VAR, nil, n);
};
};
// #104 fold-2: narrow a bare f32-context float literal AFTER the
// child walk above — the post-order exprtype dispatch (below) re-
// stamps a bare N_FLOATLIT back to untyped_float, so coercing earlier
// (e.g. in checkletassign) would be undone. Placed here, the f32
// stamp on n.rhs / n.lhs sticks; cgen's fold-1 narrow then fires. let
// / return only — see coercefloatlit's docstring for the rule-10 scope
// (the cstage twin coerces in clet / cstmt N_RETURN). c.fnret is set
// by resolvefnbody for the enclosing fn, mirroring checkretassign.
if (k == nkind.N_LET) { coercefloatlit(c, n.rhs, n.lhs); };
if (k == nkind.N_RETURN) { coercefloatlit(c, n.lhs, c.fnret); };
// A.6.0: post-order dispatch of exprtype on every expression-yielding
// node kind so n.type_ stamps fire universally — not only when reached
// through checkletassign / checkretassign / checktryprop / the size-
// align-offset fold. Mirrors cstage cmd/wcc/check.c cstmt's recursive
// cexpr (cmd/wcc/check.c:1567 N_EXPRSTMT, :1570 N_RETURN, :1584 N_IF
// cond, etc.). Plumbing-only: stamps fire from existing exprtype kind
// arms (literals + idents); per-kind stamp coverage lands in A.6.1.
// Stamps are tinfocache-backed idempotent so multi-walk via let /
// return / try entry points is safe. N_DOT is dispatched in its own
// early-return branch above; not listed here. N_LET / N_RETURN /
// N_EXPRSTMT / N_IF / N_FOR / N_FORRANGE / N_BLOCK / N_MATCH-as-stmt
// are not value-typed nodes; their expression children get stamped on
// the recursive descent into them. Type-expression kinds (N_T*) are
// covered separately by the tinfofornode block above.
// #258: desugar an array arg/rhs into an implicit full slice at the
// call-arg and assignment contexts (let / return drive their own
// desugar in checkletassign / checkretassign). Placed post-child-walk
// so arg/operand types are stamped, and before the end-dispatch
// exprtype below so a regular N_CALL is still an N_CALL (not folded to
// an N_INTLIT by the size/align intercept). Mirrors cstage's post-
// order cexpr desugar at the call-arg / N_ASSIGN sites.
if (k == nkind.N_CALL) { desugarcallargs(c, n); };
if (k == nkind.N_ASSIGN) { checkassign(c, n); };
if (k == nkind.N_INTLIT || k == nkind.N_FLOATLIT ||
k == nkind.N_STRLIT || k == nkind.N_RUNELIT ||
k == nkind.N_TRUE || k == nkind.N_FALSE ||
k == nkind.N_NIL || k == nkind.N_VOIDLIT ||
k == nkind.N_IDENT || k == nkind.N_BIN ||
k == nkind.N_UN || k == nkind.N_CALL ||
k == nkind.N_INDEX || k == nkind.N_CAST ||
k == nkind.N_STRUCTLIT || k == nkind.N_ARRLIT ||
k == nkind.N_RECV ||
k == nkind.N_SLICE || k == nkind.N_SPREAD ||
k == nkind.N_TUPLE || k == nkind.N_TRYPROP ||
k == nkind.N_TRYUNW || k == nkind.N_TYPETEST ||
k == nkind.N_TYPEASSERT || k == nkind.N_YIELD ||
k == nkind.N_MATCH) {
let _t: *node = exprtype(c, n, nil);
};
};
// ---- type-level helpers (AST-level, no resolved tinfo) --------------
//
// The selfhost check operates on AST type expressions rather than
// resolved Type structs. These helpers mirror what cmd/wcc/check.c
// does with tinfo, but only on the subset of cases this checker
// needs to enforce: tagged-union exhaustiveness, ? subset
// propagation, and !-flag semantics.
// unwrapbang — strip an nkind.N_TBANG wrapper; leaves other nodes alone.
fn unwrapbang(n: *node) *node = {
if (n == nil) { return nil; };
if (n.kind == nkind.N_TBANG) { return n.lhs; };
return n;
};
// aliassym — resolve a single nkind.N_TNAME to its IMMEDIATE type
// symbol (one level, no chain walk). Returns nil for non-TNAME nodes,
// unresolvable names, or non-SK_TYPE bindings. #64 factors the lookup
// out of resolvealias so tinfofornode's TY_NAMED build can reach the
// decl sym (nominal identity = sym.type_ ptr-identity) instead of
// flattening to the underlying. Mirrors cstage resolve_typename
// (cmd/wcc/check.c:60-88), which returns the sym's NAMED, not the base.
fn aliassym(c: *checker, n: *node) *sym = {
if (n == nil) { return nil; };
if (n.kind != nkind.N_TNAME) { return nil; };
let nm: str = n.str;
// #51: pkg.alias type refs land here as a single TNAME whose
// str is the joined form (lib/ww/parse/parse.ww:258-265 in
// parsetype). Split on the rightmost '.' and bind the leaf in
// the head module's scope. Mirrors cstage resolve_typename
// cmd/wcc/check.c:74-83 strrchr branch — without this the
// raw `os.oserror` lookup misses and checkisas false-positives
// every cross-module tagged scrutinee.
let dotidx: i32 = -1;
let i: i32 = 0;
for (i < nm.len) {
if (nm[i] == 46u8) { dotidx = i; };
i += 1;
};
let s: *sym = nil;
if (dotidx >= 0) {
let head: str;
head.ptr = nm.ptr;
head.len = dotidx;
let leaf: str;
leaf.ptr = nm.ptr + ((dotidx + 1): u64);
leaf.len = nm.len - dotidx - 1;
s = scopelookupinmodule(c.cur, head, leaf);
} else {
// #53: same-module preference. Mirrors cstage
// cmd/wcc/check.c:66 scope_lookup_prefer. Without this,
// two modules each declaring `type invalid = ...` collide
// on the head-first bucket walk: e.g. utf8.invalid `!void`
// vs strconv.invalid `!i32` resolves to whichever
// registered first, driving localloadop MOVSXD/MOVQ
// divergence at 994/995. Other bare-leaf callers in this
// file (L1597 exprtype N_IDENT, L1795/L2720 N_DOT-callee
// leaf, L600 varianterr, L647 scruttype) tracked as #55.
s = scopelookupprefer(c.cur, c.curmod, nm);
// #61 A.5: bare TNAME that collides with an imported
// module bareword. Two shapes hit this:
// - `let l: lex;` where `lex` struct lives in
// `package lex;` (mod matches leaf).
// - `let t: tok;` where `tok` struct lives in
// `package lex;` (mod differs from leaf — tok.ww
// declares `package lex;`).
// scopelookup bucket-walks the flat scope and can land
// on the SK_USE entry first; without the fallback we'd
// return the unresolved TNAME and tinfofornode aborts on
// body == n. scopelookuptype walks the same bucket but
// filters on SK_TYPE so the struct entry surfaces
// regardless of its declaring package. Mirrors the
// bare-vs-qualified pattern from task #57.
if (s != nil) {
if (s.skind != skind.SK_TYPE) {
let sm: *sym = scopelookuptype(c.cur, nm);
if (sm != nil) { s = sm; };
};
};
};
if (s == nil) { return nil; };
if (s.skind != skind.SK_TYPE) { return nil; };
return s;
};
// resolvealias — if n is an nkind.N_TNAME pointing at a typedecl, return
// the typedecl's body (possibly recursively). Pass-through for any
// other node. The chain stops once we hit a non-nkind.N_TNAME node or a
// name we can't resolve.
fn resolvealias(c: *checker, n: *node) *node = {
let cur: *node = n;
for (cur != nil) {
if (cur.kind != nkind.N_TNAME) { return cur; };
let s: *sym = aliassym(c, cur);
if (s == nil) { return cur; };
let body: *node = nil;
if (s.decl != nil) { body = s.decl.lhs; };
if (body == nil) { return cur; };
cur = unwrapbang(body);
};
return n;
};
// typeeqast — structural equality on AST type expressions, mod
// the `!` wrapper. Mirrors variant_match in cgen + check.c: NAMED
// types compare by string (the closest stand-in for pointer
// identity at the AST level); other nodes recurse by kind.
fn typeeqast(a: *node, b: *node) bool = {
let aa: *node = unwrapbang(a);
let bb: *node = unwrapbang(b);
if (aa == nil) { return bb == nil; };
if (bb == nil) { return false; };
if (aa.kind != bb.kind) { return false; };
let k: nkind = aa.kind;
if (k == nkind.N_TNAME) { return streq(aa.str, bb.str); };
if (k == nkind.N_TPTR) { return typeeqast(aa.lhs, bb.lhs); };
if (k == nkind.N_TSLICE){ return typeeqast(aa.lhs, bb.lhs); };
if (k == nkind.N_TCHAN) { return typeeqast(aa.lhs, bb.lhs); };
if (k == nkind.N_TFN) {
// Divergence: cstage type.c:239 compares resolved Type; we
// compare AST. See #178.
if (!typeeqast(aa.lhs, bb.lhs)) { return false; };
let pa: *node = aa.list;
let pb: *node = bb.list;
for (pa != nil) {
if (pb == nil) { return false; };
let ac: bool = streq(pa.str, "...");
let bc: bool = streq(pb.str, "...");
if (ac != bc) { return false; };
if (!ac) {
let va: bool = pa.op == tkind.TK_ELLIPSIS;
let vb: bool = pb.op == tkind.TK_ELLIPSIS;
if (va != vb) { return false; };
if (!typeeqast(pa.lhs, pb.lhs)) { return false; };
};
pa = pa.next;
pb = pb.next;
};
return pb == nil;
};
// #206: tuple structural equality — mirror of cstage type.c:261-268
// (TY_TUPLE). Needed since a tuple-RETURN fn pointer compares its
// N_TFN return node (aa.lhs) here; without it `*fn(x)(a,b)` never
// proves structurally equal to itself, so the #206 punt-tightening
// would confidently reject a bare-&fn into a structural `*fn(...)`
// slot (test 766 fn_tuple_return). Elements are N_TPARAM-wrapped
// (parse.ww:302-318), so compare each link's .lhs.
if (k == nkind.N_TTUPLE) {
let pa: *node = aa.list;
let pb: *node = bb.list;
for (pa != nil) {
if (pb == nil) { return false; };
if (!typeeqast(pa.lhs, pb.lhs)) { return false; };
pa = pa.next;
pb = pb.next;
};
return pb == nil;
};
// #47 gap-B: tagged structural equality — mirror of cstage
// type.c:288-300 (TY_TAGGED). A tuple member with a TAGGED element
// (e.g. ((void|size),(void|size),size)) recurses here from the
// N_TTUPLE arm; without it the per-element compare falls to the
// catch-all and the whole case-against-tagged-scrutinee is rejected.
// Variants are DIRECT .list nodes (casevariantin walks tagged.list
// + typeeqast(v,..) directly), NOT N_TPARAM-wrapped like tuple elems.
// Divergence: cstage's nullable-flag check (type.c:293) is a resolved-
// Type property with no AST analogue; for case-match both sides share
// a spelling so it's moot — no phantom AST nullable check.
if (k == nkind.N_TTAGGED) {
let pa: *node = aa.list;
let pb: *node = bb.list;
for (pa != nil) {
if (pb == nil) { return false; };
// #115: a `...inner` spread variant stays unflattened at
// this AST layer (casevariantin flattens only at the OUTER
// level; cstage flattens in resolve_type before type_eq ever
// runs). A position-by-position compare cannot honour it —
// the N_TNAME leg is pure streq, so `...ab` would silently
// match a plain `ab`, accepting a case cstage rejects.
// Conservatively loud-reject any spread until the flatten
// lands; b1c's (void|size) has none, so byte-id is untouched.
if (pa.op == tkind.TK_ELLIPSIS) { return false; };
if (pb.op == tkind.TK_ELLIPSIS) { return false; };
if (!typeeqast(pa, pb)) { return false; };
pa = pa.next;
pb = pb.next;
};
return pb == nil;
};
// Conservative: anything else (struct/array) fails the cheap
// check. Selfhost code doesn't currently rely on equality at
// these shapes for the targeted checks.
return false;
};
// varianterr — does this variant carry the `!` mark? Either
// the variant itself is nkind.N_TBANG or it's an alias whose typedecl
// body is `!T`. Mirrors C check.c's iserror-after-NAMED rule.
fn varianterr(c: *checker, v: *node) bool = {
if (v == nil) { return false; };
if (v.kind == nkind.N_TBANG) { return true; };
if (v.kind == nkind.N_TNAME) {
let s: *sym = scopelookup(c.cur, v.str);
if (s != nil) {
if (s.skind == skind.SK_TYPE) {
if (s.decl != nil) {
if (s.decl.lhs != nil) {
if (s.decl.lhs.kind == nkind.N_TBANG) {
return true;
};
};
};
};
};
};
return false;
};
// taggedhaserr — true iff any variant of `n` (assumed
// nkind.N_TTAGGED) is `!`-marked. Picks the explicit-flag semantics over
// the legacy "first variant = success" rule.
fn taggedhaserr(c: *checker, n: *node) bool = {
let v: *node = n.list;
for (v != nil) {
if (varianterr(c, v)) { return true; };
v = v.next;
};
return false;
};
// iserrvariant — under flag-aware mode (any !-marked variant),
// returns true iff `v` is `!`-marked. Under legacy mode (no flags),
// returns true iff `v` is not the first variant of `tagged`.
fn iserrvariant(c: *checker, tagged: *node, v: *node) bool = {
if (taggedhaserr(c, tagged)) {
return varianterr(c, v);
};
// Legacy: first variant of the union is success.
if (tagged.list == v) { return false; };
return true;
};
// scruttype — resolve the type expression for a match's
// scrutinee. Handles nkind.N_IDENT (look up local/param's declared
// type) and nkind.N_DOT (module-qualified ref). Returns nil if we
// can't statically determine the type. Used by exhaustiveness.
fn scruttype(c: *checker, e: *node) *node = {
if (e == nil) { return nil; };
if (e.kind == nkind.N_IDENT) {
let s: *sym = scopelookup(c.cur, e.str);
if (s == nil) { return nil; };
if (s.decl == nil) { return nil; };
// For nkind.N_LET / nkind.N_PARAM: declared type is decl.lhs.
return s.decl.lhs;
};
// #51: `match (pkg.var)` / `pkg.var is T` — module-qualified ref.
// lhs is N_IDENT (module bareword), str is the leaf. Bind via
// scopelookupinmodule so the declared type carries the same
// shape resolvealias' dotted-name branch now consumes. Falls
// silently to nil when lhs is a value (struct-field access) —
// the rest of the lenient-check contract.
if (e.kind == nkind.N_DOT) {
if (e.lhs == nil) { return nil; };
if (e.lhs.kind != nkind.N_IDENT) { return nil; };
let s: *sym = scopelookupinmodule(c.cur, e.lhs.str, e.str);
if (s == nil) { return nil; };
if (s.decl == nil) { return nil; };
return s.decl.lhs;
};
return nil;
};
// mktname — fabricate an nkind.N_TNAME node with str = `nm`. Used by
// exprtype to return primitive type nodes for literal
// expressions. The arena keeps them around as long as the checker.
fn mktname(c: *checker, nm: str) *node = {
let n: *node = newnode(nkind.N_TNAME, "", 0, 0);
n.str = nm;
return n;
};
// #43: SSoT for primitive type byte sizes. astsize's N_TNAME-primitive
// arm and every wwstage cgen size walker (slotsize/fieldsize/letemit-
// size/elemsizeof/paramfieldsize) consult this table so a future
// ty_str.size bump (#1) lands in one place. Returns -1 for non-prim
// names; callers fall back to alias/struct/enum lookup. Cstage's
// equivalent SSoT is cmd/wcc/type.c:46-79 (ty_void/ty_bool/.../ty_str).
fn primtypesize(nm: str) i64 = {
if (streq(nm, "void")) { return 0i64; };
if (streq(nm, "bool")) { return 1i64; };
if (streq(nm, "i8") || streq(nm, "u8")) { return 1i64; };
if (streq(nm, "i16") || streq(nm, "u16")) { return 2i64; };
if (streq(nm, "i32") || streq(nm, "u32") || streq(nm, "f32") || streq(nm, "rune")) { return 4i64; };
if (streq(nm, "i64") || streq(nm, "u64") || streq(nm, "f64")) { return 8i64; };
if (streq(nm, "int") || streq(nm, "uint") || streq(nm, "uintptr") || streq(nm, "size")) { return 8i64; };
// str IS []u8: 24B, sourced from the slice header SSoT so str and
// []u8 can never drift; no second hardcoded 24 (#1/Phase 3).
if (streq(nm, "str")) { return tyslicesize(); };
return -1i64;
};
// #43: SSoT for slice header size (ptr+len+cap = 24B today). Mirrors
// cstage cmd/wcc/type.c:103 (ty_slice->size = 24). Bumping a slice's
// header layout in #34 touches only this constant.
fn tyslicesize() i64 = { return 24i64; }; // sizelint-ok: SSoT for ty_slice header (#64)
// #42: AST-level layout helpers for the size(T)/align(T)/offset(e.f)
// fold. Mirror cstage resolve_type's size/align computation
// (cmd/wcc/check.c:286-528) on AST nodes — wwstage check.ww never
// materialises tinfo for user types so the fold has to walk the AST
// directly. Struct layout follows cstage check.c:471-526 (align each
// field, max align for the whole record, round size up to alignment).
fn astalign(c: *checker, t: *node) i64 = {
if (t == nil) { return 1i64; };
let k: nkind = t.kind;
if (k == nkind.N_TBANG) { return astalign(c, t.lhs); };
if (k == nkind.N_TPTR) { return 8i64; };
if (k == nkind.N_TSLICE) { return 8i64; };
if (k == nkind.N_TCHAN) { return 8i64; };
if (k == nkind.N_TFN) { return 8i64; };
if (k == nkind.N_TARRAY) { return astalign(c, t.lhs); };
if (k == nkind.N_TTAGGED) { return 8i64; };
if (k == nkind.N_TTUPLE) {
let m: i64 = 1i64;
let p: *node = t.list;
for (p != nil) {
let pa: i64 = astalign(c, p.lhs);
if (pa > m) { m = pa; };
p = p.next;
};
return m;
};
if (k == nkind.N_TSTRUCT) {
let m: i64 = 1i64;
let f: *node = t.list;
for (f != nil) {
if (f.kind == nkind.N_TFIELD) {
let fa: i64 = astalign(c, f.lhs);
if (fa > m) { m = fa; };
};
f = f.next;
};
return m;
};
if (k == nkind.N_TENUM) {
if (t.lhs != nil) { return astalign(c, t.lhs); };
return 4i64;
};
if (k == nkind.N_TNAME) {
let nm: str = t.str;
if (streq(nm, "void") || streq(nm, "bool") || streq(nm, "i8") || streq(nm, "u8")) { return 1i64; };
if (streq(nm, "i16") || streq(nm, "u16")) { return 2i64; };
if (streq(nm, "i32") || streq(nm, "u32") || streq(nm, "f32") || streq(nm, "rune")) { return 4i64; };
if (streq(nm, "i64") || streq(nm, "u64") || streq(nm, "f64") || streq(nm, "int") || streq(nm, "uint") || streq(nm, "uintptr") || streq(nm, "size") || streq(nm, "str")) { return 8i64; };
let resolved: *node = resolvealias(c, t);
if (resolved != nil && resolved != t) {
return astalign(c, resolved);
};
};
return 1i64;
};
fn astsize(c: *checker, t: *node) i64 = {
if (t == nil) { return 0i64; };
let k: nkind = t.kind;
if (k == nkind.N_TBANG) { return astsize(c, t.lhs); };
if (k == nkind.N_TPTR) { return 8i64; };
if (k == nkind.N_TSLICE) { return tyslicesize(); };
if (k == nkind.N_TCHAN) { return 8i64; };
if (k == nkind.N_TFN) { return 8i64; };
if (k == nkind.N_TARRAY) {
let elen: i64 = 0i64;
if (t.rhs != nil) {
if (t.rhs.kind == nkind.N_INTLIT) { elen = t.rhs.uval: i64; };
};
return astsize(c, t.lhs) * elen;
};
if (k == nkind.N_TTUPLE) {
// Route through the type table, NOT a packed element-sum:
// slot layout is the tuple SSoT (C-t0, user-ratified) and
// tupleelemslot is its one wwstage answer. The packed walk
// this replaces was a C-t0 escape — size((u32,u32)) folded
// to 8 here while cstage (check.c N_TTUPLE) and the wwstage
// type table both said 16 (task #22 commit 0).
let ti: *tinfo = tinfofornode(c, t);
if (ti != nil) { return ti.size: i64; };
return 0i64;
};
if (k == nkind.N_TSTRUCT) {
let off: i64 = 0i64;
let maxal: i64 = 1i64;
let f: *node = t.list;
for (f != nil) {
if (f.kind == nkind.N_TFIELD) {
let fa: i64 = astalign(c, f.lhs);
if (fa > maxal) { maxal = fa; };
off = (off + fa - 1i64) & ~(fa - 1i64);
off += astsize(c, f.lhs);
};
f = f.next;
};
return (off + maxal - 1i64) & ~(maxal - 1i64);
};
if (k == nkind.N_TTAGGED) {
// 8 (tag) + max variant payload, rounded up to 8.
let maxsz: i64 = 0i64;
let v: *node = t.list;
for (v != nil) {
let sz: i64 = astsize(c, v);
if (sz > maxsz) { maxsz = sz; };
v = v.next;
};
let pad: i64 = (maxsz + 7i64) & ~7i64;
return 8i64 + pad;
};
if (k == nkind.N_TENUM) {
if (t.lhs != nil) { return astsize(c, t.lhs); };
return 4i64;
};
if (k == nkind.N_TNAME) {
let nm: str = t.str;
let ps: i64 = primtypesize(nm);
if (ps >= 0i64) { return ps; };
let resolved: *node = resolvealias(c, t);
if (resolved != nil && resolved != t) {
return astsize(c, resolved);
};
};
return 0i64;
};
// astunsized — #108(b): true iff `t` contains an unsized component. A
// type is unsized iff it is the abstract `opaque` (size/align ==
// SIZE_UNDEFINED) OR an aggregate (array / struct / tuple / tagged)
// with a recursively-unsized member. The wwstage has NO type-decl
// construction guards (those are cstage-only, rule-10), so its size()/
// align() FOLD must detect every opaque-containing type itself — a
// leaf-only check would silently fold size([4]opaque) / size(struct{x:
// opaque}) / size((opaque, i32)) to garbage (rule 7). Does NOT peel
// TPTR/TSLICE/TCHAN/TFN — `*opaque` (8B) and `[]opaque` (24B header)
// are sized and legal behind indirection. Cstage twin: the leaf
// `m == SIZE_UNDEFINED` size/align guard PLUS the per-construction
// require_sized guards that reject unsized aggregates at the type decl
// (so the cstage size/align fold only ever sees a leaf opaque); harec
// ref/harec/src/check.c:2720, type_store.c:1147 (tuple) / :449 (tagged).
fn astunsized(c: *checker, t: *node) bool = {
if (t == nil) { return false; };
let u: *node = resolvealias(c, unwrapbang(t));
if (u == nil) { return false; };
let k: nkind = u.kind;
if (k == nkind.N_TNAME) {
if (streq(u.str, "opaque")) { return true; };
return false;
};
if (k == nkind.N_TARRAY) { return astunsized(c, u.lhs); };
if (k == nkind.N_TTUPLE) {
let p: *node = u.list;
for (p != nil) {
if (astunsized(c, p.lhs)) { return true; };
p = p.next;
};
return false;
};
if (k == nkind.N_TSTRUCT) {
let f: *node = u.list;
for (f != nil) {
if (f.kind == nkind.N_TFIELD) {
if (astunsized(c, f.lhs)) { return true; };
};
f = f.next;
};
return false;
};
if (k == nkind.N_TTAGGED) {
let v: *node = u.list;
for (v != nil) {
if (astunsized(c, v)) { return true; };
v = v.next;
};
return false;
};
return false;
};
// matchyieldtype — port of cstage cmd/wcc/check.c:110-135. Walks a
// match arm body for the first `yield expr;` and returns its operand
// type as a resolved *tinfo (the match-as-expression's type). Returns
// nil if no yield is reachable from `body`. Doesn't descend into a
// nested N_MATCH — each match opens its own yield scope. bname/btype
// carry the enclosing arm's case-binding name + declared type node.
//
// #264: returns a *tinfo (not a type *node) because the post-walk call
// READS the operand's cached node.type_ (a tinfo), mirroring cstage's
// match_yield_type which reads body->lhs->type (cmd/wcc/check.c:121-122).
// The two callsites differ on whether the operand is stamped yet:
// - POST-walk (exprtype N_MATCH post-order, resolvewalk L631): the
// in-scope N_MCASE arm walk (L411) has already stamped the operand,
// so we read body.lhs.type_ directly — NO exprtype re-run. Re-running
// exprtype out of the (popped) arm scope returned nil and the
// N_UN/N_BIN/N_INDEX restamp arms overwrote the good deref/element
// stamp with nil -> asserttyped:un/bin/index. Reading cached avoids
// the re-derive entirely, so the clobber cannot occur by construction.
// - PRE-walk (checkletassign L302 / checkretassign L303 run exprtype
// on the match rhs BEFORE the L533 in-scope descent): the operand is
// nil here, so we re-derive via exprtype — benign (nil->nil no-op on
// the unstamped operand) AND load-bearing: it types the void-arm
// literal (`yield -1`) so the let/return-assign has a usable type
// node for isassignable. cstage is single-pass (no pre-walk call), so
// its match_yield_type has no such branch; eliminating this pre-walk
// call is #279. `nodeout` carries that re-derived type NODE back to
// the consumer for the pre-walk assignability check (isassignable is
// node-based); it stays nil on the post-walk cached read, where the
// consumer's *node return is discarded (no nested match-as-subexpr
// consumes it — only checkletassign/checkretassign at the pre-walk
// call use it). The #241 `yield <binder>` fallback (the dominant
// match-bind-then-yield idiom, Hare's parseint `case let t => yield t`)
// stays, now resolving btype to a tinfo.
fn matchyieldtype(c: *checker, body: *node, bname: str, btype: *node,
nodeout: **node) *tinfo = {
if (body == nil) { return nil; };
let k: nkind = body.kind;
if (k == nkind.N_YIELD) {
if (body.lhs == nil) { return nil; };
if (body.lhs.type_ != nil) {
// For the bare-binder idiom `yield <binder>`, ALSO surface
// btype as the *node: the tuple-destructure consumers
// (N_MLET/N_MASSIGN at resolvewalk L482/L503) read
// exprtype(N_MATCH)'s *node return as an N_TTUPLE to
// distribute onto `let (a,b) = match(x){ case let t => yield
// t }` (test 945 match_yield — the only tuple-destructure-of-
// match idiom in tree, grep-confirmed). btype is the binder's
// declared type node, which IS the match's type here; the
// cached tinfo returned below equals tinfofornode(btype).
if (body.lhs.kind == nkind.N_IDENT && bname.len > 0
&& streq(body.lhs.str, bname)) {
*nodeout = btype;
};
return body.lhs.type_: *tinfo;
};
let t: *node = exprtype(c, body.lhs, nil);
if (t != nil) {
*nodeout = t;
return tinfofornode(c, t);
};
if (body.lhs.kind == nkind.N_IDENT && bname.len > 0
&& streq(body.lhs.str, bname)) {
*nodeout = btype;
return tinfofornode(c, btype);
};
return nil;
};
if (k == nkind.N_MATCH) { return nil; };
if (k == nkind.N_BLOCK) {
let s: *node = body.list;
for (s != nil) {
let t: *tinfo = matchyieldtype(c, s, bname, btype, nodeout);
if (t != nil) { return t; };
s = s.next;
};
return nil;
};
if (k == nkind.N_IF) {
let t: *tinfo = matchyieldtype(c, body.body, bname, btype, nodeout);
if (t != nil) { return t; };
return matchyieldtype(c, body.els, bname, btype, nodeout);
};
if (k == nkind.N_FOR || k == nkind.N_FORRANGE) {
return matchyieldtype(c, body.body, bname, btype, nodeout);
};
return nil;
};
// astoffset — byte offset of `dot.str` inside the struct type of
// `dot.lhs`. Mirrors cstage cmd/wcc/check.c:932-961: peel one N_TPTR
// (for `p.field` where p is *Struct), require N_TSTRUCT, walk fields
// honouring per-field alignment, return -1 if the field name is
// absent so the caller can flag the error and fold to 0.
fn astoffset(c: *checker, dot: *node) i64 = {
if (dot == nil) { return -1i64; };
if (dot.kind != nkind.N_DOT) { return -1i64; };
let recv: *node = scruttype(c, dot.lhs);
if (recv == nil) { return -1i64; };
let rtyp: *node = resolvealias(c, unwrapbang(recv));
if (rtyp == nil) { return -1i64; };
if (rtyp.kind == nkind.N_TPTR) {
rtyp = resolvealias(c, unwrapbang(rtyp.lhs));
};
if (rtyp == nil) { return -1i64; };
if (rtyp.kind != nkind.N_TSTRUCT) { return -1i64; };
let off: i64 = 0i64;
let f: *node = rtyp.list;
for (f != nil) {
if (f.kind == nkind.N_TFIELD) {
let fa: i64 = astalign(c, f.lhs);
off = (off + fa - 1i64) & ~(fa - 1i64);
if (streq(f.str, dot.str)) { return off; };
off += astsize(c, f.lhs);
};
f = f.next;
};
return -1i64;
};
// arenau64tos — decimal string for the folded INTLIT's `str` field.
// Cstage uses aprintf("%llu") at the same site (cmd/wcc/check.c:921);
// wwstage cgen only reads `uval` for N_INTLIT codegen so `str` is
// just for the AST printer, but set it for parity with the parser's
// own literal-emit shape.
fn arenau64tos(v: u64) str = {
let buf: []u8 = alloc([], 24u64)!;
let i: i32 = 23;
buf[i] = 0u8;
if (v == 0u64) { i -= 1; buf[i] = 48u8; };
let n: u64 = v;
for (n > 0u64) {
i -= 1;
buf[i] = (48u64 + (n % 10u64)): u8;
n /= 10u64;
};
let r: str;
r.ptr = buf.ptr + (i: u64);
r.len = 23 - i;
return r;
};
// foldtointlit — mutate `n` in place to an N_INTLIT with value `v`.
// Used by the #42 size/align/offset intercepts so cgen sees the
// folded literal rather than an unresolved call. Mirrors cstage
// cmd/wcc/check.c:919-927 / :951-958.
fn foldtointlit(c: *checker, n: *node, v: i64) void = {
n.kind = nkind.N_INTLIT;
n.uval = v: u64;
n.str = arenau64tos(v: u64);
n.lhs = nil;
n.list = nil;
let empty: str;
n.tsuffix = empty;
};
// foldbinop — shared constant binary-op core for the two compile-time
// integer evaluators in this file: enumvalfold (enum member exprs) and
// evaldefconst (top-level def rhs, #88). One op table so the cstage
// (cmd/wcc/check.c fold_binop) and wwstage stamp the bit-identical
// literal — rule 10 lives at the check pass for #88. Returns false on
// division by zero or an op outside the constant subset; the caller
// maps that to its own diagnostic.
fn foldbinop(op: tkind, a: u64, b: u64, out: *u64) bool = {
if (op == tkind.TK_PLUS) { *out = a + b; return true; };
if (op == tkind.TK_MINUS) { *out = a - b; return true; };
if (op == tkind.TK_STAR) { *out = a * b; return true; };
if (op == tkind.TK_SLASH) {
if (b == 0u64) { return false; };
*out = a / b; return true;
};
if (op == tkind.TK_PERCENT) {
if (b == 0u64) { return false; };
*out = a % b; return true;
};
if (op == tkind.TK_AMP) { *out = a & b; return true; };
if (op == tkind.TK_PIPE) { *out = a | b; return true; };
if (op == tkind.TK_CARET) { *out = a ^ b; return true; };
if (op == tkind.TK_LSHIFT) { *out = a << b; return true; };
if (op == tkind.TK_RSHIFT) { *out = a >> b; return true; };
return false;
};
// deffolderr — loud diagnostic + checker error count bump for an
// unfoldable def rhs (cycle / narrowing-cast / bad op). c.errs > 0
// gates cgen off in main.ww:165, so this fails the build rather than
// emitting a missing DATA row silently (rule 7). cstage twin: err()
// in cmd/wcc/check.c.
fn deffolderr(c: *checker, n: *node, msg: str) void = {
cerr(n.file);
cerr(": error: ");
cerr(msg);
cerr("\n");
c.errs += 1;
};
// defcastfits — wwstage twin of cstage def_cast_fits (cmd/wcc/check.c):
// does the folded u64 `v` survive narrowing to integer target `t`?
// Identity / widening / same-width casts always fit; a genuine
// narrowing cast whose value falls outside the target range must NOT
// be silently truncated (rule 7 / drew). Width via the type table
// (t.size, rule 13); the 8s are CHAR_BIT and the u64 byte-width, not
// type-layout sizes, so they sit outside rule 13's scope. Pure-u64 so
// the range check is bit-identical to cstage (rule 10).
fn defcastfits(t: *tinfo, v: u64) bool = {
if (!typeisint(t)) { return true; }; // non-int target: keep value
let w: u64 = t.size;
if (w >= 8u64) { return true; }; // 64-bit target: no narrowing
let bits: u64 = w * 8u64;
if (typeisunsigned(t)) { return (v >> bits) == 0u64; };
// signed: truncate to `bits` then sign-extend; fits iff unchanged
let mask: u64 = (1u64 << bits) - 1u64;
let sign: u64 = 1u64 << (bits - 1u64);
let ext: u64 = ((v & mask) ^ sign) - sign;
return ext == v;
};
// evaldefconst — fold a top-level def's rhs to a u64 constant,
// resolving sibling and imported def references, casts, and
// arithmetic (#88). Reuses foldintliteral (leaf/unary) + foldbinop
// (arith); the ONLY thing it does that enumvalfold doesn't is resolve
// an identifier through the checker's flat scope (scopelookupprefer
// for a bare sibling ref, scopelookupinmodule for `mod.NAME`) to the
// referent def's own rhs, then recurse.
//
// Why this stays distinct from enumvalfold rather than a full merge
// (rule 8 WHY): enum-member eval carries implicit prev+1 auto-increment
// and forward-only sibling lookup over the member chain; def eval has
// neither — it resolves through the scope/decl graph, which references
// forward and across modules. The two lookup models don't reconcile
// cleanly, so they share the arith core (foldbinop) + leaf fold
// (foldintliteral) and keep separate top-level shapes.
//
// `depth` bounds a def->def->def chain; a cycle (def A = B; def B = A,
// incl. cross-module) hits the cap and fails loud rather than hanging
// (rule 7), mirroring the cgen nsteps>=16 abort precedent.
fn evaldefconst(c: *checker, n: *node, out: *u64, depth: i32) bool = {
if (n == nil) { return false; };
if (depth >= 16) {
deffolderr(c, n, "def value: reference chain too deep (cycle?)");
return false;
};
if (foldintliteral(n, out)) { return true; };
let k: nkind = n.kind;
if (k == nkind.N_BIN) {
let a: u64 = 0u64;
let b: u64 = 0u64;
if (!evaldefconst(c, n.lhs, &a, depth + 1)) { return false; };
if (!evaldefconst(c, n.rhs, &b, depth + 1)) { return false; };
if (foldbinop(n.op, a, b, out)) { return true; };
if ((n.op == tkind.TK_SLASH || n.op == tkind.TK_PERCENT) && b == 0u64) {
deffolderr(c, n, "def value: division by zero");
} else {
deffolderr(c, n, "def value: unsupported binary op");
};
return false;
};
if (k == nkind.N_UN) {
// foldintliteral already covers unary-over-leaf; this arm
// catches unary over a resolved ref, e.g. `-A`.
let v: u64 = 0u64;
if (!evaldefconst(c, n.lhs, &v, depth + 1)) { return false; };
if (n.op == tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; };
if (n.op == tkind.TK_TILDE) { *out = ~v; return true; };
if (n.op == tkind.TK_PLUS) { *out = v; return true; };
deffolderr(c, n, "def value: unsupported unary op");
return false;
};
if (k == nkind.N_CAST) {
// n.lhs = value; n.type_ = resolved target (stamped by
// exprtype's N_CAST arm during resolvewalk). Strip the cast
// keeping the value; a narrowing cast that loses it fails loud.
let v: u64 = 0u64;
if (!evaldefconst(c, n.lhs, &v, depth + 1)) { return false; };
let t: *tinfo = (n.type_): *tinfo;
if (!defcastfits(t, v)) {
deffolderr(c, n, "def value: narrowing cast loses value");
return false;
};
*out = v;
return true;
};
if (k == nkind.N_IDENT) {
let s: *sym = scopelookupprefer(c.cur, c.curmod, n.str);
if (s == nil) { return false; };
if (s.skind != skind.SK_DEF) { return false; };
if (s.decl == nil) { return false; };
if (s.decl.rhs == nil) { return false; };
return evaldefconst(c, s.decl.rhs, out, depth + 1);
};
if (k == nkind.N_DOT) {
if (n.lhs == nil) { return false; };
if (n.lhs.kind != nkind.N_IDENT) { return false; };
let s: *sym = scopelookupinmodule(c.cur, n.lhs.str, n.str);
if (s == nil) { return false; };
if (s.skind != skind.SK_DEF) { return false; };
if (s.decl == nil) { return false; };
if (s.decl.rhs == nil) { return false; };
return evaldefconst(c, s.decl.rhs, out, depth + 1);
};
return false;
};
// stampintlit — rewrite a const-folded def rhs in place to its literal
// value, preserving the node's resolved type_ so the DATA-row emit
// width and pass-3 asserttyped see a properly-typed literal leaf. Lets
// cgen's existing emitdefconstants lay down the row with no codegen
// change (#88). Shape mirrors foldtointlit (the #42 stamp).
fn stampintlit(n: *node, v: u64) void = {
n.kind = nkind.N_INTLIT;
n.uval = v;
n.op = tkind.TK_NONE;
n.str = arenau64tos(v);
n.lhs = nil;
n.rhs = nil;
n.cond = nil;
n.body = nil;
n.els = nil;
n.list = nil;
let empty: str;
n.tsuffix = empty;
// n.type_ left intact (the type exprtype inferred for the rhs).
};
// enumvalfold — fold an enum member's value expression to a u64
// constant. The Hare-fidelity set: literal leaves, unary +/-/~,
// binary arithmetic (+ - * / %), bitwise (& | ^), shifts (<< >>),
// and sibling backref. Mirrors cstage cmd/wcc/check.c:185-208
// (fold_int_literal) + :210-284 (eval_enum_value); the wider
// constexpr evaluator is at ref/harec/src/eval.c (harec resolves
// each enum member via eval_expr per ref/harec/src/check.c:4419-
// 4434). Wwstage cgen.ww:158-227 (foldintliteral + enumevalmember)
// already ships this set for codegen — check now matches.
//
// `body` is the N_TENUM whose .list is the member chain. `until`
// is the member currently being resolved; sibling lookup walks
// forward from body.list and stops at `until` to enforce harec's
// lnext forward-only-ref discipline (ref/harec/src/check.c:4436-
// 4438). `e` starts as that member's lhs and recurses into its
// children. Returns false on unfoldable shape, unknown sibling,
// or division by zero — callers bail the wrapping N_DOT fold.
//
// Recursion bound: O(N²) worst case on chained sibling backrefs
// (each ident lookup re-walks 0..until). Enum bodies are tiny in
// practice — harec accepts the same shape without memoisation per
// resolve_enum_field's wrap_resolver chain
// (ref/harec/src/check.c:4438) — so the quadratic is harmless.
fn enumvalfold(body: *node, until: *node, e: *node, out: *u64) bool = {
if (e == nil) { return false; };
let k: nkind = e.kind;
if (k == nkind.N_INTLIT) { *out = e.uval; return true; };
if (k == nkind.N_RUNELIT) { *out = e.uval; return true; };
if (k == nkind.N_TRUE) { *out = 1u64; return true; };
if (k == nkind.N_FALSE) { *out = 0u64; return true; };
if (k == nkind.N_NIL) { *out = 0u64; return true; };
if (k == nkind.N_UN) {
let v: u64 = 0u64;
if (!enumvalfold(body, until, e.lhs, &v)) { return false; };
let op: tkind = e.op;
if (op == tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; };
if (op == tkind.TK_TILDE) { *out = ~v; return true; };
if (op == tkind.TK_PLUS) { *out = v; return true; };
return false;
};
if (k == nkind.N_BIN) {
let a: u64 = 0u64;
let b: u64 = 0u64;
if (!enumvalfold(body, until, e.lhs, &a)) { return false; };
if (!enumvalfold(body, until, e.rhs, &b)) { return false; };
return foldbinop(e.op, a, b, out);
};
if (k == nkind.N_IDENT) {
let prev: u64 = (-1i64): u64;
let m: *node = body.list;
for (m != nil && m != until) {
let val: u64 = 0u64;
if (m.lhs == nil) {
val = prev + 1u64;
} else {
if (!enumvalfold(body, m, m.lhs, &val)) { return false; };
};
prev = val;
if (streq(m.str, e.str)) { *out = val; return true; };
m = m.next;
};
return false;
};
return false;
};
// stampenumvals — give every node in each enum-member value-expr a
// non-nil type_. resolvewalk's post-order exprtype (L543) stamps the
// literal leaves, but a sibling backref (`B = A + 4`) resolves to
// nothing — enum members aren't installed as scope idents — so the
// backref N_IDENT and the N_BIN/N_UN wrapping it stay nil. asserttyped
// walks the enum DEFINITION (whether or not a member is `.`-accessed)
// and its value-node invariant then fires on those. harec checks each
// member's value-expr at the enum's underlying type
// (ref/harec/src/check.c:4419 — check_expression with type->alias.type),
// so the whole constant subtree carries the underlying integer type;
// mirror that. The value itself is folded to a constant at every use
// site (enumvalfold) and at codegen (cgen.ww enumevalmember), so cgen
// never reads these node types — this stamp is checker metadata only.
fn stampenumvals(c: *checker, n: *node) void = {
let under: *tinfo = c.tc.tyi32;
if (n.lhs != nil) {
let s: *tinfo = tinfofornode(c, n.lhs);
if (s != nil) { under = s; };
};
let m: *node = n.list;
for (m != nil) {
stampnilexpr(m.lhs, under);
m = m.next;
};
};
// stampnilexpr — stamp nil-typed nodes in a constant expr subtree to
// `ti`. lhs/rhs cover the enum constexpr grammar enumvalfold accepts
// (literals, unary, binary, sibling backref); non-nil nodes keep the
// type exprtype already derived.
fn stampnilexpr(n: *node, ti: *tinfo) void = {
if (n == nil) { return; };
if (n.type_ == nil) { n.type_ = ti: *void; };
stampnilexpr(n.lhs, ti);
stampnilexpr(n.rhs, ti);
};
// #61 A.5 helper: per-element slot size when `pt` appears inside a
// tuple. Mirrors cgenutil.ww slotsize TTUPLE — cstage's tuple ABI
// spills each element into its own register / 8B eightbyte, so narrow
// scalars pad to 8 (cgen's let_emit_size + AX:DX:CX positional layout).
// str/slice and composites consult `pt.size` so a future #1 bump on
// any primitive layout propagates through the typ.ww SSoT seed
// instead of getting baked into this detour. pointer/fn/chan stay
// 8; void contributes 0 (never appears in tuples emitted by user
// code, but kept for SSoT symmetry with cgen's N_TNAME-"void"
// fallback arm).
fn tupleelemslot(pt: *tinfo) u64 = {
if (pt == nil) { return 8u64; };
// #63 Phase-N step 1: peel TY_NAMED before this structural query.
// #64 builds per-decl NAMED wrappers (tinfofornode), so the peel
// now fires on aliased operands; byte-id holds because it collapses
// NAMED to the alias-invariant underlying this read consumes.
let t: *tinfo = pt;
t = tichase(t);
if (t == nil) { return 8u64; };
let pk: tykind = t.kind;
if (pk == tykind.TY_VOID) { return 0u64; };
if (pk == tykind.TY_STR) { return t.size; };
if (pk == tykind.TY_SLICE) { return t.size; };
// #22 (user-ratified 2026-06-04): slot = roundup8(size(elem)) — 8B
// is a FLOOR, not a ceiling. (str,str)=48B predates this; tagged
// was the one truncated >8B kind (the #237 fieldslotsize-missing-
// TY_TUPLE precedent: fieldslotsize below already carried this
// arm). Cstage twin: check.c N_TTUPLE; cgen accessor: tuple_eslot
// / tupeslot.
if (pk == tykind.TY_TAGGED) { return (t.size + 7u64) & ~7u64; };
if (pk == tykind.TY_PTR || pk == tykind.TY_FN ||
pk == tykind.TY_CHAN || pk == tykind.TY_I64 ||
pk == tykind.TY_U64 || pk == tykind.TY_INT ||
pk == tykind.TY_UINT || pk == tykind.TY_UINTPTR ||
pk == tykind.TY_SIZE || pk == tykind.TY_F64) { return 8u64; };
if (pk == tykind.TY_BOOL || pk == tykind.TY_RUNE ||
pk == tykind.TY_I8 || pk == tykind.TY_I16 ||
pk == tykind.TY_I32 || pk == tykind.TY_U8 ||
pk == tykind.TY_U16 || pk == tykind.TY_U32 ||
pk == tykind.TY_F32 || pk == tykind.TY_ENUM) { return 8u64; };
// Composite — one 8B eightbyte, NOT t.slotsize: cgen's cursor
// transport strides `wide ? size : 8` at every tuple site (both
// stages), so a composite element rides one register word today.
// The checker mirrors what cgen emits (tuple arc C-t0; cstage
// check.c N_TTUPLE twin) — a slotsize answer here would re-open the
// checker-vs-cgen layout split the slot-SSoT ruling closed. No
// composite-element tuple exists in the corpus; transport for >8B
// composites is its own unwired gap.
return 8u64;
};
// #61 A.5 helper: per-field slot size mirroring cgenutil.ww
// registerstruct/fieldsize. Nested struct fields contribute their
// slot-padded total (si.totsize equivalent); primitives keep their
// natural width (struct interior packing is unaffected by stack-slot
// pad-to-8); arrays use their slot-padded element-stride * elen.
fn fieldslotsize(ft: *tinfo) u64 = {
if (ft == nil) { return 8u64; };
// #63 Phase-N step 1: peel TY_NAMED before this structural query.
// #64 builds per-decl NAMED wrappers (tinfofornode), so the peel
// now fires on aliased operands; byte-id holds because it collapses
// NAMED to the alias-invariant underlying this read consumes.
let t: *tinfo = ft;
t = tichase(t);
if (t == nil) { return 8u64; };
let fk: tykind = t.kind;
if (fk == tykind.TY_STRUCT) { return t.slotsize; };
if (fk == tykind.TY_ARRAY) { return t.slotsize; };
if (fk == tykind.TY_TAGGED) { return t.size; };
// str / slice read t.size so the typ.ww SSoT seed is the single
// source for #1 (str→24) / #34 (slice graduation) — no hardcoded
// literal here to drift.
if (fk == tykind.TY_SLICE) { return t.size; };
if (fk == tykind.TY_PTR || fk == tykind.TY_FN ||
fk == tykind.TY_CHAN) { return 8u64; };
if (fk == tykind.TY_STR) { return t.size; };
// #237: a tuple-typed struct field carries its own slot total (the
// per-element slot sum stamped at the N_TTUPLE arm above — slices at
// 24 each). Without this it fell to the 8B default below, undersizing
// the enclosing struct's slotsize (size stayed correct), so a `let s:S`
// slot was too small — a silent stack-corrupting miscompile. Aligns
// with cgenutil.ww fieldsize, which already returns the tuple's size.
if (fk == tykind.TY_TUPLE) { return t.slotsize; };
// Primitives keep natural width inside structs (matches
// cgenutil fieldsize: primsize, not pad-to-8).
if (fk == tykind.TY_BOOL || fk == tykind.TY_RUNE ||
fk == tykind.TY_I8 || fk == tykind.TY_I16 ||
fk == tykind.TY_I32 || fk == tykind.TY_I64 ||
fk == tykind.TY_U8 || fk == tykind.TY_U16 ||
fk == tykind.TY_U32 || fk == tykind.TY_U64 ||
fk == tykind.TY_INT || fk == tykind.TY_UINT ||
fk == tykind.TY_UINTPTR || fk == tykind.TY_SIZE ||
fk == tykind.TY_F32 || fk == tykind.TY_F64 ||
fk == tykind.TY_ENUM) { return t.size; };
return 8u64;
};
// #61 audit §1.8 — resolve a type-expression AST node to its *tinfo.
// Mirrors cstage's resolve_type (cmd/wcc/check.c:286-565) which
// produces ty_* singletons / arena-allocated composites from a Node*.
// 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.
//
// 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);
if (cached != nil) { return cached; };
let r: *tinfo = nil;
let k: nkind = n.kind;
switch (k) {
case 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, "size")) { r = c.tc.tysize; }; // #85 fold-2
if (streq(nm, "opaque")) { r = c.tc.tyopaque; }; // #108(a)
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) {
// #64 Phase-N step 2 (THE FLIP): build a per-decl
// TY_NAMED wrapper instead of collapsing the alias to
// its underlying. sym.type_ caches the wrapper so every
// TNAME resolving to the same decl yields the SAME tinfo
// pointer — ptr-identity IS nominal identity (the whole
// point; typeeq is the only consumer, wired in step 3).
// CHAINS, not flatten: under is the IMMEDIATE body's
// tinfo, so `type a = b` gives NAMED(a).under = NAMED(b)
// — mirrors cstage's two-phase type_named
// (cmd/wcc/check.c:1900-1929): pass1 creates the NAMED,
// pass2 patches under/size off resolve_type(d->lhs),
// where resolve_typename returns the inner NAMED.
let s: *sym = aliassym(c, n);
if (s != nil) {
if (s.type_ != nil) {
r = s.type_;
} else {
let body: *node = nil;
if (s.decl != nil) {
body = unwrapbang(s.decl.lhs);
};
if (body != nil) {
// PRE-BIND before resolving under: a
// self-referential field (`type node =
// struct {next: *node}`) re-finds this
// NAMED via sym.type_ instead of re-
// entering the chain. Mirrors cstage
// pass1's sym->type install
// (check.c:1909/1917) ahead of pass2's
// under patch, and A.2's TSTRUCT/TFN/
// TTAGGED tinfocachebind cycle-break.
let named: *tinfo = typenamed(s.name, nil);
s.type_ = named;
named.resolving = 1;
let under: *tinfo = tinfofornode(c, body);
// #62/#69: alias-root cycle (`type a = b;
// type b = a` / `type a = a`) — checked
// BEFORE clearing the flag so self-aliases
// trip on their own mark. tyerr instead of
// the cyclic under keeps the table ACYCLIC
// by construction: every NAMED-chain chase
// loop stays terminating. Mirrors cstage
// resolve_typedecl.
if (circularnamed(c, under, n)) {
under = c.tc.tyerr;
};
named.resolving = 0;
// peellint-ok: construction — the one
// WRITE that builds the NAMED link;
// not a peel, can't route via tichase.
named.under = under;
if (under != nil) {
named.size = under.size;
named.align = under.align;
named.slotsize = under.slotsize;
};
r = named;
};
};
};
};
case 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);
case nkind.N_TPTR:
r = typeptr(tinfofornode(c, n.lhs));
case nkind.N_TSLICE:
r = typeslice(tinfofornode(c, n.lhs));
case nkind.N_TCHAN:
r = typechan(tinfofornode(c, n.lhs));
case 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).
//
// #61 A.5: ti.size = natural (sub.size * elen), ti.slotsize =
// slot-padded (sub.slotsize * elen) — typearray handles both.
// Reverts A.4's r.size override (which conflated stride with
// natural size); the slot-padded stride now lives in slotsize
// where cgenutil's fast-path reads it.
let elen: u64 = 0u64;
if (n.rhs != nil) {
if (n.rhs.kind == nkind.N_INTLIT) { elen = n.rhs.uval; };
};
let sub: *tinfo = tinfofornode(c, n.lhs);
// #62/#69: `type a = [2]a` value cycle — loud, cstage twin.
if (circularnamed(c, sub, n)) { sub = c.tc.tyerr; };
r = typearray(sub, elen);
case 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(tykind.TY_FN);
r.size = 8u64;
r.align = 8u64;
r.slotsize = 8u64;
tinfocachebind(c.tc, n, r);
r.ret = tinfofornode(c, n.lhs);
case 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(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;
r.slotsize = storage.size;
case nkind.N_TTUPLE:
// Slot layout is the tuple SSoT (tuple arc C-t0, user-ratified):
// ti.size = ti.slotsize = per-element slot sum (tupleelemslot —
// a str/slice its header, everything else one 8B eightbyte),
// the stride cgen's cursor transport actually writes. ww-
// internal ABI only (tuples never cross extern);
// size((u32,u32))=16 is observable via size() and diverges from
// Hare (harec type_store.c:533-580 anonymous-struct rule) AND
// from ww's own structs (which pack narrow fields post-
// fldloadop) — that internal inconsistency is what task #60
// eventually fixes; re-open before any serialization/FFI/
// density use. Pre-C-t0 ti.size was the packed raw sum while
// cgen strode 8B slots — the checker-says-8/cgen-does-16 split
// behind the packed-tuple miscompile family (#32/#33/#48).
// Pre-bind for cycle protection (recursive tuple shapes).
r = newtype(tykind.TY_TUPLE);
tinfocachebind(c.tc, n, r);
// #57 A.6.3i-phase-1: populate r.tupleelems as a ttupleelem
// linked list (head=positional 0) in lock-step with the
// size/align accumulator. Harec analog ref/harec/src/type_
// store.c:532-589 tuple_init_from_atype — {type, offset, next}
// per member onto type->tuple.next chain. Diverges from cstage
// cmd/wcc/check.c N_TTUPLE which stores tuple positionals on
// t->params (Tparam, no offset, consumer recomputes by
// walking); the offset-stored shape lets consumers
// (dotchainresolve) read offsets directly per the A.6
// stamp-once-read-many arc. Direct analog 26724fe (#50 phase 1,
// A.6.3f-a) for the head/tail append pattern. Offsets are
// slot-cumulative (C-t0), distinct from harec's
// add_padding(&offset, memb.align) at type_store.c:561.
let teh: *ttupleelem = nil;
let tet: *ttupleelem = nil;
let slottotal: u64 = 0u64;
let maxal: u64 = 1u64;
let p: *node = n.list;
for (p != nil) {
let pt: *tinfo = tinfofornode(c, p.lhs);
// #62/#69: tuple-member value cycle — loud, cstage twin.
if (circularnamed(c, pt, p.lhs)) { pt = c.tc.tyerr; };
let te: *ttupleelem = alloc(ttupleelem{type_=pt, offset=slottotal, tnext=nil})!;
if (teh == nil) { teh = te; } else { tet.tnext = te; };
tet = te;
if (pt != nil) {
if (pt.align > maxal) { maxal = pt.align; };
slottotal += tupleelemslot(pt);
};
p = p.next;
};
r.tupleelems = teh;
r.size = slottotal;
r.align = maxal;
r.slotsize = slottotal;
case 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.
//
// #61 A.5: alongside the natural layout (cstage parity), walk
// the same fields with the slot-padded sizing cgenutil.ww
// registerstruct uses (fieldsize → si.totsize for nested
// struct; size-derived alignment; final round to 8). That
// slot total lands in ti.slotsize so the cgen fast-path can
// graduate TY_STRUCT off the AST walker.
r = newtype(tykind.TY_STRUCT);
tinfocachebind(c.tc, n, r);
// #57 A.6.3i-phase-1: populate r.fields as a tfield linked
// list (head=first declared field) in lock-step with the
// natural-layout offset accumulator. Mirrors cstage cmd/wcc/
// check.c:468-527 (Tfield {name, type, offset, next} per
// member onto t->fields). Direct analog 26724fe (#50 phase 1,
// A.6.3f-a) for the head/tail append pattern. Harec cite:
// ref/harec/include/types.h:109-115 struct_field and
// ref/harec/src/type_store.c:314-347 struct_init_from_atype.
// Anonymous-embed promotion not populated here (#13 per
// the cstage cite at check.ww:1263).
let fh: *tfield = nil;
let ft_: *tfield = nil;
let off: u64 = 0u64;
let maxalign: u64 = 1u64;
let soff: u64 = 0u64;
let f: *node = n.list;
for (f != nil) {
if (f.kind == nkind.N_TFIELD) {
let ft: *tinfo = tinfofornode(c, f.lhs);
// #62/#69: struct-field value cycle (`type s1 =
// struct { x: s2 }; type s2 = struct { x: s1 }`)
// — loud; pre-#62 this stack-overflowed the slot
// walkers. cstage twin.
if (circularnamed(c, ft, f)) { ft = c.tc.tyerr; };
if (ft != nil) {
if (ft.align > maxalign) { maxalign = ft.align; };
if (ft.align > 0u64) {
off = (off + ft.align - 1u64) & ~(ft.align - 1u64);
};
let fldoff: u64 = off;
let tf: *tfield = alloc(tfield{name=f.str, type_=ft, offset=fldoff, tnext=nil})!;
if (fh == nil) { fh = tf; } else { ft_.tnext = tf; };
ft_ = tf;
off += ft.size;
// Slot-padded layout (mirror of cgenutil
// fieldsize + registerstruct align rules).
let fsz: u64 = fieldslotsize(ft);
let faln: u64 = 1u64;
if (fsz >= 8u64) { faln = 8u64; }
else { if (fsz >= 4u64) { faln = 4u64; }
else { if (fsz >= 2u64) { faln = 2u64; }; }; };
if ((soff & (faln - 1u64)) != 0u64) {
soff = (soff + faln - 1u64) & ~(faln - 1u64);
};
soff += fsz;
};
};
f = f.next;
};
r.fields = fh;
if (maxalign > 0u64) {
r.size = (off + maxalign - 1u64) & ~(maxalign - 1u64);
};
r.align = maxalign;
if ((soff & 7u64) != 0u64) {
soff = (soff + 7u64) & ~7u64;
};
r.slotsize = soff;
case nkind.N_TTAGGED:
// Cstage cmd/wcc/check.c:347-435: 8B tag + max(variant)
// rounded up to 8. Pre-bind for cycle protection (recursive
// sum-type shapes through NAMED variants).
r = newtype(tykind.TY_TAGGED);
tinfocachebind(c.tc, n, r);
// #50 / A.6.3f phase 1: populate ti.params as a tparam linked
// list (head=first source variant). #61a: flatten `...inner`
// tagged spreads into the chain and stamp each variant's
// iserror. Mirrors cstage check.c:366-389 — dealias one NAMED
// level, require TY_TAGGED, splice its (already-flattened)
// variants in declaration order; otherwise append the single
// variant. size/align stays accounted off the surface member
// (vt), so r.size is byte-identical to pre-#61a: the flatten +
// iserror are additive, with no #61a-stage readers (the variant
// machinery + cgwidentaggedstore/matchscrutt migrate onto the
// chain in #61b/c). Same shared Tparam shape ww reuses across
// struct-fields / tuple-fields / fn-params / tagged-variants
// (sea-of-stars per rule 12). Phase 2 (#50b) retired cgenutil's
// AST-keyed nullableptrtag onto the chain.
let head: *tparam = nil;
let tail: *tparam = nil;
let maxsz: u64 = 0u64;
let al: u64 = 8u64;
let v: *node = n.list;
for (v != nil) {
let vt: *tinfo = tinfofornode(c, v);
// #62/#69: union-member value cycle — loud, cstage twin.
if (circularnamed(c, vt, v)) { vt = c.tc.tyerr; };
let isspread: bool = (v.op == tkind.TK_ELLIPSIS);
let vu: *tinfo = vt;
// Full chase (F2a batch-4 c3-B1): the single peel left a
// 2-level-alias inner union a SURFACE member — the box
// sized off the inner union's own header, not its
// spliced variants (cs twin check.c:755 chases).
if (isspread) { vu = tichase(vu); };
if (isspread && vu != nil && vu.kind == tykind.TY_TAGGED) {
// #209: a `...inner` spread's PAYLOAD is its
// members, not the whole inner union — size/align
// off each spliced member (cstage check.c:660-667
// st->size), NOT the surface member vt (which would
// over-size by the inner union's own 8B tag word and
// desync the `field` slot from cstage's). Spliced
// variants carry the inner union's already-stamped
// iserror; no re-derivation.
let src: *tparam = vu.params;
for (src != nil) {
let st: *tinfo = src.type_;
if (st != nil) {
if (st.size > maxsz) { maxsz = st.size; };
if (st.align > al) { al = st.align; };
};
let tp: *tparam = alloc(tparam{name="", type_=src.type_, iserror=src.iserror, tnext=nil})!;
if (head == nil) { head = tp; } else { tail.tnext = tp; };
tail = tp;
src = src.tnext;
};
} else {
if (vt != nil) {
if (vt.size > maxsz) { maxsz = vt.size; };
if (vt.align > al) { al = vt.align; };
};
let ve: bool = varianterr(c, v);
let tp: *tparam = alloc(tparam{name="", type_=vt, iserror=ve, tnext=nil})!;
if (head == nil) { head = tp; } else { tail.tnext = tp; };
tail = tp;
};
v = v.next;
};
r.params = head;
// #61 A.3 nullable fold: `(*T | void)` collapses to a single
// 8B pointer slot, null is the void variant. Mirrors
// cmd/wcc/check.c:412-426 — bare TNAME("void"), not `!void`,
// and not NAMED. AST-kind discrimination retained: wwstage
// tinfo carries no `iserror` field, so cstage's tinfo-level
// (kind==TY_VOID && !iserror) check doesn't port symmetrically.
let a: *node = n.list;
if (a != nil) {
let b: *node = a.next;
if (b != nil && b.next == nil) {
let aptr: bool = (a.kind == nkind.N_TPTR);
let bptr: bool = (b.kind == nkind.N_TPTR);
let avoid: bool = (a.kind == nkind.N_TNAME);
if (avoid) { avoid = streq(a.str, "void"); };
let bvoid: bool = (b.kind == nkind.N_TNAME);
if (bvoid) { bvoid = streq(b.str, "void"); };
let isnull: bool = false;
if (aptr) { if (bvoid) { isnull = true; }; };
if (avoid) { if (bptr) { isnull = true; }; };
if (isnull) {
r.size = 8u64;
r.align = 8u64;
r.nullable = 1;
r.slotsize = 8u64;
return r;
};
};
};
let pad: u64 = (maxsz + 7u64) & ~7u64;
r.size = 8u64 + pad;
r.align = al;
r.slotsize = 8u64 + pad;
};
if (r != nil) {
// #61 A.5: any arm that didn't set slotsize gets ti.size as
// the default (covers primitives via prim() + the ptr/slice/
// chan paths which already populate slotsize, plus TBANG which
// inherits the inner's tinfo unchanged).
if (r.slotsize == 0u64) { r.slotsize = r.size; };
tinfocachebind(c.tc, n, r);
};
return r;
};
// unifyarith — usual-arithmetic-conversion analogue at the AST-tnode
// layer. Mirrors cstage cmd/wcc/check.c:580-596 `unify_arith` and harec
// ref/harec/src/types.c type_promote. Trailing `return ltn` covers
// mismatched typed pairs; cstage flags the same shape — wwstage's
// checker stays silent here per existing discipline.
//
// Nil-on-valid classification (5-lite-b #34, A.6.2.1c #24): both ltn
// and rtn can be nil when an operand was an inherent-IDENT bail
// (exprtype N_IDENT arm L1596-1599 — SK_USE module ref or pseudo-
// builtin callee with sym.decl == nil; #19 retires these as
// dedicated AST kinds). Propagation, not silent gap — asserttyped
// gates those idents at the consumer layer.
fn unifyarith(c: *checker, ltn: *node, rtn: *node) *node = {
let lu: bool = isuntypedint(ltn) || isuntypedfloat(ltn);
let ru: bool = isuntypedint(rtn) || isuntypedfloat(rtn);
if (lu && ru) {
if (isuntypedfloat(ltn) || isuntypedfloat(rtn)) {
return mktname(c, "untyped_float");
};
return mktname(c, "untyped_int");
};
let conf: bool = false;
if (lu) {
if (isassignable(c, rtn, ltn, &conf)) { return rtn; };
};
if (ru) {
if (isassignable(c, ltn, rtn, &conf)) { return ltn; };
};
if (typeeqast(ltn, rtn)) { return ltn; };
// Mismatched typed pair — return ltn so the binop stamps something;
// 5-lite-b: the trailing nil-on-mismatch shape was eliminated when
// the helper was split out of binoptype.
return ltn;
};
// coercefloatlit — twin of cstage cmd/wcc/check.c coerce_floatlit (see there
// for the full rationale + the rule-10 scope note). Stamp an un-suffixed
// float literal (whose type_ is the untyped_float singleton) as f32 when the
// target type resolves to f32, so fold-1's cgen narrow (isf32type, cgenexpr.
// ww) fires off the now-f32 node.type_. SCOPED to a DIRECT untyped_float
// N_FLOATLIT at let-init / return only: the wwstage cgen's exprfloatkind
// (cgenutil.ww) hardcodes N_FLOATLIT -> f64 and cgbin / the unary negate pick
// f32 off the operands' float-kind, not the node stamp, so a stamped literal
// inside an arith-binop / behind a unary minus does NOT narrow there —
// binop / unary-minus / assign / call-arg / struct-field wait on #120.
fn coercefloatlit(c: *checker, e: *node, target: *node) void = {
if (e == nil) { return; };
if (target == nil) { return; };
let tu: *node = resolvealias(c, unwrapbang(target));
if (tu == nil) { return; };
if (tu.kind != nkind.N_TNAME) { return; };
if (!streq(tu.str, "f32")) { return; };
if (e.kind == nkind.N_FLOATLIT) {
if ((e.type_: *tinfo) == c.tc.tyuntypedfloat) {
let f32t: *node = mktname(c, "f32");
e.type_ = tinfofornode(c, f32t): *void;
};
};
};
// binoptype — derive the result tnode of an N_BIN operator expression.
// Mirrors cstage cmd/wcc/check.c:598-640 `cbinop`. Operates on tnodes
// returned by exprtype; ptr arithmetic / bitwise / shifts / comparisons /
// logicals all reflect cstage's rules. Type-mismatch diagnostics are
// elided here (cstage gates the same shape).
fn binoptype(c: *checker, e: *node) *node = {
let op: tkind = e.op;
let ltn: *node = exprtype(c, e.lhs, nil);
let rtn: *node = exprtype(c, e.rhs, nil);
if (op == tkind.TK_PLUS || op == tkind.TK_MINUS) {
if (ltn != nil && ltn.kind == nkind.N_TPTR && isinttypeast(rtn)) {
return ltn;
};
};
if (op == tkind.TK_PLUS) {
if (isinttypeast(ltn) && rtn != nil && rtn.kind == nkind.N_TPTR) {
return rtn;
};
};
if (op == tkind.TK_MINUS) {
if (ltn != nil && rtn != nil
&& ltn.kind == nkind.N_TPTR && rtn.kind == nkind.N_TPTR) {
return mktname(c, "i64");
};
};
if (op == tkind.TK_PLUS || op == tkind.TK_MINUS ||
op == tkind.TK_STAR || op == tkind.TK_SLASH ||
op == tkind.TK_PERCENT || op == tkind.TK_AMP ||
op == tkind.TK_PIPE || op == tkind.TK_CARET ||
op == tkind.TK_LSHIFT || op == tkind.TK_RSHIFT) {
return unifyarith(c, ltn, rtn);
};
if (op == tkind.TK_EQ || op == tkind.TK_NEQ ||
op == tkind.TK_LT || op == tkind.TK_LE ||
op == tkind.TK_GT || op == tkind.TK_GE) {
// cstage routes every comparison through unify_arith (check.c:952/
// 955 cbinop), which loud-rejects an error-typed operand paired
// with a differing type, e.g. strconv.invalid != i32. wwstage's
// unifyarith stays silent on generic typed mismatches (5-lite-b),
// but the error-type case is a real cs!=ww edge and must reject to
// match cstage (#246, rule 10). Scoped to error operands so the
// broad differing-types flip (typeeqast vs cstage type_eq
// asymmetry risk) stays out.
if (varianterr(c, ltn) || varianterr(c, rtn)) {
if (!typeeqast(ltn, rtn)) {
deffolderr(c, e, "operands have differing types");
};
};
return mktname(c, "bool");
};
if (op == tkind.TK_AND || op == tkind.TK_OR) {
return mktname(c, "bool");
};
// Unreachable for valid input: op is one of TK_PLUS/MINUS/STAR/
// SLASH/PERCENT/AMP/PIPE/CARET/LSHIFT/RSHIFT/EQ/NEQ/LT/LE/GT/GE/
// AND/OR per parser invariant (lib/ww/parse/expr.ww binary-op
// table); all are handled above. 5-lite-b #34.
return nil;
};
// unoptype — derive the result tnode of an N_UN unary expression. Mirrors
// cstage cmd/wcc/check.c:642-687 `cunop` and harec ref/harec/src/types.c
// type_promote. The slice/str .len/.cap pseudo-field address-of widening
// to *i64 mirrors check.c:672-682 directly — its purpose is documented
// at the cstage site.
fn unoptype(c: *checker, e: *node) *node = {
let op: tkind = e.op;
let opt: *node = exprtype(c, e.lhs, nil);
if (op == tkind.TK_MINUS || op == tkind.TK_PLUS) { return opt; };
if (op == tkind.TK_NOT) { return mktname(c, "bool"); };
if (op == tkind.TK_TILDE) { return opt; };
if (op == tkind.TK_STAR) {
// opt nil here means the operand was an inherent-IDENT bail
// (exprtype N_IDENT arm L1596-1599 — SK_USE / pseudo-builtin
// sym.decl == nil — or another helper's nil propagation);
// 5-lite-b #34, A.6.2.1c #24. The `u == nil` post-resolvealias
// check was eliminated here — unwrapbang(non-nil) returns
// non-nil (parser invariant N_TBANG.lhs always set) and
// resolvealias passes through non-nil unchanged (L513
// `for (cur != nil)` only exits via `return cur` or `return n`).
if (opt == nil) { return nil; };
let u: *node = resolvealias(c, unwrapbang(opt));
// Invalid input (non-pointer dereference); cstage errors at
// cmd/wcc/check.c:660. 5-lite-b #34.
if (u.kind != nkind.N_TPTR) { return nil; };
return u.lhs;
};
if (op == tkind.TK_AMP) {
if (e.lhs != nil && e.lhs.kind == nkind.N_DOT) {
let fld: str = e.lhs.str;
if (streq(fld, "len") || streq(fld, "cap")) {
let base: *node = e.lhs.lhs;
if (base != nil && base.type_ != nil) {
// Full chase per hop (F2a batch-4 c3-B2):
// the one-peel-per-hop walk lost 2-level
// alias bases out of the slice/str detect
// (cs twin check.c:1198-1201 chases both
// hops). NOTE the ww cgen ADDRESS tail
// stays loud for ALL alias bases (task
// #96) — this aligns the stamped type
// only; rows graduate with #96.
let bu: *tinfo = tichase(base.type_: *tinfo);
if (bu != nil && bu.kind == tykind.TY_PTR) { bu = bu.sub; };
bu = tichase(bu);
if (bu != nil) {
if (bu.kind == tykind.TY_SLICE || bu.kind == tykind.TY_STR) {
let pp: *node = newnode(nkind.N_TPTR, "", 0, 0);
pp.lhs = mktname(c, "i64");
return pp;
};
};
};
};
};
// #206: `&fn` must type as `*fn(...)` — a pointer to the fn's
// full signature — not `*<rettype>`. exprtype's N_IDENT arm
// returns a fn decl's lhs (the return type), so the generic
// `*opt` below would mistype `&myread` as `*i32`; a `*fn`
// laundered into a `*alias` slot then slips past the nominal
// pointer-fn reject in isassignable. Synthesize the N_TFN from
// the fn decl (N_FNDECL and N_TFN share parseparams' param
// shape) so the address-of carries the signature. Mirrors
// cstage, where a fn ident already types as TY_FN
// (cmd/wcc/check.c:668), so `&fn` is `*fn` natively.
if (e.lhs != nil) {
if (e.lhs.kind == nkind.N_IDENT) {
let fs: *sym = scopelookup(c.cur, e.lhs.str);
if (fs != nil) {
if (fs.skind == skind.SK_FN) {
if (fs.decl != nil) {
if (fs.decl.kind == nkind.N_FNDECL) {
let synth: *node = newnode(nkind.N_TFN, "", 0, 0);
synth.lhs = fs.decl.lhs;
synth.list = fs.decl.list;
let pf: *node = newnode(nkind.N_TPTR, "", 0, 0);
pf.lhs = synth;
return pf;
};
};
};
};
};
// #124: a cross-module `&mod.fn` — same synthesis as the
// N_IDENT arm, but the fn decl lives in the qualifier module
// (scopelookupinmodule). Without this the address-of falls to
// the generic `*opt` path and a const `[](str,*fn)` table's
// nested cross-module &fn element fails the isassignable
// typeeq → confident reject; cstage types `&mod.fn` as `*fn`
// natively (a dotted fn ref is TY_FN), so this aligns ww UP to
// cstage's actual acceptance reason.
if (e.lhs.kind == nkind.N_DOT) {
if (e.lhs.lhs != nil && e.lhs.lhs.kind == nkind.N_IDENT) {
let fs: *sym = scopelookupinmodule(c.cur, e.lhs.lhs.str, e.lhs.str);
if (fs != nil) {
if (fs.skind == skind.SK_FN) {
if (fs.decl != nil) {
if (fs.decl.kind == nkind.N_FNDECL) {
let synth: *node = newnode(nkind.N_TFN, "", 0, 0);
synth.lhs = fs.decl.lhs;
synth.list = fs.decl.list;
let pf: *node = newnode(nkind.N_TPTR, "", 0, 0);
pf.lhs = synth;
return pf;
};
};
};
};
};
};
};
// opt nil → propagation from inherent-IDENT bail (5-lite-b
// #34). Generic &expr widens to *opt; without opt we can't
// synthesize the pointer node.
if (opt == nil) { return nil; };
let pp: *node = newnode(nkind.N_TPTR, "", 0, 0);
pp.lhs = opt;
return pp;
};
// Unreachable for valid input: op is one of TK_MINUS/PLUS/NOT/
// TILDE/STAR/AMP per parser invariant (lib/ww/parse/expr.ww unary
// op set); all are handled above. 5-lite-b #34.
return nil;
};
// indexresult — derive the result tnode of an N_INDEX expression.
// Mirrors cstage cmd/wcc/check.c:870-894 and harec ref/harec/src/types.c
// type_promote dispatch. Slice/array → elem; str → u8; `*[N]T` decays
// to T (pointer-to-array); `*[]T` does NOT decay (yields []T via the
// generic *U → U fallback — Hare-faithful, a pointer-to-slice is a 1D
// array of slices, not of T); generic *T → T.
fn indexresult(c: *checker, e: *node) *node = {
let basetn: *node = exprtype(c, e.lhs, nil);
let _idx: *node = exprtype(c, e.rhs, nil);
let u: *node = resolvealias(c, unwrapbang(basetn));
// basetn nil → propagation from inherent-IDENT bail at exprtype
// N_IDENT arm L1596-1599 (5-lite-b #34, A.6.2.1c #24).
// unwrapbang(nil)=nil and resolvealias(nil)=nil pass through.
if (u == nil) { return nil; };
if (u.kind == nkind.N_TSLICE) { return u.lhs; };
if (u.kind == nkind.N_TARRAY) { return u.lhs; };
if (u.kind == nkind.N_TNAME) {
if (streq(u.str, "str")) { return mktname(c, "u8"); };
};
if (u.kind == nkind.N_TPTR) {
let inner: *node = u.lhs;
let iu: *node = resolvealias(c, unwrapbang(inner));
if (iu != nil && iu.kind == nkind.N_TARRAY) {
return iu.lhs;
};
return inner;
};
// Invalid input (non-indexable base — cstage errors at
// cmd/wcc/check.c:893). 5-lite-b #34.
return nil;
};
// exprtype — best-effort type-AST inference for an expression
// node. Handles literals, identifiers, calls, casts, binary/unary
// ops, indexing, module-qualified refs + enum-member folds; returns
// nil for shapes we don't statically know (struct field access into
// non-primitive types, etc).
// `hint`: optional declared-type AST passed by the caller (let
// target, assign target). nil = "no hint, derive from self". Threaded
// for use by A.6.1's STRUCTLIT/ARRLIT arms which can't self-type and
// need the enclosing declared type to resolve. Ignored by every arm
// in A.6.0; the param is plumbed here so the per-kind work that
// follows doesn't ripple a fresh signature change. Mirrors harec's
// `check_expression(..., result_type, ...)` per
// `feedback_hare_frontend_reference.md`.
//
// Dispatcher invariant (5-lite-a #33): every value-producing nkind
// listed in resolvewalk's post-order dispatch (L474-489) reaches a
// stamping arm here that sets e.type_ before returning. No
// fall-through. Arms that return nil (binoptype trailing, unoptype
// TK_STAR opt-nil, indexresult u-nil, N_DOT outer fold-miss, N_SLICE
// non-sliceable base, etc.) are propagation from a callee's nil —
// not silent gaps. Mirror of harec's
// `assert(expr->result)` at ref/harec/src/check.c:3810. The
// asserttyped pass at L2871 enforces the invariant on every
// dispatched node post-checker, with gates for the residual
// inherent-IDENT bails (SK_USE, pseudo-builtin sym.decl==nil,
// N_DOT-LHS syntactic position, EXPR_ASSERT-family abort/assert) until
// #19 retires the bail shape.
fn exprtype(c: *checker, e: *node, hint: *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) {
// Typed-int literal (`7u32`, `0i8`): tsuffix names a builtin
// primitive. Mirrors cstage cmd/wcc/check.c:694-701 cexpr's
// `lookup_builtin(n->tsuffix)`; falls through to untyped_int
// when the suffix doesn't resolve.
if (e.tsuffix.len > 0) {
let suf: *node = mktname(c, e.tsuffix);
let ti: *tinfo = tinfofornode(c, suf);
if (ti != nil) {
e.type_ = ti: *void;
return suf;
};
};
let tn: *node = mktname(c, "untyped_int");
e.type_ = tinfofornode(c, tn): *void;
return tn;
};
if (k == nkind.N_FLOATLIT) {
// Typed-float literal (`1.5f32`, `0.0f64`): tsuffix names a
// builtin primitive. Mirrors cstage cmd/wcc/check.c:702-709
// cexpr's `lookup_builtin(n->tsuffix)`; falls through to
// untyped_float when the suffix doesn't resolve.
if (e.tsuffix.len > 0) {
let suf: *node = mktname(c, e.tsuffix);
let ti: *tinfo = tinfofornode(c, suf);
if (ti != nil) {
e.type_ = ti: *void;
return suf;
};
};
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) {
// #55: bare-leaf value-ident must prefer curmod. Flat-scope
// scopelookup bucket-walks and can bind a same-leaf symbol from
// the wrong module under a foreign curmod, dragging its decl's
// return-type node (e.g. `read` -> io.read under curmod=os, whose
// bare `error` then binds strconv.error not io.error). Mirrors
// cstage cmd/wcc/check.c:66 scope_lookup_prefer; sibling #56 at
// L2439, #53 at L688. Tracked in the cluster note at L685-687.
let s: *sym = scopelookupprefer(c.cur, c.curmod, e.str);
if (s == nil) { return nil; };
if (s.decl == nil) { return nil; };
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_BIN) {
let tn: *node = binoptype(c, e);
e.type_ = tinfofornode(c, tn): *void;
return tn;
};
if (k == nkind.N_UN) {
let tn: *node = unoptype(c, e);
e.type_ = tinfofornode(c, tn): *void;
return tn;
};
if (k == nkind.N_INDEX) {
let tn: *node = indexresult(c, e);
e.type_ = tinfofornode(c, tn): *void;
return tn;
};
if (k == nkind.N_CAST) {
// `expr: T` — explicit cast; the type expr is e.rhs. Mirrors
// cstage cmd/wcc/check.c:737 `n->type = resolve_type(c, n->rhs)`.
e.type_ = tinfofornode(c, e.rhs): *void;
return e.rhs;
};
if (k == nkind.N_CALL) {
let callee: *node = e.lhs;
if (callee == nil) { return nil; };
// #31: synthesize the `alloc(value)` / `alloc([], n)` builtin
// return shape so checkletassign sees the same `(*T | nomem)` /
// `([]T | nomem)` cstage's check.c stamps at L981-1006. Without
// this, exprtype returns the seeded decl's nil lhs and the let
// silently accepts `let p: *T = alloc(v);` — rule 10 trap.
// Same-module gate mirrors cstage's `c->cur_mod &&
// scope_lookup_in_module(...)` check from task #23.
if (callee.kind == nkind.N_IDENT) {
if (streq(callee.str, "alloc")) {
let shadowed: bool = false;
if (c.curmod.len > 0) {
if (scopelookupinmodule(c.cur, c.curmod, "alloc") != nil) {
shadowed = true;
};
};
if (!shadowed) {
if (e.list != nil) {
// Slice form: `alloc([], n)`.
if (e.list.kind == nkind.N_ARRLIT) {
if (e.list.list == nil) {
if (e.list.next != nil) {
if (e.list.next.next == nil) {
// B' (#3): an empty `[]` has no element type;
// ww gets it only from a let annotation (the
// #45 retype). Any other empty alloc has no
// hint, so refuse to guess rather than default
// to u8 (was a silent u8-default + #5 value-form
// miscompile). Align DOWN to harec, which errors
// the same way: ref/harec/src/check.c:1801-1802.
// The e.type_ != nil guard is the wwstage-only half:
// resolvewalk re-types every value node context-free
// (L648) AFTER checkletassign already rescued+stamped
// this node, so a stamped node is a rescued one — do
// not re-error it. cstage cexpr is single-visit (clet
// only) so it needs only the allococtx check.
if (e != c.allococtx && e.type_ == nil) {
deffolderr(c, e, "cannot infer slice element type for alloc([], n) without a type hint; annotate the binding, e.g. let x: []T = alloc([], n)");
return nil;
};
let sl: *node = newnode(nkind.N_TSLICE, "", 0, 0);
sl.lhs = mktname(c, "u8");
let nome: *node = mktname(c, "nomem");
sl.next = nome;
let tt: *node = newnode(nkind.N_TTAGGED, "", 0, 0);
tt.list = sl;
e.type_ = tinfofornode(c, tt): *void;
return tt;
};
};
};
};
// Value form: `alloc(value)`.
if (e.list.next == nil) {
let argt: *node = exprtype(c, e.list, nil);
let ptr: *node = newnode(nkind.N_TPTR, "", 0, 0);
ptr.lhs = argt;
let nome: *node = mktname(c, "nomem");
ptr.next = nome;
let tt: *node = newnode(nkind.N_TTAGGED, "", 0, 0);
tt.list = ptr;
e.type_ = tinfofornode(c, tt): *void;
return tt;
};
};
};
};
};
// #42: size(T) / align(T) / offset(e.f) typed-builtin intercepts.
// Fold the N_CALL in place to an N_INTLIT so cgen never sees an
// unresolved size/align/offset symbol. Same-module shadow gate
// mirrors the alloc precedent (#23) so a user `fn size(...)`
// inside this module suppresses the builtin. Mirrors cstage
// cmd/wcc/check.c:907-960.
if (callee.kind == nkind.N_IDENT) {
let bname: str = callee.str;
let issize: bool = streq(bname, "size");
let isalign: bool = streq(bname, "align");
let isoffset: bool = streq(bname, "offset");
if (issize || isalign || isoffset) {
let shadowed: bool = false;
if (c.curmod.len > 0) {
if (scopelookupinmodule(c.cur, c.curmod, bname) != nil) {
shadowed = true;
};
};
if (!shadowed) {
if (e.list != nil) {
// Post-fold the node IS an N_INTLIT-shaped
// untyped_int constant. Mirrors cstage
// cmd/wcc/check.c:926/958 which stamps
// ty_untyped_int after the fold. The return
// tnode mktname("i32") is the assignability
// target for callers, not the constant's
// own type.
let utn: *node = mktname(c, "untyped_int");
if (issize) {
// #108(b): rule-10 twin of the cstage
// size/align unsized guard.
if (astunsized(c, e.list)) {
deffolderr(c, e, "cannot take size of unsized type 'opaque'");
};
let v: i64 = astsize(c, e.list);
foldtointlit(c, e, v);
e.type_ = tinfofornode(c, utn): *void;
return mktname(c, "i32");
};
if (isalign) {
if (astunsized(c, e.list)) {
deffolderr(c, e, "cannot take align of unsized type 'opaque'");
};
let v: i64 = astalign(c, e.list);
foldtointlit(c, e, v);
e.type_ = tinfofornode(c, utn): *void;
return mktname(c, "i32");
};
// offset(e.f): the arg is a value expression
// (N_DOT), parsed via parsearglist — not a
// type expression.
if (isoffset) {
if (e.list.next == nil && e.list.kind == nkind.N_DOT) {
let off: i64 = astoffset(c, e.list);
if (off < 0i64) {
cerr("offset: no field '");
cerr(e.list.str);
cerr("'\n");
c.errs += 1;
off = 0i64;
};
foldtointlit(c, e, off);
e.type_ = tinfofornode(c, utn): *void;
return mktname(c, "i32");
};
};
};
};
};
};
// len(x) / append(s,...) / free(p) — Hare pseudo-builtins.
// Mirror cstage cmd/wcc/check.c:896-1011 (rule 10 requires
// stage byte-id; both stages stamp the same shape). No shadow
// guard: cstage's len/append/free intercepts have none either
// (check.c:901/962/1005), and the names are seeded into c.top
// at L85-88 so a user same-module decl dup-silences. Harec
// models these as dedicated AST kinds — EXPR_LEN at
// ref/harec/src/check.c:2630 (result `&builtin_type_size`),
// EXPR_APPEND at :745 (result `(nomem | void)`), EXPR_FREE at
// :2443 (result `&builtin_type_void`). The cstage divergence
// (len → i32 not size, append → void not tagged) pre-dates
// this task; #19 (Drew's δ — dedicated AST kinds) is the
// Hare-faithful path. This is the intercept-shim minimum to
// unblock #15 (A.6.2.1e assertion enable).
if (callee.kind == nkind.N_IDENT) {
// abort([msg]) / assert(cond[, msg]) — the EXPR_ASSERT
// family (harec ref/harec/src/check.c:877,893), lowered
// to rt_abort. Builtin only when no user symbol shadows
// the name — the same scopelookupprefer gate as cstage
// cmd/wcc/check.c:1536-1572 and isassertfam; a shadowed
// call stays on the regular path (#58; the #45
// flat-scope root is filed as task #14).
// The TY_ERR stamp on the callee is cgen's routing key
// (cstage spells it `n->lhs->type = ty_err`).
if (streq(callee.str, "abort")
&& scopelookupprefer(c.cur, c.curmod, "abort") == nil) {
if (e.list != nil) {
let mt: *node = exprtype(c, e.list, nil);
let conf: bool = false;
if (!isassignable(c, mktname(c, "str"), mt, &conf)) {
cerr("abort: message must be str\n");
c.errs += 1;
};
if (e.list.next != nil) {
cerr("abort: at most one arg\n");
c.errs += 1;
};
};
let tn: *node = mktname(c, "void");
e.type_ = tinfofornode(c, tn): *void;
callee.type_ = c.tc.tyerr: *void;
return tn;
};
if (streq(callee.str, "assert") && e.list != nil
&& scopelookupprefer(c.cur, c.curmod, "assert") == nil) {
let ct: *node = exprtype(c, e.list, nil);
if (ct != nil) {
// No alias peel: cstage compares ty_bool by
// IDENTITY (cmd/wcc/check.c:1560), so a
// `type myb = bool` cond is rejected there —
// align down (rule 10). Widening belongs to
// the alias-peel choke-point arc (task #5,
// #47/#68), both stages together.
let cu: *node = unwrapbang(ct);
let condok: bool = false;
if (cu.kind == nkind.N_TNAME) {
if (streq(cu.str, "bool")
|| streq(cu.str, "untyped_bool")) {
condok = true;
};
};
if (!condok) {
cerr("assert: cond must be bool\n");
c.errs += 1;
};
};
if (e.list.next != nil) {
let mt: *node = exprtype(c, e.list.next, nil);
let conf: bool = false;
if (!isassignable(c, mktname(c, "str"), mt, &conf)) {
cerr("assert: message must be str\n");
c.errs += 1;
};
if (e.list.next.next != nil) {
cerr("assert: at most two args\n");
c.errs += 1;
};
};
let tn: *node = mktname(c, "void");
e.type_ = tinfofornode(c, tn): *void;
callee.type_ = c.tc.tyerr: *void;
return tn;
};
if (streq(callee.str, "len")) {
let tn: *node = mktname(c, "i32");
e.type_ = tinfofornode(c, tn): *void;
return tn;
};
if (streq(callee.str, "append") || streq(callee.str, "free")) {
let tn: *node = mktname(c, "void");
e.type_ = tinfofornode(c, tn): *void;
return tn;
};
// delete(xs[i]) / delete(xs[lo:hi]) — slice removal,
// the delete-half of #35 (insert() is the twin arm
// below). Mirrors cstage cmd/wcc/check.c's delete
// arm. harec ref/harec/src/check.c:1981-2027 accepts
// both an indexing place (EXPR_ACCESS/ACCESS_INDEX)
// and a slicing place (EXPR_SLICE — Hare spells it
// delete(xs[i..j])); either way the OBJECT must be a
// slice. The range form is the fold-5a prereq P2
// (regex.ha:333 delete(jump_idxs[group_level][..])).
if (streq(callee.str, "delete")) {
let d: *node = e.list;
if (d == nil || d.next != nil) {
cerr("delete: takes exactly one argument\n");
c.errs += 1;
};
if (d != nil) {
exprtype(c, d, nil);
// harec check.c:2016's reject; wording
// adapted to ww's delete: prefix.
if (d.kind != nkind.N_INDEX && d.kind != nkind.N_SLICE) {
cerr("delete: operand must be an indexing or slicing expression\n");
c.errs += 1;
} else {
let basetn: *node = exprtype(c, d.lhs, nil);
let u: *node = resolvealias(c, unwrapbang(basetn));
// harec check.c:2024 wording; a
// fixed-size [N]T base and a str
// base land here.
if (u == nil || u.kind != nkind.N_TSLICE) {
cerr("delete must operate on a slice\n");
c.errs += 1;
};
};
};
let tn: *node = mktname(c, "void");
e.type_ = tinfofornode(c, tn): *void;
return tn;
};
// insert(xs[idx], v) — single-element slice insertion
// before idx, delete()'s twin (the insert-half of
// #35). Mirrors cstage cmd/wcc/check.c's insert arm.
// harec models append/insert in ONE checker arm
// (ref/harec/src/check.c:745 check_expr_append_insert;
// "insert" at :786): operand 1 must be an indexing
// place over a slice; idx == len is a legal
// end-insert. The spread form and the with-length
// form (harec :821/:837) stay filed on #35; a range
// PLACE is not Hare (harec asserts ACCESS_INDEX at
// :784) — rejected, no task cite.
if (streq(callee.str, "insert")) {
let d: *node = e.list;
if (d == nil || d.next == nil || d.next.next != nil) {
cerr("insert: takes exactly two arguments\n");
c.errs += 1;
};
if (d != nil) {
exprtype(c, d, nil);
if (d.kind == nkind.N_SLICE) {
cerr("insert: range place is invalid; operand must be an indexing expression xs[i]\n");
c.errs += 1;
} else {
if (d.kind != nkind.N_INDEX) {
cerr("insert: operand must be an indexing expression xs[i]\n");
c.errs += 1;
} else {
let basetn: *node = exprtype(c, d.lhs, nil);
let u: *node = resolvealias(c, unwrapbang(basetn));
// harec check.c:807 wording; a
// fixed-size [N]T base lands here.
if (u == nil || u.kind != nkind.N_TSLICE) {
cerr("insert must operate on a slice\n");
c.errs += 1;
};
};
};
if (d.next != nil) {
if (d.next.kind == nkind.N_SPREAD) {
cerr("insert: spread form insert(xs[i], vs...) unimplemented (task #35)\n");
c.errs += 1;
} else {
exprtype(c, d.next, nil);
};
};
};
let tn: *node = mktname(c, "void");
e.type_ = tinfofornode(c, tn): *void;
return tn;
};
};
let nm: str;
nm.ptr = nil; nm.len = 0;
if (callee.kind == nkind.N_IDENT) { nm = callee.str; };
if (callee.kind == nkind.N_DOT) { nm = callee.str; };
// #56: bare-leaf N_IDENT calls go through scopelookupprefer so
// `foo()` inside module M binds to M.foo rather than another
// module's same-leaf foo at the head of the flat scope bucket.
// Mirrors cstage cexpr N_IDENT routing through
// scope_lookup_prefer with c->cur_mod.
//
// #6a-A: a module-qualified `mod.fn()` callee resolves via the
// callee.lhs module hint when `mod` is an import (SK_USE) —
// scopelookupinmodule(mod, leaf) — mirroring cstage cexpr N_DOT
// (cmd/wcc/check.c:1035 scope_lookup_in_module) and cgen's
// rettupleof (cgen.ww:2263 fnretlookupmod). Closing this at the
// N_CALL root (vs the N_MLET backfill) makes every consumer of a
// module-qual call result — destructure binding AND a bare
// `mod.fn().0` rvalue — read the right return type via the one
// expr path, harec-faithful (binding-unpack does zero callee
// resolution, ref/harec/src/check.c:1354-1419). Without it the
// bare-leaf scopelookup grabbed whichever same-leaf fn heads the
// flat scope — wrong on a cross-module shadow (753_convwrap_audit:
// alpha.foo (i64,str) vs beta.foo (i64,i64)).
//
// #6a-D: a D-class callee whose `mod` leaf is itself a type/fn
// (SK_TYPE/SK_FN — random.random / fnmatch.fnmatch collision)
// resolves through scopelookupprefer to that same-leaf entry, not
// the coexisting SK_USE, when curmod matches the colliding decl's
// package — so the SK_USE gate below misses and the call stays
// nil-stamped. scopelookupuselocal re-resolves `mod` to the SK_USE
// that coexists in the same scope (coexistence-equivalent of
// cstage's use_alias; see lib/ww/sym.ww + memory
// module_type_name_collision).
//
// #181: a non-named callee (N_UN TK_STAR deref of a *fn local,
// `(*f)(...)`; or any other expression-as-callee shape) has no
// leaf to resolve here — skip the SK_FN name-lookup and let the
// fn-VALUE fallback below peel TPTR / dealias to TFN. Mirrors
// harec check_autodereference at ref/harec/src/check.c:1566.
// cgen post-#180+#185 already lowers the deref-call correctly,
// so lifting the asserttyped bail is silent-SIGSEGV-safe.
if (nm.len > 0) {
let s: *sym = nil;
if (callee.kind == nkind.N_IDENT) {
s = scopelookupprefer(c.cur, c.curmod, nm);
} else {
let ms: *sym = nil;
if (callee.lhs != nil && callee.lhs.kind == nkind.N_IDENT) {
ms = scopelookupprefer(c.cur, c.curmod, callee.lhs.str);
if (ms != nil && ms.skind != skind.SK_USE) {
let mu: *sym = scopelookupuselocal(ms.scope, callee.lhs.str);
if (mu != nil) { ms = mu; };
};
};
// #208: only a module-qualified callee (SK_USE receiver)
// resolves its leaf by name here. A value receiver
// (`s.read(...)`, `(*p).read()`, `a.b.read()`) is a
// fn-pointer FIELD call whose result type comes from the
// FIELD's fn type, not a global-leaf lookup. The old
// `scopelookup(c.cur, nm)` else-arm bound whichever
// same-leaf global fn headed the flat scope bucket —
// order-sensitive (os.read:i64 vs io.read:(i32|eof|closed)
// flipped by combined.ww concat order), yielding a
// false `return: not assignable`. Leaving s nil falls
// through to the fn-VALUE path below (exprtype(callee) →
// TPTR peel → TFN.ret), matching cstage cmd/wcc/check.c
// :1378-1433 (call result IS the callee type's ret; no
// global-leaf path) and harec check_autodereference
// (ref/harec/src/check.c:1566-1581).
if (ms != nil && ms.skind == skind.SK_USE) {
s = scopelookupinmodule(c.cur, callee.lhs.str, nm);
};
};
if (s != nil) { if (s.skind == skind.SK_FN) { if (s.decl != nil) {
// fn-decl's lhs is the return-type AST node. Mirrors cstage
// cmd/wcc/check.c:984+ regular-CALL `n->type =
// build_fn_type(c, s->decl)->ret` shape.
e.type_ = tinfofornode(c, s.decl.lhs): *void;
return s.decl.lhs;
}; }; };
};
// A callee that is a fn-VALUE — a fn-pointer struct field
// (`w.emit(...)`), local, or param — has no free SK_FN entry, so
// the name lookup above misses. Read the result off the checked
// callee node's own type instead: autodereference + dealias to
// the TY_FN, then take its result. Mirrors harec's check_expr_call
// `expr->result = type_dealias(check_autodereference(lvalue->
// result))->func.result` (ref/harec/src/check.c:1566-1581). The
// name path stays primary because a fn-NAME callee node in wwstage
// already carries its RETURN type (fn-decl.lhs), not its fn-type —
// so a `fn make() fn() void` callee would otherwise mis-yield void.
let ct: *node = resolvealias(c, unwrapbang(exprtype(c, callee, nil)));
for (ct != nil && ct.kind == nkind.N_TPTR) {
ct = resolvealias(c, unwrapbang(ct.lhs));
};
if (ct != nil) { if (ct.kind == nkind.N_TFN) {
let res: *node = ct.lhs;
e.type_ = tinfofornode(c, res): *void;
return res;
}; };
return nil;
};
if (k == nkind.N_DOT) {
// A.6.1.5a — fold cases only. Mirrors cstage cmd/wcc/check.c
// :740-832. Struct field + pseudo-field (.len/.cap/.ptr) lands
// in A.6.1.5b. SK_USE gates case 1; a #6a-D dot-lhs collision
// (random.random / fnmatch.fnmatch — the module leaf is also a
// same-scope type/fn) re-resolves through scopelookupuselocal so
// the SK_USE coexisting alongside the type/fn wins (the
// coexistence-equivalent of cstage's use_alias; see installdecl
// docstring + lib/ww/sym.ww). Enum-member fold delegates
// non-literal lhs shapes (sibling backref, unary, binary, shift)
// to enumvalfold, matching cstage cmd/wcc/check.c:210-284 and
// harec's enum-resolve constexpr set at
// ref/harec/src/check.c:4419-4434.
let lhsn: *node = e.lhs;
if (lhsn != nil) { if (lhsn.kind == nkind.N_IDENT) {
let ms: *sym = scopelookupprefer(c.cur, c.curmod, lhsn.str);
if (ms != nil && ms.skind != skind.SK_USE) {
let mu: *sym = scopelookupuselocal(ms.scope, lhsn.str);
if (mu != nil) { ms = mu; };
};
if (ms != nil) {
// Fold case 1: module-qualified ref. Mirror cstage
// check.c:749-775. cstage returns ty_err on SK_USE
// with missing leaf (extern decl); wwstage falls
// through to outer case — cgen has its own module-
// qualified resolution and the lenient checker policy
// keeps the silent miss documented at scruttype L656.
if (ms.skind == skind.SK_USE) {
let fs: *sym = scopelookupinmodule(c.cur, lhsn.str, e.str);
if (fs != nil) { if (fs.decl != nil) {
let tn: *node = fs.decl.lhs;
if (tn != nil) {
e.type_ = tinfofornode(c, tn): *void;
return tn;
};
}; };
};
// Fold case 2 inner: bare `EnumT.MEMBER` where EnumT
// is an SK_TYPE in the flat scope. Mirror cstage
// check.c:780-803.
if (ms.skind == skind.SK_TYPE) { if (ms.decl != nil) {
let body: *node = ms.decl.lhs;
let ub: *node = resolvealias(c, unwrapbang(body));
if (ub != nil) { if (ub.kind == nkind.N_TENUM) {
let prev: u64 = (-1i64): u64;
let m: *node = ub.list;
for (m != nil) {
let val: u64 = 0u64;
if (m.lhs == nil) {
val = prev + 1u64;
} else {
if (!enumvalfold(ub, m, m.lhs, &val)) { return nil; };
};
prev = val;
if (streq(m.str, e.str)) {
foldtointlit(c, e, val: i64);
e.type_ = tinfofornode(c, body): *void;
return body;
};
m = m.next;
};
}; };
}; };
};
}; };
// Fold case 2 outer: base resolves to enum, e.g.
// `pkg.EnumT.MEMBER` where the inner N_DOT (pkg.EnumT) folded
// via case 1 above to the enum body. Mirror cstage
// check.c:805-832. Peel one TPTR for `(*EnumT).MEMBER` (rare
// but cstage handles it at L808).
let basetn: *node = exprtype(c, lhsn, nil);
if (basetn != nil) {
let bu: *node = resolvealias(c, unwrapbang(basetn));
if (bu != nil) { if (bu.kind == nkind.N_TPTR) {
bu = resolvealias(c, unwrapbang(bu.lhs));
}; };
if (bu != nil) { if (bu.kind == nkind.N_TENUM) {
let prev: u64 = (-1i64): u64;
let m: *node = bu.list;
for (m != nil) {
let val: u64 = 0u64;
if (m.lhs == nil) {
val = prev + 1u64;
} else {
if (!enumvalfold(bu, m, m.lhs, &val)) { return nil; };
};
prev = val;
if (streq(m.str, e.str)) {
foldtointlit(c, e, val: i64);
e.type_ = tinfofornode(c, basetn): *void;
return basetn;
};
m = m.next;
};
}; };
// A.6.1.5b stamp cases — mirror cstage check.c:833-866. Pure
// type-AST stamps; never rewrite e.kind. Lenient on misses
// (cstage errors); falls through to nil under scruttype L656.
//
// Pseudo-fields .len/.cap/.ptr on slice/str/array. Cstage
// L833-842. `str` lives as N_TNAME("str") in wwstage — no
// dedicated N_TSTR kind — so test the trio shape here.
if (bu != nil) {
let isstr: bool = (bu.kind == nkind.N_TNAME) && streq(bu.str, "str");
if (bu.kind == nkind.N_TSLICE || bu.kind == nkind.N_TARRAY || isstr) {
if (streq(e.str, "len")) {
let tn: *node = mktname(c, "i32");
e.type_ = tinfofornode(c, tn): *void;
return tn;
};
if (streq(e.str, "cap")) {
let tn: *node = mktname(c, "i32");
e.type_ = tinfofornode(c, tn): *void;
return tn;
};
if (streq(e.str, "ptr")) {
let elem: *node = bu.lhs;
if (isstr) { elem = mktname(c, "u8"); };
let pp: *node = newnode(nkind.N_TPTR, "", 0, 0);
pp.lhs = elem;
e.type_ = tinfofornode(c, pp): *void;
return pp;
};
};
};
// Struct field walk. Cstage L843-849 errors on missing field.
if (bu != nil) { if (bu.kind == nkind.N_TSTRUCT) {
let f: *node = bu.list;
for (f != nil) {
if (f.kind == nkind.N_TFIELD) { if (streq(f.str, e.str)) {
e.type_ = tinfofornode(c, f.lhs): *void;
return f.lhs;
}; };
f = f.next;
};
}; };
// Tuple positional access `t.0`, `t.1`, …. Cstage L850-866
// errors on non-numeric / out-of-range; wwstage falls
// through. fldnumidx (cgenutil) returns -1 on non-digit.
if (bu != nil) { if (bu.kind == nkind.N_TTUPLE) {
let idx: i32 = fldnumidx(e.str);
if (idx >= 0) {
let p: *node = bu.list;
for (idx > 0 && p != nil) {
p = p.next;
idx -= 1;
};
if (p != nil) {
let pt: *node = p.lhs;
e.type_ = tinfofornode(c, pt): *void;
return pt;
};
};
}; };
};
return nil;
};
if (k == nkind.N_STRUCTLIT) {
// A.6.1.6 — head-only stamp of the struct-lit's overall type.
// Mirror cstage cmd/wcc/check.c:1161-1197; field-level walk
// (cstage L1178-1194) parked behind #23 / Phase 2 — field
// values are walked by the post-order exprtype dispatch at
// L460-489, so each field expr still gets its own n.type_.
//
// Parser at lib/ww/parse/expr.ww:147-148 always plants the
// TYPE_IDENT in e.lhs; e.lhs == nil would be a future Hare-
// style anonymous struct lit we don't yet parse — bail.
if (e.lhs == nil) { return nil; };
if (e.lhs.kind == nkind.N_IDENT) {
let ms: *sym = scopelookupprefer(c.cur, c.curmod, e.lhs.str);
if (ms != nil) { if (ms.skind == skind.SK_TYPE) { if (ms.decl != nil) {
let tn: *node = ms.decl.lhs;
if (tn != nil) {
// #66 Phase-N step 3: stamp e.type_ to the NOMINAL
// per-decl NAMED, not the flattened body. `overflow{}`
// where `type overflow = !void` must carry
// NAMED(overflow) so the typeeq variant match
// (cgenutil flatvariantidx) selects the overflow arm
// instead of falling to the scalar shape fallback;
// stamping tinfofornode(tn) gave the body (TY_VOID)
// and lost nominal identity. Resolve through a
// synthesized TNAME to reuse tinfofornode's TY_NAMED
// build/cache (check.ww:1157) — the SAME NAMED ptr
// the union variant resolved to. Mirrors cstage
// resolving overflow{} to the overflow Type, and ww's
// own N_CAST / N_IDENT arms which already stamp NAMED.
// Return the body node tn unchanged: byte-id rides
// e.type_ (cgen), while the checker's AST-level
// assign/return checks keep their prior input.
let tnm: *node = mktname(c, e.lhs.str);
let nti: *tinfo = tinfofornode(c, tnm);
if (nti != nil) { e.type_ = nti: *void; }
else { e.type_ = tinfofornode(c, tn): *void; };
// #251: range-check array-typed field inits
// (`enc{m=[65,..]}`). The head-stamp above is
// field-walk-free — general per-field assignability
// is parked behind #23. This is the ISOLATED
// array-field accept-if-fits ONLY: it reuses the
// stable N_TSTRUCT field-list walk (astoffset
// precedent), zero coupling to the parked #23 walk,
// and closes the rule-7 silent out-of-range truncate
// at this site (cstage check.c:1546 range-checks via
// arrlit_init_fits).
let stn: *node = resolvealias(c, tn);
if (stn != nil && stn.kind == nkind.N_TSTRUCT) {
let fi: *node = e.list;
for (fi != nil) {
if (fi.kind == nkind.N_FIELD
&& fi.lhs != nil
&& fi.lhs.kind == nkind.N_ARRLIT) {
let ftn: *node = nil;
let tf: *node = stn.list;
for (tf != nil) {
if (tf.kind == nkind.N_TFIELD) {
if (streq(tf.str, fi.str)) { ftn = tf.lhs; };
};
tf = tf.next;
};
if (ftn != nil) {
checkarrlitfits(c, ftn, fi.lhs);
};
};
fi = fi.next;
};
};
return tn;
};
}; }; };
// Lenient on miss: cstage L1170 errors, wwstage falls
// through (scruttype L656 / A.6.1.5b N_DOT struct-miss).
return nil;
};
// Synthetic type-expr (`(*T){...}` etc). Mirror cstage L1175
// resolve_type(c, n->lhs).
e.type_ = tinfofornode(c, e.lhs): *void;
return e.lhs;
};
if (k == nkind.N_ARRLIT) {
// A.6.1.7 — head-only stamp of the array-lit's overall type.
// Mirror cstage cmd/wcc/check.c:1198-1211: walk elements,
// skip the `...` repeat sentinel (parse/expr.ww:101-105),
// first-element-wins for the elem type, count non-skipped
// elements, synthesize an N_TARRAY{elt, INTLIT count}. Empty
// list defaults to `[0]i32` per cstage L1209. Per-element
// stamps still fire via the post-order dispatch at L460-489
// (N_ARRLIT is in the kind list since A.6.0); the re-walk in
// the loop below is tinfocache-idempotent (L467).
//
// Documented cstage divergence: cstage L1206 applies
// type_default to lift untyped_int → i32 etc; wwstage stamps
// the raw exprtype result, matching the alloc-value-form
// precedent at L1509. Consumers default-type via the declared
// `let` slot until A.6.3 lands. Mixed-type elements follow
// cstage first-element-wins; no unify check (future scope).
let elt: *node = nil;
let count: u64 = 0u64;
let it: *node = e.list;
for (it != nil) {
let skip: bool = false;
if (it.kind == nkind.N_FIELD) {
if (streq(it.str, "...")) { skip = true; };
};
if (!skip) {
let t: *node = exprtype(c, it, nil);
if (elt == nil) {
// #19: N_STRUCTLIT exprtype returns the struct BODY
// (N_TSTRUCT) per #66, but the array->slice
// isassignable arm typeeqast's the declared element
// N_TNAME against this element shape — a TNAME-vs-
// TSTRUCT kind mismatch reads confident-false and
// rejects. cstage infers the NAMED type here. Capture
// the named type for a named struct literal so su.lhs
// is the same N_TNAME shape (typeeqast is already
// streq-keyed for TNAME). Non-named elements keep the
// existing first-element shape.
if (it.kind == nkind.N_STRUCTLIT
&& it.lhs != nil
&& it.lhs.kind == nkind.N_IDENT) {
elt = mktname(c, it.lhs.str);
} else {
elt = t;
};
};
count += 1u64;
};
it = it.next;
};
// #103/#108: default the inferred element's UNTYPED flavor to its
// concrete type, mirroring cstage cmd/wcc/check.c:1856
// `elt = type_default(t)` (type.c:237 untyped_int→int after the
// #108 polarity flip). Keeping the raw untyped_int (the prior
// documented divergence) sized the synthesized [N]untyped_int
// INCONSISTENTLY — untyped_int.size is 0, so the cgarrlitfillbp
// STORE strode the 8 sentinel while slotsize (letslotsize slot)
// and elemsizeofc (cgindex READ stride) read the 0-size element
// → frame under-alloc SEGV + stride-1 index reads, cs≠ww. drew
// Hare-fidelity: harec lower_flexible defaults a flexible iconst
// to `int` (ref/harec/src/types.c:835). int = machine word (8B);
// the old i32 truncated values >2^31 (#263 trap).
if (elt != nil && elt.kind == nkind.N_TNAME) {
if (streq(elt.str, "untyped_int")) {
elt = mktname(c, "int");
} else { if (streq(elt.str, "untyped_float")) {
elt = mktname(c, "f64");
} else { if (streq(elt.str, "untyped_str")) {
elt = mktname(c, "str");
} else { if (streq(elt.str, "untyped_rune")) {
elt = mktname(c, "rune");
} else { if (streq(elt.str, "untyped_bool")) {
elt = mktname(c, "bool");
}; }; }; }; };
};
// Empty inferred arrlit: default to int, symmetric with cstage
// check.c:1859 empty-fallback ty_int (#103). Was "i32".
if (elt == nil) { elt = mktname(c, "int"); };
let arr: *node = newnode(nkind.N_TARRAY, "", 0, 0);
arr.lhs = elt;
let cn: *node = newnode(nkind.N_INTLIT, "", 0, 0);
cn.uval = count;
arr.rhs = cn;
e.type_ = tinfofornode(c, arr): *void;
// #103: stamp the synthesized array NODE too. checkletassign
// plants this node on the inferred let's n.lhs; slotsize /
// elemsizeofc / letslotsize all read n.lhs.type_, which was nil
// here (only e.type_ was set) — falling to the 8 / 0 sentinels.
arr.type_ = e.type_;
return arr;
};
if (k == nkind.N_SLICE) {
// A.6.2.0a — head-only stamp of the slice expression's overall
// type. Mirrors cstage cmd/wcc/check.c:1214-1228 N_SLICE: peel
// alias on the base; [N]T → []T, []T → []T (return base), str
// → str, *T (non-nil sub) → []T. Slice bounds (e.rhs start,
// e.cond end) are already covered by the post-order dispatch
// at L460-489 (typically N_INTLIT/N_IDENT/N_BIN, all in the
// dispatch list), so we do not double-walk them here.
// Documented cstage divergence: cstage L1227 errors on a
// non-sliceable base; wwstage returns nil (lenient on miss),
// matching scruttype L656 / A.6.1.5b N_DOT precedent.
let basetn: *node = exprtype(c, e.lhs, nil);
let bu: *node = resolvealias(c, unwrapbang(basetn));
if (bu == nil) { return nil; };
if (bu.kind == nkind.N_TARRAY) {
let sl: *node = newnode(nkind.N_TSLICE, "", 0, 0);
sl.lhs = bu.lhs;
e.type_ = tinfofornode(c, sl): *void;
return sl;
};
if (bu.kind == nkind.N_TSLICE) {
e.type_ = tinfofornode(c, basetn): *void;
return basetn;
};
if (bu.kind == nkind.N_TNAME) {
if (streq(bu.str, "str")) {
let tn: *node = mktname(c, "str");
e.type_ = tinfofornode(c, tn): *void;
return tn;
};
};
// Retained divergence: *[N]T does NOT decay here —
// `p[lo:hi]` types as [][N]T (C-pointer-slicing), unlike
// the index route (idxeffti) and unlike Hare. Loud on the
// usual []T annotation; for-range likewise. Team task #18
// (#61-residual A).
if (bu.kind == nkind.N_TPTR && bu.lhs != nil) {
let sl: *node = newnode(nkind.N_TSLICE, "", 0, 0);
sl.lhs = bu.lhs;
e.type_ = tinfofornode(c, sl): *void;
return sl;
};
return nil;
};
if (k == nkind.N_TUPLE) {
// A.6.2.0b — head-only stamp of the tuple expression's
// overall type. Mirrors cstage cmd/wcc/check.c:1437-1451
// N_TUPLE: walk e.list, type each element via exprtype, and
// assemble an N_TTUPLE whose .list chains N_TPARAM wrappers
// (one per element) so shared element-type ASTs (sym.decl.lhs,
// another tuple's element, struct field's .lhs) keep their
// own .next untouched — see lib/ww/ast.ww:101 and the
// A.6.2.0b-pre parser precedent at lib/ww/parse/parse.ww:302.
// Per-element exprtype recursion is tinfocache-idempotent
// (resolvewalk L460-489 already dispatches into N_TUPLE
// children). Lenient on empty list: grammar requires >= 2
// elements (lib/ww/parse/expr.ww:117 single-elem returns the
// expression), so empty is unreachable and yields nil here
// (matches scruttype L656 / A.6.1.5b N_DOT lenient-on-miss).
if (e.list == nil) { return nil; };
let head: *node = nil;
let tail: *node = nil;
let it: *node = e.list;
for (it != nil) {
let elemt: *node = exprtype(c, it, nil);
let w: *node = newnode(nkind.N_TPARAM, "", 0, 0);
w.lhs = elemt;
if (head == nil) { head = w; }
else { tail.next = w; };
tail = w;
it = it.next;
};
let tt: *node = newnode(nkind.N_TTUPLE, "", 0, 0);
tt.list = head;
e.type_ = tinfofornode(c, tt): *void;
return tt;
};
if (k == nkind.N_RECV) {
// A.6.2.0d — head-only stamp of the receive expression's
// overall type. Mirrors cstage cmd/wcc/check.c:1230-1236
// N_RECV: peel alias on the channel base; chan T → T.
// Documented cstage divergence: cstage L1234 errors on a
// non-chan base; wwstage returns nil (lenient on miss),
// matching scruttype L656 / A.6.1.5b N_DOT precedent.
let basetn: *node = exprtype(c, e.lhs, nil);
let bu: *node = resolvealias(c, unwrapbang(basetn));
if (bu == nil) { return nil; };
if (bu.kind == nkind.N_TCHAN) {
e.type_ = tinfofornode(c, bu.lhs): *void;
return bu.lhs;
};
return nil;
};
if (k == nkind.N_SPREAD) {
// A.6.2.0e — pass-through stamp. Mirrors cstage check.c:1212-1213.
// The spread expression `xs...` carries the operand's type.
let t: *node = exprtype(c, e.lhs, nil);
if (t != nil) { e.type_ = tinfofornode(c, t): *void; };
return t;
};
if (k == nkind.N_MATCH) {
// A.6.2.0g — port of cstage cmd/wcc/check.c:1316-1330 match-as-
// expression stamp. The match's type is the first arm's yield
// operand type; void if no arm yields. Wwstage skips cstage's
// arm-yield-unification check (L1322-1327) — that's a checker
// concern, this arm only stamps. Closes the consumer half of
// the match-as-expression contract that A.6.2.0f opened on the
// producer side (N_YIELD).
// #264: consume matchyieldtype's *tinfo DIRECTLY (no tinfofornode
// round-trip) — the post-walk call reads the operand's cached
// stamp, so the out-of-scope re-derive that clobbered *p/p[i]/*p+1
// is never reached. `retn` captures the pre-walk re-derived type
// node (nil on the post-walk cached read) for the assignability
// node the consumers (checkletassign/checkretassign) read off this
// arm's *node return; see matchyieldtype's docstring + #279.
let yt: *tinfo = nil;
let retn: *node = nil;
let cs: *node = e.list;
for (cs != nil) {
// cs.str/cs.lhs = the arm's case-binding name + declared
// type (the N_MCASE binder); fed to matchyieldtype's #241
// scope-popped `yield <binder>` fallback.
let armn: *node = nil;
let t: *tinfo = matchyieldtype(c, cs.body, cs.str, cs.lhs, &armn);
if (t != nil) { yt = t; retn = armn; break; };
cs = cs.next;
};
if (yt == nil) {
yt = tinfofornode(c, mktname(c, "void"));
};
e.type_ = yt: *void;
return retn;
};
if (k == nkind.N_YIELD) {
// A.6.2.0f — pass-through stamp; cstage check.c:1708 does NOT
// stamp N_YIELD (statement-shaped). Wwstage's A.6.2 invariant
// requires every post-dispatch kind have type_ set. Yield's
// value type is the operand's type per Hare's unified stmt/expr
// AST (ref/hare/hare/ast/expr.ha:449-461 — yield_expr is an
// expression with a type). Bare `yield;` (no operand) stamps
// void.
if (e.lhs == nil) {
let v: *node = mktname(c, "void");
e.type_ = tinfofornode(c, v): *void;
return v;
};
let t: *node = exprtype(c, e.lhs, nil);
if (t != nil) { e.type_ = tinfofornode(c, t): *void; };
return t;
};
if (k == nkind.N_TRYPROP) {
// success unwrap: the success-variant type of operand's
// tagged union.
let opt: *node = exprtype(c, e.lhs, nil);
let ou: *node = resolvealias(c, unwrapbang(opt));
if (ou == nil) { return nil; };
if (ou.kind != nkind.N_TTAGGED) { return nil; };
// Hare semantics: success = first non-error variant if
// any !-flag is present; else first variant.
if (taggedhaserr(c, ou)) {
let v: *node = ou.list;
for (v != nil) {
if (!iserrvariant(c, ou, v)) {
e.type_ = tinfofornode(c, v): *void;
return v;
};
v = v.next;
};
return nil;
};
e.type_ = tinfofornode(c, ou.list): *void;
return ou.list;
};
if (k == nkind.N_TRYUNW) {
// `e!` abort-on-error unwrap; success variant is what the
// receiver gets, identical to `?` shape modulo control flow.
// #31: required so `let p: *T = alloc(v)!;` resolves to *T.
let opt: *node = exprtype(c, e.lhs, nil);
let ou: *node = resolvealias(c, unwrapbang(opt));
if (ou == nil) { return nil; };
if (ou.kind != nkind.N_TTAGGED) { return nil; };
if (taggedhaserr(c, ou)) {
let v: *node = ou.list;
for (v != nil) {
if (!iserrvariant(c, ou, v)) {
e.type_ = tinfofornode(c, v): *void;
return v;
};
v = v.next;
};
return nil;
};
e.type_ = tinfofornode(c, ou.list): *void;
return ou.list;
};
if (k == nkind.N_TYPEASSERT) {
// `e as T` → T. Mirrors cstage cmd/wcc/check.c TYPEASSERT
// `n->type = resolve_type(c, n->rhs)`.
e.type_ = tinfofornode(c, e.rhs): *void;
return e.rhs;
};
if (k == nkind.N_TYPETEST) {
// `e is T` → bool
let tn: *node = mktname(c, "bool");
e.type_ = tinfofornode(c, tn): *void;
return tn;
};
return nil;
};
// isuntypedint / is_str_like / is_bool_like — helpers used
// by the assignability check below to allow common AST shapes
// through without needing real type inference.
fn isuntypedint(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind != nkind.N_TNAME) { return false; };
return streq(t.str, "untyped_int");
};
fn isuntypedfloat(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind != nkind.N_TNAME) { return false; };
return streq(t.str, "untyped_float");
};
fn isuntypednil(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind != nkind.N_TNAME) { return false; };
return streq(t.str, "untyped_nil");
};
// isinttypeast — int-typed AST node. Either a primitive int name
// (i8..i64/u8..u64/int/uint/uintptr/rune) or an N_TENUM. Floats are
// excluded so the enum↔int reinterpret in checkisas (#52) refuses a
// surprise `enum as f64` shape. Mirrors cstage's type_isint
// (cmd/wcc/type.c) restricted to the kinds reachable from AST.
fn isinttypeast(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind == nkind.N_TENUM) { return true; };
if (t.kind != nkind.N_TNAME) { return false; };
let s: str = t.str;
if (streq(s, "i8")) { return true; };
if (streq(s, "i16")) { return true; };
if (streq(s, "i32")) { return true; };
if (streq(s, "i64")) { return true; };
if (streq(s, "u8")) { return true; };
if (streq(s, "u16")) { return true; };
if (streq(s, "u32")) { return true; };
if (streq(s, "u64")) { return true; };
if (streq(s, "int")) { return true; };
if (streq(s, "uint")) { return true; };
if (streq(s, "uintptr")) { return true; };
if (streq(s, "size")) { return true; };
if (streq(s, "rune")) { return true; };
return false;
};
fn isnumerictname(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind != nkind.N_TNAME) { return false; };
let s: str = t.str;
if (streq(s, "i8")) { return true; };
if (streq(s, "i16")) { return true; };
if (streq(s, "i32")) { return true; };
if (streq(s, "i64")) { return true; };
if (streq(s, "u8")) { return true; };
if (streq(s, "u16")) { return true; };
if (streq(s, "u32")) { return true; };
if (streq(s, "u64")) { return true; };
if (streq(s, "int")) { return true; };
if (streq(s, "uint")) { return true; };
if (streq(s, "uintptr")) { return true; };
if (streq(s, "size")) { return true; };
if (streq(s, "rune")) { return true; };
if (streq(s, "f32")) { return true; };
if (streq(s, "f64")) { return true; };
return false;
};
fn isstrtname(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind != nkind.N_TNAME) { return false; };
return streq(t.str, "str");
};
// addrfnptrmatches — true iff `ptr` (after alias-resolve) is a
// pointer whose referent resolves to a fn type structurally equal to
// `synth` (a synthetic N_TFN built from a fn decl's ret + params).
// Mirror of cstage addrfn_ptr_matches (cmd/wcc/check.c, project #206);
// reuses typeeqast — the same structural-fn comparator cstage uses via
// type_eq(TY_FN,TY_FN) — for symmetry.
fn addrfnptrmatches(c: *checker, ptr: *node, synth: *node) bool = {
if (ptr == nil) { return false; };
let pu: *node = resolvealias(c, unwrapbang(ptr));
if (pu == nil) { return false; };
if (pu.kind != nkind.N_TPTR) { return false; };
let ref: *node = resolvealias(c, unwrapbang(pu.lhs));
if (ref == nil) { return false; };
if (ref.kind != nkind.N_TFN) { return false; };
return typeeqast(synth, ref);
};
// assignableaddrfn — project #206 Option C gate. Mirror of cstage
// assignable_addrfn (cmd/wcc/check.c). A bare `&fn` types structurally
// as `*fn(...)`, nominally distinct from a `*alias` fn-pointer slot;
// isassignable stays nominal (the pointer-fn arm below confidently
// rejects a laundered `*fn` value, like harec types.c:1039-1066). This
// admits only the shape harec adopts via its address-of hint (harec
// check.c:3594-3626): a DIRECT `&`-of-fn-ident whose signature
// structurally matches the destination's pointed-to fn alias, or the
// single matching ptr-to-fn variant of a tagged dst (>=2 same-sig
// variants → ambiguous, reject). Lives at the assignment-boundary
// caller sites — not in exprtype — because the alias identity is
// nominal-lossy once typed and the direct-&fn shape survives only on
// the rhs node. N_FNDECL and N_TFN share parseparams' param-node shape
// (lib/ww/parse/decl.ww + parse.ww), so a synthetic N_TFN over the fn
// decl's lhs/list compares correctly under typeeqast.
fn assignableaddrfn(c: *checker, dst: *node, rhs: *node) bool = {
if (dst == nil) { return false; };
if (rhs == nil) { return false; };
if (rhs.kind != nkind.N_UN) { return false; };
if (rhs.op != tkind.TK_AMP) { return false; };
let id: *node = rhs.lhs;
if (id == nil) { return false; };
if (id.kind != nkind.N_IDENT) { return false; };
let s: *sym = scopelookup(c.cur, id.str);
if (s == nil) { return false; };
if (s.skind != skind.SK_FN) { return false; };
if (s.decl == nil) { return false; };
let d: *node = s.decl;
if (d.kind != nkind.N_FNDECL) { return false; };
let synth: *node = newnode(nkind.N_TFN, "", 0, 0);
synth.lhs = d.lhs;
synth.list = d.list;
let du: *node = resolvealias(c, unwrapbang(dst));
if (du == nil) { return false; };
if (du.kind == nkind.N_TPTR) { return addrfnptrmatches(c, du, synth); };
if (du.kind == nkind.N_TTAGGED) {
let nmatch: i32 = 0;
let v: *node = du.list;
for (v != nil) {
if (addrfnptrmatches(c, v, synth)) { nmatch = nmatch + 1; };
v = v.next;
};
return nmatch == 1;
};
return false;
};
// isassignable — AST-level approximation of C check.c
// type_assignable. Returns true when we know the assignment is
// OK, false only when we're confident it isn't, and "skip" (true)
// when we can't tell — to avoid false positives. The trailing bool
// `confident` lets the caller decide whether to emit an error
// when the result is false: if !confident, the caller should not
// flag it.
fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
*confident = false;
if (dst == nil) { return true; }; // no declared target
if (src == nil) { return true; }; // unknown src type
*confident = true;
let du: *node = resolvealias(c, unwrapbang(dst));
let su: *node = resolvealias(c, unwrapbang(src));
if (du == nil) { *confident = false; return true; };
if (su == nil) { *confident = false; return true; };
if (typeeqast(du, su)) { return true; };
// #258: implicit [N]T -> []T array-to-slice borrow. Hare admits an
// array with a defined length wherever its element slice is expected
// (ref/harec/src/types.c:1080-1097, the SLICE-dst arm). Element types
// must match exactly — no element decay; a mismatch is a CONFIDENT
// reject (mirror cstage type.c type_assignable's #258 arm + fallthrough
// to 0). The acceptance sites then desugar the array expr to an
// explicit full slice via desugararrayslice; cgen is untouched.
if (du.kind == nkind.N_TSLICE) {
if (su.kind == nkind.N_TARRAY) {
if (typeeqast(du.lhs, su.lhs)) { return true; };
return false;
};
};
// untyped numeric → any numeric named type.
if (isuntypedint(su)) {
if (isnumerictname(du)) { return true; };
// (T | ...) tagged: only OK if some variant accepts untyped_int.
if (du.kind == nkind.N_TTAGGED) {
let v: *node = du.list;
for (v != nil) {
let vu: *node = resolvealias(c, unwrapbang(v));
if (vu != nil) {
if (isnumerictname(vu)) { return true; };
};
v = v.next;
};
*confident = false;
return true;
};
// Known non-numeric primitive: confidently wrong.
if (du.kind == nkind.N_TNAME) {
if (streq(du.str, "bool")) { return false; };
if (streq(du.str, "void")) { return false; };
if (streq(du.str, "str")) { return false; };
};
// Unknown shapes: stay quiet.
*confident = false;
return true;
};
if (isuntypedfloat(su)) {
if (isnumerictname(du)) { return true; };
if (du.kind == nkind.N_TNAME) {
if (streq(du.str, "bool")) { return false; };
if (streq(du.str, "void")) { return false; };
if (streq(du.str, "str")) { return false; };
};
*confident = false;
return true;
};
if (isuntypednil(su)) {
// nil → ptr/slice/chan/fn/nullable
if (du.kind == nkind.N_TPTR) { return true; };
if (du.kind == nkind.N_TSLICE) { return true; };
if (du.kind == nkind.N_TCHAN) { return true; };
if (du.kind == nkind.N_TFN) { return true; };
// nullable `(*T | void)` — already accepted by typeeqast
// when matched whole; nil is OK there too.
if (du.kind == nkind.N_TTAGGED) {
let v: *node = du.list;
for (v != nil) {
if (v.kind == nkind.N_TPTR) { return true; };
if (v.kind == nkind.N_TSLICE){ return true; };
v = v.next;
};
};
*confident = false;
return true;
};
// Tagged-union variant inclusion: src is one of dst's variants.
// #199(α): direct variant only — no transitive drill into a
// NAMED-tagged wrapper variant. Cgen has no wrapped-slot layout
// (taggedvariantindext returns -1 → tag=0 silent miscompile on
// io.underread → (size|io.eof|io.error)). ww-stricter than Hare;
// harec keeps the drill at types.c:702-739 (#199b deferred port).
// Restores SSoT with `is`/`as` non-recursive lookup (#198 sibling).
// Callers compose `let inner: Wrapper = sub; let r: parent = inner;`.
if (du.kind == nkind.N_TTAGGED && su.kind != nkind.N_TTAGGED) {
let v: *node = du.list;
for (v != nil) {
// Spread `...wrapper` keeps the recursive drill: the
// wrapper's flat variants are intentionally inlined into
// the parent set, and AST-level params haven't been
// expanded yet (cstage flattens at resolve_type; wwstage
// stays AST-keyed). Plain wrapper variant gets the
// direct-only gate.
let vspread: bool = (v.op == tkind.TK_ELLIPSIS);
let vu: *node = resolvealias(c, unwrapbang(v));
let vtagged: bool = false;
if (vu != nil) { if (vu.kind == nkind.N_TTAGGED) { vtagged = true; }; };
if (vtagged && !vspread) {
if (typeeqast(v, src)) { return true; };
} else {
let innerconf: bool = false;
if (isassignable(c, v, src, &innerconf)) { return true; };
};
v = v.next;
};
return false;
};
// tagged → tagged: structural variant list compare. Skip
// (don't be confident) — common when forwarding a fallible
// return through another fn with the same shape but possibly
// a different surface spelling.
if (du.kind == nkind.N_TTAGGED && su.kind == nkind.N_TTAGGED) {
// #205: NAMED-variant nominal compare BEFORE permissive
// fallthrough. Mirror of cstage type.c type_assignable
// tagged→tagged arm (#199 α concrete→tagged sibling).
// When src is a NAMED-tagged wrapper and dst has a direct
// NAMED-tagged variant equal to src, accept by nominal
// identity — wrapper's leaves are NOT direct variants of
// dst, so a structural subset walk would reject. SSoT
// with `is`/`as` variant lookup (#198 family).
let v: *node = du.list;
for (v != nil) {
let vu: *node = resolvealias(c, unwrapbang(v));
if (vu != nil) {
if (vu.kind == nkind.N_TTAGGED) {
if (typeeqast(v, src)) { return true; };
};
};
v = v.next;
};
*confident = false;
return true;
};
// tagged → non-tagged: requires `?` / `!` / match to project a
// variant. #31: this is what traps `let p: *T = alloc(v);`
// where the builtin returns `(*T | nomem)` and the LHS is bare.
if (su.kind == nkind.N_TTAGGED && du.kind != nkind.N_TTAGGED) {
return false;
};
// Two known primitives with different names are confidently
// incompatible. `i32 ↔ bool`, `str ↔ i32`, etc.
if (du.kind == nkind.N_TNAME && su.kind == nkind.N_TNAME) {
let known_d: bool = isnumerictname(du) || isstrtname(du);
if (!known_d) { if (streq(du.str, "bool")) { known_d = true; }; };
if (!known_d) { if (streq(du.str, "void")) { known_d = true; }; };
let known_s: bool = isnumerictname(su) || isstrtname(su);
if (!known_s) { if (streq(su.str, "bool")) { known_s = true; }; };
if (!known_s) { if (streq(su.str, "void")) { known_s = true; }; };
if (known_d) {
if (known_s) {
// Both primitives, different names → no.
return false;
};
};
};
// #206: two pointers whose referents both resolve to fn types are
// NOMINALLY assignable only when structurally equal — and that case
// already returned true via typeeqast at the top. Reaching here
// means the fn signatures differ, or a bare structural `*fn` value
// is being laundered into a `*alias` slot: confidently NOT
// assignable, mirror of cstage's nominal type_assignable (harec
// types.c:1039-1066). The direct `&fn` adopt-the-alias case is
// handled at the assignment caller sites via assignableaddrfn, NOT
// here. Without this the lenient catch-all below silently accepted
// the laundering shape.
if (du.kind == nkind.N_TPTR) {
if (su.kind == nkind.N_TPTR) {
let dref: *node = resolvealias(c, unwrapbang(du.lhs));
let sref: *node = resolvealias(c, unwrapbang(su.lhs));
if (dref != nil) {
if (sref != nil) {
if (dref.kind == nkind.N_TFN) {
if (sref.kind == nkind.N_TFN) {
return false;
};
};
};
};
};
};
// Anything else: don't claim confidence.
*confident = false;
return true;
};
// ---- match exhaustiveness --------------------------------------------
//
// For every match arm, verify that every variant of the scrutinee's
// tagged-union type is handled by some case (or a default arm
// exists). Multi-pattern `case A | B =>` covers all alts.
// qualleaf — rightmost dotted segment of a (possibly module-qualified)
// type name; the whole name when unqualified.
fn qualleaf(nm: str) str = {
let dotidx: i32 = -1;
let i: i32 = 0;
for (i < nm.len) {
if (nm[i] == 46u8) { dotidx = i; };
i += 1;
};
if (dotidx < 0) { return nm; };
let leaf: str;
leaf.ptr = nm.ptr + ((dotidx + 1): u64);
leaf.len = nm.len - dotidx - 1;
return leaf;
};
// qualmod — module qualifier of a type name (segment before the
// rightmost '.'), or `defmod` when unqualified.
fn qualmod(nm: str, defmod: str) str = {
let dotidx: i32 = -1;
let i: i32 = 0;
for (i < nm.len) {
if (nm[i] == 46u8) { dotidx = i; };
i += 1;
};
if (dotidx < 0) { return defmod; };
let head: str;
head.ptr = nm.ptr;
head.len = dotidx;
return head;
};
// casevariantpairmatch — a case pattern names variant `v` of the tagged
// union iff their (module, leaf) PAIRS match. Each name reduces to
// (qualmod, qualleaf): a DOTTED name keeps its own qualifier; a BARE
// name is attributed `unionmod`, the union's defining module. So a
// cross-module `case errors.unsupported` matches the bare `unsupported`
// in errors.error's body, while a foreign `othermod.unsupported` is
// REJECTED (qualifier differs) and Shape A's dotted `errors.error`
// variant of io.error keeps matching `case errors.error` (defmod is
// NOT forced onto a dotted variant — drew #13). Approximates harec's
// resolved-Type variant identity (cmd/wcc/check.c:1651) at the AST
// level via the existing nmod/aliassym machinery (cf. #51/#53); the
// precise Type-identity form is #10 (tinfo SSoT). typeeqast handles the
// exact-string cases first, so this fires only on the qualified-vs-bare
// mix.
fn casevariantpairmatch(v: *node, pat: *node, unionmod: str) bool = {
let vv: *node = unwrapbang(v);
let pp: *node = unwrapbang(pat);
if (vv == nil) { return false; };
if (pp == nil) { return false; };
if (vv.kind != nkind.N_TNAME) { return false; };
if (pp.kind != nkind.N_TNAME) { return false; };
if (!streq(qualleaf(vv.str), qualleaf(pp.str))) { return false; };
return streq(qualmod(vv.str, unionmod), qualmod(pp.str, unionmod));
};
// taggeddefmod — defining module of the typedecl whose body IS the
// tagged union (follows alias hops via aliassym, the #51/#53 machinery).
// Bare variants in that body are defined here: io.error =
// !(errors.error | underread | nomem) (module io) -> "io"; errors.error
// -> "errors". Falls back to c.curmod when the chain can't resolve.
fn taggeddefmod(c: *checker, st: *node) str = {
let defmod: str = c.curmod;
let cur: *node = unwrapbang(st);
for (cur != nil) {
if (cur.kind != nkind.N_TNAME) { return defmod; };
let s: *sym = aliassym(c, cur);
if (s == nil) { return defmod; };
if (s.decl == nil) { return defmod; };
if (s.decl.nmod.len > 0) { defmod = s.decl.nmod; };
let body: *node = unwrapbang(s.decl.lhs);
if (body == nil) { return defmod; };
if (body.kind == nkind.N_TTAGGED) { return defmod; };
cur = body;
};
return defmod;
};
fn casecovers(c: *checker, cs: *node, want: *node, unionmod: str) bool = {
if (cs.lhs != nil) {
if (typeeqast(cs.lhs, want)) { return true; };
if (casevariantpairmatch(want, cs.lhs, unionmod)) { return true; };
};
let alt: *node = cs.list;
for (alt != nil) {
if (typeeqast(alt, want)) { return true; };
if (casevariantpairmatch(want, alt, unionmod)) { return true; };
alt = alt.next;
};
return false;
};
fn errmatchvariant(c: *checker, n: *node, vname: *node) void = {
cerr("match: variant not handled");
if (vname != nil) {
if (vname.kind == nkind.N_TNAME) {
cerr(" (");
cerr(vname.str);
cerr(")");
};
};
cerr("\n");
c.errs += 1;
};
// casevariantin — true iff `pat` (a `case T` pattern, including
// each alt of a multi-pattern) names a variant of the tagged
// union `tagged`.
//
// #209: a `...inner` spread variant (v.op == TK_ELLIPSIS, parse.ww:284)
// is NOT a variant in itself — its inner tagged union's MEMBERS are. The
// AST `tagged.list` keeps the spread unexpanded (only the #61a tinfo
// build flattens it), so recurse into the inner union's members,
// attributing them the inner union's defining module. Mirrors cstage's
// resolve_type flatten (cmd/wcc/check.c:651-674) at the AST layer; the
// cgen side already dispatches off the flattened tinfo.params (#61a).
fn casevariantin(c: *checker, tagged: *node, pat: *node, unionmod: str) bool = {
let v: *node = tagged.list;
for (v != nil) {
if (v.op == tkind.TK_ELLIPSIS) {
let inner: *node = resolvealias(c, unwrapbang(v));
if (inner != nil) {
if (inner.kind == nkind.N_TTAGGED) {
if (casevariantin(c, inner, pat,
taggeddefmod(c, v))) {
return true;
};
v = v.next;
continue;
};
};
};
if (typeeqast(v, pat)) { return true; };
if (casevariantpairmatch(v, pat, unionmod)) { return true; };
v = v.next;
};
return false;
};
fn errbadcase(c: *checker, pat: *node) void = {
cerr("case: not a variant of scrutinee");
if (pat != nil) {
if (pat.kind == nkind.N_TNAME) {
cerr(" (");
cerr(pat.str);
cerr(")");
};
};
cerr("\n");
c.errs += 1;
};
fn checkmatchexhaust(c: *checker, n: *node) void = {
if (n == nil) { return; };
if (n.lhs == nil) { return; };
let st: *node = scruttype(c, n.lhs);
// F9 (task #12): direct `match (expr?)` / `match (expr!)` — cstage
// types the try-result as the success variant and rejects when it
// is not itself a tagged union (check.c "match on non-tagged-
// union"); scruttype's IDENT/DOT-only resolution let the form slip
// through silently (cs≠ww). The reject below is gated on the
// try-form so the lenient-miss contract for other unresolvable
// scrutinees is untouched; a tagged success keeps flowing into the
// normal exhaustiveness walk, matching cstage's accept.
let istry: bool = false;
if (st == nil) {
if (n.lhs.kind == nkind.N_TRYPROP || n.lhs.kind == nkind.N_TRYUNW) {
istry = true;
st = exprtype(c, n.lhs, nil);
};
};
let u: *node = resolvealias(c, unwrapbang(st));
if (u == nil) { return; };
if (u.kind != nkind.N_TTAGGED) {
if (istry) {
cerr("match on non-tagged-union try-result\n");
c.errs += 1;
};
return;
};
// #13: the union's defining module, so a bare body variant can be
// matched against a module-qualified cross-module case pattern (and
// a foreign-qualifier pattern correctly rejected). See
// casevariantpairmatch.
let unionmod: str = taggeddefmod(c, st);
// Validity: every `case T` pattern (and multi-pattern alts)
// must name a variant of u. Catches typos and dead arms that
// the dispatch would never reach.
let cs0: *node = n.list;
for (cs0 != nil) {
if (cs0.lhs != nil) {
if (!casevariantin(c, u, cs0.lhs, unionmod)) {
errbadcase(c, cs0.lhs);
};
let alt: *node = cs0.list;
for (alt != nil) {
if (!casevariantin(c, u, alt, unionmod)) {
errbadcase(c, alt);
};
alt = alt.next;
};
};
cs0 = cs0.next;
};
// Default arm absorbs anything; skip exhaustiveness.
let cs: *node = n.list;
for (cs != nil) {
if (cs.lhs == nil) { return; }; // default
cs = cs.next;
};
// For each variant of u, look for a covering case.
let v: *node = u.list;
for (v != nil) {
checkvariantcovered(c, n, v, unionmod);
v = v.next;
};
};
// checkvariantcovered — emit "variant not handled" unless some case arm
// covers `v`. #209: a `...inner` spread variant expands to its inner
// union's members (each attributed the inner union's defining module),
// so the phantom spread node is never itself reported uncovered — its
// members are checked instead. Mirrors the casevariantin spread recursion
// + cstage's flattened u->params exhaustiveness walk (cmd/wcc/check.c:
// 1648-1669). Leaf variants keep their NODE so errmatchvariant names them.
fn checkvariantcovered(c: *checker, n: *node, v: *node, unionmod: str) void = {
if (v.op == tkind.TK_ELLIPSIS) {
let inner: *node = resolvealias(c, unwrapbang(v));
if (inner != nil) {
if (inner.kind == nkind.N_TTAGGED) {
let im: str = taggeddefmod(c, v);
let m: *node = inner.list;
for (m != nil) {
checkvariantcovered(c, n, m, im);
m = m.next;
};
return;
};
};
};
let covered: bool = false;
let cs2: *node = n.list;
for (cs2 != nil) {
if (casecovers(c, cs2, v, unionmod)) {
covered = true;
cs2 = nil;
} else {
cs2 = cs2.next;
};
};
if (!covered) { errmatchvariant(c, n, v); };
};
// ---- let init / return assignability --------------------------------
//
// AST-level approximation: when we can infer src's type and dst is
// explicitly declared, verify isassignable. We only emit an error
// when isassignable says "false with confidence." If we can't tell
// (binary ops, complex exprs we don't infer), we stay quiet — full
// type inference lives only on the C side.
fn errnotassign(c: *checker, dst: *node, src: *node, where: str) void = {
cerr(where);
cerr(": not assignable");
if (src != nil) {
if (src.kind == nkind.N_TNAME) {
cerr(" (");
cerr(src.str);
cerr(" → ");
if (dst != nil) {
if (dst.kind == nkind.N_TNAME) {
cerr(dst.str);
};
};
cerr(")");
};
};
cerr("\n");
c.errs += 1;
};
// checkarrlitfits — #130/#251 array-init accept-if-fits, shared by the
// let / def / struct-field init sites. arrtn is the declared [N]T type
// node, rhs the N_ARRLIT. Per element: foldable int/rune literal →
// defcastfits range-check against T (in-range accept, out-of-range LOUD
// reject — rule-7/Drew, Hare range-checks at the literal-value level);
// non-foldable → isassignable to the element type. Mirrors cstage
// cmd/wcc/check.c arrlit_init_fits. The element WIDTH is driven by the
// declared type at cgen, so no element-type restamp is needed — #251
// proved coerce_floatlit's restamp shape does NOT transfer (cgen reads
// the declared type's element size, not the literal node's stamp).
fn checkarrlitfits(c: *checker, arrtn: *node, rhs: *node) void = {
if (arrtn == nil) { return; };
if (rhs == nil) { return; };
if (arrtn.kind != nkind.N_TARRAY) { return; };
if (rhs.kind != nkind.N_ARRLIT) { return; };
// #71: more elements than the declared [N] passed every per-element
// check below and then smashed the frame at cgen (each element is
// stored at its natural offset — the overflow clobbered neighbours
// and even the saved BP). Reject loud before the element walk.
// A nil or zero length child stays exempt: nil is the un-inferred
// [_] sentinel in def/struct-field contexts and 0 doubles as both
// [0] and the cstage [_] sentinel (conflation: task #11); the let
// paths stamp the real length before reaching here. A non-INTLIT
// length child (def-named [N]) is also exempt — task #13.
// Under-long (count < N, no `...`) stays accepted as before; Hare
// rejects it — task #10.
if (arrtn.rhs != nil && arrtn.rhs.kind == nkind.N_INTLIT
&& arrtn.rhs.uval > 0u64) {
let cnt: u64 = 0u64;
let ce: *node = rhs.list;
for (ce != nil) {
let cskip: bool = false;
if (ce.kind == nkind.N_FIELD) {
if (streq(ce.str, "...")) { cskip = true; };
};
if (!cskip) { cnt += 1u64; };
ce = ce.next;
};
if (cnt > arrtn.rhs.uval) {
cerr("array literal has ");
cerr(strconv.u64tos(cnt, strconv.base.DEC));
cerr(" elements but declared array holds ");
cerr(strconv.u64tos(arrtn.rhs.uval, strconv.base.DEC));
cerr("\n");
c.errs += 1;
return;
};
};
let elemtn: *node = arrtn.lhs;
let at: *tinfo = tinfofornode(c, arrtn);
let et: *tinfo = nil;
if (at != nil) { et = at.sub; };
et = tichase(et);
let e: *node = rhs.list;
for (e != nil) {
let skip: bool = false;
if (e.kind == nkind.N_FIELD) {
if (streq(e.str, "...")) { skip = true; };
};
if (!skip) {
let ev: *node = e;
for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; };
// #71: a NESTED array-literal element must run the same
// count check against the inner [N] — cstage catches the
// nested shape through its typed-literal assignability net
// (the literal's stamped [2][3]T fails type_assignable),
// which wwstage's untyped elements have no analog of; the
// silent accept emitted corrupted DATA / smashed frames.
// Recursion through the one choke point closes any depth.
// #105: a named-alias element type ([2]row) arrives as
// N_TNAME and bypassed the kind test — the module
// static-DATA emitter then silently TRUNCATED the overlong
// inner literal (exit-masked once the #60 read fix removed
// the segv). resolvealias is transitive, so alias spellings
// of any depth take the same recursion; a direct N_TARRAY
// passes through unchanged.
let eltr: *node = resolvealias(c, elemtn);
if (eltr != nil && eltr.kind == nkind.N_TARRAY
&& ev != nil && ev.kind == nkind.N_ARRLIT) {
checkarrlitfits(c, eltr, ev);
e = e.next;
continue;
};
let v: u64 = 0u64;
let folded: bool = false;
if (et != nil) {
if (typeisint(et)) {
if (ev != nil) {
folded = foldintliteral(ev, &v);
};
};
};
if (folded) {
if (!defcastfits(et, v)) {
let m: str = "array element out of range\n";
cerr(m);
c.errs += 1;
return;
};
} else {
let est: *node = exprtype(c, ev, elemtn);
let conf2: bool = false;
if (est != nil) {
if (!isassignable(c, elemtn, est, &conf2)) {
if (conf2) {
// #206: direct `&fn` array element.
if (!assignableaddrfn(c, elemtn, ev)) {
errnotassign(c, elemtn, est, "array element");
return;
};
};
};
};
};
};
e = e.next;
};
};
// desugararrayslice — #258. The single shared lowering for the implicit
// [N]T -> []T borrow. isassignable already admits an array with a defined
// length into a matching []T slot (see isassignable's #258 arm); here we
// rewrite the array expr to the explicit full slice `arr[0:len(arr)]` (an
// N_SLICE over the array base), reusing the existing slice cgen — #252/
// #257/#135 made array bases (incl struct-field arrays) correct. No new
// array->slice store cgen; pushargsrev / cgslice / cglet already lower an
// N_SLICE identically to cstage, so the borrow header is byte-id across
// stages. Twin of cstage cmd/wcc/check.c desugar_arrayslice.
//
// Returns the (possibly new) node for the caller's tree slot: `val`
// unchanged when the shape doesn't match, else a fresh N_SLICE whose base
// is `val` (which keeps its stamped array type_). The original sibling
// link transfers to the N_SLICE so a desugared call-arg keeps its place.
// rejectarrlitborrow — #31/#33 twin of cstage reject_arrlit_borrow. The
// array-literal → slice borrow is supported only at a `let` init (where
// checkletassign re-stamps + the cgslice N_ARRLIT-base arm spills the
// literal to a per-borrow backing slot). In call-arg / return / assign
// position there is no addressable backing — loud-reject so the gap is a
// compile error, not a dangling-ptr miscompile. Both stages reject here
// (rule-10, byte-id-trivial: no asm). Full non-let support is #33.
fn rejectarrlitborrow(c: *checker, dsttn: *node, val: *node) bool = {
if (val == nil) { return false; };
if (val.kind != nkind.N_ARRLIT) { return false; };
let du: *node = resolvealias(c, unwrapbang(dsttn));
if (du == nil) { return false; };
if (du.kind != nkind.N_TSLICE) { return false; };
let m: str = "array literal cannot borrow as a slice here; bind it to a `let` first\n";
cerr(m);
c.errs += 1;
return true;
};
fn desugararrayslice(c: *checker, dsttn: *node, srctn: *node, val: *node) *node = {
if (dsttn == nil) { return val; };
if (srctn == nil) { return val; };
if (val == nil) { return val; };
let du: *node = resolvealias(c, unwrapbang(dsttn));
let su: *node = resolvealias(c, unwrapbang(srctn));
if (du == nil) { return val; };
if (su == nil) { return val; };
if (du.kind != nkind.N_TSLICE) { return val; };
if (su.kind != nkind.N_TARRAY) { return val; };
if (!typeeqast(du.lhs, su.lhs)) { return val; };
let sl: *node = newnode(nkind.N_SLICE, val.file, val.line, val.col);
sl.lhs = val; // sliced base; lo (.rhs) / hi (.cond) nil → 0 : len(arr)
let slt: *node = newnode(nkind.N_TSLICE, "", 0, 0);
slt.lhs = su.lhs;
sl.type_ = tinfofornode(c, slt): *void;
sl.next = val.next;
val.next = nil;
return sl;
};
// calleefndecl — resolve a call's callee to its fn-decl node so the
// arg/param lockstep (desugarcallargs) can read declared param types.
// Mirrors exprtype's N_CALL resolution (bare-leaf via scopelookupprefer,
// module-qualified via the SK_USE receiver). nil for builtins / fn-value
// callees — they have no declared param list to drive the #258 desugar,
// and the selfhost corpus has no array→slice arg there anyway.
fn calleefndecl(c: *checker, callee: *node) *node = {
if (callee == nil) { return nil; };
if (callee.kind == nkind.N_IDENT) {
let s: *sym = scopelookupprefer(c.cur, c.curmod, callee.str);
if (s != nil) {
if (s.skind == skind.SK_FN) { return s.decl; };
};
return nil;
};
if (callee.kind == nkind.N_DOT) {
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
let ms: *sym = scopelookupprefer(c.cur, c.curmod, callee.lhs.str);
if (ms != nil && ms.skind != skind.SK_USE) {
let mu: *sym = scopelookupuselocal(ms.scope, callee.lhs.str);
if (mu != nil) { ms = mu; };
};
if (ms != nil) {
if (ms.skind == skind.SK_USE) {
let fs: *sym = scopelookupinmodule(c.cur, callee.lhs.str, callee.str);
if (fs != nil) {
if (fs.skind == skind.SK_FN) { return fs.decl; };
};
};
};
};
};
};
return nil;
};
// desugarcallargs — #258 at the call-arg context. Lockstep the call's
// args against the callee's declared params and desugar an array arg
// passed where a []T param is expected. Mirrors cstage cmd/wcc/check.c's
// non-variadic call-arg arm. Variadic (`T...`) slots are skipped (the
// arg flows into the gather as an element, not the slice itself).
fn desugarcallargs(c: *checker, n: *node) void = {
if (n == nil) { return; };
let decl: *node = calleefndecl(c, n.lhs);
if (decl == nil) { return; };
let param: *node = decl.list;
let prev: *node = nil;
let a: *node = n.list;
for (a != nil) {
let nexta: *node = a.next;
if (param != nil) {
if (param.kind == nkind.N_PARAM) {
if (param.op != tkind.TK_ELLIPSIS) {
let atype: *node = exprtype(c, a, nil);
// #258: an array arg into a []T param with a
// MISMATCHED element is not a borrow — loud reject,
// mirror cstage's call-arg type_assignable failure.
// Gated to the array→slice-param shape so wwstage's
// broader call-arg leniency (it runs no general
// param typecheck) is untouched.
let pu: *node = resolvealias(c, unwrapbang(param.lhs));
let au: *node = resolvealias(c, unwrapbang(atype));
if (pu != nil) { if (au != nil) {
if (pu.kind == nkind.N_TSLICE) { if (au.kind == nkind.N_TARRAY) {
if (!typeeqast(pu.lhs, au.lhs)) {
errnotassign(c, param.lhs, atype, "argument");
};
}; };
}; };
// #31/#33: bare array-literal arg has no backing
// — loud-reject (supported only at a `let`).
if (!rejectarrlitborrow(c, param.lhs, a)) {
let rep: *node = desugararrayslice(c, param.lhs, atype, a);
if (rep != a) {
if (prev == nil) { n.list = rep; } else { prev.next = rep; };
a = rep;
};
};
};
if (param.op != tkind.TK_ELLIPSIS) { param = param.next; };
};
};
prev = a;
a = nexta;
};
};
// checkassign — #258 at the assignment context. wwstage runs no other
// N_ASSIGN typecheck (cstage's lives in cexpr); this exists solely to
// route an array→slice rhs through the shared desugar so w6c_ww emits
// the same borrow as w6c (rule-10). No error diagnostics — cstage gates
// the shape.
fn checkassign(c: *checker, n: *node) void = {
if (n == nil) { return; };
if (n.lhs == nil) { return; };
if (n.rhs == nil) { return; };
let ltn: *node = exprtype(c, n.lhs, nil);
let rtn: *node = exprtype(c, n.rhs, nil);
// #31/#33: bare array-literal rhs has no backing — loud-reject
// (supported only at a `let`).
if (!rejectarrlitborrow(c, ltn, n.rhs)) {
n.rhs = desugararrayslice(c, ltn, rtn, n.rhs);
};
};
// inferarraylen — `let xs: [_]T = arrlit;` length inference (#7). The
// parser leaves a `[_]` array's length child nil as the infer sentinel
// (parse.ww, mirror cstage parse.c:186). Count the array-literal's
// elements (skipping the `...` repeat marker, same walk as the N_ARRLIT
// exprtype at L2905) and stamp a synthesized N_INTLIT length node so
// tinfofornode / cgen / `.len` all read the real count — the wwstage
// analogue of cstage clet's `declared = type_array(.., iu->alen)` patch.
// A `[_]T` with no array-literal initialiser can't infer: loud error,
// never a silent zero-length array (rule 7). Idempotent (skips once the
// length child is set), so the module-level double-call (checkfile's
// pre-resolvewalk call + checkletassign here) raises at most one error.
fn inferarraylen(c: *checker, n: *node) void = {
if (n == nil) { return; };
if (n.lhs == nil) { return; };
if (n.lhs.kind != nkind.N_TARRAY) { return; };
if (n.lhs.rhs != nil) { return; }; // explicit [N] or already inferred
if (n.rhs == nil || n.rhs.kind != nkind.N_ARRLIT) {
cerr("error: [_]T needs an array-literal initialiser\n");
c.errs += 1;
let z: *node = newnode(nkind.N_INTLIT, "", 0, 0);
z.uval = 0u64;
n.lhs.rhs = z; // sentinel: idempotent, error already raised
return;
};
let cnt: u64 = 0u64;
let it: *node = n.rhs.list;
for (it != nil) {
let skip: bool = false;
if (it.kind == nkind.N_FIELD) {
if (streq(it.str, "...")) { skip = true; };
};
if (!skip) { cnt += 1u64; };
it = it.next;
};
let cn: *node = newnode(nkind.N_INTLIT, "", 0, 0);
cn.uval = cnt;
n.lhs.rhs = cn;
};
fn checkletassign(c: *checker, n: *node) void = {
if (n == nil) { return; };
inferarraylen(c, n); // #7: must run before the n.rhs==nil bail
if (n.rhs == nil) { return; }; // no init
// hint = nil for A.6.0; A.6.1 will pass n.lhs once STRUCTLIT/ARRLIT
// arms consume it. Plumbing-only at this point.
// B' (#3): a `let x: []T = alloc([], n)` is the one context that lets
// the empty alloc infer its element type (the #45 retype runs AFTER
// exprtype, so flag the exact call node up front; exprtype errors on
// any empty alloc that isn't this one). Peel the same ?/! wrapper #45
// peels so the flagged node matches.
let octx: *node = nil;
if (n.lhs != nil && n.lhs.kind == nkind.N_TSLICE) {
let inner: *node = n.rhs;
if (inner.kind == nkind.N_TRYPROP) {
inner = inner.lhs;
} else { if (inner.kind == nkind.N_TRYUNW) {
inner = inner.lhs;
}; };
if (inner != nil && inner.kind == nkind.N_CALL) {
let callee: *node = inner.lhs;
let a0: *node = inner.list;
let a1: *node = nil;
let a2: *node = nil;
if (a0 != nil) { a1 = a0.next; };
if (a1 != nil) { a2 = a1.next; };
if (callee != nil
&& callee.kind == nkind.N_IDENT
&& streq(callee.str, "alloc")
&& a0 != nil && a0.kind == nkind.N_ARRLIT
&& a0.list == nil
&& a1 != nil && a2 == nil) {
octx = inner;
};
};
};
let savedoctx: *node = c.allococtx;
c.allococtx = octx;
let src: *node = exprtype(c, n.rhs, nil);
c.allococtx = savedoctx;
// Inferred binding (`let r = expr;`, no type annotation). Mirror
// cstage cmd/wcc/check.c:1477 clet `if (t == NULL && initt) t =
// type_default(initt);` and ref/harec/src/check.c:1422
// check_expr_binding. wwstage carries the let's type on decl.lhs
// (exprtype N_IDENT at L1546 reads s.decl.lhs); cstage carries it
// on Sym.type — same observable result, rule-10 byte-id holds.
// Defaulting (untyped_int → i32, etc.) is exprtype's job at use
// sites, not the binding site.
if (n.lhs == nil) {
// #24 (fold-5 prereq): a struct-lit init's exprtype returns the
// decl's BODY node (N_TSTRUCT, L3103 per #66) — but every cgen
// local-arm dispatch (cgdot read, cgassign tagged-field store,
// cgun addr-of) is N_TNAME-keyed, so planting the body dropped
// `p.f` to the module-qualified `MOVQ f(SB)` fallback (link-fail
// name-leak; #211 family). Normalize to the synthesized TNAME so
// the inferred binding is indistinguishable from the annotated
// one downstream. cstage needs no twin: check.c:1477 clet carries
// Sym.type (tinfo), and its emission is annotation-invariant
// (probed identical asm annotated vs not).
if (src != nil && src.kind == nkind.N_TSTRUCT
&& n.rhs.kind == nkind.N_STRUCTLIT
&& n.rhs.lhs != nil && n.rhs.lhs.kind == nkind.N_IDENT) {
let ltn: *node = mktname(c, n.rhs.lhs.str);
ltn.type_ = tinfofornode(c, ltn): *void;
n.lhs = ltn;
return;
};
if (src != nil) { n.lhs = src; };
return;
};
if (src == nil) { return; }; // can't infer
// #45: alloc([], n) defers element type to the let-init context
// (Hare-style). exprtype's alloc-slice branch synthesizes
// ([]u8 | nomem) / []u8 (for the ?/! wrap) with no LHS context;
// when the let declares []T, retype src to []T / ([]T | nomem)
// so isassignable sees exact equality. cgenstmt cglet drives the
// element size from n.lhs already (cmd/wcc/cgenstmt.ww), so this
// stays symmetric with cstage check.c clet's parallel retype.
if (n.lhs.kind == nkind.N_TSLICE) {
let wrapped: bool = false;
let inner: *node = n.rhs;
if (inner.kind == nkind.N_TRYPROP) {
wrapped = true;
inner = inner.lhs;
} else { if (inner.kind == nkind.N_TRYUNW) {
wrapped = true;
inner = inner.lhs;
}; };
if (inner != nil && inner.kind == nkind.N_CALL) {
let callee: *node = inner.lhs;
let a0: *node = inner.list;
let a1: *node = nil;
let a2: *node = nil;
if (a0 != nil) { a1 = a0.next; };
if (a1 != nil) { a2 = a1.next; };
if (callee != nil
&& callee.kind == nkind.N_IDENT
&& streq(callee.str, "alloc")
&& a0 != nil && a0.kind == nkind.N_ARRLIT
&& a0.list == nil
&& a1 != nil && a2 == nil) {
let shadowed: bool = false;
if (c.curmod.len > 0) {
if (scopelookupinmodule(c.cur, c.curmod, "alloc") != nil) {
shadowed = true;
};
};
if (!shadowed) {
let sl: *node = newnode(nkind.N_TSLICE, "", 0, 0);
sl.lhs = n.lhs.lhs;
if (wrapped) {
src = sl;
} else {
let nome: *node = mktname(c, "nomem");
sl.next = nome;
let tt: *node = newnode(nkind.N_TTAGGED, "", 0, 0);
tt.list = sl;
src = tt;
};
};
};
};
};
// #130: array-init accept-if-fits. When lhs is [N]T and rhs is an
// array literal, per-element check: foldable int literal →
// defcastfits range-check (reject out-of-range loud, rule-7/Drew —
// Hare range-checks at literal-value level); non-foldable →
// isassignable to the element type. This BOTH accepts in-range
// bare-int (the #130 headline, matching cstage) AND closes the
// wwstage over-accept where str→u8 / out-of-range silently passed
// (#146 merged). Mirrors cstage check.c arrlit_init_fits. Scoped
// to the array path; scalar-init range-check is a separate
// language-wide gap (#148).
if (n.lhs.kind == nkind.N_TARRAY && n.rhs.kind == nkind.N_ARRLIT) {
checkarrlitfits(c, n.lhs, n.rhs);
return;
};
// #25/#31: an array literal initialising a SLICE local. Re-stamp the
// literal as [count]T (the slice element) so the #258 borrow's exact-
// element typeeq holds and the cgen N_SLICE-over-N_ARRLIT arm reads the
// declared element width. Run the same per-element coercion + range-
// check the array path runs (checkarrlitfits against a synthesized
// [count]T), then drive isassignable + the borrow off [count]T. Twin of
// cstage arrlit_init_fits' slice arm. Local-only (c.cur != c.top): the
// borrow runs at runtime; module-level slice-from-arrlit stays #32.
if (c.cur != c.top && n.lhs.kind == nkind.N_TSLICE
&& n.rhs.kind == nkind.N_ARRLIT) {
let cnt: u64 = 0u64;
let e0: *node = n.rhs.list;
for (e0 != nil) {
let skip: bool = false;
if (e0.kind == nkind.N_FIELD) {
if (streq(e0.str, "...")) { skip = true; };
};
if (!skip) { cnt += 1u64; };
e0 = e0.next;
};
let cn: *node = newnode(nkind.N_INTLIT, "", 0, 0);
cn.uval = cnt;
let arr: *node = newnode(nkind.N_TARRAY, "", 0, 0);
arr.lhs = n.lhs.lhs; // declared slice element type
arr.rhs = cn;
checkarrlitfits(c, arr, n.rhs);
n.rhs.type_ = tinfofornode(c, arr): *void;
// #31: stash the [count]T tnode on the arrlit (arrlit.lhs is free
// — the parser sets only .list) so the cgslice N_ARRLIT-base arm
// can size the backing NODE-wise via elemsizeofc(base.lhs). wwstage
// narrow-primitive tinfos are unsized (i32/u8 .size==0, #8), so the
// element width must come from the type NODE, not the tinfo.
n.rhs.lhs = arr;
src = arr;
};
let conf: bool = false;
let ok: bool = isassignable(c, n.lhs, src, &conf);
// #206: direct `&fn` → `*alias` / `(*alias | void)` slot.
if (!ok) { if (assignableaddrfn(c, n.lhs, n.rhs)) { ok = true; }; };
if (!conf) { return; };
if (!ok) { errnotassign(c, n.lhs, src, "let"); };
// #258: `let s: []T = arr` borrows the array as a full slice. The
// desugar lowers to a runtime `arr[0:len]` N_SLICE, so it only
// applies to LOCAL lets (a fn body executes the borrow). A MODULE-
// level let is static data with no runtime to run the borrow — its
// rhs must stay the raw N_ARRLIT so cgen can materialize it as DATA
// (#18). cstage splits this by checker: clet (the desugar site,
// cmd/wcc/check.c:1993) runs only from cstmt (local), while module-
// level lets are checked in check_file pass-2 (check.c:2549) which
// never desugars. wwstage runs ONE checkletassign for both (pass-2
// @checkfile + resolvewalk post-order), so mirror cstage's split
// here: skip at module scope (c.cur == c.top, the same module-scope
// test as L184). Keeps the #130 module-level assignability check
// above intact.
if (c.cur != c.top) {
n.rhs = desugararrayslice(c, n.lhs, src, n.rhs);
};
};
fn checkretassign(c: *checker, n: *node) void = {
if (n == nil) { return; };
if (n.lhs == nil) {
// bare `return;` — OK iff fnret is void or a tagged union
// with a void variant. Skip flagging for now; cgen handles
// the void-variant tag synthesis already.
return;
};
if (c.fnret == nil) { return; };
let src: *node = exprtype(c, n.lhs, nil);
if (src == nil) { return; };
let conf: bool = false;
let ok: bool = isassignable(c, c.fnret, src, &conf);
// #206: direct `&fn` returned into a `*alias` / `(*alias | void)`.
if (!ok) { if (assignableaddrfn(c, c.fnret, n.lhs)) { ok = true; }; };
if (!conf) { return; };
if (!ok) { errnotassign(c, c.fnret, src, "return"); };
// #258: `return arr` borrows the array as a full slice.
// #31/#33: bare array-literal has no backing — loud-reject
// (supported only at a `let`).
if (!rejectarrlitborrow(c, c.fnret, n.lhs)) {
n.lhs = desugararrayslice(c, c.fnret, src, n.lhs);
};
};
// ---- is / as validity ------------------------------------------------
//
// `e is T` and `e as T` require that e's declared type be a tagged
// union and that T name one of its variants. Operates on AST type
// expressions; falls back silently when we can't determine e's
// type (matches the case-variant rule for match).
fn checkisas(c: *checker, n: *node) void = {
if (n == nil) { return; };
// e is in n.lhs (value), T is in n.rhs (type expr).
let st: *node = scruttype(c, n.lhs);
// F9 (task #12): direct `expr? is T` / `expr! is T` — cstage cexpr
// types the ?/! result as the success variant and the non-tagged
// gate below then rejects (check.c "is on non-tagged-union").
// scruttype only resolves IDENT/DOT, so the direct try-form slipped
// through the lenient-miss contract and wwstage silently ACCEPTED
// (cs≠ww). Resolve the try-result here; a tagged success (named
// union variant) flows on into the variant checks, matching
// cstage's accept.
if (st == nil && n.lhs != nil) {
if (n.lhs.kind == nkind.N_TRYPROP || n.lhs.kind == nkind.N_TRYUNW) {
st = exprtype(c, n.lhs, nil);
};
};
let u: *node = resolvealias(c, unwrapbang(st));
if (u == nil) { return; };
// #52: enum ↔ int reinterpret (`enum as intT` / `intT as enum`).
// Mirrors cstage cmd/wcc/check.c:1346-1357 — N_TYPEASSERT with an
// enum on either side and integer types on both reinterprets in
// the same register, no tag check involved. Returns early before
// the tagged-union gate so lib/time/instant.ww `(c as i32)` and
// the lib/os syscall casts stop false-positiving. `is` (TYPETEST)
// stays rejected on non-tagged operands — cstage cmd/wcc/check.c
// gates the bypass on N_TYPEASSERT only.
if (n.kind == nkind.N_TYPEASSERT) {
let v: *node = resolvealias(c, unwrapbang(n.rhs));
let lhsenum: bool = false;
let rhsenum: bool = false;
if (u != nil) { if (u.kind == nkind.N_TENUM) { lhsenum = true; }; };
if (v != nil) { if (v.kind == nkind.N_TENUM) { rhsenum = true; }; };
if (lhsenum || rhsenum) {
if (isinttypeast(u)) { if (isinttypeast(v)) { return; }; };
};
};
if (u.kind != nkind.N_TTAGGED) {
cerr("is/as: operand is not a tagged union\n");
c.errs += 1;
return;
};
let want: *node = n.rhs;
if (want == nil) { return; };
// #198: route through tinfo.params (the #61a-flattened chain built
// at L1789-1845 N_TTAGGED) — the prior AST u.list walk via
// casevariantin false-rejects every variant that arrives via a
// `...inner` spread. Mirrors cstage cmd/wcc/check.c:1662-1675
// u->params + type_eq, and matches cgen's own #179 cgmatch / #66
// Phase-N flatvariantidxt lookup (the SSoT cgtagvariantidx already
// keys off at cgenexpr.ww:155). project_tinfo_lossy_nominal: name-
// keying was the pre-Phase-N workaround for tinfo lossy on nominal
// identity; typeeq inside flatvariantidxt now handles NAMED ptr-id.
let utinfo: *tinfo = tinfofornode(c, u);
let wanttinfo: *tinfo = tinfofornode(c, want);
if (utinfo != nil) { if (wanttinfo != nil) {
// exact-only (#95-c3): is/as acceptance is nominal variant
// membership; the cgen tag-synthesis chain/structural arms must
// not widen acceptance here, nor surface the >=2 cgen fatal
// during check (rule-10, #107). casevariantin below keeps the
// #198 spread fallback.
if (flatvariantidxt(utinfo, wanttinfo, true) >= 0) { return; };
}; };
if (casevariantin(c, u, want, taggeddefmod(c, st))) { return; };
cerr("is/as: not a variant of operand");
if (want.kind == nkind.N_TNAME) {
cerr(" (");
cerr(want.str);
cerr(")");
};
cerr("\n");
c.errs += 1;
};
// ---- ? subset propagation --------------------------------------------
//
// For `expr?`, the operand's error subset must be a subset of the
// enclosing fn's return-type variants. Mirrors C check.c. Operand
// is nkind.N_TRYPROP or nkind.N_TRYUNW (the F8 cardinality gate covers
// both; the subset walk is ?-only); its lhs is the value-bearing expr;
// we look at the expr's *declared* type for nkind.N_IDENT/nkind.N_CALL
// cases.
fn exprtypeoftry(c: *checker, e: *node) *node = {
if (e == nil) { return nil; };
if (e.kind == 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;
};
if (e.kind == nkind.N_CALL) {
// callee return type lookup: callee is e.lhs (nkind.N_IDENT or
// nkind.N_DOT). We need the fn-decl's lhs (return-type AST).
let callee: *node = e.lhs;
if (callee == nil) { return nil; };
let nm: str;
nm.ptr = nil; nm.len = 0;
if (callee.kind == nkind.N_IDENT) { nm = callee.str; };
if (callee.kind == nkind.N_DOT) { nm = callee.str; };
if (nm.len == 0) { return nil; };
let s: *sym = scopelookup(c.cur, nm);
if (s == nil) { return nil; };
if (s.skind != skind.SK_FN) { return nil; };
if (s.decl == nil) { return nil; };
return s.decl.lhs;
};
return nil;
};
fn checktryprop(c: *checker, n: *node) void = {
if (n == nil) { return; };
let t: *node = exprtypeoftry(c, n.lhs);
let u: *node = resolvealias(c, unwrapbang(t));
if (u == nil) { return; };
if (u.kind != nkind.N_TTAGGED) { return; };
// F8 interim gate (task #5): try-propagation assumes ONE success
// member end-to-end — exprtype collapses to the first non-error
// variant and cgen emits a single tag compare, so any OTHER
// success member is silently mistaken for an error (? propagates
// it; ! aborts on it). One class, both ops (#133 precedent).
// Until the honest subset-union result typing lands (task #14,
// harec check.c:2759-2835), reject loud. Mirrors cstage check.c
// N_TRYPROP/N_TRYUNW.
let haserr: bool = false;
let nsucc: int = 0;
let v: *node = u.list;
for (v != nil) {
if (iserrvariant(c, u, v)) { haserr = true; } else { nsucc += 1; };
v = v.next;
};
if (nsucc > 1) {
if (n.kind == nkind.N_TRYPROP) { cerr("?"); } else { cerr("!"); };
cerr(": multi-success union unwired (task #14): bind and match instead\n");
c.errs += 1;
return;
};
// `!` has no propagation, so no error-subset check (mirrors
// cstage's N_TRYPROP-only guard on the subset walk).
if (n.kind != nkind.N_TRYPROP) { return; };
if (!haserr) { return; };
// Enclosing fn must return a tagged union with each operand
// error variant present.
let r: *node = resolvealias(c, unwrapbang(c.fnret));
if (r == nil) {
cerr("?: enclosing fn has no tagged-union return\n");
c.errs += 1;
return;
};
if (r.kind != nkind.N_TTAGGED) {
cerr("?: enclosing fn return is not tagged\n");
c.errs += 1;
return;
};
let ev: *node = u.list;
for (ev != nil) {
if (iserrvariant(c, u, ev)) {
let found: bool = false;
let rv: *node = r.list;
for (rv != nil) {
if (typeeqast(rv, ev)) {
found = true;
rv = nil;
} else { rv = rv.next; };
};
if (!found) {
cerr("?: error variant not in enclosing return\n");
c.errs += 1;
};
};
ev = ev.next;
};
};
// install_param — when entering a fn body, define its params in a
// fresh local scope.
//
// TODO(#11): cstage check.c (post-#32) errors `param '%s' redeclared`
// when two params share a name. The fn body's scope IS fresh here
// (resolvefnbody opens it before calling us), so guarding scopedefine's
// nil return would be sound — but we defer until #11 wires checkfile
// into w6c_ww so the diagnostic class lands as a single coordinated
// step rather than dribbling in. Matches the cstage-only neg-case
// precedent at test/wcc/708 + test/wcc/696.
fn installparams(c: *checker, params: *node) void = {
let p: *node = params;
for (p != nil) {
if (p.kind == nkind.N_PARAM) {
// Hare-style variadic `T...`: normalize p.lhs to []T so
// downstream consumers (N_IDENT exprtype lookups via
// s.decl.lhs, cgen's variadic-slot synthesis) see the
// effective slice type. Mirrors cstage check.c:455
// `tp->type = type_slice(c->a, pt)` and harec
// check_func_type. Surface-fidelity preserved: wwdump
// -a runs parser only and never reaches this mutation.
if (p.op == tkind.TK_ELLIPSIS) {
if (p.lhs != nil && p.lhs.kind != nkind.N_TSLICE) {
let sl: *node = newnode(nkind.N_TSLICE, "", 0, 0);
sl.lhs = p.lhs;
p.lhs = sl;
};
};
let nm: str = p.str;
if (nm.len > 0) {
checkmoduleshadow(c, nm, "param");
scopedefine(c.cur, nm, skind.SK_PARAM, nil, p);
};
};
p = p.next;
};
};
// resolvefnbody — open a child scope for the fn, install its params,
// then walk the body. Local lets installed by walk_stmt (a future
// extension); for the current pass we just resolve-walk without
// per-statement scopes.
fn resolvefnbody(c: *checker, fnnode: *node) void = {
let outer: *scope = c.cur;
c.cur = newscope(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) {
resolvewalk(c, fnnode.body);
};
c.fnret = prevret;
c.cur = outer;
};
// isassertfam — `abort`/`assert` are language builtins, not value
// calls. harec models each as a dedicated EXPR_ASSERT whose result is
// builtin void (assert) or never (bare abort) at
// ref/harec/src/check.c:877,893; there is no callee ident, so nothing
// is left untyped. wwstage parses them as an N_CALL over a bare
// N_IDENT callee that binds to no decl, so both the call and its
// callee carry no type by design. Recognized exactly as the cstage
// builtin intercept (cmd/wcc/check.c:1314,1328): the reserved name
// with no shadowing user symbol.
fn isassertfam(c: *checker, id: *node) bool = {
if (id == nil) { return false; };
if (id.kind != nkind.N_IDENT) { return false; };
if (!streq(id.str, "abort") && !streq(id.str, "assert")) {
return false;
};
return scopelookupprefer(c.cur, c.curmod, id.str) == nil;
};
// asserttyped — post-checker invariant gate (#15, A.6.2.1e). Walks the
// file tree and fires for any node in resolvewalk's value-producing
// dispatch set (L474-489) whose n.type_ remained nil. Mirror of harec's
// `assert(expr->result)` at ref/harec/src/check.c:3810. The invariant
// is ARMED: a non-exempt nil-typed value node writes its one-line
// diagnostic to stderr and bails (os.exit 1), so a stamping regression
// fails loud rather than shipping a partially-typed tree. The
// 990_selfhost / 901 probes drive the wwstage checker over the resolved
// units that exercise this gate.
//
// Gates (per Drew 2026-05-22 — "guards value-producing expression
// nodes; SK_USE refs and bare builtin callees are syntactic positions,
// gate them out with WHY pointing at #19"):
//
// 1. N_IDENT whose resolved sym kind is SK_USE — module references
// (`os` in `os.write`). Harec models these via EXPR_ACCESS whose
// lookup-target is an OBJ_USE directly; there is no intermediate
// "ident-as-value" expr. Until #19 ports that AST shape, skip.
// 2. N_IDENT whose resolved sym has decl == nil — pseudo-builtin
// callees (len/append/free/alloc/size/align/offset, seeded in
// checkinit L85-97 with decl=nil). Harec spells these as
// dedicated EXPR_* kinds (EXPR_LEN, EXPR_APPEND, EXPR_FREE,
// EXPR_ALLOC at ref/harec/src/check.c:2630/745/2443/...).
// Drew's δ (#19) retires the seeded-SK_FN-with-nil-decl hack.
// 3. N_IDENT at the LHS-of-N_DOT syntactic position — the bare
// name half of a member-access expr is a lookup target, not a
// value-producing sub-expression. Harec's EXPR_ACCESS stores the
// member as a string, not a node.
// 4. The EXPR_ASSERT family — an N_CALL whose callee is `abort` or
// `assert`, and the bare N_IDENT callee itself (see isassertfam).
// harec's EXPR_ASSERT carries a void/never result with no callee
// ident (ref/harec/src/check.c:877,893); wwstage's
// N_CALL-over-bare-ident shape leaves both nodes nil by design.
//
// `indot` tracks gate 3: true only when the immediate caller is an
// N_DOT recursing into its .lhs.
fn asserttyped(c: *checker, n: *node, indot: bool) void = {
if (n == nil) { return; };
let k: nkind = n.kind;
let isexpr: bool =
k == nkind.N_INTLIT || k == nkind.N_FLOATLIT ||
k == nkind.N_STRLIT || k == nkind.N_RUNELIT ||
k == nkind.N_TRUE || k == nkind.N_FALSE ||
k == nkind.N_NIL || k == nkind.N_VOIDLIT ||
k == nkind.N_IDENT || k == nkind.N_BIN ||
k == nkind.N_UN || k == nkind.N_CALL ||
k == nkind.N_INDEX || k == nkind.N_CAST ||
k == nkind.N_STRUCTLIT || k == nkind.N_ARRLIT ||
k == nkind.N_RECV || k == nkind.N_DOT ||
k == nkind.N_SLICE || k == nkind.N_SPREAD ||
k == nkind.N_TUPLE || k == nkind.N_TRYPROP ||
k == nkind.N_TRYUNW || k == nkind.N_TYPETEST ||
k == nkind.N_TYPEASSERT || k == nkind.N_YIELD ||
k == nkind.N_MATCH;
let skip: bool = false;
if (isexpr && k == nkind.N_IDENT) {
if (indot) { skip = true; };
if (!skip) {
let s: *sym = scopelookup(c.cur, n.str);
if (s != nil) {
if (s.skind == skind.SK_USE) { skip = true; };
if (s.decl == nil) { skip = true; };
};
};
if (!skip) { if (isassertfam(c, n)) { skip = true; }; };
};
if (isexpr && k == nkind.N_CALL) {
if (isassertfam(c, n.lhs)) { skip = true; };
};
if (isexpr && !skip) {
if (n.type_ == nil) {
cerr("asserttyped: ");
let kn: str = nkname(k);
cerr(kn);
cerr(" ");
if (n.file.len > 0) {
cerr(n.file);
cerr(":");
let ls: str = strconv.i32tos(n.line, strconv.base.DEC);
cerr(ls);
};
if (n.str.len > 0) {
cerr(" '");
cerr(n.str);
cerr("'");
};
cerr("\n");
os.exit(1);
};
};
if (k == nkind.N_DOT) {
if (n.lhs != nil) { asserttyped(c, n.lhs, true); };
return;
};
if (n.attr != nil) { asserttyped(c, n.attr, false); };
if (n.lhs != nil) { asserttyped(c, n.lhs, false); };
if (n.rhs != nil) { asserttyped(c, n.rhs, false); };
if (n.cond != nil) { asserttyped(c, n.cond, false); };
if (n.body != nil) { asserttyped(c, n.body, false); };
if (n.els != nil) { asserttyped(c, n.els, false); };
let m: *node = n.list;
for (m != nil) {
asserttyped(c, m, false);
m = m.next;
};
};
export fn checkinit(c: *checker, tc: *tctx) void = {
c.tc = tc;
c.top = newscope(nil);
c.cur = c.top;
c.nresolved = 0;
c.nunresolved = 0;
c.errs = 0;
c.verbose = 0;
c.fnret = nil;
let empty: str;
c.curmod = empty;
c.file = nil;
c.allococtx = nil;
seedprimitives(c);
};
export fn checkfile(c: *checker, file: *node) void = {
if (file == nil) { return; };
if (file.kind != nkind.N_FILE) { return; };
c.file = file;
// Pass 1: install all top-level names.
let d: *node = file.list;
for (d != nil) {
installdecl(c, file, d);
d = d.next;
};
// Pass 2: walk decl bodies/types and resolve identifiers.
// Track the per-decl module bareword so bare-leaf lookups inside
// the body prefer same-module entries over alphabetically-earlier
// same-leaf imports.
d = file.list;
for (d != nil) {
c.curmod = declmod(file, d);
// A.6.2.1-pre — attr-subtree gap: top-level dispatch below walks
// d.lhs / d.body per kind but never d.attr, leaving `@symbol("…")`
// arg literals (N_STRLIT) outside the post-order exprtype
// dispatch. Mirror resolvewalk L405 which descends n.attr on
// inner nodes.
if (d.attr != nil) { resolvewalk(c, d.attr); };
let k: nkind = d.kind;
switch (k) {
case nkind.N_FNDECL:
if (d.lhs != nil) { resolvewalk(c, d.lhs); }; // return type
resolvefnbody(c, d);
case nkind.N_DEF:
if (d.lhs != nil) { resolvewalk(c, d.lhs); };
if (d.rhs != nil) { resolvewalk(c, d.rhs); };
// #251: `def D: [N]T = [int/rune lits]` array-init
// accept-if-fits. The wwstage def path otherwise runs NO
// init-assignability check (cstage check.c:2424 does), so
// an out-of-range / str element silently over-accepted
// (rule-7). Same predicate as the let path; scoped to the
// array-init shape (a no-op for every other def).
if (d.lhs != nil && d.rhs != nil) {
checkarrlitfits(c, d.lhs, d.rhs);
};
// #88: const-fold sibling/imported def refs, casts, and
// arithmetic so cgen's literal-only emitdefconstants can
// lay down the DATA row. GATED on the plain literal fold
// missing first, so existing literal/unary defs keep
// their rhs node and the emitted bytes stay byte-identical.
if (d.rhs != nil) {
let dv: u64 = 0u64;
if (!foldintliteral(d.rhs, &dv)) {
if (evaldefconst(c, d.rhs, &dv, 0)) {
stampintlit(d.rhs, dv);
};
};
};
case nkind.N_TYPEDECL:
if (d.lhs != nil) { resolvewalk(c, d.lhs); };
case nkind.N_LET:
// #7: a module-level `let xs: [_]T = arrlit;` must infer
// its length BEFORE resolvewalk stamps d.lhs's tinfo —
// otherwise the array tinfo caches the alen=0 sentinel and
// the patched length child never reaches the size/data
// reads. Idempotent with the checkletassign call below.
inferarraylen(c, d);
if (d.lhs != nil) { resolvewalk(c, d.lhs); };
if (d.rhs != nil) { resolvewalk(c, d.rhs); };
// #130: top-level let assignability — the subtree
// resolvewalk above stamps types but never runs the
// init-assignability check (function-body lets get it
// via resolvewalk's post-order L247; top-level lets
// were missed). Needed for the array accept-if-fits
// range-check to fire on module-level `let A:[N]u8=[..]`.
checkletassign(c, d);
// #133: const-fold a const-EXPR rhs (N_BIN /
// unary-over-N_BIN / def-ref) to a literal so cgen's
// literal-only emitletdataw lays the DATA row +
// defaultinferredlets recognises it, mirroring the
// N_DEF arm above. GATED on the plain literal fold
// missing first (existing literal/unary-literal lets
// keep their node, byte-identical) AND the const-fold
// succeeding (a str/struct/slice/call/runtime-operand
// rhs returns false silently and is left untouched).
// Stamp LAST — checkletassign consumes the pre-stamp type.
if (d.rhs != nil) {
let dv: u64 = 0u64;
if (!foldintliteral(d.rhs, &dv)) {
if (evaldefconst(c, d.rhs, &dv, 0)) {
stampintlit(d.rhs, dv);
};
};
};
};
d = d.next;
};
// Pass 3 (#15, A.6.2.1e): post-checker invariant gate. Walks each
// decl with its curmod set so asserttyped's gate lookups resolve
// against the same module context exprtype saw during pass 2.
d = file.list;
for (d != nil) {
c.curmod = declmod(file, d);
asserttyped(c, d, false);
d = d.next;
};
let empty: str;
c.curmod = empty;
};