Files
ww/selfhost/cmd/wcc/check.ww
Hojun-Cho 62b9d20383 toolchain: banner purge + WHY-only comment sweep (rule 8)
selfhost/, cmd/, internal/ join the tree-wide sweep: every section
banner dies (91 selfhost + the cmd C-style dividers -> 0); narration
and stale contracts deleted (pre-#22 bundler notes, retired
single-PT_LOAD and no-archive claims, superseded ABI tables); every
ref/harec/qbe cite, task cite, encoding/ELF contract, and rule-10
twin pointer kept; lost lifetime/rationale lines restored where the
sweep over-cut (elf_globals ownership, kwtab linear-scan). Comment-
only proven: all five wwstage tool binaries byte-identical across
the sweep; test-commit, test-byteid (161+1399, 0 pinned-divergent),
and test-bootstrap (fixed point + 991-995 byte-id) all exit 0.
The read-through banked 66 latent-bug leads (checkpoint).
2026-08-08 23:14:03 +09:00

7432 lines
318 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.
// Port of cmd/wcc/check.c.
package wcc;
import os;
import syntax;
import strconv;
type checker = struct {
tc: *syntax.tctx,
top: *syntax.scope,
cur: *syntax.scope,
nresolved: i32,
nunresolved: i32,
errs: i32,
loops: i32, // break/continue loop-nesting guard; cstage
// twin cmd/wcc/check.c:598 (c->loops)
istest: i32, // #15: `w6c_ww -T` — collect @test fns +
// synth the entry; loud-reject a user main.
verbose: i32, // when non-zero, log each unresolved name
fnret: *syntax.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: *syntax.node, // N_FILE root; used by checkmoduleshadow
// to consult the declaring source's own
// `use` directives.
allococtx: *syntax.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: *syntax.tinfo, n: *syntax.node) bool = {
if (t == nil) { return false; };
if (t.kind != syntax.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);
};
fn seedprimitives(c: *checker) void = {
syntax.scopedefine(c.top, "void", syntax.skind.SK_TYPE, c.tc.tyvoid, nil);
syntax.scopedefine(c.top, "bool", syntax.skind.SK_TYPE, c.tc.tybool, nil);
syntax.scopedefine(c.top, "rune", syntax.skind.SK_TYPE, c.tc.tyrune, nil);
syntax.scopedefine(c.top, "i8", syntax.skind.SK_TYPE, c.tc.tyi8, nil);
syntax.scopedefine(c.top, "i16", syntax.skind.SK_TYPE, c.tc.tyi16, nil);
syntax.scopedefine(c.top, "i32", syntax.skind.SK_TYPE, c.tc.tyi32, nil);
syntax.scopedefine(c.top, "i64", syntax.skind.SK_TYPE, c.tc.tyi64, nil);
syntax.scopedefine(c.top, "u8", syntax.skind.SK_TYPE, c.tc.tyu8, nil);
syntax.scopedefine(c.top, "u16", syntax.skind.SK_TYPE, c.tc.tyu16, nil);
syntax.scopedefine(c.top, "u32", syntax.skind.SK_TYPE, c.tc.tyu32, nil);
syntax.scopedefine(c.top, "u64", syntax.skind.SK_TYPE, c.tc.tyu64, nil);
syntax.scopedefine(c.top, "int", syntax.skind.SK_TYPE, c.tc.tyint, nil);
syntax.scopedefine(c.top, "uint", syntax.skind.SK_TYPE, c.tc.tyuint, nil);
syntax.scopedefine(c.top, "uintptr", syntax.skind.SK_TYPE, c.tc.tyuintptr, nil);
syntax.scopedefine(c.top, "f32", syntax.skind.SK_TYPE, c.tc.tyf32, nil);
syntax.scopedefine(c.top, "f64", syntax.skind.SK_TYPE, c.tc.tyf64, nil);
syntax.scopedefine(c.top, "str", syntax.skind.SK_TYPE, c.tc.tystr, nil);
syntax.scopedefine(c.top, "never", syntax.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: *syntax.node = syntax.newnode(syntax.nkind.N_TNAME, empty, 0, 0);
tnvoid.str = "void";
let bang: *syntax.node = syntax.newnode(syntax.nkind.N_TBANG, empty, 0, 0);
bang.lhs = tnvoid;
let nomemdecl: *syntax.node = syntax.newnode(syntax.nkind.N_TYPEDECL, empty, 0, 0);
nomemdecl.str = "nomem";
nomemdecl.lhs = bang;
syntax.scopedefine(c.top, "nomem", syntax.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.
syntax.scopedefine(c.top, "len", syntax.skind.SK_FN, nil, nil);
syntax.scopedefine(c.top, "alloc", syntax.skind.SK_FN, nil, nil);
syntax.scopedefine(c.top, "free", syntax.skind.SK_FN, nil, nil);
syntax.scopedefine(c.top, "append", syntax.skind.SK_FN, nil, nil);
syntax.scopedefine(c.top, "delete", syntax.skind.SK_FN, nil, nil);
syntax.scopedefine(c.top, "insert", syntax.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.
syntax.scopedefine(c.top, "size", syntax.skind.SK_FN, nil, nil);
syntax.scopedefine(c.top, "align", syntax.skind.SK_FN, nil, nil);
syntax.scopedefine(c.top, "offset", syntax.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: *syntax.node, d: *syntax.node) str = {
let empty: str;
if (d == nil) { return empty; };
if (d.nmod.len == 0) { return empty; };
if (file == nil) { return empty; };
let u: *syntax.node = file.list;
for (u != nil) {
// M1 #22: a decl is imported iff some `use` directive's full
// dotted import path equals the decl's module (now the path).
// Single-level packages have usepath == leaf so this is
// unchanged; nested (`encoding.utf8`) match here, not on leaf.
if (u.kind == syntax.nkind.N_USE) {
if (syntax.streq(u.usepath, d.nmod)) { return d.nmod; };
};
u = u.next;
};
return empty;
};
// usepath — map a `use` alias (leaf bareword the user writes, `utf8`)
// to the full dotted import path it binds (`encoding.utf8`), for the
// module-qualified resolution and codegen hint (M1 #22). Single-level
// packages have usepath == alias so the result is unchanged. The
// current tree has one occurrence per leaf, so the map is unambiguous.
fn usepathfor(file: *syntax.node, alias: str) str = {
let empty: str;
if (file == nil) { return empty; };
if (alias.len == 0) { return empty; };
let u: *syntax.node = file.list;
for (u != nil) {
if (u.kind == syntax.nkind.N_USE) {
if (syntax.streq(u.str, alias)) {
if (u.usepath.len != 0) { return u.usepath; };
return u.str;
};
};
u = u.next;
};
return empty;
};
// modkeyfor — usepathfor with leaf-alias fallback: the module key for a
// path-keyed scopelookupinmodule given the alias the user wrote (M1 #22).
fn modkeyfor(c: *checker, alias: str) str = {
let mk: str = usepathfor(c.file, alias);
if (mk.len == 0) { return alias; };
return mk;
};
// 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: *syntax.node, modtag: str, name: str) bool = {
if (file == nil) { return false; };
if (name.len == 0) { return false; };
let u: *syntax.node = file.list;
for (u != nil) {
if (u.kind == syntax.nkind.N_USE) {
// Skip self-imports: lib/fmt/fmt_test.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 (syntax.streq(u.nmod, u.usepath)) {
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 (syntax.streq(um, modtag)) { m = true; }; };
if (m) {
if (syntax.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: *syntax.scope = c.cur;
for (s != nil) {
let r: *syntax.sym = syntax.scopelookuplocal(s, name);
if (r != nil) {
if (r.skind == syntax.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).
// #23: top-level duplicate type/def/fn/let now reject loud here, keyed
// on (name, mod) exactly as cstage's install pass does
// (cmd/wcc/check.c:2852/2911/2932/2955) — see dupdecl below. (Same-scope
// LOCAL dup `let a=1; let a=2;` is a different path and stays deferred to
// #11; test/wcc/708 + test/wcc/696 are that cstage-only neg-case.)
fn installdecl(c: *checker, file: *syntax.node, d: *syntax.node) void = {
if (d == nil) { return; };
let k: syntax.nkind = d.kind;
let nm: str = d.str;
let mod: str = declmod(file, d);
// check-(c) self-import: a package may not import itself. Pure
// owner==leaf string compare, package-model-independent. check-(a)
// unused + (b)/(d) membership DEFERRED to task #8 (filename-keyed
// pulls lack import->file->symbol provenance). Message byte-identical
// to cstage check.c.
if (k == syntax.nkind.N_USE) {
// M1 #22: self-import ⟺ the imported path equals the use's own
// (owning) module path. Compares paths, not leaves.
if (mod.len != 0 && syntax.streq(d.usepath, mod)) {
cerr("self-import: package '"); cerr(mod);
cerr("' cannot import itself\n"); c.errs += 1i32;
};
// #30 value-before-use: a same-leaf VALUE/type decl is already
// installed (source order placed `fn aa` before `import aa`).
// Promote it in place with use_alias instead of installing a
// coexisting SK_USE, so `aa.member` resolves through the N_DOT
// use_alias guard. This is the order-mirror of installtop's
// value-arm promote and reaches the IDENTICAL single-sym end
// state (skind=value, use_alias=1, decl=value). cstage is order-
// independent — it installs all N_USE in a dedicated first pass
// (cmd/wcc/check.c:2811+) so its value-arm promote always finds
// the SK_USE; wwstage installs in source order, so this direction
// is closed here, mirroring cstage's self-import N_USE arm
// (check.c:2823-2834 `if (prev) prev->use_alias = 1`). A pure
// duplicate import (prev already SK_USE) keeps the coexisting-
// entry path (orthogonal to #30, pre-existing wwstage behavior).
let prev: *syntax.sym = syntax.scopelookuplocal(c.top, nm);
if (prev != nil) { if (prev.skind != syntax.skind.SK_USE) {
prev.use_alias = 1i32;
return;
}; };
syntax.scopedefine(c.top, nm, syntax.skind.SK_USE, nil, d); return;
};
if (k == syntax.nkind.N_DEF) { installtop(c, d, nm, mod, syntax.skind.SK_DEF, "def"); return; };
if (k == syntax.nkind.N_TYPEDECL) { installtop(c, d, nm, mod, syntax.skind.SK_TYPE, "type"); return; };
if (k == syntax.nkind.N_FNDECL) { installtop(c, d, nm, mod, syntax.skind.SK_FN, "fn"); return; };
if (k == syntax.nkind.N_LET) {
// A module-scope initializer must be link-time data: an
// alloc/call rhs runs code, emitletdataw's fold-fail
// skipped the DATAW slot silently, and every reference
// died at LINK time ("undefined reference") — reject at
// the declaration instead (rule 7). Hare rejects at check
// time too (ref/harec/src/check.c:4360); ww has no @init
// path. Mirrors cstage check_file N_LET.
let rr: *syntax.node = d.rhs;
for (rr != nil) {
if (rr.kind == syntax.nkind.N_CAST) { rr = rr.lhs; continue; };
if (rr.kind == syntax.nkind.N_TRYPROP) { rr = rr.lhs; continue; };
if (rr.kind == syntax.nkind.N_TRYUNW) { rr = rr.lhs; continue; };
break;
};
if (rr != nil) {
if (rr.kind == syntax.nkind.N_ALLOC || rr.kind == syntax.nkind.N_CALL) {
cerr(d.file); cerr(": error: module-scope let ");
cerr(nm);
cerr(": runtime initializer unsupported (alloc/call; rule 7)\n");
c.errs += 1;
};
};
installtop(c, d, nm, mod, syntax.skind.SK_VAR, "let");
return;
};
};
// installtop — install a top-level decl name into c.top, rejecting a
// genuine within-module duplicate loud (#23) with cstage's exact
// "duplicate <kind> %s" wording (cmd/wcc/check.c:2852/2911/2932/2955).
//
// scopedefineinmodule returns nil only on a same-(name, mod) re-install.
// Same-leaf cross-package decls carry distinct mods (the flat-bundle
// model) and an imported-module bareword's SK_USE keys on mod="", so a
// coexisting same-leaf type/fn in its own package never collides.
//
// The one nil that is NOT a user duplicate: a redeclaration of a
// pre-seeded builtin (the predeclared `nomem`, or a primtype name). cstage
// keeps no builtins in the scope at all — resolve_typename consults
// lookup_builtin FIRST (cmd/wcc/check.c:69) and the builtin always wins,
// so a user `type nomem = !void` / `type int = ...` installs dead and is
// silently ignored, never a duplicate error. wwstage seeds builtins INTO
// c.top, so the same redeclaration surfaces here as a collision; we mirror
// cstage by dropping it (the seeded builtin stays, and wins resolution)
// rather than erroring. A pre-seeded builtin is identified by its sym
// carrying no real source decl (primtypes: decl=nil; `nomem`: a
// checkinit-synthesized N_TYPEDECL with an empty .file) — user decls
// always carry their parsed source file.
fn installtop(c: *checker, d: *syntax.node, nm: str, mod: str, k: syntax.skind, kind: str) void = {
// #30: a top-level value/type decl whose leaf ALSO names an imported
// module PROMOTES that same-leaf SK_USE in place — one correctly-kinded
// sym carrying use_alias=1, so bare refs (call/structlit/var) resolve to
// the value/type while `name.member` still resolves the module via the
// N_DOT use_alias guard. Mirrors cstage check.c:2831/2848/2871/2907/
// 2928/2951 exactly (its USE-first install pass guarantees the SK_USE is
// present when the value decl lands). A bundled `ascii` module + a
// primary-package `@test fn ascii` (989_lib_byteid) is the live case.
// wwstage installs in source order, so this arm handles the use-before-
// value direction; the value-before-use direction (`fn aa` then `import
// aa`) is closed by the symmetric promote in installdecl's N_USE arm
// (set use_alias on the pre-installed value sym). Both directions reach
// the identical single-sym end state, so cstage and wwstage agree on
// every source order (#30).
let puse: *syntax.sym = syntax.scopelookuplocal(c.top, nm);
if (puse != nil) { if (puse.skind == syntax.skind.SK_USE) {
puse.skind = k;
puse.decl = d;
puse.use_alias = 1i32;
if (mod.len > 0) { if (puse.mod.len == 0) { puse.mod = mod; }; };
return;
}; };
if (syntax.scopedefineinmodule(c.top, nm, mod, k, nil, d) != nil) { return; };
let prev: *syntax.sym = syntax.scopesamekeysym(c.top, nm, mod);
if (prev != nil) {
if (prev.decl == nil) { return; };
if (prev.decl.file.len == 0) { return; };
};
cerr(d.file); cerr(": error: duplicate "); cerr(kind);
cerr(" "); cerr(nm); cerr("\n");
c.errs += 1;
};
// 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: *syntax.node, elems: *syntax.node,
define: bool, what: str) void = {
let b: *syntax.node = binds;
let pt: *syntax.node = elems;
for (b != nil) {
let et: *syntax.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);
syntax.scopedefine(c.cur, bnm, syntax.skind.SK_VAR, nil, b);
};
};
if (b.type_ == nil) {
let src: *syntax.node = b.lhs;
if (src == nil) { src = et; };
if (src != nil) {
let ti: *syntax.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: *syntax.node) void = {
if (n == nil) { return; };
let k: syntax.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 == syntax.nkind.N_MATCH) { checkmatchexhaust(c, n); };
if (k == syntax.nkind.N_TRYPROP) { checktryprop(c, n); };
if (k == syntax.nkind.N_TRYUNW) { checktryprop(c, n); };
if (k == syntax.nkind.N_TYPETEST) { checkisas(c, n); };
if (k == syntax.nkind.N_TYPEASSERT) { checkisas(c, n); };
if (k == syntax.nkind.N_LET) { checkletassign(c, n); };
if (k == syntax.nkind.N_RETURN) { checkretassign(c, n); };
// `use IDENT;` — name is a module label, not a free ident.
if (k == syntax.nkind.N_USE) { return; };
if (k == syntax.nkind.N_IDENT) {
let nm: str = n.str;
if (nm.len > 0) {
let s: *syntax.sym = syntax.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 == syntax.nkind.N_TNAME) {
let nm: str = n.str;
if (nm.len > 0) {
let s: *syntax.sym = syntax.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: *syntax.sym = syntax.scopelookup(c.cur, head);
if (m != nil) {
let leaf: str;
leaf.ptr = nm.ptr + (dot + 1): u64;
leaf.len = nm.len - (dot + 1);
s = syntax.scopelookupinmodule(c.cur, modkeyfor(c, 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 == syntax.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: *syntax.node = nil;
let it: *syntax.node = exprtype(c, n.lhs, nil);
if (it != nil) {
let et: *syntax.node = nil;
if (it.kind == syntax.nkind.N_TSLICE) { et = it.lhs; };
if (it.kind == syntax.nkind.N_TARRAY) { et = it.lhs; };
if (et != nil) { if (et.kind == syntax.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.
let et: *syntax.node = nil;
let it: *syntax.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 == syntax.nkind.N_TSLICE) { et = it.lhs; };
if (it.kind == syntax.nkind.N_TARRAY) { et = it.lhs; };
// A str binding is u8 (cstage check.c: elem =
// ty_u8). The old N_FORRANGE fallback decl made
// exprtype stamp the binding str, so call-arg
// marshaling pushed the 3-word str ABI for a
// 1-word scalar.
if (it.kind == syntax.nkind.N_TNAME) {
if (syntax.streq(it.str, "str")) {
et = mktname(c, "u8");
};
};
};
if (et != nil) {
let bn: *syntax.node = syntax.newnode(syntax.nkind.N_LET, n.file, n.line, n.col);
bn.str = bnm;
bn.lhs = et;
syntax.scopedefine(c.cur, bnm, syntax.skind.SK_VAR, nil, bn);
} else {
syntax.scopedefine(c.cur, bnm, syntax.skind.SK_VAR, nil, n);
};
};
};
// `break`/`continue` in the body target this loop; the `else`
// block runs at normal cond-false exit (skipped by break) and
// targets an ENCLOSING loop. Mirrors cstage cmd/wcc/check.c:2494.
c.loops += 1;
if (n.body != nil) { resolvewalk(c, n.body); };
c.loops -= 1;
if (n.els != nil) { resolvewalk(c, n.els); };
return;
};
// `for (init; cond; post) body` / `for (cond) body` — the body is the
// break/continue target. Walk init/cond/post outside the loop count
// (they hold no statements), bump only around the body, and keep the
// `else` outside (it targets an enclosing loop, like N_FORRANGE above).
// Mirrors cstage cmd/wcc/check.c:2529 (N_FOR, c->loops++).
if (k == syntax.nkind.N_FOR) {
if (n.lhs != nil) { resolvewalk(c, n.lhs); };
if (n.cond != nil) { resolvewalk(c, n.cond); };
if (n.rhs != nil) { resolvewalk(c, n.rhs); };
c.loops += 1;
if (n.body != nil) { resolvewalk(c, n.body); };
c.loops -= 1;
if (n.els != nil) { resolvewalk(c, n.els); };
return;
};
// `break`/`continue` outside any enclosing for/for-range is an error
// in Hare (the reference), C, and Go. Align wwstage DOWN to cstage's
// rejection (rule 10). Mirrors cmd/wcc/check.c:2611-2615.
if (k == syntax.nkind.N_BREAK || k == syntax.nkind.N_CONTINUE) {
if (c.loops == 0) {
cerr(n.file);
cerr(":");
cerr(strconv.i32tos(n.line, strconv.base.DEC));
cerr(":");
cerr(strconv.i32tos(n.col, strconv.base.DEC));
cerr(": error: ");
if (k == syntax.nkind.N_BREAK) { cerr("break"); }
else { cerr("continue"); };
cerr(" outside loop\n");
c.errs += 1;
};
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 == syntax.nkind.N_MCASE) {
if (n.lhs != nil) { resolvewalk(c, n.lhs); };
// `case T1 | T2` alts ride n.list; each needs its type_
// stamp or cgmatch's tag lookup collapses to 0. Mirrors
// cmd/wcc/check.c:2140 (per-alt resolve).
let alt: *syntax.node = n.list;
for (alt != nil) {
resolvewalk(c, alt);
alt = alt.next;
};
let outer: *syntax.scope = c.cur;
c.cur = syntax.newscope(outer);
let nm: str = n.str;
if (nm.len > 0) {
checkmoduleshadow(c, nm, "binding");
syntax.scopedefine(c.cur, nm, syntax.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 == syntax.nkind.N_BLOCK) {
let outer: *syntax.scope = c.cur;
c.cur = syntax.newscope(outer);
let m: *syntax.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 == syntax.nkind.N_MLET) {
if (n.rhs != nil) { resolvewalk(c, n.rhs); };
let pt: *syntax.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: *syntax.node = exprtype(c, n.rhs, nil);
if (rty != nil) { if (rty.kind == syntax.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 == syntax.nkind.N_MASSIGN) {
if (n.rhs != nil) { resolvewalk(c, n.rhs); };
let pt: *syntax.node = nil;
// #242: consume the rhs tuple type for ANY rhs (see N_MLET).
if (n.rhs != nil) {
let rty: *syntax.node = exprtype(c, n.rhs, nil);
if (rty != nil && rty.kind == syntax.nkind.N_TTUPLE) {
pt = rty.list;
} else {
// #38/F2 (review item 39): the multi-assign rhs must be a
// tuple. cstage check.c:2562-2568 errors "multi-assign rhs
// is not a tuple (got %s)" when cexpr(rhs)->kind != TY_TUPLE
// — and, like ww's exprtype (N_CALL returns the decl's bare
// return tnode, :3319), it does NOT chase the NAMED wrapper,
// so a tuple-ALIAS return (`fn f() pair`) is rejected too.
// Without this ww distributed nil element widths and
// cgmassign silently dropped the str len/cap stores. ww's
// piecewise cerr can't splice the type spelling, so the
// "(got %s)" tail is omitted.
deffolderr(c, n, "multi-assign rhs is not a tuple");
};
};
let l: *syntax.node = n.list;
for (l != nil) { resolvewalk(c, l); l = l.next; };
stamptuplebinds(c, n.list, pt, false, "");
return;
};
if (k == syntax.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.
let _t: *syntax.node = exprtype(c, n, nil);
return;
};
if (k == syntax.nkind.N_FIELD) {
if (n.lhs != nil) { resolvewalk(c, n.lhs); };
return;
};
if (k == syntax.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: *syntax.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 == syntax.nkind.N_TNAME || k == syntax.nkind.N_TPTR ||
k == syntax.nkind.N_TSLICE || k == syntax.nkind.N_TCHAN ||
k == syntax.nkind.N_TBANG || k == syntax.nkind.N_TARRAY ||
k == syntax.nkind.N_TFN || k == syntax.nkind.N_TSTRUCT ||
k == syntax.nkind.N_TTUPLE || k == syntax.nkind.N_TTAGGED ||
k == syntax.nkind.N_TENUM) {
if (n.type_ == nil) {
let ti: *syntax.tinfo = tinfofornode(c, n);
if (ti != nil) { n.type_ = ti: *void; };
};
};
if (k == syntax.nkind.N_TENUM) { stampenumvals(c, n); validateenummembers(c, n); };
if (k == syntax.nkind.N_TSTRUCT) { validatestructfields(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 == syntax.nkind.N_LET) {
let nm: str = n.str;
if (nm.len > 0) {
checkmoduleshadow(c, nm, "let");
let s: *syntax.sym = syntax.scopedefine(c.cur, nm, syntax.skind.SK_VAR, nil, n);
// catB-22: flag a `const` binding so checkassign can
// reject a later reassignment. The parser stamps
// n.op = TK_CONST (parse/stmt.ww). Mirror cstage
// cmd/wcc/check.c:2408.
if (s != nil) {
if (n.op == syntax.tkind.TK_CONST) { s.is_const = 1i32; };
};
};
};
// #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 == syntax.nkind.N_LET) { coercefloatlit(c, n.rhs, n.lhs); };
if (k == syntax.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 == syntax.nkind.N_CALL) { desugarcallargs(c, n); };
if (k == syntax.nkind.N_ASSIGN) { checkassign(c, n); };
if (k == syntax.nkind.N_INTLIT || k == syntax.nkind.N_FLOATLIT ||
k == syntax.nkind.N_STRLIT || k == syntax.nkind.N_RUNELIT ||
k == syntax.nkind.N_TRUE || k == syntax.nkind.N_FALSE ||
k == syntax.nkind.N_NIL || k == syntax.nkind.N_VOIDLIT ||
k == syntax.nkind.N_IDENT || k == syntax.nkind.N_BIN ||
k == syntax.nkind.N_UN || k == syntax.nkind.N_CALL ||
k == syntax.nkind.N_INDEX || k == syntax.nkind.N_CAST ||
k == syntax.nkind.N_STRUCTLIT || k == syntax.nkind.N_ARRLIT ||
k == syntax.nkind.N_RECV ||
k == syntax.nkind.N_SLICE || k == syntax.nkind.N_SPREAD ||
k == syntax.nkind.N_TUPLE || k == syntax.nkind.N_TRYPROP ||
k == syntax.nkind.N_TRYUNW || k == syntax.nkind.N_TYPETEST ||
k == syntax.nkind.N_TYPEASSERT || k == syntax.nkind.N_YIELD ||
k == syntax.nkind.N_MATCH) {
let _t: *syntax.node = exprtype(c, n, nil);
};
};
// 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.
fn unwrapbang(n: *syntax.node) *syntax.node = {
if (n == nil) { return nil; };
if (n.kind == syntax.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: *syntax.node) *syntax.sym = {
if (n == nil) { return nil; };
if (n.kind != syntax.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: *syntax.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 = syntax.scopelookupinmodule(c.cur, modkeyfor(c, 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. The exprtype N_IDENT / N_DOT-callee
// + exprtypeoftry / &fn-synth bare-leaf callers now all use
// scopelookupprefer (the #56/#4/#11a wave). The only bare-leaf
// type lookups left — varianterr (:1051) + scruttype (:1098) —
// stay mod-blind but have no reproducible divergence; #58.
s = syntax.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 != syntax.skind.SK_TYPE) {
// #58/#50: prefer the curmod-matching SK_TYPE.
// Two modules exporting the same type leaf (e.g.
// utf8.invalid !void vs strconv.invalid !i32)
// otherwise resolve install-order-dependent here
// when a value binding shadows the leaf; cstage
// passes c->cur_mod to scope_lookup_type (sym.c).
let sm: *syntax.sym = syntax.scopelookuptype(c.cur, c.curmod, nm);
if (sm != nil) { s = sm; };
};
};
};
if (s == nil) { return nil; };
if (s.skind != syntax.skind.SK_TYPE) { return nil; };
return s;
};
fn resolvealias(c: *checker, n: *syntax.node) *syntax.node = {
let cur: *syntax.node = n;
for (cur != nil) {
if (cur.kind != syntax.nkind.N_TNAME) { return cur; };
let s: *syntax.sym = aliassym(c, cur);
if (s == nil) { return cur; };
let body: *syntax.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 fast-path, else by resolved-decl identity
// (the AST analog of cstage type.c:278 `TY_NAMED: a == b` and harec
// types.c:579 `STORAGE_ALIAS: ident_equal`). #14 B-full Layer 1:
// bare `oserror` vs qualified `os.oserror` resolve to the SAME
// SK_TYPE sym (aliassym maps both via #51/#53), so a cross-module
// nominal forward compares equal where the surface streq said false.
fn typeeqast(c: *checker, a: *syntax.node, b: *syntax.node) bool = {
let aa: *syntax.node = unwrapbang(a);
let bb: *syntax.node = unwrapbang(b);
if (aa == nil) { return bb == nil; };
if (bb == nil) { return false; };
// Identity fast-path, mirroring cstage type_eq's first line
// (cmd/wcc/type.c:250 `if (a == b) return 1`). Enum (and struct/
// array) type nodes are shared from their decl, so two references to
// the SAME `os.flag` resolve to one N_TENUM node; without this the
// catch-all below returns false and the #26 reject fires on a
// same-enum binop like `os.flag.WRONLY | os.flag.CREATE` (w6l), which
// cstage accepts via this identity check.
if (aa == bb) { return true; };
if (aa.kind != bb.kind) { return false; };
let k: syntax.nkind = aa.kind;
if (k == syntax.nkind.N_TNAME) {
if (syntax.streq(aa.str, bb.str)) { return true; };
let sa: *syntax.sym = aliassym(c, aa);
let sb: *syntax.sym = aliassym(c, bb);
if (sa != nil && sa == sb) { return true; };
return false;
};
if (k == syntax.nkind.N_TPTR) { return typeeqast(c, aa.lhs, bb.lhs); };
if (k == syntax.nkind.N_TSLICE){ return typeeqast(c, aa.lhs, bb.lhs); };
if (k == syntax.nkind.N_TCHAN) { return typeeqast(c, aa.lhs, bb.lhs); };
if (k == syntax.nkind.N_TFN) {
// Divergence: cstage type.c:239 compares resolved Type; we
// compare AST. See #178.
if (!typeeqast(c, aa.lhs, bb.lhs)) { return false; };
let pa: *syntax.node = aa.list;
let pb: *syntax.node = bb.list;
for (pa != nil) {
if (pb == nil) { return false; };
let ac: bool = syntax.streq(pa.str, "...");
let bc: bool = syntax.streq(pb.str, "...");
if (ac != bc) { return false; };
if (!ac) {
let va: bool = pa.op == syntax.tkind.TK_ELLIPSIS;
let vb: bool = pb.op == syntax.tkind.TK_ELLIPSIS;
if (va != vb) { return false; };
// installparams normalizes a DECL's `T...` param lhs to
// a marked []T in place; a fn TYPE expr stays surface.
// Peel exactly the marked wrapper so both sides compare
// at the declared element type (cstage's type_eq sees
// type_slice on BOTH sides, check.c:908-917).
let la: *syntax.node = pa.lhs;
let lb: *syntax.node = pb.lhs;
if (la != nil && la.kind == syntax.nkind.N_TSLICE
&& la.op == syntax.tkind.TK_ELLIPSIS) { la = la.lhs; };
if (lb != nil && lb.kind == syntax.nkind.N_TSLICE
&& lb.op == syntax.tkind.TK_ELLIPSIS) { lb = lb.lhs; };
if (!typeeqast(c, la, lb)) { 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 == syntax.nkind.N_TTUPLE) {
let pa: *syntax.node = aa.list;
let pb: *syntax.node = bb.list;
for (pa != nil) {
if (pb == nil) { return false; };
if (!typeeqast(c, 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 == syntax.nkind.N_TTAGGED) {
let pa: *syntax.node = aa.list;
let pb: *syntax.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 == syntax.tkind.TK_ELLIPSIS) { return false; };
if (pb.op == syntax.tkind.TK_ELLIPSIS) { return false; };
if (!typeeqast(c, 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: *syntax.node) bool = {
if (v == nil) { return false; };
if (v.kind == syntax.nkind.N_TBANG) { return true; };
if (v.kind == syntax.nkind.N_TNAME) {
// #58: mod-blind by leaf — latent. A same-leaf error-vs-plain
// type pair across two modules could in principle flip iserror,
// but the union's variants resolve at its declaration before
// varianterr runs, so no repro exists. Tracked: #58.
let s: *syntax.sym = syntax.scopelookup(c.cur, v.str);
if (s != nil) {
if (s.skind == syntax.skind.SK_TYPE) {
if (s.decl != nil) {
if (s.decl.lhs != nil) {
if (s.decl.lhs.kind == syntax.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: *syntax.node) bool = {
let v: *syntax.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: *syntax.node, v: *syntax.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: *syntax.node) *syntax.node = {
if (e == nil) { return nil; };
if (e.kind == syntax.nkind.N_IDENT) {
// #58: mod-blind by leaf — lenient-only. Feeds match
// exhaustiveness; codegen reads the stamped n.type_, so a
// mis-resolution cannot drive a wrong binary (no repro).
// Tracked: #58.
let s: *syntax.sym = syntax.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 == syntax.nkind.N_DOT) {
if (e.lhs == nil) { return nil; };
if (e.lhs.kind != syntax.nkind.N_IDENT) { return nil; };
let s: *syntax.sym = syntax.scopelookupinmodule(c.cur, modkeyfor(c, e.lhs.str), e.str);
if (s == nil) { return nil; };
if (s.decl == nil) { return nil; };
return s.decl.lhs;
};
return nil;
};
fn mktname(c: *checker, nm: str) *syntax.node = {
let n: *syntax.node = syntax.newnode(syntax.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 (syntax.streq(nm, "void")) { return 0i64; };
if (syntax.streq(nm, "bool")) { return 1i64; };
if (syntax.streq(nm, "i8") || syntax.streq(nm, "u8")) { return 1i64; };
if (syntax.streq(nm, "i16") || syntax.streq(nm, "u16")) { return 2i64; };
if (syntax.streq(nm, "i32") || syntax.streq(nm, "u32") || syntax.streq(nm, "f32") || syntax.streq(nm, "rune")) { return 4i64; };
if (syntax.streq(nm, "i64") || syntax.streq(nm, "u64") || syntax.streq(nm, "f64")) { return 8i64; };
if (syntax.streq(nm, "int") || syntax.streq(nm, "uint") || syntax.streq(nm, "uintptr") || syntax.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 (syntax.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; };
// #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: *syntax.node) i64 = {
if (t == nil) { return 1i64; };
let k: syntax.nkind = t.kind;
if (k == syntax.nkind.N_TBANG) { return astalign(c, t.lhs); };
if (k == syntax.nkind.N_TPTR) { return 8i64; };
if (k == syntax.nkind.N_TSLICE) { return 8i64; };
if (k == syntax.nkind.N_TCHAN) { return 8i64; };
if (k == syntax.nkind.N_TFN) { return 8i64; };
if (k == syntax.nkind.N_TARRAY) { return astalign(c, t.lhs); };
if (k == syntax.nkind.N_TTAGGED) {
// Route through the normalization SSoT: a lone survivor
// collapses (align((i32|never))==align(i32)==4, not the tag
// word's 8), matching cstage align() reading resolve_type(...)
// ->align (cmd/wcc/check.c:1550). Same #1 family as astsize.
let ti: *syntax.tinfo = tinfofornode(c, t);
if (ti != nil) { return ti.align: i64; };
return 8i64;
};
if (k == syntax.nkind.N_TTUPLE) {
let m: i64 = 1i64;
let p: *syntax.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 == syntax.nkind.N_TSTRUCT) {
let m: i64 = 1i64;
let f: *syntax.node = t.list;
for (f != nil) {
if (f.kind == syntax.nkind.N_TFIELD) {
let fa: i64 = astalign(c, f.lhs);
if (fa > m) { m = fa; };
};
f = f.next;
};
return m;
};
if (k == syntax.nkind.N_TENUM) {
if (t.lhs != nil) { return astalign(c, t.lhs); };
return 4i64;
};
if (k == syntax.nkind.N_TNAME) {
let nm: str = t.str;
if (syntax.streq(nm, "void") || syntax.streq(nm, "bool") || syntax.streq(nm, "i8") || syntax.streq(nm, "u8")) { return 1i64; };
if (syntax.streq(nm, "i16") || syntax.streq(nm, "u16")) { return 2i64; };
if (syntax.streq(nm, "i32") || syntax.streq(nm, "u32") || syntax.streq(nm, "f32") || syntax.streq(nm, "rune")) { return 4i64; };
if (syntax.streq(nm, "i64") || syntax.streq(nm, "u64") || syntax.streq(nm, "f64") || syntax.streq(nm, "int") || syntax.streq(nm, "uint") || syntax.streq(nm, "uintptr") || syntax.streq(nm, "size") || syntax.streq(nm, "str")) { return 8i64; };
let resolved: *syntax.node = resolvealias(c, t);
if (resolved != nil && resolved != t) {
return astalign(c, resolved);
};
};
return 1i64;
};
fn astsize(c: *checker, t: *syntax.node) i64 = {
if (t == nil) { return 0i64; };
let k: syntax.nkind = t.kind;
if (k == syntax.nkind.N_TBANG) { return astsize(c, t.lhs); };
if (k == syntax.nkind.N_TPTR) { return 8i64; };
if (k == syntax.nkind.N_TSLICE) { return tyslicesize(); };
if (k == syntax.nkind.N_TCHAN) { return 8i64; };
if (k == syntax.nkind.N_TFN) { return 8i64; };
if (k == syntax.nkind.N_TARRAY) {
// #141: a def-dimensioned field array sized to 0 here, so the
// struct loop (off += astsize(field)) overlapped the next
// field; arrayelen folds the def.
let elen: i64 = arrayelen(c, t.rhs): i64;
return astsize(c, t.lhs) * elen;
};
if (k == syntax.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: *syntax.tinfo = tinfofornode(c, t);
if (ti != nil) { return ti.size: i64; };
return 0i64;
};
if (k == syntax.nkind.N_TSTRUCT) {
let off: i64 = 0i64;
let maxal: i64 = 1i64;
let f: *syntax.node = t.list;
for (f != nil) {
if (f.kind == syntax.nkind.N_TFIELD) {
let fa: i64 = astalign(c, f.lhs);
if (fa > maxal) { maxal = fa; };
// packed: no inter-field padding (harec
// type_store.c:206-213); align value unchanged.
if (t.packed == 0) {
off = (off + fa - 1i64) & ~(fa - 1i64);
};
off += astsize(c, f.lhs);
};
f = f.next;
};
// packed: skip trailing pad-to-align (harec type_store.c:886).
if (t.packed != 0) { return off; };
return (off + maxal - 1i64) & ~(maxal - 1i64);
};
if (k == syntax.nkind.N_TTAGGED) {
// Route through tinfofornode (the tagged-normalization SSoT)
// so the size() fold matches cgen's layout AND cstage: the raw
// 8+roundup8(max) here ignored never-drop / dedup / single-
// collapse / nullable, so size((*u8|void)) folded 16 not 8 and
// size((i32|never)) 16 not 4 (#1). Mirrors the N_TTUPLE arm
// above and cstage size() reading resolve_type(...)->size
// (cmd/wcc/check.c:1547-1550).
let ti: *syntax.tinfo = tinfofornode(c, t);
if (ti != nil) { return ti.size: i64; };
return 0i64;
};
if (k == syntax.nkind.N_TENUM) {
if (t.lhs != nil) { return astsize(c, t.lhs); };
return 4i64;
};
if (k == syntax.nkind.N_TNAME) {
let nm: str = t.str;
let ps: i64 = primtypesize(nm);
if (ps >= 0i64) { return ps; };
let resolved: *syntax.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: *syntax.node) bool = {
if (t == nil) { return false; };
let u: *syntax.node = resolvealias(c, unwrapbang(t));
if (u == nil) { return false; };
let k: syntax.nkind = u.kind;
if (k == syntax.nkind.N_TNAME) {
if (syntax.streq(u.str, "opaque")) { return true; };
return false;
};
if (k == syntax.nkind.N_TARRAY) { return astunsized(c, u.lhs); };
if (k == syntax.nkind.N_TTUPLE) {
let p: *syntax.node = u.list;
for (p != nil) {
if (astunsized(c, p.lhs)) { return true; };
p = p.next;
};
return false;
};
if (k == syntax.nkind.N_TSTRUCT) {
let f: *syntax.node = u.list;
for (f != nil) {
if (f.kind == syntax.nkind.N_TFIELD) {
if (astunsized(c, f.lhs)) { return true; };
};
f = f.next;
};
return false;
};
if (k == syntax.nkind.N_TTAGGED) {
let v: *syntax.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: *syntax.node, bname: str, btype: *syntax.node,
nodeout: **syntax.node) *syntax.tinfo = {
if (body == nil) { return nil; };
let k: syntax.nkind = body.kind;
if (k == syntax.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 == syntax.nkind.N_IDENT && bname.len > 0
&& syntax.streq(body.lhs.str, bname)) {
*nodeout = btype;
};
return body.lhs.type_: *syntax.tinfo;
};
let t: *syntax.node = exprtype(c, body.lhs, nil);
if (t != nil) {
*nodeout = t;
return tinfofornode(c, t);
};
if (body.lhs.kind == syntax.nkind.N_IDENT && bname.len > 0
&& syntax.streq(body.lhs.str, bname)) {
*nodeout = btype;
return tinfofornode(c, btype);
};
return nil;
};
if (k == syntax.nkind.N_MATCH) { return nil; };
if (k == syntax.nkind.N_BLOCK) {
let s: *syntax.node = body.list;
for (s != nil) {
let t: *syntax.tinfo = matchyieldtype(c, s, bname, btype, nodeout);
if (t != nil) { return t; };
s = s.next;
};
return nil;
};
if (k == syntax.nkind.N_IF) {
let t: *syntax.tinfo = matchyieldtype(c, body.body, bname, btype, nodeout);
if (t != nil) { return t; };
return matchyieldtype(c, body.els, bname, btype, nodeout);
};
if (k == syntax.nkind.N_FOR || k == syntax.nkind.N_FORRANGE) {
return matchyieldtype(c, body.body, bname, btype, nodeout);
};
return nil;
};
// yieldclass — #38/F2 (review item 6): the coarse assignability family of a
// match-arm yield type, used to approximate cstage's type_assignable in the
// cross-arm unification (ww has tinfo typeeq but no tinfo type_assignable).
// 1=numeric (int/float/enum/rune + untyped_int/float/rune, NAMED-chased),
// 2=str (str + untyped_str), 3=bool, 0=unknown/other (ptr/struct/tuple/slice/
// tagged/...). Class 0 stays LENIENT so the unification rejects only a DEFINITE
// family mismatch (the catA repro: an int arm vs a str arm) and never an
// untyped->concrete promotion (untyped_int vs i32/f64 both land in class 1)
// that cstage accepts. The families mirror typeisnum/typeisstr (lib/ww/typ.ww).
fn yieldclass(t: *syntax.tinfo) i32 = {
let u: *syntax.tinfo = tichase(t);
if (u == nil) { return 0i32; };
if (syntax.typeisnum(u)) { return 1i32; };
if (syntax.typeisstr(u)) { return 2i32; };
if (u.kind == syntax.tykind.TY_BOOL) { return 3i32; };
if (u.kind == syntax.tykind.TY_UNTYPED_BOOL) { return 3i32; };
return 0i32;
};
// aststructoffset — byte offset of `name` inside struct AST `stn`,
// walking fields with per-field alignment and descending embeds
// (#59.13: promoted names live at base + inner offset; cstage folds
// offset() over the flattened tinfo, check.c:1691-1698, so it is
// embed-transparent there). -1 on a miss.
fn aststructoffset(c: *checker, stn: *syntax.node, name: str, depth: i32) i64 = {
if (depth > EMBEDDEPTHMAX) { return -1i64; };
let off: i64 = 0i64;
let f: *syntax.node = stn.list;
for (f != nil) {
if (f.kind == syntax.nkind.N_TFIELD) {
let fa: i64 = astalign(c, f.lhs);
// packed: no inter-field padding (harec type_store.c:206-213).
if (stn.packed == 0) {
off = (off + fa - 1i64) & ~(fa - 1i64);
};
if (f.str.len != 0) {
if (syntax.streq(f.str, name)) { return off; };
} else {
let inner: *syntax.node = structembedbody(c, f.lhs);
if (inner != nil) {
let r: i64 = aststructoffset(c, inner, name, depth + 1);
if (r >= 0i64) { return off + r; };
};
};
off += astsize(c, f.lhs);
};
f = f.next;
};
return -1i64;
};
// aststructfieldtype — the declared type expr of `name` inside struct
// AST `stn`, descending embeds (#59.13: a promoted name resolves to
// the inner field's own type node — the exact node the non-embed walk
// would have returned had the field been declared inline). nil on a
// miss. Shared by the exprtype N_DOT struct arm and the #251
// struct-lit field walk.
fn aststructfieldtype(c: *checker, stn: *syntax.node, name: str, depth: i32) *syntax.node = {
if (depth > EMBEDDEPTHMAX) { return nil; };
let f: *syntax.node = stn.list;
for (f != nil) {
if (f.kind == syntax.nkind.N_TFIELD) {
if (f.str.len != 0) {
if (syntax.streq(f.str, name)) { return f.lhs; };
} else {
let inner: *syntax.node = structembedbody(c, f.lhs);
if (inner != nil) {
let r: *syntax.node = aststructfieldtype(c, inner, name, depth + 1);
if (r != nil) { return r; };
};
};
};
f = f.next;
};
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: *syntax.node) i64 = {
if (dot == nil) { return -1i64; };
if (dot.kind != syntax.nkind.N_DOT) { return -1i64; };
let recv: *syntax.node = scruttype(c, dot.lhs);
if (recv == nil) { return -1i64; };
let rtyp: *syntax.node = resolvealias(c, unwrapbang(recv));
if (rtyp == nil) { return -1i64; };
if (rtyp.kind == syntax.nkind.N_TPTR) {
rtyp = resolvealias(c, unwrapbang(rtyp.lhs));
};
if (rtyp == nil) { return -1i64; };
if (rtyp.kind != syntax.nkind.N_TSTRUCT) { return -1i64; };
return aststructoffset(c, rtyp, dot.str, 0);
};
// 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: *syntax.node, v: i64) void = {
n.kind = syntax.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: syntax.tkind, a: u64, b: u64, out: *u64) bool = {
if (op == syntax.tkind.TK_PLUS) { *out = a + b; return true; };
if (op == syntax.tkind.TK_MINUS) { *out = a - b; return true; };
if (op == syntax.tkind.TK_STAR) { *out = a * b; return true; };
if (op == syntax.tkind.TK_SLASH) {
if (b == 0u64) { return false; };
*out = a / b; return true;
};
if (op == syntax.tkind.TK_PERCENT) {
if (b == 0u64) { return false; };
*out = a % b; return true;
};
if (op == syntax.tkind.TK_AMP) { *out = a & b; return true; };
if (op == syntax.tkind.TK_PIPE) { *out = a | b; return true; };
if (op == syntax.tkind.TK_CARET) { *out = a ^ b; return true; };
if (op == syntax.tkind.TK_LSHIFT) { *out = a << b; return true; };
if (op == syntax.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: *syntax.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: *syntax.tinfo, v: u64) bool = {
if (!syntax.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 (syntax.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: *syntax.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: syntax.nkind = n.kind;
if (k == syntax.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 == syntax.tkind.TK_SLASH || n.op == syntax.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 == syntax.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 == syntax.tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; };
if (n.op == syntax.tkind.TK_TILDE) { *out = ~v; return true; };
if (n.op == syntax.tkind.TK_PLUS) { *out = v; return true; };
deffolderr(c, n, "def value: unsupported unary op");
return false;
};
if (k == syntax.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: *syntax.tinfo = (n.type_): *syntax.tinfo;
if (!defcastfits(t, v)) {
deffolderr(c, n, "def value: narrowing cast loses value");
return false;
};
*out = v;
return true;
};
if (k == syntax.nkind.N_IDENT) {
let s: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, n.str);
if (s == nil) { return false; };
if (s.skind != syntax.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 == syntax.nkind.N_DOT) {
if (n.lhs == nil) { return false; };
if (n.lhs.kind != syntax.nkind.N_IDENT) { return false; };
let s: *syntax.sym = syntax.scopelookupinmodule(c.cur, modkeyfor(c, n.lhs.str), n.str);
if (s == nil) { return false; };
if (s.skind != syntax.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: *syntax.node, v: u64) void = {
n.kind = syntax.nkind.N_INTLIT;
n.uval = v;
n.op = syntax.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).
};
// arrayelen — an array type's dimension as an element count. An
// N_INTLIT yields its uval; a def-ref or const-expr dim (`[MAX]u8`,
// MAX a def) folds through evaldefconst (#141, ken oracle); a nil
// child (the `[_]T` inferred-length sentinel) or non-const rhs gives
// 0. cstage carries the folded length in the resolved Type, but
// wwstage computes size lazily on independent paths (no AST-stamp
// SSoT), so every N_TARRAY length reader routes here to fold the dim
// identically — astsize (struct layout), tinfofornode (canonical
// tinfo), checkarrlitfits (count gate).
fn arrayelen(c: *checker, rhs: *syntax.node) u64 = {
if (rhs == nil) { return 0u64; };
if (rhs.kind == syntax.nkind.N_INTLIT) { return rhs.uval; };
let v: u64 = 0u64;
if (evaldefconst(c, rhs, &v, 0)) { return v; };
return 0u64;
};
// 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: *syntax.node, until: *syntax.node, e: *syntax.node, out: *u64) bool = {
if (e == nil) { return false; };
let k: syntax.nkind = e.kind;
if (k == syntax.nkind.N_INTLIT) { *out = e.uval; return true; };
if (k == syntax.nkind.N_RUNELIT) { *out = e.uval; return true; };
if (k == syntax.nkind.N_TRUE) { *out = 1u64; return true; };
if (k == syntax.nkind.N_FALSE) { *out = 0u64; return true; };
if (k == syntax.nkind.N_NIL) { *out = 0u64; return true; };
if (k == syntax.nkind.N_UN) {
let v: u64 = 0u64;
if (!enumvalfold(body, until, e.lhs, &v)) { return false; };
let op: syntax.tkind = e.op;
if (op == syntax.tkind.TK_MINUS) { *out = (-(v: i64)): u64; return true; };
if (op == syntax.tkind.TK_TILDE) { *out = ~v; return true; };
if (op == syntax.tkind.TK_PLUS) { *out = v; return true; };
return false;
};
if (k == syntax.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 == syntax.nkind.N_IDENT) {
let prev: u64 = (-1i64): u64;
let m: *syntax.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 (syntax.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: *syntax.node) void = {
let under: *syntax.tinfo = c.tc.tyi32;
if (n.lhs != nil) {
let s: *syntax.tinfo = tinfofornode(c, n.lhs);
if (s != nil) { under = s; };
};
let m: *syntax.node = n.list;
for (m != nil) {
stampnilexpr(m.lhs, under);
m = m.next;
};
};
// structembedbody — resolve an embed member's type expr to its struct
// body AST, or nil (non-struct embed; the caller owns the diagnostic).
fn structembedbody(c: *checker, t: *syntax.node) *syntax.node = {
let u: *syntax.node = resolvealias(c, unwrapbang(t));
if (u == nil) { return nil; };
if (u.kind != syntax.nkind.N_TSTRUCT) { return nil; };
return u;
};
// Embed-descend depth cap (#59.13). Embed cycles are loud-rejected by
// circularnamed at the tinfo flatten; the AST diagnostic/lookup walks
// only have to TERMINATE on them, not report them twice.
def EMBEDDEPTHMAX: i32 = 32;
// structhasfield — does struct AST `stn` declare `name`, directly or
// promoted through an embed?
fn structhasfield(c: *checker, stn: *syntax.node, name: str, depth: i32) bool = {
if (depth > EMBEDDEPTHMAX) { return false; };
let f: *syntax.node = stn.list;
for (f != nil) {
if (f.kind == syntax.nkind.N_TFIELD) {
if (f.str.len != 0) {
if (syntax.streq(f.str, name)) { return true; };
} else {
let inner: *syntax.node = structembedbody(c, f.lhs);
if (inner != nil) {
if (structhasfield(c, inner, name, depth + 1)) { return true; };
};
};
};
f = f.next;
};
return false;
};
// earlierhasfield — does any field of `outer` declared BEFORE `upto`
// carry `name`, directly or promoted through an embed? The "existing
// field" set of cstage check.c:945-950 / :972-980.
fn earlierhasfield(c: *checker, outer: *syntax.node, upto: *syntax.node, name: str) bool = {
let e: *syntax.node = outer.list;
for (e != upto) {
if (e.kind == syntax.nkind.N_TFIELD) {
if (e.str.len != 0) {
if (syntax.streq(e.str, name)) { return true; };
} else {
let inner: *syntax.node = structembedbody(c, e.lhs);
if (inner != nil) {
if (structhasfield(c, inner, name, 0)) { return true; };
};
};
};
e = e.next;
};
return false;
};
// embedcollides — err for every name `inner` promotes into `outer`
// that a field before `upto` already declares. Mirrors cstage
// check.c:971-980.
fn embedcollides(c: *checker, outer: *syntax.node, upto: *syntax.node, inner: *syntax.node, depth: i32) void = {
if (depth > EMBEDDEPTHMAX) { return; };
let f: *syntax.node = inner.list;
for (f != nil) {
if (f.kind == syntax.nkind.N_TFIELD) {
if (f.str.len != 0) {
if (earlierhasfield(c, outer, upto, f.str)) {
cerr(upto.file);
cerr(": error: embedded field '");
cerr(f.str);
cerr("' collides with existing field\n");
c.errs += 1;
};
} else {
let deeper: *syntax.node = structembedbody(c, f.lhs);
if (deeper != nil) {
embedcollides(c, outer, upto, deeper, depth + 1);
};
};
};
f = f.next;
};
};
// validatestructfields — reject a struct decl carrying two fields with
// the same name, and the two invalid embed shapes (#59.13: a non-struct
// embed; an embed whose promoted name collides with an existing field).
// Pure diagnostic: a read-only walk, no n.type_ / offset / checker-state
// mutation (the byte-id safety condition — valid programs have no dup,
// so codegen is untouched). Cstage twin: cmd/wcc/check.c:943-950 (named
// arm) + :961-985 (embed arm) of resolve_type's N_TSTRUCT; cstage errs
// inside the flatten, ww keeps the flatten silent because it re-runs
// per size/align query. Fires once per struct decl from resolvewalk's
// eager type-decl dispatch (rule-10 symmetric with cstage's
// once-per-resolve_type), not from the per-query size/align/offset
// recompute arms (use-site = double-fire, drew-dv ruling).
fn validatestructfields(c: *checker, n: *syntax.node) void = {
let f: *syntax.node = n.list;
for (f != nil) {
if (f.kind == syntax.nkind.N_TFIELD) {
if (f.str.len != 0) {
if (earlierhasfield(c, n, f, f.str)) {
cerr(f.file);
cerr(": error: duplicate field '");
cerr(f.str);
cerr("'\n");
c.errs += 1;
};
} else {
let inner: *syntax.node = structembedbody(c, f.lhs);
if (inner == nil) {
cerr(f.file);
cerr(": error: embedded type must be a struct\n");
c.errs += 1;
} else {
embedcollides(c, n, f, inner, 0);
};
};
};
f = f.next;
};
};
// validateenummembers — reject the three invalid enum-decl shapes
// cstage rejects at cmd/wcc/check.c:1000-1042 (the N_TENUM arm of
// resolve_type): a non-integer storage type, a duplicate member name,
// and an unfoldable (non-constant) member value-expr. Pure diagnostic:
// it mutates no n.type_ / member value / checker state beyond bumping
// c.errs, so valid programs (integer storage, distinct members,
// foldable values) emit nothing and their stamping/codegen is untouched
// — the byte-id safety condition. Fires once per enum decl from
// resolvewalk's eager type-decl dispatch, sibling to validatestructfields
// (rule-10 symmetric with cstage's once-per-resolve_type), not from the
// per-query size/align arms.
fn validateenummembers(c: *checker, n: *syntax.node) void = {
// storage type: cstage resolves n->lhs then gates type_isint
// (check.c:1004-1009); default storage is i32, always integer.
if (n.lhs != nil) {
let s: *syntax.tinfo = tinfofornode(c, n.lhs);
if (!syntax.typeisint(s)) {
cerr(n.lhs.file);
cerr(": error: enum storage type must be integer\n");
c.errs += 1;
};
};
let m: *syntax.node = n.list;
for (m != nil) {
if (m.kind == syntax.nkind.N_TENUMMEMBER && m.str.len != 0) {
// duplicate member: cstage compares each member against
// the earlier ones (check.c:1024-1032).
let e: *syntax.node = n.list;
for (e != m) {
if (e.kind == syntax.nkind.N_TENUMMEMBER
&& e.str.len != 0
&& syntax.streq(e.str, m.str)) {
cerr(m.file);
cerr(": error: duplicate enum member '");
cerr(m.str);
cerr("'\n");
c.errs += 1;
break;
};
e = e.next;
};
};
// unfoldable value: cstage delegates to eval_enum_value
// (check.c:1020), which emits a SHAPE-SPECIFIC reason —
// "enum value: unknown identifier 'B'" for a forward sibling
// ref (check.c:316), "...division by zero" (327), "...unsupported
// binary/unary op" (330/343), else the generic constant-expr
// message (349). enumvalfold returns a bare bool (no reason), so
// wwstage collapses all of these to the generic "enum value must
// be a constant integer expression". Both stages REJECT (errs
// counted, build gated) → functionally symmetric, no asm impact;
// the message-specificity gap is the lone divergence, retained
// and tracked as task #10 (rule-7: documented, not silent).
// `until = m` enforces harec's forward-only sibling-ref discipline.
if (m.lhs != nil) {
let v: u64 = 0u64;
if (!enumvalfold(n, m, m.lhs, &v)) {
cerr(m.file);
cerr(": error: enum value must be a constant integer expression\n");
c.errs += 1;
};
};
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: *syntax.node, ti: *syntax.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: *syntax.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: *syntax.tinfo = pt;
t = tichase(t);
if (t == nil) { return 8u64; };
let pk: syntax.tykind = t.kind;
if (pk == syntax.tykind.TY_VOID) { return 0u64; };
if (pk == syntax.tykind.TY_STR) { return t.size; };
if (pk == syntax.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 == syntax.tykind.TY_TAGGED) { return (t.size + 7u64) & ~7u64; };
if (pk == syntax.tykind.TY_PTR || pk == syntax.tykind.TY_FN ||
pk == syntax.tykind.TY_CHAN || pk == syntax.tykind.TY_I64 ||
pk == syntax.tykind.TY_U64 || pk == syntax.tykind.TY_INT ||
pk == syntax.tykind.TY_UINT || pk == syntax.tykind.TY_UINTPTR ||
pk == syntax.tykind.TY_SIZE || pk == syntax.tykind.TY_F64) { return 8u64; };
if (pk == syntax.tykind.TY_BOOL || pk == syntax.tykind.TY_RUNE ||
pk == syntax.tykind.TY_I8 || pk == syntax.tykind.TY_I16 ||
pk == syntax.tykind.TY_I32 || pk == syntax.tykind.TY_U8 ||
pk == syntax.tykind.TY_U16 || pk == syntax.tykind.TY_U32 ||
pk == syntax.tykind.TY_F32 || pk == syntax.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: *syntax.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: *syntax.tinfo = ft;
t = tichase(t);
if (t == nil) { return 8u64; };
let fk: syntax.tykind = t.kind;
if (fk == syntax.tykind.TY_STRUCT) { return t.slotsize; };
if (fk == syntax.tykind.TY_ARRAY) { return t.slotsize; };
if (fk == syntax.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 == syntax.tykind.TY_SLICE) { return t.size; };
if (fk == syntax.tykind.TY_PTR || fk == syntax.tykind.TY_FN ||
fk == syntax.tykind.TY_CHAN) { return 8u64; };
if (fk == syntax.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 == syntax.tykind.TY_TUPLE) { return t.slotsize; };
// Primitives keep natural width inside structs (matches
// cgenutil fieldsize: primsize, not pad-to-8).
if (fk == syntax.tykind.TY_BOOL || fk == syntax.tykind.TY_RUNE ||
fk == syntax.tykind.TY_I8 || fk == syntax.tykind.TY_I16 ||
fk == syntax.tykind.TY_I32 || fk == syntax.tykind.TY_I64 ||
fk == syntax.tykind.TY_U8 || fk == syntax.tykind.TY_U16 ||
fk == syntax.tykind.TY_U32 || fk == syntax.tykind.TY_U64 ||
fk == syntax.tykind.TY_INT || fk == syntax.tykind.TY_UINT ||
fk == syntax.tykind.TY_UINTPTR || fk == syntax.tykind.TY_SIZE ||
fk == syntax.tykind.TY_F32 || fk == syntax.tykind.TY_F64 ||
fk == syntax.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.
// variantpresent — tagged-union dedup predicate. Mirrors cstage
// variant_present/variant_match (cmd/wcc/check.c:111-126): NAMED types
// are nominal (pointer-identical), everything else structural — exactly
// typeeq's contract (TY_NAMED → only same ptr, else structural; lib/ww/
// typ.ww:581). nil guards match variant_match's `a==NULL||b==NULL → 0`
// so two unresolved variants never collapse.
fn variantpresent(head: *syntax.tparam, vt: *syntax.tinfo) bool = {
if (vt == nil) { return false; };
let p: *syntax.tparam = head;
for (p != nil) {
if (p.type_ != nil) {
if (syntax.typeeq(p.type_, vt)) { return true; };
};
p = p.tnext;
};
return false;
};
fn tinfofornode(c: *checker, n: *syntax.node) *syntax.tinfo = {
if (n == nil) { return nil; };
let cached: *syntax.tinfo = syntax.tinfocachelookup(c.tc, n);
if (cached != nil) { return cached; };
let r: *syntax.tinfo = nil;
let k: syntax.nkind = n.kind;
switch (k) {
case syntax.nkind.N_TNAME:
let nm: str = n.str;
if (syntax.streq(nm, "void")) { r = c.tc.tyvoid; };
if (syntax.streq(nm, "bool")) { r = c.tc.tybool; };
if (syntax.streq(nm, "rune")) { r = c.tc.tyrune; };
if (syntax.streq(nm, "i8")) { r = c.tc.tyi8; };
if (syntax.streq(nm, "i16")) { r = c.tc.tyi16; };
if (syntax.streq(nm, "i32")) { r = c.tc.tyi32; };
if (syntax.streq(nm, "i64")) { r = c.tc.tyi64; };
if (syntax.streq(nm, "u8")) { r = c.tc.tyu8; };
if (syntax.streq(nm, "u16")) { r = c.tc.tyu16; };
if (syntax.streq(nm, "u32")) { r = c.tc.tyu32; };
if (syntax.streq(nm, "u64")) { r = c.tc.tyu64; };
if (syntax.streq(nm, "int")) { r = c.tc.tyint; };
if (syntax.streq(nm, "uint")) { r = c.tc.tyuint; };
if (syntax.streq(nm, "uintptr")) { r = c.tc.tyuintptr; };
if (syntax.streq(nm, "size")) { r = c.tc.tysize; }; // #85 fold-2
if (syntax.streq(nm, "opaque")) { r = c.tc.tyopaque; }; // #108(a)
if (syntax.streq(nm, "f32")) { r = c.tc.tyf32; };
if (syntax.streq(nm, "f64")) { r = c.tc.tyf64; };
if (syntax.streq(nm, "str")) { r = c.tc.tystr; };
if (syntax.streq(nm, "never")) { r = c.tc.tynever; };
if (syntax.streq(nm, "untyped_int")) { r = c.tc.tyuntypedint; };
if (syntax.streq(nm, "untyped_float")) { r = c.tc.tyuntypedfloat; };
if (syntax.streq(nm, "untyped_str")) { r = c.tc.tyuntypedstr; };
if (syntax.streq(nm, "untyped_rune")) { r = c.tc.tyuntypedrune; };
if (syntax.streq(nm, "untyped_bool")) { r = c.tc.tyuntypedbool; };
if (syntax.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: *syntax.sym = aliassym(c, n);
if (s != nil) {
if (s.type_ != nil) {
r = s.type_;
} else {
let body: *syntax.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: *syntax.tinfo = syntax.typenamed(s.name, nil);
s.type_ = named;
named.resolving = 1;
let under: *syntax.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;
named.under = under;
if (under != nil) {
named.size = under.size;
named.align = under.align;
named.slotsize = under.slotsize;
};
r = named;
};
};
};
};
case syntax.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 syntax.nkind.N_TPTR:
r = syntax.typeptr(tinfofornode(c, n.lhs));
case syntax.nkind.N_TSLICE:
r = syntax.typeslice(tinfofornode(c, n.lhs));
case syntax.nkind.N_TCHAN:
r = syntax.typechan(tinfofornode(c, n.lhs));
case syntax.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.
// #38/F2 (review item 2): a non-const, non-`[_]T` dimension is
// LOUD here, mirroring cstage resolve_type N_TARRAY
// (cmd/wcc/check.c:715 "array length must be an integer literal").
// arrayelen folds 0 for both the `[_]T` sentinel AND a runtime
// dim, so the size-walker reader (astsize, via arrayelen) can't
// tell them apart; resolve the type HERE — the one resolve seam
// cstage gates at — so c.errs trips before any 0-sized slot ships.
// The fold is inlined (not via arrayelen) to error exactly once:
// evaldefconst itself reports div-by-zero / unsupported-op, so a
// guard that re-folds would double-report. arrayelen stays the
// fold SSoT for astsize's later size() read.
let elen: u64 = 0u64; // #141: fold def-dim
if (n.rhs != nil) {
if (n.rhs.kind == syntax.nkind.N_INTLIT) {
elen = n.rhs.uval;
} else {
let v: u64 = 0u64;
if (evaldefconst(c, n.rhs, &v, 0)) {
elen = v;
} else {
cerr(n.file); cerr(": ");
cerr("error: array length must be an integer literal\n");
c.errs += 1;
};
};
};
let sub: *syntax.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 = syntax.typearray(sub, elen);
case syntax.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 = syntax.newtype(syntax.tykind.TY_FN);
r.size = 8u64;
r.align = 8u64;
r.slotsize = 8u64;
syntax.tinfocachebind(c.tc, n, r);
r.ret = tinfofornode(c, n.lhs);
// Params must be populated (cstage check.c N_TFN builds the
// Tparam chain): with params nil on every fn tinfo, typeeq saw
// any two same-return fn types equal, so tagged-union dedup
// collapsed `(*fn(A) T | *fn(B) T)` to a bare 8B pointer and
// match read the pointer word as a tag.
{
let fphead: *syntax.tparam = nil;
let fptail: *syntax.tparam = nil;
let fpn: *syntax.node = n.list;
for (fpn != nil) {
if (syntax.streq(fpn.str, "...")) {
r.variadic = 1;
fpn = fpn.next;
continue;
};
let fpt: *syntax.tinfo = tinfofornode(c, fpn.lhs);
let fpv: bool = fpn.op == syntax.tkind.TK_ELLIPSIS;
if (fpv) { fpt = syntax.typeslice(fpt); };
let ftp: *syntax.tparam = alloc(syntax.tparam{name=fpn.str, type_=fpt, iserror=false, variadic=fpv, tnext=nil})!;
if (fphead == nil) { fphead = ftp; }
else { fptail.tnext = ftp; };
fptail = ftp;
fpn = fpn.next;
};
r.params = fphead;
};
case syntax.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 = syntax.newtype(syntax.tykind.TY_ENUM);
let storage: *syntax.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 syntax.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 = syntax.newtype(syntax.tykind.TY_TUPLE);
syntax.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: *syntax.ttupleelem = nil;
let tet: *syntax.ttupleelem = nil;
let slottotal: u64 = 0u64;
let maxal: u64 = 1u64;
let p: *syntax.node = n.list;
for (p != nil) {
let pt: *syntax.tinfo = tinfofornode(c, p.lhs);
// #62/#69: tuple-member value cycle — loud, cstage twin.
if (circularnamed(c, pt, p.lhs)) { pt = c.tc.tyerr; };
// #24: a composite element (array/struct/nested tuple
// >8B) cannot ride the 8B cursor slot — the #60 layout
// drops it on construction and segvs on t.N[i] read.
// Reject until #60/DISP-A inlines it; cstage twin.
let cu: *syntax.tinfo = tichase(pt);
if (cu != nil && (cu.kind == syntax.tykind.TY_ARRAY
|| cu.kind == syntax.tykind.TY_STRUCT
|| cu.kind == syntax.tykind.TY_TUPLE)) {
cerr("error: tuple element must be a scalar, str, slice, or tagged-union (composite element deferred to task #60)\n");
c.errs += 1;
pt = c.tc.tyerr;
};
let te: *syntax.ttupleelem = alloc(syntax.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 syntax.nkind.N_TSTRUCT:
// Cstage cmd/wcc/check.c:468-527: per-field alignment, max
// align for the whole record, total rounded up to alignment.
// Embeds promote the inner struct's fields (#59.13, below).
//
// 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 = syntax.newtype(syntax.tykind.TY_STRUCT);
syntax.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.
let fh: *syntax.tfield = nil;
let ft_: *syntax.tfield = nil;
let off: u64 = 0u64;
let maxalign: u64 = 1u64;
let soff: u64 = 0u64;
let f: *syntax.node = n.list;
for (f != nil) {
if (f.kind == syntax.nkind.N_TFIELD) {
let ft: *syntax.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; };
// packed: no inter-field padding (harec
// type_store.c:206-213); align still tracks the
// max field align below.
if (n.packed == 0 && ft.align > 0u64) {
off = (off + ft.align - 1u64) & ~(ft.align - 1u64);
};
if (f.str.len == 0) {
// Embed (#59.13): promote the inner struct's
// already-flattened fields at base+src.offset,
// mirroring cstage check.c:961-990. Diagnostics
// (non-struct embed, name collisions) live in
// validatestructfields — this arm can re-run
// per size/align query and must stay silent
// (the drew-dv once-per-decl ruling).
let inner: *syntax.tinfo = ft;
for (inner != nil && inner.kind == syntax.tykind.TY_NAMED) {
inner = inner.under;
};
if (inner != nil && inner.kind == syntax.tykind.TY_STRUCT) {
let base: u64 = off;
let src: *syntax.tfield = inner.fields;
for (src != nil) {
let tf: *syntax.tfield = alloc(syntax.tfield{name=src.name, type_=src.type_, offset=base + src.offset, tnext=nil})!;
if (fh == nil) { fh = tf; } else { ft_.tnext = tf; };
ft_ = tf;
src = src.tnext;
};
off = base + inner.size;
// cgenutil registerstruct descends embeds
// through the resolved inner struct AST;
// plant it on the (otherwise unused) TFIELD
// rhs — cgen has no alias resolver.
f.rhs = resolvealias(c, unwrapbang(f.lhs));
} else {
// error-path layout kept defined (cstage
// `off += ft ? ft->size : 0`); the build
// already fails via validatestructfields.
off += ft.size;
};
} else {
let fldoff: u64 = off;
let tf: *syntax.tfield = alloc(syntax.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;
r.packed = n.packed;
// packed: skip the trailing pad-to-align (harec
// type_store.c:886 `!packed`); align value unchanged.
if (n.packed != 0) {
r.size = off;
} else {
if (maxalign > 0u64) {
r.size = (off + maxalign - 1u64) & ~(maxalign - 1u64);
};
};
r.align = maxalign;
// packed: slotsize == size — cstage has no slotsize, it uses the
// (packed) size everywhere incl struct-ABI copy (cmd/w6c/cgen.c:
// 15020). Aligning slotsize DOWN to size keeps every wwstage
// slot-padded consumer byte-identical to cstage (rule 10).
if (n.packed != 0) {
r.slotsize = r.size;
} else {
if ((soff & 7u64) != 0u64) {
soff = (soff + 7u64) & ~7u64;
};
r.slotsize = soff;
};
case syntax.nkind.N_TTAGGED:
// THE tagged-union normalization SSoT (astsize/astalign delegate
// here). Mirrors cstage resolve_type N_TTAGGED (cmd/wcc/check.c:
// 801-882): 8B tag + max(variant) rounded up to 8, AFTER type-set
// normalization — drop `never` (807/824), dedup variants by
// variantpresent==variant_match (837/825), collapse a lone
// survivor to that variant (847-848), fold a two-variant
// `(*T|void)` to an 8B nullable pointer (859-874). #1/#3: astsize
// (check.ww) AND this arm formerly sized the RAW declaration list
// (no drop/dedup/collapse), so size((i32|never)) folded 16 not 4
// and (i32|i32|str) numbered str's tag 2 not 1 (cgen reads the
// deduped ti.params). Pre-bind for cycle protection (recursive
// sum-type shapes through NAMED variants).
r = syntax.newtype(syntax.tykind.TY_TAGGED);
syntax.tinfocachebind(c.tc, n, r);
// #50 / A.6.3f phase 1: populate ti.params as a tparam linked
// list (head=first surviving variant). #61a: flatten `...inner`
// tagged spreads into the chain and stamp each variant's iserror.
// Same shared Tparam shape ww reuses across struct-fields /
// tuple-fields / fn-params / tagged-variants (sea-of-stars per
// rule 12).
let head: *syntax.tparam = nil;
let tail: *syntax.tparam = nil;
let maxsz: u64 = 0u64;
let al: u64 = 8u64;
let nv: i32 = 0;
let v: *syntax.node = n.list;
for (v != nil) {
let vt: *syntax.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 == syntax.tkind.TK_ELLIPSIS);
let vu: *syntax.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 == syntax.tykind.TY_TAGGED) {
// #209: a `...inner` spread's PAYLOAD is its
// members, not the whole inner union — size/align
// off each surviving spliced member (cstage check.c:
// 822-834 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: *syntax.tparam = vu.params;
for (src != nil) {
let st: *syntax.tinfo = src.type_;
if (st != nil && st.kind == syntax.tykind.TY_NEVER) {
src = src.tnext; continue;
};
if (variantpresent(head, st)) {
src = src.tnext; continue;
};
if (st != nil) {
if (st.size > maxsz) { maxsz = st.size; };
if (st.align > al) { al = st.align; };
};
let tp: *syntax.tparam = alloc(syntax.tparam{name="", type_=src.type_, iserror=src.iserror, variadic=false, tnext=nil})!;
if (head == nil) { head = tp; } else { tail.tnext = tp; };
tail = tp;
nv += 1;
src = src.tnext;
};
} else {
if (vt != nil && vt.kind == syntax.tykind.TY_NEVER) {
v = v.next; continue;
};
if (variantpresent(head, vt)) {
v = v.next; continue;
};
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: *syntax.tparam = alloc(syntax.tparam{name="", type_=vt, iserror=ve, variadic=false, tnext=nil})!;
if (head == nil) { head = tp; } else { tail.tnext = tp; };
tail = tp;
nv += 1;
};
v = v.next;
};
// Collapse on the NORMALIZED survivor count (cstage check.c:
// 847-882). The trailing tinfocachebind below re-binds n→r, so
// a collapsed `r` shadows the pre-bound tagged placeholder.
if (nv == 0) {
r = c.tc.tynever;
} else if (nv == 1) {
r = head.type_;
} else {
r.params = head;
// #61 A.3 nullable fold on the normalized pair so
// `(*T|void|never)` and dup'd shapes fold too. Mirrors
// cmd/wcc/check.c:859-874 — bare TY_VOID (NOT `!void`,
// carried by the tparam iserror flag), not NAMED. The
// per-variant iserror (varianterr) now lets this port at
// the tinfo layer; pre-#1 it AST-keyed n.list instead.
let isnull: bool = false;
if (nv == 2) {
let pa: *syntax.tparam = head;
let pb: *syntax.tparam = head.tnext;
let aptr: bool = (pa.type_ != nil && pa.type_.kind == syntax.tykind.TY_PTR);
let bptr: bool = (pb.type_ != nil && pb.type_.kind == syntax.tykind.TY_PTR);
let avoid: bool = (pa.type_ != nil && pa.type_.kind == syntax.tykind.TY_VOID && !pa.iserror);
let bvoid: bool = (pb.type_ != nil && pb.type_.kind == syntax.tykind.TY_VOID && !pb.iserror);
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;
} else {
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; };
syntax.tinfocachebind(c.tc, n, r);
};
return r;
};
// unifyarith — usual-arithmetic-conversion analogue at the AST-tnode
// layer. Mirrors cstage cmd/wcc/check.c:1034-1069 `unify_arith` and harec
// ref/harec/src/types.c type_promote. A typed/typed operand mismatch is
// loud (#26), aligning wwstage down to cstage; the only promotion left is
// the one-sided alias-vs-base chase (cstage check.c:1060-1067).
//
// Nil-on-valid classification (5-lite-b #34, A.6.2.1c #24): ltn or 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, so a nil operand never reaches the loud reject below.
fn unifyarith(c: *checker, e: *syntax.node, ltn: *syntax.node, rtn: *syntax.node) *syntax.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; };
};
// A rune LITERAL is untyped_rune in cstage (cmd/wcc/check.c:1296),
// assignable to any integer or rune (cmd/wcc/type.c:379), so cstage's
// unify_arith accepts `ch == 'x'` / `c - '0'` and returns the typed
// side. wwstage stamps N_RUNELIT concrete `rune` (the #29 divergence,
// exprtype :2927), so the literal does not reach the isuntyped arms
// above. Mirror cstage here off the operand node so the rune-literal /
// integer mix stays valid (35 selfhost+lib sites) under the #26 reject.
if (e != nil) {
let lr: bool = e.lhs != nil && e.lhs.kind == syntax.nkind.N_RUNELIT;
let rr: bool = e.rhs != nil && e.rhs.kind == syntax.nkind.N_RUNELIT;
if (lr && isinttypeast(rtn)) { return rtn; };
if (rr && isinttypeast(ltn)) { return ltn; };
};
if (typeeqast(c, ltn, rtn)) { return ltn; };
// One-sided alias vs its (transitive) base promotes to the alias
// side; alias-vs-different-alias stays rejected even when the bases
// agree. Mirrors cstage unify_arith (cmd/wcc/check.c:1060-1067) and
// harec type_promote (ref/harec/src/check.c:1083-1105). The error
// axis (varianterr) must agree on both sides so a `!i32` alias does
// NOT promote against a plain i32 (#246, r949_errtype_* fixtures).
// A user alias is an SK_TYPE sym WITH a decl body; the primitives are
// SK_TYPE too but decl == nil (check.ww:95-112), so aliassym alone
// would mis-flag i32 as "named". This decl != nil gate is the wwstage
// analog of cstage's `kind == TY_NAMED` (primitives are TY_I32 etc).
let ls: *syntax.sym = aliassym(c, unwrapbang(ltn));
let rs: *syntax.sym = aliassym(c, unwrapbang(rtn));
let la: bool = ls != nil && ls.decl != nil;
let ra: bool = rs != nil && rs.decl != nil;
if (!(la && ra)) {
let da: *syntax.node = resolvealias(c, ltn);
let db: *syntax.node = resolvealias(c, rtn);
if (da != nil && db != nil
&& varianterr(c, ltn) == varianterr(c, rtn)
&& typeeqast(c, da, db)) {
if (la) { return ltn; };
return rtn;
};
};
// ww requires explicit integer conversion in binary ops (Go-faithful,
// user-blessed #26): a typed/typed operand mismatch is loud, NOT
// promoted. Diverges from Hare type_promote's implicit signed-widen
// (ref/harec/src/check.c:1079/1337, STORAGE_INT 1129-1134). cstage
// already rejects the same shape (cmd/wcc/check.c:1068); this aligns
// wwstage down. A nil operand is an inherent-IDENT bail (see header) —
// propagate it, never reject.
if (ltn != nil && rtn != nil) {
deffolderr(c, e, "operands have differing types");
};
return ltn;
};
// coercefloatlit — twin of cstage cmd/wcc/check.c coerce_floatlit (see there
// for the full rationale). 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 untyped_float -> f32. #120 broadens the reach (was
// let-init / return only): descend the harec lower_implicit_cast operand
// shapes so every untyped float LEAF in an f32 context gets the stamp — a
// unary ± / paren-cast wrapper, both arith-binop operands (the only path that
// reaches a literal-on-both-sides under an f32 target, per-leaf so no f64
// intermediate double-rounds), and each array-literal element against the
// array's element type. The comparison sibling (no f32 target above) is in
// binoptype.
fn coercefloatlit(c: *checker, e: *syntax.node, target: *syntax.node) void = {
if (e == nil) { return; };
if (target == nil) { return; };
let tu: *syntax.node = resolvealias(c, unwrapbang(target));
if (tu == nil) { return; };
let k: syntax.nkind = e.kind;
if (k == syntax.nkind.N_UN) {
if (e.op == syntax.tkind.TK_MINUS || e.op == syntax.tkind.TK_PLUS) {
coercefloatlit(c, e.lhs, target);
};
return;
};
if (k == syntax.nkind.N_CAST) {
coercefloatlit(c, e.lhs, target);
return;
};
if (k == syntax.nkind.N_BIN) {
if (e.op == syntax.tkind.TK_PLUS || e.op == syntax.tkind.TK_MINUS ||
e.op == syntax.tkind.TK_STAR || e.op == syntax.tkind.TK_SLASH) {
coercefloatlit(c, e.lhs, target);
coercefloatlit(c, e.rhs, target);
// harec lowers the binop's RESULT to the hint too, not only
// its operands. A literal-on-both-sides binop node stays
// untyped_float; cstage's arith cgen keys op-width on the
// BINOP node (cgen.c:4944), so the node must carry f32 to keep
// both stages on ADDSS. Mirror cstage coerce_floatlit N_BIN.
if (tu.kind == syntax.nkind.N_TNAME && syntax.streq(tu.str, "f32")) {
if ((e.type_: *syntax.tinfo) == c.tc.tyuntypedfloat) {
let f32b: *syntax.node = mktname(c, "f32");
e.type_ = tinfofornode(c, f32b): *void;
};
};
};
return;
};
if (k == syntax.nkind.N_ARRLIT) {
if (tu.kind == syntax.nkind.N_TARRAY) {
let it: *syntax.node = e.list;
for (it != nil) {
coercefloatlit(c, it, tu.lhs);
it = it.next;
};
};
return;
};
if (k == syntax.nkind.N_FLOATLIT) {
if (tu.kind == syntax.nkind.N_TNAME && syntax.streq(tu.str, "f32")) {
if ((e.type_: *syntax.tinfo) == c.tc.tyuntypedfloat) {
let f32t: *syntax.node = mktname(c, "f32");
e.type_ = tinfofornode(c, f32t): *void;
};
};
return;
};
};
// coercerunelit — #29: an un-suffixed rune literal narrowing into an
// integer slot. Same intent as coercefloatlit (:2324) but CHECKER-only:
// wwstage stamps N_RUNELIT concrete `rune` (:2652), so isassignable's
// untyped arms never fire and the operand falls to the "two known
// primitives, different names → confident reject" arm (:4139) — the #29
// over-reject. cstage stamps N_RUNELIT untyped_rune (cmd/wcc/check.c:1296)
// which type_assignable admits into ANY integer UNCONDITIONALLY
// (cmd/wcc/type.c:379 — coarse, NO range check; harec's range-precise
// promote_flexible at types.c:928 is the reference cstage already
// flattened, so wwstage mirrors cstage's coarse rule, not harec's gate).
// Return the integer target tnode so the caller overrides its `src`/
// `atype` and isassignable sees su==target → accept. Does NOT touch
// e.type_: cgen narrows a rune immediate by the DESTINATION width (proven
// byte-id at let/call-arg/index-store; #251 array-elem twin) and for a
// tagged-union target the variant boxing falls to taggedvariantindext's
// scalar-shape fallback (cgenutil.ww:3054) which picks the first scalar
// variant — the same u8 variant cstage's untyped_rune boxes into — so the
// unchanged node stamp keeps cgen provably identical to the pre-fix
// bare-literal lowering. Array-LITERAL elements are NOT routed here: they
// keep harec's per-element range gate via checkarrlitfits's foldint /
// defcastfits path (:4634). For a tagged-union target (fnmatch pat_next's
// (u8 | star | ...)) return the first integer variant.
fn coercerunelit(c: *checker, e: *syntax.node, target: *syntax.node) *syntax.node = {
if (e == nil) { return nil; };
if (e.kind != syntax.nkind.N_RUNELIT) { return nil; };
if (target == nil) { return nil; };
let tu: *syntax.node = resolvealias(c, unwrapbang(target));
if (tu == nil) { return nil; };
if (isinttypeast(tu)) { return tu; };
if (tu.kind == syntax.nkind.N_TTAGGED) {
let v: *syntax.node = tu.list;
for (v != nil) {
let vu: *syntax.node = resolvealias(c, unwrapbang(v));
if (vu != nil) {
if (isinttypeast(vu)) { return vu; };
};
v = v.next;
};
};
return nil;
};
// 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: *syntax.node) *syntax.node = {
let op: syntax.tkind = e.op;
let ltn: *syntax.node = exprtype(c, e.lhs, nil);
let rtn: *syntax.node = exprtype(c, e.rhs, nil);
// #120 (B): a binop/compare with one f32 operand lowers an untyped-
// float peer to f32 — harec unifies both operands to the operand type
// (ref/harec/src/check.c:1347-1348). A comparison's result is bool, so
// no f32 target is above its operands and this sibling is their only
// lowering path. coercefloatlit's internal f32-target + untyped-leaf
// gates make each call inert unless the OTHER operand resolves f32 and
// this one carries an untyped float leaf; a both-untyped pair stays f64.
// Mirror cstage cbinop (cmd/wcc/check.c:1190).
coercefloatlit(c, e.rhs, ltn);
coercefloatlit(c, e.lhs, rtn);
// #38/F2: ptr ± int / int + ptr use intkindast (the type_isint mirror,
// incl untyped_int / untyped_rune / alias-chased) — NOT isinttypeast,
// which misses untyped_int. cstage's ptr-arith arm gates on type_isint
// (check.c:1179/1181), so `p + 1` (untyped_int) returns the ptr there
// and never reaches the isnum gate; aligning these arms keeps the new
// arithmetic gate below from rejecting valid pointer arithmetic.
if (op == syntax.tkind.TK_PLUS || op == syntax.tkind.TK_MINUS) {
if (ltn != nil && ltn.kind == syntax.nkind.N_TPTR && intkindast(c, rtn)) {
return ltn;
};
};
if (op == syntax.tkind.TK_PLUS) {
if (intkindast(c, ltn) && rtn != nil && rtn.kind == syntax.nkind.N_TPTR) {
return rtn;
};
};
if (op == syntax.tkind.TK_MINUS) {
if (ltn != nil && rtn != nil
&& ltn.kind == syntax.nkind.N_TPTR && rtn.kind == syntax.nkind.N_TPTR) {
return mktname(c, "i64");
};
};
// #38/F2 (review item 5): operand-kind gates the wwstage checker
// elided. Mirror cstage cbinop (cmd/wcc/check.c:1186-1199): arithmetic
// wants numeric operands, bitwise wants integer operands. Without these
// `let c: str = a + b;` (str+str) compiled to an integer ADD of the two
// header pointers, silent garbage. The ptr-arithmetic special cases
// above already returned, so a survivor here is plain value arithmetic.
if (op == syntax.tkind.TK_PLUS || op == syntax.tkind.TK_MINUS ||
op == syntax.tkind.TK_STAR || op == syntax.tkind.TK_SLASH ||
op == syntax.tkind.TK_PERCENT) {
if ((ltn != nil && !numkindast(c, ltn)) ||
(rtn != nil && !numkindast(c, rtn))) {
deffolderr(c, e, "arithmetic on non-numeric type");
};
return unifyarith(c, e, ltn, rtn);
};
if (op == syntax.tkind.TK_AMP || op == syntax.tkind.TK_PIPE ||
op == syntax.tkind.TK_CARET || op == syntax.tkind.TK_LSHIFT ||
op == syntax.tkind.TK_RSHIFT) {
if ((ltn != nil && !intkindast(c, ltn)) ||
(rtn != nil && !intkindast(c, rtn))) {
deffolderr(c, e, "bitwise on non-integer type");
};
return unifyarith(c, e, ltn, rtn);
};
if (op == syntax.tkind.TK_EQ || op == syntax.tkind.TK_NEQ ||
op == syntax.tkind.TK_LT || op == syntax.tkind.TK_LE ||
op == syntax.tkind.TK_GT || op == syntax.tkind.TK_GE) {
// cstage routes every comparison through unify_arith (check.c:1195/
// 1200 cbinop), which loud-rejects a differing-types pair, e.g.
// strconv.invalid != i32. wwstage routes the ORDERED arm through
// unifyarith below (#26), but EQ/NEQ stays here on the error-type-
// only reject: a full unifyarith route for EQ/NEQ would expose the
// typeeqast-vs-cstage-type_eq asymmetry on struct / tuple / fn-ptr
// equality. The error-type case (#246, rule 10) is the one real
// cs!=ww edge EQ/NEQ must still reject.
if (varianterr(c, ltn) || varianterr(c, rtn)) {
if (!typeeqast(c, ltn, rtn)) {
deffolderr(c, e, "operands have differing types");
};
};
// #38/F2 (review item 5): ordered comparisons want numeric
// operands (cstage check.c:1198-1199). EQ/NEQ stay UNGATED —
// cstage's cbinop equality arm (check.c:1194-1196) gates nothing,
// so str==str / ptr==ptr remain valid; aligning those down would
// over-reject what cstage accepts.
if (op == syntax.tkind.TK_LT || op == syntax.tkind.TK_LE ||
op == syntax.tkind.TK_GT || op == syntax.tkind.TK_GE) {
if ((ltn != nil && !numkindast(c, ltn)) ||
(rtn != nil && !numkindast(c, rtn))) {
deffolderr(c, e, "ordered comparison on non-numeric");
};
// #26: an ordered comparison routes through unifyarith for
// the differing-types reject — cstage's cbinop sends every
// comparison through unify_arith (cmd/wcc/check.c:1195/1200),
// so `int < len(s):i32` is loud there. Guarded off error
// operands (the #246 block above already rejects those, so
// this avoids a double diagnostic) and scoped to ordered,
// keeping the typeeqast-vs-type_eq asymmetry risk off the
// untouched EQ/NEQ arm.
if (!varianterr(c, ltn) && !varianterr(c, rtn)) {
unifyarith(c, e, ltn, rtn);
};
};
return mktname(c, "bool");
};
if (op == syntax.tkind.TK_AND || op == syntax.tkind.TK_OR) {
// #38/F2 (review item 5): logical operands must be bool (cstage
// check.c:1208-1211 gates each side via type_chase_named). cstage
// formats per-side with tokname; ww's piecewise cerr can't splice
// the op token, so one combined wording — the build-based reject
// test gates on rc, not on exact text.
if ((ltn != nil && !boolkindast(c, ltn)) ||
(rtn != nil && !boolkindast(c, rtn))) {
deffolderr(c, e, "logical operand is not bool");
};
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: *syntax.node) *syntax.node = {
let op: syntax.tkind = e.op;
let opt: *syntax.node = exprtype(c, e.lhs, nil);
// #38/F2 (review item 5): unop operand-kind gates the wwstage checker
// elided. Mirror cstage cunop (cmd/wcc/check.c:1224-1238): unary +/-
// want a numeric operand, ~ wants an integer, ! wants a bool. A nil
// opt is an already-errored / inherent-IDENT bail — not re-rejected.
if (op == syntax.tkind.TK_MINUS || op == syntax.tkind.TK_PLUS) {
if (opt != nil && !numkindast(c, opt)) {
if (op == syntax.tkind.TK_MINUS) {
deffolderr(c, e, "- on non-numeric");
} else {
deffolderr(c, e, "+ on non-numeric");
};
};
return opt;
};
if (op == syntax.tkind.TK_NOT) {
if (opt != nil && !boolkindast(c, opt)) {
deffolderr(c, e, "! on non-bool");
};
return mktname(c, "bool");
};
if (op == syntax.tkind.TK_TILDE) {
if (opt != nil && !intkindast(c, opt)) {
deffolderr(c, e, "~ on non-integer");
};
return opt;
};
if (op == syntax.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: *syntax.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 != syntax.nkind.N_TPTR) { return nil; };
return u.lhs;
};
if (op == syntax.tkind.TK_AMP) {
if (e.lhs != nil && e.lhs.kind == syntax.nkind.N_DOT) {
let fld: str = e.lhs.str;
if (syntax.streq(fld, "len") || syntax.streq(fld, "cap")) {
let base: *syntax.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: *syntax.tinfo = tichase(base.type_: *syntax.tinfo);
if (bu != nil && bu.kind == syntax.tykind.TY_PTR) { bu = bu.sub; };
bu = tichase(bu);
if (bu != nil) {
if (bu.kind == syntax.tykind.TY_SLICE || bu.kind == syntax.tykind.TY_STR) {
let pp: *syntax.node = syntax.newnode(syntax.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 == syntax.nkind.N_IDENT) {
// #4: curmod preference. A bare `&handler` whose leaf
// also names a fn in a LATER module otherwise binds
// the foreign signature (scopedefineinmodule prepends);
// cstage types a fn ident via scope_lookup_prefer with
// cur_mod (cmd/wcc/check.c:1305).
let fs: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, e.lhs.str);
if (fs != nil) {
if (fs.skind == syntax.skind.SK_FN) {
if (fs.decl != nil) {
if (fs.decl.kind == syntax.nkind.N_FNDECL) {
let synth: *syntax.node = syntax.newnode(syntax.nkind.N_TFN, "", 0, 0);
synth.lhs = fs.decl.lhs;
synth.list = fs.decl.list;
let pf: *syntax.node = syntax.newnode(syntax.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 == syntax.nkind.N_DOT) {
if (e.lhs.lhs != nil && e.lhs.lhs.kind == syntax.nkind.N_IDENT) {
let fs: *syntax.sym = syntax.scopelookupinmodule(c.cur, modkeyfor(c, e.lhs.lhs.str), e.lhs.str);
if (fs != nil) {
if (fs.skind == syntax.skind.SK_FN) {
if (fs.decl != nil) {
if (fs.decl.kind == syntax.nkind.N_FNDECL) {
let synth: *syntax.node = syntax.newnode(syntax.nkind.N_TFN, "", 0, 0);
synth.lhs = fs.decl.lhs;
synth.list = fs.decl.list;
let pf: *syntax.node = syntax.newnode(syntax.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: *syntax.node = syntax.newnode(syntax.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:1491-1535 (N_INDEX arm) 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: *syntax.node) *syntax.node = {
let basetn: *syntax.node = exprtype(c, e.lhs, nil);
let idxtn: *syntax.node = exprtype(c, e.rhs, nil);
// cstage cmd/wcc/check.c:1491-1495: index operand must be integer.
// typeisint is the tinfo chaser (follows TY_ENUM.sub / TY_NAMED.under),
// so an alias-int or named-enum index passes; the AST-keyed isinttypeast
// would falsely reject `type ix = i32`. Both nil-guards mirror cstage's
// `idx != ty_err` cascade-suppression so a broken subexpr emits one error.
if (idxtn != nil) {
let it: *syntax.tinfo = tinfofornode(c, idxtn);
if (it != nil && !syntax.typeisint(it)) {
cerr(e.file);
cerr(": error: index must be integer\n");
c.errs += 1;
};
};
let u: *syntax.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 == syntax.nkind.N_TSLICE) { return u.lhs; };
if (u.kind == syntax.nkind.N_TARRAY) { return u.lhs; };
if (u.kind == syntax.nkind.N_TNAME) {
// rule-9 divergence-doc (drew .ai/drew-14-ruling.md): `str[i] ->
// u8` is a deliberate Go-like direct byte-index, a SANCTIONED ww
// divergence from Hare's `strings::toutf8(s)[i]` (the Hare
// reference checker rejects str-index, harec check.c:362).
// lib/strings is load-bearing on it (compare/dup/join).
if (syntax.streq(u.str, "str")) {
// #14: reject INDEX of a bare def-global scalar str — an
// inline compile-time constant with no storage to index
// (cgen refs an unbacked symbol -> link-fail ww / segfault
// cs). let/param/local + string-literal operands stay
// valid; only the SK_DEF scalar-str operand is
// unindexable. cstage twin: check.c N_INDEX TY_STR arm.
if (e.lhs != nil && e.lhs.kind == syntax.nkind.N_IDENT) {
let s: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, e.lhs.str);
if (s != nil && s.skind == syntax.skind.SK_DEF) {
cerr("error: cannot index a def-constant str '");
cerr(e.lhs.str);
cerr("'; bind it to a `let` (def strings are inline constants, not storage-backed)\n");
c.errs += 1;
return nil;
};
};
return mktname(c, "u8");
};
};
if (u.kind == syntax.nkind.N_TPTR) {
let inner: *syntax.node = u.lhs;
let iu: *syntax.node = resolvealias(c, unwrapbang(inner));
if (iu != nil && iu.kind == syntax.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: *syntax.node, hint: *syntax.node) *syntax.node = {
if (e == nil) { return nil; };
let k: syntax.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 == syntax.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: *syntax.node = mktname(c, e.tsuffix);
let ti: *syntax.tinfo = tinfofornode(c, suf);
if (ti != nil) {
e.type_ = ti: *void;
return suf;
};
};
let tn: *syntax.node = mktname(c, "untyped_int");
e.type_ = tinfofornode(c, tn): *void;
return tn;
};
if (k == syntax.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: *syntax.node = mktname(c, e.tsuffix);
let ti: *syntax.tinfo = tinfofornode(c, suf);
if (ti != nil) {
e.type_ = ti: *void;
return suf;
};
};
let tn: *syntax.node = mktname(c, "untyped_float");
e.type_ = tinfofornode(c, tn): *void;
return tn;
};
if (k == syntax.nkind.N_STRLIT) {
let tn: *syntax.node = mktname(c, "str");
e.type_ = tinfofornode(c, tn): *void;
return tn;
};
if (k == syntax.nkind.N_RUNELIT) {
let tn: *syntax.node = mktname(c, "rune");
e.type_ = tinfofornode(c, tn): *void;
return tn;
};
if (k == syntax.nkind.N_TRUE) {
let tn: *syntax.node = mktname(c, "bool");
e.type_ = tinfofornode(c, tn): *void;
return tn;
};
if (k == syntax.nkind.N_FALSE) {
let tn: *syntax.node = mktname(c, "bool");
e.type_ = tinfofornode(c, tn): *void;
return tn;
};
if (k == syntax.nkind.N_VOIDLIT) {
let tn: *syntax.node = mktname(c, "void");
e.type_ = tinfofornode(c, tn): *void;
return tn;
};
if (k == syntax.nkind.N_NIL) {
let tn: *syntax.node = mktname(c, "untyped_nil");
e.type_ = tinfofornode(c, tn): *void;
return tn;
};
if (k == syntax.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: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, e.str);
if (s == nil) { return nil; };
if (s.decl == nil) { return nil; };
// #34: a bare fn-name rvalue types as its FN TYPE, not its return
// type. decl.lhs is the RETURN type for an N_FNDECL, so synthesize
// the N_TFN over (ret=decl.lhs, params=decl.list) — the shape
// assignableaddrfn builds (:3841). Mirrors cstage: build_fn_type at
// fn-decl install (cmd/wcc/check.c:2915/2931) stored on the sym and
// returned verbatim by cexpr N_IDENT (check.c:1313); harec EXP_ACCESS
// yields the fn object's type with NO decay (ref/harec/src/check.c
// :341-343; the fn obj is built .storage=STORAGE_FUNCTION at :4279-
// 4288, assignable iff dealias-equal fn types, types.c:1001). Was the
// root of the #24 fn-family over-rejects (`let p: fn()i32 = g` compared
// i32 vs the fn type). cgen lowers a fn rvalue name-keyed via
// fnretlookup (cgenexpr.ww:1100), never off this stamp → byte-id-neutral.
if (s.skind == syntax.skind.SK_FN) {
let ft: *syntax.node = syntax.newnode(syntax.nkind.N_TFN, "", 0, 0);
ft.lhs = s.decl.lhs;
ft.list = s.decl.list;
e.type_ = tinfofornode(c, ft): *void;
return ft;
};
// #142: a TYPE name used as a VALUE (an error-singleton
// `return too_long;`) must stamp the per-decl NAMED, not the
// flattened body — three structurally identical !void
// singletons in one union are indistinguishable by body, so
// flatvariantidxt's NAMED pass either mis-tags or
// loud-rejects as ambiguous. Resolve through a synthesized
// TNAME to reuse tinfofornode's TY_NAMED build/cache (the
// SAME NAMED ptr the union variant resolved to) — the #66
// N_STRUCTLIT precedent verbatim; cstage mirror
// cmd/wcc/check.c:1414 `n->type = s->type`. The BODY node
// still returns so AST-level assign/return checks keep
// their prior input.
if (s.skind == syntax.skind.SK_TYPE) {
let tnm: *syntax.node = mktname(c, e.str);
let nti: *syntax.tinfo = tinfofornode(c, tnm);
if (nti != nil) {
e.type_ = nti: *void;
return s.decl.lhs;
};
};
let t: *syntax.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: *syntax.tinfo = tinfofornode(c, t);
if (ti != nil) {
e.type_ = ti: *void;
t.type_ = ti: *void;
};
};
};
return t;
};
if (k == syntax.nkind.N_BIN) {
let tn: *syntax.node = binoptype(c, e);
// #59.9: checkisas pre-stamps an enum OR-fold (`(m.A|m.B) as
// u32`) TY_ENUM and folds the member N_DOTs to int literals;
// this post-order revisit re-derives from those now-untyped
// literals and clobbered the stamp, so cgtypeassert's #27b
// reinterpret gate missed and emitted a phantom tagged assert
// (unconditional exit 1). Keep an existing enum stamp; the
// returned tnode contract for callers is unchanged.
let pre599: *syntax.tinfo = e.type_: *syntax.tinfo;
if (pre599 != nil) {
let prech: *syntax.tinfo = tichase(pre599);
if (prech != nil) {
if (prech.kind == syntax.tykind.TY_ENUM) {
return tn;
};
};
};
e.type_ = tinfofornode(c, tn): *void;
return tn;
};
if (k == syntax.nkind.N_UN) {
let tn: *syntax.node = unoptype(c, e);
e.type_ = tinfofornode(c, tn): *void;
return tn;
};
if (k == syntax.nkind.N_INDEX) {
let tn: *syntax.node = indexresult(c, e);
e.type_ = tinfofornode(c, tn): *void;
return tn;
};
if (k == syntax.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 == syntax.nkind.N_CALL) {
let callee: *syntax.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 == syntax.nkind.N_IDENT) {
if (syntax.streq(callee.str, "alloc")) {
let shadowed: bool = false;
if (c.curmod.len > 0) {
if (syntax.scopelookupinmodule(c.cur, c.curmod, "alloc") != nil) {
shadowed = true;
};
};
if (!shadowed) {
if (e.list != nil) {
// Slice form: `alloc([], n)`.
if (e.list.kind == syntax.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: *syntax.node = syntax.newnode(syntax.nkind.N_TSLICE, "", 0, 0);
sl.lhs = mktname(c, "u8");
let nome: *syntax.node = mktname(c, "nomem");
sl.next = nome;
let tt: *syntax.node = syntax.newnode(syntax.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: *syntax.node = exprtype(c, e.list, nil);
let ptr: *syntax.node = syntax.newnode(syntax.nkind.N_TPTR, "", 0, 0);
ptr.lhs = argt;
let nome: *syntax.node = mktname(c, "nomem");
ptr.next = nome;
let tt: *syntax.node = syntax.newnode(syntax.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 == syntax.nkind.N_IDENT) {
let bname: str = callee.str;
let issize: bool = syntax.streq(bname, "size");
let isalign: bool = syntax.streq(bname, "align");
let isoffset: bool = syntax.streq(bname, "offset");
if (issize || isalign || isoffset) {
// #38/F2 (review item 7): UNCONDITIONAL intercept — cstage
// gates size/align/offset on NOTHING (cmd/wcc/check.c:1538-
// 1578), unlike user-shadowable alloc/abort/assert
// (check.c:1625/1740/1755). The removed same-module shadow
// gate had no cstage twin (its "Mirrors check.c:907-960"
// cite was stale — that range is N_TFN/N_TSTRUCT layout) and
// was already dead for primary-file decls (declmod "" →
// curmod.len==0 skipped it).
if (e.list != nil) {
// size/align resolve the arg as a TYPE; an unresolvable
// name (`size(localvar)`) is LOUD here, mirroring cstage
// resolve_type "unknown type '%s'" (check.c:93) — astsize
// otherwise folded the unresolved N_TNAME to 0 silently
// (rc=0 wrong binary). offset's arg is a value N_DOT and
// keeps its own "no field" diagnostic below.
if ((issize || isalign) && e.list.kind == syntax.nkind.N_TNAME
&& tinfofornode(c, e.list) == nil) {
cerr(e.list.file);
cerr(": error: unknown type '");
cerr(e.list.str);
cerr("'\n");
c.errs += 1;
};
// Post-fold the node IS an N_INTLIT-shaped
// untyped_int constant. Mirrors cstage
// cmd/wcc/check.c:1570/1602 which stamps
// ty_untyped_int after the fold AND returns it
// to the caller (the Hare-correct type), so a
// `let x: size = size(T)` binding is assignable.
let utn: *syntax.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, "untyped_int");
};
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, "untyped_int");
};
// 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 == syntax.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, "untyped_int");
};
};
};
};
};
// 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 == syntax.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 (syntax.streq(callee.str, "abort")
&& syntax.scopelookupprefer(c.cur, c.curmod, "abort") == nil) {
if (e.list != nil) {
let mt: *syntax.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: *syntax.node = mktname(c, "void");
e.type_ = tinfofornode(c, tn): *void;
callee.type_ = c.tc.tyerr: *void;
return tn;
};
if (syntax.streq(callee.str, "assert") && e.list != nil
&& syntax.scopelookupprefer(c.cur, c.curmod, "assert") == nil) {
let ct: *syntax.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: *syntax.node = unwrapbang(ct);
let condok: bool = false;
if (cu.kind == syntax.nkind.N_TNAME) {
if (syntax.streq(cu.str, "bool")
|| syntax.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: *syntax.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: *syntax.node = mktname(c, "void");
e.type_ = tinfofornode(c, tn): *void;
callee.type_ = c.tc.tyerr: *void;
return tn;
};
if (syntax.streq(callee.str, "len")) {
let tn: *syntax.node = mktname(c, "i32");
e.type_ = tinfofornode(c, tn): *void;
return tn;
};
if (syntax.streq(callee.str, "append") || syntax.streq(callee.str, "free")) {
let tn: *syntax.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 (syntax.streq(callee.str, "delete")) {
let d: *syntax.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 != syntax.nkind.N_INDEX && d.kind != syntax.nkind.N_SLICE) {
cerr("delete: operand must be an indexing or slicing expression\n");
c.errs += 1;
} else {
let basetn: *syntax.node = exprtype(c, d.lhs, nil);
let u: *syntax.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 != syntax.nkind.N_TSLICE) {
cerr("delete must operate on a slice\n");
c.errs += 1;
};
};
};
let tn: *syntax.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 (syntax.streq(callee.str, "insert")) {
let d: *syntax.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 == syntax.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 != syntax.nkind.N_INDEX) {
cerr("insert: operand must be an indexing expression xs[i]\n");
c.errs += 1;
} else {
let basetn: *syntax.node = exprtype(c, d.lhs, nil);
let u: *syntax.node = resolvealias(c, unwrapbang(basetn));
// harec check.c:807 wording; a
// fixed-size [N]T base lands here.
if (u == nil || u.kind != syntax.nkind.N_TSLICE) {
cerr("insert must operate on a slice\n");
c.errs += 1;
};
};
};
if (d.next != nil) {
if (d.next.kind == syntax.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: *syntax.node = mktname(c, "void");
e.type_ = tinfofornode(c, tn): *void;
return tn;
};
};
let nm: str;
nm.ptr = nil; nm.len = 0;
if (callee.kind == syntax.nkind.N_IDENT) { nm = callee.str; };
if (callee.kind == syntax.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: *syntax.sym = nil;
if (callee.kind == syntax.nkind.N_IDENT) {
s = syntax.scopelookupprefer(c.cur, c.curmod, nm);
} else {
let ms: *syntax.sym = nil;
if (callee.lhs != nil && callee.lhs.kind == syntax.nkind.N_IDENT) {
ms = syntax.scopelookupprefer(c.cur, c.curmod, callee.lhs.str);
if (ms != nil && ms.skind != syntax.skind.SK_USE) {
let mu: *syntax.sym = syntax.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 scope-install 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 == syntax.skind.SK_USE || ms.use_alias != 0i32)) {
s = syntax.scopelookupinmodule(c.cur, modkeyfor(c, callee.lhs.str), nm);
// Module-qualified callee whose leaf isn't scope-keyed
// under its module: align to cstage, which stamps ty_err
// here and lets cgen emit the call (cmd/wcc/check.c:1834-
// 1843; rule-10). This now tolerates only a genuinely-
// extern leaf (raw w6c on a single module-qualified file
// with no driver concat), the module-qualified arm of #27.
// The -T synth's `test.run` NO LONGER lands here: #80
// PREPENDS the synth `use test;` before Pass 1, so declmod
// keys lib/test's `run` under "test" (not "") and the synth
// `test.run` type-resolves — the E1 bridge is closed. cgen
// keys the CALL off run's `//ww:module test` directive
// either way, so the resolution change is asm-neutral.
if (s == nil) {
e.type_ = c.tc.tyerr: *void;
callee.type_ = c.tc.tyerr: *void; // N_DOT node itself (asserttyped checks it)
return nil;
};
};
};
if (s != nil) { if (s.skind == syntax.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: *syntax.node = resolvealias(c, unwrapbang(exprtype(c, callee, nil)));
// #14/#181-cgen: ONE pointer-peel only, mirroring cstage
// cmd/wcc/check.c:1947. #181-cgen lowers an indirect call by using the
// callee VALUE as the target, the fn address for a single `*fn` but only
// the *address of* the fn-ptr for `**fn` — so a deref-less `**fn` call
// peeled past one level ships a CALL through a fn-ptr address (segfault).
// Multi-level fn-ptr autoderef is a deferred FEATURE (#181); the
// unsupported shape stays a loud reject in BOTH stages (rule 7/10).
if (ct != nil && ct.kind == syntax.nkind.N_TPTR) {
ct = resolvealias(c, unwrapbang(ct.lhs));
};
if (ct != nil) { if (ct.kind == syntax.nkind.N_TFN) {
let res: *syntax.node = ct.lhs;
e.type_ = tinfofornode(c, res): *void;
return res;
}; };
// Mirror cstage's loud `n->type = err(c, ..., "calling non-function")`
// (check.c:1948): a callee that isn't a TY_FN after the single peel is a
// hard reject. The tyerr stamp doubles as a once-guard — exprtype is not
// memoized (unlike cstage's cexpr n->type cache), so a second pass over
// this N_CALL must not re-emit; it also suppresses the post-checker
// asserttyped gate (L6683). One diagnostic, cgen gated off (w6c L186).
if (e.type_ == nil) {
cerr(e.file); cerr(":");
cerr(strconv.i32tos(e.line, strconv.base.DEC));
cerr(": error: calling non-function\n");
c.errs += 1;
e.type_ = c.tc.tyerr: *void;
};
return nil;
};
if (k == syntax.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: *syntax.node = e.lhs;
if (lhsn != nil) { if (lhsn.kind == syntax.nkind.N_IDENT) {
let ms: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, lhsn.str);
if (ms != nil && ms.skind != syntax.skind.SK_USE) {
let mu: *syntax.sym = syntax.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 == syntax.skind.SK_USE || ms.use_alias != 0i32) {
let fs: *syntax.sym = syntax.scopelookupinmodule(c.cur, modkeyfor(c, lhsn.str), e.str);
if (fs != nil) { if (fs.decl != nil) {
// #34: a module-qualified bare fn rvalue `mod.fn` types as
// its FN TYPE (twin of the N_IDENT arm, :2688); decl.lhs is
// the RETURN type for an N_FNDECL. Pins 706 (`let p1: fn()i32
// = mod1.ping`).
if (fs.skind == syntax.skind.SK_FN) {
let ft: *syntax.node = syntax.newnode(syntax.nkind.N_TFN, "", 0, 0);
ft.lhs = fs.decl.lhs;
ft.list = fs.decl.list;
e.type_ = tinfofornode(c, ft): *void;
return ft;
};
// #142 twin of the N_IDENT SK_TYPE arm: a
// module-qualified TYPE name used as a VALUE
// (`mod.too_long`) stamps the per-decl NAMED.
// The sym caches its NAMED (tinfofornode
// N_TNAME arm) once any type-position ref
// resolved; a nil cache falls through to the
// prior body stamp.
if (fs.skind == syntax.skind.SK_TYPE) {
if (fs.type_ != nil) {
e.type_ = fs.type_: *void;
return fs.decl.lhs;
};
};
let tn: *syntax.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 == syntax.skind.SK_TYPE) { if (ms.decl != nil) {
let body: *syntax.node = ms.decl.lhs;
let ub: *syntax.node = resolvealias(c, unwrapbang(body));
if (ub != nil) { if (ub.kind == syntax.nkind.N_TENUM) {
let prev: u64 = (-1i64): u64;
let m: *syntax.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 (syntax.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: *syntax.node = exprtype(c, lhsn, nil);
if (basetn != nil) {
let bu: *syntax.node = resolvealias(c, unwrapbang(basetn));
if (bu != nil) { if (bu.kind == syntax.nkind.N_TPTR) {
bu = resolvealias(c, unwrapbang(bu.lhs));
}; };
if (bu != nil) { if (bu.kind == syntax.nkind.N_TENUM) {
let prev: u64 = (-1i64): u64;
let m: *syntax.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 (syntax.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 == syntax.nkind.N_TNAME) && syntax.streq(bu.str, "str");
if (bu.kind == syntax.nkind.N_TSLICE || bu.kind == syntax.nkind.N_TARRAY || isstr) {
if (syntax.streq(e.str, "len")) {
let tn: *syntax.node = mktname(c, "i32");
e.type_ = tinfofornode(c, tn): *void;
return tn;
};
// A fixed array has no capacity word — .cap is
// invalid (drew ruling). cstage check.c errs
// symmetrically. c.errs>0 gates cgen off → build
// fails (the #11 inferarraylen idiom).
if (bu.kind == syntax.nkind.N_TARRAY && syntax.streq(e.str, "cap")) {
cerr("error: no field 'cap' on a fixed-size array (arrays have no capacity; use .len)\n");
c.errs += 1;
return nil;
};
if (syntax.streq(e.str, "cap")) {
let tn: *syntax.node = mktname(c, "i32");
e.type_ = tinfofornode(c, tn): *void;
return tn;
};
// rule-9 divergence-doc (drew, task #13): array.ptr ≡
// &A[0], a sanctioned ww spelling / faithful Hare
// desugaring (14 live consumers). KEEP — .ptr valid.
// See .ai/drew-gapa-ptr-ruling.md.
if (syntax.streq(e.str, "ptr")) {
let elem: *syntax.node = bu.lhs;
if (isstr) { elem = mktname(c, "u8"); };
let pp: *syntax.node = syntax.newnode(syntax.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. Embed-descending (#59.13): promoted names stamp
// the inner field's own type node.
if (bu != nil) { if (bu.kind == syntax.nkind.N_TSTRUCT) {
let ftn: *syntax.node = aststructfieldtype(c, bu, e.str, 0);
if (ftn != nil) {
e.type_ = tinfofornode(c, ftn): *void;
return ftn;
};
}; };
// 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 == syntax.nkind.N_TTUPLE) {
let idx: i32 = fldnumidx(e.str);
if (idx >= 0) {
let p: *syntax.node = bu.list;
for (idx > 0 && p != nil) {
p = p.next;
idx -= 1;
};
if (p != nil) {
let pt: *syntax.node = p.lhs;
e.type_ = tinfofornode(c, pt): *void;
return pt;
};
};
}; };
};
return nil;
};
if (k == syntax.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 == syntax.nkind.N_IDENT) {
let ms: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, e.lhs.str);
if (ms != nil) { if (ms.skind == syntax.skind.SK_TYPE) { if (ms.decl != nil) {
let tn: *syntax.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: *syntax.node = mktname(c, e.lhs.str);
let nti: *syntax.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: *syntax.node = resolvealias(c, tn);
if (stn != nil && stn.kind == syntax.nkind.N_TSTRUCT) {
let fi: *syntax.node = e.list;
for (fi != nil) {
if (fi.kind == syntax.nkind.N_FIELD
&& fi.lhs != nil) {
let ftn: *syntax.node = aststructfieldtype(c, stn, fi.str, 0);
if (ftn != nil) {
// #120: `S{ f: 1.0 }` narrows the
// field init to the field's f32.
// Mirror cstage N_STRUCTLIT site.
coercefloatlit(c, fi.lhs, ftn);
if (fi.lhs.kind == syntax.nkind.N_ARRLIT) {
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 == syntax.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: *syntax.node = nil;
let count: u64 = 0u64;
let it: *syntax.node = e.list;
for (it != nil) {
let skip: bool = false;
if (it.kind == syntax.nkind.N_FIELD) {
if (syntax.streq(it.str, "...")) { skip = true; };
};
if (!skip) {
let t: *syntax.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 == syntax.nkind.N_STRUCTLIT
&& it.lhs != nil
&& it.lhs.kind == syntax.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 == syntax.nkind.N_TNAME) {
if (syntax.streq(elt.str, "untyped_int")) {
elt = mktname(c, "int");
} else { if (syntax.streq(elt.str, "untyped_float")) {
elt = mktname(c, "f64");
} else { if (syntax.streq(elt.str, "untyped_str")) {
elt = mktname(c, "str");
} else { if (syntax.streq(elt.str, "untyped_rune")) {
elt = mktname(c, "rune");
} else { if (syntax.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: *syntax.node = syntax.newnode(syntax.nkind.N_TARRAY, "", 0, 0);
// #6(niche): stamp the SYNTHESIZED element TNAME so the inferred
// array's cgen is IDENTICAL to an explicit [N]T's (whose element is
// resolvewalk-stamped). For a scalar element (int/u8) cgen's
// fallback reads correctly with a nil elt.type_ (byte-id either
// way), but a multi-word element (str, 24B) needs the resolved
// element tinfo to emit the 3-word header element load — without it
// cgen drops to the 8B-scalar path and `xs[i].len` reads the slot
// stride, not the length (silent cs!=ww). Idempotent: elt may be a
// real exprtype result whose type_ is already set.
if (elt.type_ == nil) { elt.type_ = tinfofornode(c, elt): *void; };
arr.lhs = elt;
let cn: *syntax.node = syntax.newnode(syntax.nkind.N_INTLIT, "", 0, 0);
cn.uval = count;
// #6(niche): stamp the SYNTHESIZED count literal. When this arr is
// planted on an inferred array global's n.lhs (the array twin of
// #135/#150-B), asserttyped walks arr.rhs (this N_INTLIT, an expr
// node) and trips `asserttyped: int` — an explicit [N]T's count is
// resolvewalk-stamped, the synthesized inferred one wasn't. The
// element TNAME (arr.lhs) needs no stamp: N_TNAME is not an
// asserttyped expr kind. tinfofornode only resolves type-kind
// nodes, so type the count off a fresh `int` TNAME; cgen reads
// arr.rhs.uval, so this stamp is byte-id-inert.
cn.type_ = tinfofornode(c, mktname(c, "int")): *void;
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 == syntax.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: *syntax.node = exprtype(c, e.lhs, nil);
let bu: *syntax.node = resolvealias(c, unwrapbang(basetn));
if (bu == nil) { return nil; };
if (bu.kind == syntax.nkind.N_TARRAY) {
let sl: *syntax.node = syntax.newnode(syntax.nkind.N_TSLICE, "", 0, 0);
sl.lhs = bu.lhs;
e.type_ = tinfofornode(c, sl): *void;
return sl;
};
if (bu.kind == syntax.nkind.N_TSLICE) {
e.type_ = tinfofornode(c, basetn): *void;
return basetn;
};
if (bu.kind == syntax.nkind.N_TNAME) {
if (syntax.streq(bu.str, "str")) {
let tn: *syntax.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 == syntax.nkind.N_TPTR && bu.lhs != nil) {
let sl: *syntax.node = syntax.newnode(syntax.nkind.N_TSLICE, "", 0, 0);
sl.lhs = bu.lhs;
e.type_ = tinfofornode(c, sl): *void;
return sl;
};
return nil;
};
if (k == syntax.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: *syntax.node = nil;
let tail: *syntax.node = nil;
let it: *syntax.node = e.list;
for (it != nil) {
let elemt: *syntax.node = exprtype(c, it, nil);
let w: *syntax.node = syntax.newnode(syntax.nkind.N_TPARAM, "", 0, 0);
w.lhs = elemt;
if (head == nil) { head = w; }
else { tail.next = w; };
tail = w;
it = it.next;
};
let tt: *syntax.node = syntax.newnode(syntax.nkind.N_TTUPLE, "", 0, 0);
tt.list = head;
e.type_ = tinfofornode(c, tt): *void;
return tt;
};
if (k == syntax.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: *syntax.node = exprtype(c, e.lhs, nil);
let bu: *syntax.node = resolvealias(c, unwrapbang(basetn));
if (bu == nil) { return nil; };
if (bu.kind == syntax.nkind.N_TCHAN) {
e.type_ = tinfofornode(c, bu.lhs): *void;
return bu.lhs;
};
return nil;
};
if (k == syntax.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: *syntax.node = exprtype(c, e.lhs, nil);
if (t != nil) { e.type_ = tinfofornode(c, t): *void; };
return t;
};
if (k == syntax.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: *syntax.tinfo = nil;
let retn: *syntax.node = nil;
let cs: *syntax.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: *syntax.node = nil;
let t: *syntax.tinfo = matchyieldtype(c, cs.body, cs.str, cs.lhs, &armn);
if (t != nil) {
if (yt == nil) {
// First yielding arm: it is the match's type;
// retn carries its *node for the consumers (#264).
yt = t; retn = armn;
} else {
// #38/F2 (review item 6): cross-arm yield
// unification. cstage check.c:2080 rejects an arm
// whose yield is neither type_eq nor type_assignable
// to the first arm's. ww has tinfo typeeq but no
// tinfo type_assignable, so reject only a DEFINITE
// coarse-family mismatch (yieldclass) — closing the
// catA silent accept (int arm vs str arm reads the
// str header through an int-stamped slot at runtime)
// while staying lenient on same-family / untyped
// promotions cstage admits. Walks ALL arms (no early
// break): the post-walk matchyieldtype reads each
// arm's cached operand stamp, a pure read (#264).
// RETAINED DIVERGENCE (match-yield precision-gap task,
// lead #52 - distinct from the enum-reinterpret #52
// note elsewhere): the coarse yieldclass ALSO under-
// rejects same-family-but-not-assignable arms cstage
// DOES reject (untyped_int vs i64 / untyped_int vs
// untyped_float) - a precision gap, not a silent
// miscompile of the catA repro; closeable only with a
// real tinfo type_assignable.
if (!syntax.typeeq(yt, t)) {
let ca: i32 = yieldclass(yt);
let cb: i32 = yieldclass(t);
if (ca != 0i32 && cb != 0i32 && ca != cb) {
deffolderr(c, cs, "match arm yields an incompatible type");
};
};
};
};
cs = cs.next;
};
if (yt == nil) {
yt = tinfofornode(c, mktname(c, "void"));
};
e.type_ = yt: *void;
return retn;
};
if (k == syntax.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: *syntax.node = mktname(c, "void");
e.type_ = tinfofornode(c, v): *void;
return v;
};
let t: *syntax.node = exprtype(c, e.lhs, nil);
if (t != nil) { e.type_ = tinfofornode(c, t): *void; };
return t;
};
if (k == syntax.nkind.N_TRYPROP) {
// success unwrap: the success-variant type of operand's
// tagged union.
let opt: *syntax.node = exprtype(c, e.lhs, nil);
let ou: *syntax.node = resolvealias(c, unwrapbang(opt));
if (ou == nil) { return nil; };
if (ou.kind != syntax.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: *syntax.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 == syntax.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: *syntax.node = exprtype(c, e.lhs, nil);
let ou: *syntax.node = resolvealias(c, unwrapbang(opt));
if (ou == nil) { return nil; };
if (ou.kind != syntax.nkind.N_TTAGGED) { return nil; };
if (taggedhaserr(c, ou)) {
let v: *syntax.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 == syntax.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 == syntax.nkind.N_TYPETEST) {
// `e is T` → bool
let tn: *syntax.node = mktname(c, "bool");
e.type_ = tinfofornode(c, tn): *void;
return tn;
};
return nil;
};
fn isuntypedint(t: *syntax.node) bool = {
if (t == nil) { return false; };
if (t.kind != syntax.nkind.N_TNAME) { return false; };
return syntax.streq(t.str, "untyped_int");
};
fn isuntypedfloat(t: *syntax.node) bool = {
if (t == nil) { return false; };
if (t.kind != syntax.nkind.N_TNAME) { return false; };
return syntax.streq(t.str, "untyped_float");
};
fn isuntypednil(t: *syntax.node) bool = {
if (t == nil) { return false; };
if (t.kind != syntax.nkind.N_TNAME) { return false; };
return syntax.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: *syntax.node) bool = {
if (t == nil) { return false; };
if (t.kind == syntax.nkind.N_TENUM) { return true; };
if (t.kind != syntax.nkind.N_TNAME) { return false; };
let s: str = t.str;
if (syntax.streq(s, "i8")) { return true; };
if (syntax.streq(s, "i16")) { return true; };
if (syntax.streq(s, "i32")) { return true; };
if (syntax.streq(s, "i64")) { return true; };
if (syntax.streq(s, "u8")) { return true; };
if (syntax.streq(s, "u16")) { return true; };
if (syntax.streq(s, "u32")) { return true; };
if (syntax.streq(s, "u64")) { return true; };
if (syntax.streq(s, "int")) { return true; };
if (syntax.streq(s, "uint")) { return true; };
if (syntax.streq(s, "uintptr")) { return true; };
if (syntax.streq(s, "size")) { return true; };
if (syntax.streq(s, "rune")) { return true; };
return false;
};
// intkindast / numkindast / boolkindast — operand-kind classifiers for
// binoptype/unoptype's cstage-mirrored gates (#38/F2 review item 5).
// They resolvealias + unwrapbang to chase the alias (TY_NAMED) wrapper
// before classifying — exactly as cstage's type_isint/type_isnum recurse
// through TY_NAMED.under (cmd/wcc/type.c:178-180/192-193) and its AND/OR
// arm chases via type_chase_named (check.c:1206-1207). Without the chase,
// a `type myint = i32` operand would spuriously fail the gate that cstage
// (chasing) accepts. nil operands are treated as already-errored upstream
// (the unifyarith nil-bail shapes) and NOT re-rejected — the cstage twin's
// `l == ty_err` escape.
fn intkindast(c: *checker, t: *syntax.node) bool = {
if (t == nil) { return false; };
let u: *syntax.node = resolvealias(c, unwrapbang(t));
if (isinttypeast(u)) { return true; }; // int family + enum + rune
if (isuntypedint(u)) { return true; };
if (u != nil && u.kind == syntax.nkind.N_TNAME && syntax.streq(u.str, "untyped_rune")) {
return true;
};
return false;
};
fn numkindast(c: *checker, t: *syntax.node) bool = {
if (intkindast(c, t)) { return true; };
if (t == nil) { return false; };
let u: *syntax.node = resolvealias(c, unwrapbang(t));
if (u == nil) { return false; };
if (u.kind != syntax.nkind.N_TNAME) { return false; };
let s: str = u.str;
if (syntax.streq(s, "f32")) { return true; };
if (syntax.streq(s, "f64")) { return true; };
if (syntax.streq(s, "untyped_float")) { return true; };
return false;
};
fn boolkindast(c: *checker, t: *syntax.node) bool = {
if (t == nil) { return false; };
let u: *syntax.node = resolvealias(c, unwrapbang(t));
if (u == nil) { return false; };
if (u.kind != syntax.nkind.N_TNAME) { return false; };
return syntax.streq(u.str, "bool") || syntax.streq(u.str, "untyped_bool");
};
fn isnumerictname(t: *syntax.node) bool = {
if (t == nil) { return false; };
if (t.kind != syntax.nkind.N_TNAME) { return false; };
let s: str = t.str;
if (syntax.streq(s, "i8")) { return true; };
if (syntax.streq(s, "i16")) { return true; };
if (syntax.streq(s, "i32")) { return true; };
if (syntax.streq(s, "i64")) { return true; };
if (syntax.streq(s, "u8")) { return true; };
if (syntax.streq(s, "u16")) { return true; };
if (syntax.streq(s, "u32")) { return true; };
if (syntax.streq(s, "u64")) { return true; };
if (syntax.streq(s, "int")) { return true; };
if (syntax.streq(s, "uint")) { return true; };
if (syntax.streq(s, "uintptr")) { return true; };
if (syntax.streq(s, "size")) { return true; };
if (syntax.streq(s, "rune")) { return true; };
if (syntax.streq(s, "f32")) { return true; };
if (syntax.streq(s, "f64")) { return true; };
return false;
};
fn isstrtname(t: *syntax.node) bool = {
if (t == nil) { return false; };
if (t.kind != syntax.nkind.N_TNAME) { return false; };
return syntax.streq(t.str, "str");
};
// tagshape — #24/#37: the coarse variant-shape bucket of a (resolved) type
// node, the AST-side mirror of cgen taggedvariantindext's str/slice shape
// fallback (cgenutil.ww:3062-3070, `wantstr`/`wantslice` over typeisstr/
// typeisslice). Three buckets: 2=slice, 1=str, 0=scalar/other (ptr / struct
// / tuple / chan / fn / int / enum / ...). Used by the concrete→tagged
// aggregate-shape-lenient leg to keep a tagged accept lenient ONLY against a
// shape-compatible variant — same classifier cgen boxes with, so the checker
// accept and the cgen box agree (rule-12: reuse the in-tree classifier).
fn tagshape(t: *syntax.node) i32 = {
if (t == nil) { return 0i32; };
if (t.kind == syntax.nkind.N_TSLICE) { return 2i32; };
if (isstrtname(t)) { return 1i32; };
return 0i32;
};
// 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: *syntax.node, synth: *syntax.node) bool = {
if (ptr == nil) { return false; };
let pu: *syntax.node = resolvealias(c, unwrapbang(ptr));
if (pu == nil) { return false; };
if (pu.kind != syntax.nkind.N_TPTR) { return false; };
let ref: *syntax.node = resolvealias(c, unwrapbang(pu.lhs));
if (ref == nil) { return false; };
if (ref.kind != syntax.nkind.N_TFN) { return false; };
return typeeqast(c, 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: *syntax.node, rhs: *syntax.node) bool = {
if (dst == nil) { return false; };
if (rhs == nil) { return false; };
if (rhs.kind != syntax.nkind.N_UN) { return false; };
if (rhs.op != syntax.tkind.TK_AMP) { return false; };
let id: *syntax.node = rhs.lhs;
if (id == nil) { return false; };
if (id.kind != syntax.nkind.N_IDENT) { return false; };
// #4: curmod preference. Without it a `&handler` whose leaf also
// names a fn in a later module misbinds the foreign fn's signature
// here (scopedefineinmodule prepends → chain-first = last module),
// silently admitting a *fn into a foreign-sig slot or rejecting a
// valid same-module &fn. cstage uses scope_lookup_prefer with
// cur_mod (cmd/wcc/check.c:410, assignable_addrfn).
let s: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, id.str);
if (s == nil) { return false; };
if (s.skind != syntax.skind.SK_FN) { return false; };
if (s.decl == nil) { return false; };
let d: *syntax.node = s.decl;
if (d.kind != syntax.nkind.N_FNDECL) { return false; };
let synth: *syntax.node = syntax.newnode(syntax.nkind.N_TFN, "", 0, 0);
synth.lhs = d.lhs;
synth.list = d.list;
let du: *syntax.node = resolvealias(c, unwrapbang(dst));
if (du == nil) { return false; };
if (du.kind == syntax.nkind.N_TPTR) { return addrfnptrmatches(c, du, synth); };
if (du.kind == syntax.nkind.N_TTAGGED) {
let nmatch: i32 = 0;
let v: *syntax.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: *syntax.node, src: *syntax.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: *syntax.node = resolvealias(c, unwrapbang(dst));
let su: *syntax.node = resolvealias(c, unwrapbang(src));
if (du == nil) { *confident = false; return true; };
if (su == nil) { *confident = false; return true; };
if (typeeqast(c, 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 == syntax.nkind.N_TSLICE) {
if (su.kind == syntax.nkind.N_TARRAY) {
if (typeeqast(c, 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 == syntax.nkind.N_TTAGGED) {
let v: *syntax.node = du.list;
for (v != nil) {
let vu: *syntax.node = resolvealias(c, unwrapbang(v));
if (vu != nil) {
if (isnumerictname(vu)) { return true; };
// mirror cstage type_isnum(enum)=true (type.c:201 ->
// type_isint -> :178 TY_ENUM); an enum variant DOES
// accept an untyped int. Without this the #23 fix would
// flip an enum-variant union to reject while cstage
// accepts = a NEW divergence (A3 trap).
if (vu.kind == syntax.nkind.N_TENUM) { return true; };
};
v = v.next;
};
// #24: a SPREAD variant (`...formattable`) keeps the lenient
// escape — cstage flattens spreads at resolve_type so its
// type_assignable sees the spread's inlined numeric leaves and
// accepts `take(42)` into `(...formattable | bool)`; wwstage
// stays AST-keyed (#115) and cannot flatten, so a confident
// reject here would OVER-reject what cstage accepts (the new c3
// general call-arg check made this path reachable). Mirror the
// tagged→tagged spread escape (:4117). Spread decl-form flatten
// is #199b, deferred.
for (let p: *syntax.node = du.list; p != nil; p = p.next) {
if (p.op == syntax.tkind.TK_ELLIPSIS) {
*confident = false;
return true;
};
};
// #23: no DIRECT variant accepts an untyped int -> confident
// reject (mirror cstage type.c:343 `return 0`). ww does NOT
// flatten a nested union variant (#199-alpha non-drill); an int
// reachable only via a nested union (e.g. (inner|str),
// inner=(int|bool)) would otherwise silently build tag=0/payload
// with no inner-tag wrapper = malformed box. *confident is
// already true (:3777, untouched on this path) so the caller
// sees ok=false,conf=true -> loud errnotassign. DEFERRED
// divergence (task #23 / #199b): both stages then over-reject
// valid Hare (expand_tagged flattens); faithful flatten+rebox
// is post-CSP nominal-identity work.
return false;
};
// Known non-numeric primitive: confidently wrong.
if (du.kind == syntax.nkind.N_TNAME) {
if (syntax.streq(du.str, "bool")) { return false; };
if (syntax.streq(du.str, "void")) { return false; };
if (syntax.streq(du.str, "str")) { return false; };
};
// #24: untyped int into a known AGGREGATE (slice/array/ptr/fn/chan/
// tuple/struct) — confident reject. `let xs: []int = 5` read the int
// as a 24B slice header (the #24 silent-garbage; the catch-all below
// left it unconfident → silent accept). cstage type_assignable
// rejects untyped_int into a non-numeric aggregate (cmd/wcc/type.c).
// The TTAGGED case is handled above; this is the bare aggregate.
if (du.kind == syntax.nkind.N_TSLICE || du.kind == syntax.nkind.N_TARRAY
|| du.kind == syntax.nkind.N_TPTR || du.kind == syntax.nkind.N_TFN
|| du.kind == syntax.nkind.N_TCHAN || du.kind == syntax.nkind.N_TTUPLE
|| du.kind == syntax.nkind.N_TSTRUCT) {
return false;
};
// Unknown shapes: stay quiet.
*confident = false;
return true;
};
if (isuntypedfloat(su)) {
if (isnumerictname(du)) { return true; };
if (du.kind == syntax.nkind.N_TNAME) {
if (syntax.streq(du.str, "bool")) { return false; };
if (syntax.streq(du.str, "void")) { return false; };
if (syntax.streq(du.str, "str")) { return false; };
};
// A4 (#23 float-twin): (T | ...) tagged dst — accept iff a DIRECT
// variant is a float type. cstage type_assignable for an
// untyped_float src uses type_isfloat (type.c:376 — f32/f64 ONLY,
// NOT type_isnum), so an int/enum variant does NOT accept an
// untyped float. No direct float variant -> confident reject
// (cstage type.c:316 loop returns 0); ww does NOT flatten a
// nested-union float variant (#199-alpha). *confident is already
// true (:3777). Without it the catch-all (:3972) over-accepts an
// untyped float into ANY tagged (silent tag=0). Faithful
// flatten+rebox deferred post-CSP (nominal id, #23/#40).
if (du.kind == syntax.nkind.N_TTAGGED) {
let v: *syntax.node = du.list;
for (v != nil) {
let vu: *syntax.node = resolvealias(c, unwrapbang(v));
if (vu != nil) {
if (vu.kind == syntax.nkind.N_TNAME) {
if (syntax.streq(vu.str, "f32")) { return true; };
if (syntax.streq(vu.str, "f64")) { return true; };
};
};
v = v.next;
};
// #24: spread variant keeps the lenient escape (cstage flattens;
// wwstage can't) — same rationale as the untyped-int arm above.
for (let p: *syntax.node = du.list; p != nil; p = p.next) {
if (p.op == syntax.tkind.TK_ELLIPSIS) {
*confident = false;
return true;
};
};
return false;
};
// #24: untyped float into a known AGGREGATE — confident reject (twin
// of the untyped-int aggregate arm above; `let xs: []f64 = 1.5`).
if (du.kind == syntax.nkind.N_TSLICE || du.kind == syntax.nkind.N_TARRAY
|| du.kind == syntax.nkind.N_TPTR || du.kind == syntax.nkind.N_TFN
|| du.kind == syntax.nkind.N_TCHAN || du.kind == syntax.nkind.N_TTUPLE
|| du.kind == syntax.nkind.N_TSTRUCT) {
return false;
};
*confident = false;
return true;
};
if (isuntypednil(su)) {
// nil → ptr/slice/chan/fn/nullable
if (du.kind == syntax.nkind.N_TPTR) { return true; };
if (du.kind == syntax.nkind.N_TSLICE) { return true; };
if (du.kind == syntax.nkind.N_TCHAN) { return true; };
if (du.kind == syntax.nkind.N_TFN) { return true; };
// nullable `(*T | void)` — already accepted by typeeqast
// when matched whole; nil is OK there too.
if (du.kind == syntax.nkind.N_TTAGGED) {
let v: *syntax.node = du.list;
for (v != nil) {
if (v.kind == syntax.nkind.N_TPTR) { return true; };
if (v.kind == syntax.nkind.N_TSLICE) { return true; };
if (v.kind == syntax.nkind.N_TCHAN) { return true; };
if (v.kind == syntax.nkind.N_TFN) { return true; };
v = v.next;
};
// #24: spread variant keeps the lenient escape (cstage flattens;
// wwstage can't) — same rationale as the untyped-int arm above.
for (let p: *syntax.node = du.list; p != nil; p = p.next) {
if (p.op == syntax.tkind.TK_ELLIPSIS) {
*confident = false;
return true;
};
};
// A5: no nullable (ptr/slice/chan/fn) variant -> confident
// reject (cstage type.c:316 loop returns 0; nil accepts only
// into ptr/slice/chan/fn per type.c:382-385). *confident is
// already true (:3777). Without it the catch-all (:3972)
// over-accepts nil into ANY tagged (silent). The trailing
// fallthrough below stays for a NON-tagged du (nil into a bare
// scalar — a separate non-family over-accept, SIBLINGS).
return false;
};
*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 == syntax.nkind.N_TTAGGED && su.kind != syntax.nkind.N_TTAGGED) {
let v: *syntax.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 == syntax.tkind.TK_ELLIPSIS);
let vu: *syntax.node = resolvealias(c, unwrapbang(v));
let vtagged: bool = false;
if (vu != nil) { if (vu.kind == syntax.nkind.N_TTAGGED) { vtagged = true; }; };
if (vtagged && !vspread) {
if (typeeqast(c, v, src)) { return true; };
} else {
let innerconf: bool = false;
if (isassignable(c, v, src, &innerconf)) { return true; };
};
v = v.next;
};
// #24: no variant matched. A SCALAR src (known primitive, su is
// N_TNAME) is a CONFIDENT reject — `int` into `(str | bool)` (the
// #23/#199-α path). An AGGREGATE src (ptr/slice/struct/tuple/chan/
// fn — su.kind != N_TNAME) stays LENIENT: wwstage's nominal-lossy
// model can't confirm a cross-module ptr/struct variant (the `stream`
// variant is `*vtable` but io.handle's consumer hands a `*io.vtable`
// from `&cgoutstream.vt`, or a qualified `io.stream` alias — bare-vs-
// qualified NAMED identity, #10/#66; typeeqast can't span it), while
// cstage flattens+resolves and ACCEPTS (io.stream → io.handle =
// (file | stream)). Pre-c3 the loop accepted ANY src via the first
// scalar variant's lenient-true short-circuit; the c3 scalar↔aggregate
// reject removed that crutch, exposing the latent nominal gap, so
// distinguish by src shape here. The handoff's "preserve concrete→
// tagged accept" path.
//
// An AGGREGATE src (su.kind != N_TNAME) stays lenient ONLY against a
// SHAPE-COMPATIBLE variant — tagshape mirrors cgen taggedvariantindext's
// str/slice/scalar-other classifier (cgenutil.ww:3062), so the checker
// accept and the cgen box agree (rule-12). A shape-MISMATCHED aggregate
// (e.g. a `[]int` slice src into a tagged with no slice variant) is a
// CONFIDENT reject, matching cstage's nominal type_assignable; this is
// the reachable win (#24-B, rob/lead-ruled shape-narrowing over the
// blanket aggregate-lenient).
//
// RULE-7 TRACKED RESIDUAL (task #37, behind the #10/#66 nominal arc;
// NEVER silent): the SAME-COARSE-SHAPE leg still OVER-ACCEPTS a cross-
// module SAME-LEAF collision that cstage REJECTS — e.g. `mod1.stream`
// (a `*mod1.wbox`, scalar/other shape) passed where `io2.handle =
// (io2.file | io2.stream)` is wanted (io2.stream is also scalar/other,
// so the shapes match and this stays lenient). cstage rejects it on
// NOMINAL identity; wwstage accepts. PROVEN STRUCTURALLY UNREACHABLE
// here: the tagged-union variant node is a BARE name (`stream`,
// N_TNAME, no module) — BYTE-IDENTICAL for the genuine io2.stream and
// the collision mod1.stream — so isassignable, which is AST-NODE-keyed,
// has no bit to tell them apart; the distinguishing identity lives only
// in the tinfo layer (#66 per-decl TY_NAMED ptr, which cgen's
// flatvariantidxt already uses). The reject becomes reachable ONLY when
// isassignable is converted to nominal-tinfo keying = the #37 /
// #10/#66 work itself, NOT a c3-scope change. (B) shape-narrowing
// shrinks the residual from "all aggregate→tagged" to "same-coarse-
// shape same-leaf" but cannot close the same-shape ptr↔ptr collision.
// This is the LEAF-NAME nominal-collision family also documented at the
// tagged→tagged qualleaf bridge (this fn, below) — the eventual #10/#66
// sweep must convert BOTH sites uniformly (enumerate for the sweep:
// (i) this concrete→tagged shape-lenient leg, (ii) the tagged→tagged
// qualleaf bridge). Pre-c3 the collision was ALSO accepted (call-arg
// ran no check; let/return short-circuited on the scalar variant) — c3
// is NEUTRAL on it.
// #18: an ARRAY src reaching here means NO slice/array variant
// matched by element type — the loop above confident-rejects a
// [N]T-vs-[]U element mismatch and returns true on an exact match,
// so an array surviving to here matches nothing. An array's only
// legal tagged path is an exact-element slice variant (#17 desugar)
// or an array variant (#5/#60), both decided confidently in the
// loop. The shape-lenient leg below exists for nominal-lossy cross-
// module ptr/struct identity (#10/#66/#37); arrays never have it
// (element identity is structural, fully AST-known). So a [N]T here
// is a CONFIDENT reject — mirror cstage type_assignable's concrete→
// tagged arm returning 0 (cmd/wcc/type.c:344) and harec
// tagged_select_subtype (no unique assignable member, ref/harec/src/
// types.c:702 + the STORAGE_SLICE to_secondary==from_secondary gate
// :1097). Without this an untyped-int arrlit return (`[10,20,30]`
// defaults to [3]int) shape-matched the `e` (i32) variant (both
// tagshape 0) → conf=false → checkretassign bailed before
// rejectarrlitborrow = silent accept (wwstage-only, cs≠ww).
if (su.kind == syntax.nkind.N_TARRAY) {
return false; // *confident already true on this path
};
if (su.kind != syntax.nkind.N_TNAME) {
let ss: i32 = tagshape(su);
let sp: *syntax.node = du.list;
for (sp != nil) {
let svu: *syntax.node = resolvealias(c, unwrapbang(sp));
if (svu != nil) {
if (tagshape(svu) == ss) {
*confident = false;
return true;
};
};
sp = sp.next;
};
// no shape-compatible variant → confident reject (the #24-B win)
return false;
};
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 == syntax.nkind.N_TTAGGED && su.kind == syntax.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: *syntax.node = du.list;
for (v != nil) {
let vu: *syntax.node = resolvealias(c, unwrapbang(v));
if (vu != nil) {
if (vu.kind == syntax.nkind.N_TTAGGED) {
if (typeeqast(c, v, src)) { return true; };
};
};
v = v.next;
};
// Spread `...` member on either side: keep the lenient escape.
// cstage flattens spreads at resolve_type so its subset loop
// never sees one; wwstage stays AST-keyed (#115, check.ww:921),
// so a TK_ELLIPSIS variant can reach here. Routing it through
// the strict typeeqast subset loop would over-reject a valid
// spread-widen cstage accepts → new cs≠ww divergence. Spread
// decl-form layout is #199b, deferred.
let hasspread: bool = false;
for (let p: *syntax.node = du.list; p != nil; p = p.next) {
if (p.op == syntax.tkind.TK_ELLIPSIS) { hasspread = true; };
};
for (let p: *syntax.node = su.list; p != nil; p = p.next) {
if (p.op == syntax.tkind.TK_ELLIPSIS) { hasspread = true; };
};
if (hasspread) {
*confident = false;
return true;
};
// Structural subset loop — wwstage was align-DOWN-missing this;
// cstage type.c type_assignable tagged→tagged arm (type.c:360-367).
// Every src variant must appear in dst, else a loud reject; a
// genuine subset accepts.
//
// Per-variant cover is the HONEST FLOOR (drew ruling A): typeeqast
// FIRST for the exact / structural variants (e.g. []u8 slices,
// nested unions — bufio scanbytes/scanline forward `[]u8`), THEN a
// LEAF-ONLY name bridge (qualleaf, module IGNORED) for the bare-vs-
// qualified spelling mix typeeqast cannot span. A callee returning
// an INLINE union spells its variants BARE (utf8.next:
// (rune|done|more|invalid)) while the consumer annotates them
// QUALIFIED (utf8.done); the module qualifier is unrecoverable for
// an inline-union return, so leaf alone decides. Mirrors casecovers'
// typeeqast-then-leaf composition (check.ww:4136). qualmod is NOT
// compared (cf casevariantpairmatch): module identity is the #10
// gap. Sound for the bootstrap — no two of its variants share a leaf
// (drew); the cross-module same-leaf collision (a.foo vs b.foo) is a
// known over-accept deferred to #10 / filed in the #4 census.
for (let sp: *syntax.node = su.list; sp != nil; sp = sp.next) {
let ok: bool = false;
for (let dp: *syntax.node = du.list; dp != nil; dp = dp.next) {
if (typeeqast(c, dp, sp)) { ok = true; break; };
let su2: *syntax.node = unwrapbang(sp);
let du2: *syntax.node = unwrapbang(dp);
if (su2 != nil && du2 != nil &&
su2.kind == syntax.nkind.N_TNAME && du2.kind == syntax.nkind.N_TNAME &&
syntax.streq(qualleaf(su2.str), qualleaf(du2.str))) {
ok = true; break;
};
};
if (!ok) { return 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 == syntax.nkind.N_TTAGGED && du.kind != syntax.nkind.N_TTAGGED) {
return false;
};
// Two known primitives with different names are confidently
// incompatible. `i32 ↔ bool`, `str ↔ i32`, etc.
if (du.kind == syntax.nkind.N_TNAME && su.kind == syntax.nkind.N_TNAME) {
let known_d: bool = isnumerictname(du) || isstrtname(du);
if (!known_d) { if (syntax.streq(du.str, "bool")) { known_d = true; }; };
if (!known_d) { if (syntax.streq(du.str, "void")) { known_d = true; }; };
let known_s: bool = isnumerictname(su) || isstrtname(su);
if (!known_s) { if (syntax.streq(su.str, "bool")) { known_s = true; }; };
if (!known_s) { if (syntax.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 == syntax.nkind.N_TPTR) {
if (su.kind == syntax.nkind.N_TPTR) {
let dref: *syntax.node = resolvealias(c, unwrapbang(du.lhs));
let sref: *syntax.node = resolvealias(c, unwrapbang(su.lhs));
if (dref != nil) {
if (sref != nil) {
if (dref.kind == syntax.nkind.N_TFN) {
if (sref.kind == syntax.nkind.N_TFN) {
return false;
};
};
};
};
};
};
// #34: two bare fn types reaching here are NOT structurally equal
// (typeeqast returned true at the top otherwise) — the fn signatures
// differ, a confident reject mirroring cstage's structural fn
// type_assignable (`init fn() str not assignable to declared fn() i32`;
// harec types.c:1001 dealias-equal fn types). Manifests only now that
// S1/S2 stamp a bare fn rvalue with its fn type: `let p: fn()i32 = h`
// (h: fn()str) was a silent mis-accept via the lenient catch-all below.
// The matched-sig case already returned true via typeeqast at the top.
// The `&fn` (*fn) vs bare-fn KIND mismatch (du N_TFN, su N_TPTR) is a
// different shape, closed by c3's aggregate-kind reject, not here.
if (du.kind == syntax.nkind.N_TFN) {
if (su.kind == syntax.nkind.N_TFN) {
return false;
};
};
// #24: a known scalar primitive vs a known aggregate (slice / array /
// ptr / fn / chan / tuple / struct), and two aggregates of DIFFERENT
// kinds, are CONFIDENT rejects — mirror cstage type_assignable, which
// separates scalar from aggregate and rejects a kind mismatch (the int
// read as a 24B slice header was the #24 silent-garbage). du/su are
// already alias-RESOLVED (:3943/3944), so `type A = []int` arrives as
// N_TSLICE. The array→slice BORROW (su N_TARRAY into du N_TSLICE),
// untyped / nil / tagged, and the known-primitive-pair / fn-ptr / fn-fn
// shapes all returned above before reaching here. The `&fn` adopt-the-
// alias case (a *fn N_TPTR src into a bare-fn N_TFN dst — different
// aggregate kinds) is rescued at the let/return/call-arg sites by the
// assignableaddrfn UNION, so a reject here is correct (the caller's
// union accepts the genuine &fn). SAME-kind aggregate structural
// mismatches ([]int vs []str, *u8 vs *i32) stay lenient below —
// wwstage's nominal-lossy model can't span them (the #10 gap); cstage
// rejects via structural type_assignable, a filed residual under-reject,
// NOT a new over-reject. A NAMED struct/alias dst that does NOT resolve
// to a known kind stays N_TNAME-non-prim → neither set → lenient.
let dprim: bool = du.kind == syntax.nkind.N_TNAME
&& (isnumerictname(du) || isstrtname(du)
|| syntax.streq(du.str, "bool") || syntax.streq(du.str, "void"));
let sprim: bool = su.kind == syntax.nkind.N_TNAME
&& (isnumerictname(su) || isstrtname(su)
|| syntax.streq(su.str, "bool") || syntax.streq(su.str, "void"));
let daggr: bool = du.kind == syntax.nkind.N_TSLICE || du.kind == syntax.nkind.N_TARRAY
|| du.kind == syntax.nkind.N_TPTR || du.kind == syntax.nkind.N_TFN
|| du.kind == syntax.nkind.N_TCHAN || du.kind == syntax.nkind.N_TTUPLE
|| du.kind == syntax.nkind.N_TSTRUCT;
let saggr: bool = su.kind == syntax.nkind.N_TSLICE || su.kind == syntax.nkind.N_TARRAY
|| su.kind == syntax.nkind.N_TPTR || su.kind == syntax.nkind.N_TFN
|| su.kind == syntax.nkind.N_TCHAN || su.kind == syntax.nkind.N_TTUPLE
|| su.kind == syntax.nkind.N_TSTRUCT;
if (sprim && daggr) { return false; };
if (dprim && saggr) { return false; };
// #24: two aggregates of DIFFERENT kinds → confident reject (array/slice
// into ptr/fn/chan/tuple is the 24B/16B-header misread). EXEMPT a STRUCT
// on either side: wwstage's name-keyed resolvealias mis-resolves a bare
// cross-module same-leaf type name to the WRONG module's struct (#224 —
// `type s = *vtable` in sa vs `type s = struct{}` in sb; sa.read's bare
// `s` param resolves to sb's struct), so a struct-vs-ptr "mismatch" here
// is an artifact of the lossy resolution, not a real type error — cstage
// resolves `s` correctly and ACCEPTS (test 784). Same nominal-lossy
// principle as the concrete→tagged aggregate-lenient arm above; the
// struct-into-ptr genuine mismatch stays a filed #224/#10 under-reject.
if (daggr && saggr && du.kind != su.kind
&& du.kind != syntax.nkind.N_TSTRUCT && su.kind != syntax.nkind.N_TSTRUCT) {
return false;
};
// Anything else: don't claim confidence.
*confident = false;
return true;
};
// 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.
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;
};
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: *syntax.node, pat: *syntax.node, unionmod: str) bool = {
let vv: *syntax.node = unwrapbang(v);
let pp: *syntax.node = unwrapbang(pat);
if (vv == nil) { return false; };
if (pp == nil) { return false; };
if (vv.kind != syntax.nkind.N_TNAME) { return false; };
if (pp.kind != syntax.nkind.N_TNAME) { return false; };
if (!syntax.streq(qualleaf(vv.str), qualleaf(pp.str))) { return false; };
return syntax.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: *syntax.node) str = {
let defmod: str = c.curmod;
let cur: *syntax.node = unwrapbang(st);
for (cur != nil) {
if (cur.kind != syntax.nkind.N_TNAME) { return defmod; };
let s: *syntax.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: *syntax.node = unwrapbang(s.decl.lhs);
if (body == nil) { return defmod; };
if (body.kind == syntax.nkind.N_TTAGGED) { return defmod; };
cur = body;
};
return defmod;
};
fn casecovers(c: *checker, cs: *syntax.node, want: *syntax.node, unionmod: str) bool = {
if (cs.lhs != nil) {
if (typeeqast(c, cs.lhs, want)) { return true; };
if (casevariantpairmatch(want, cs.lhs, unionmod)) { return true; };
};
let alt: *syntax.node = cs.list;
for (alt != nil) {
if (typeeqast(c, alt, want)) { return true; };
if (casevariantpairmatch(want, alt, unionmod)) { return true; };
alt = alt.next;
};
return false;
};
fn errmatchvariant(c: *checker, n: *syntax.node, vname: *syntax.node) void = {
cerr("match: variant not handled");
if (vname != nil) {
if (vname.kind == syntax.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: *syntax.node, pat: *syntax.node, unionmod: str) bool = {
let v: *syntax.node = tagged.list;
for (v != nil) {
if (v.op == syntax.tkind.TK_ELLIPSIS) {
let inner: *syntax.node = resolvealias(c, unwrapbang(v));
if (inner != nil) {
if (inner.kind == syntax.nkind.N_TTAGGED) {
if (casevariantin(c, inner, pat,
taggeddefmod(c, v))) {
return true;
};
v = v.next;
continue;
};
};
};
if (typeeqast(c, v, pat)) { return true; };
if (casevariantpairmatch(v, pat, unionmod)) { return true; };
v = v.next;
};
return false;
};
fn errbadcase(c: *checker, pat: *syntax.node) void = {
cerr("case: not a variant of scrutinee");
if (pat != nil) {
if (pat.kind == syntax.nkind.N_TNAME) {
cerr(" (");
cerr(pat.str);
cerr(")");
};
};
cerr("\n");
c.errs += 1;
};
fn checkmatchexhaust(c: *checker, n: *syntax.node) void = {
if (n == nil) { return; };
if (n.lhs == nil) { return; };
let st: *syntax.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 == syntax.nkind.N_TRYPROP || n.lhs.kind == syntax.nkind.N_TRYUNW) {
istry = true;
st = exprtype(c, n.lhs, nil);
};
};
let u: *syntax.node = resolvealias(c, unwrapbang(st));
if (u == nil) { return; };
if (u.kind != syntax.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: *syntax.node = n.list;
for (cs0 != nil) {
if (cs0.lhs != nil) {
if (!casevariantin(c, u, cs0.lhs, unionmod)) {
errbadcase(c, cs0.lhs);
};
let alt: *syntax.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: *syntax.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: *syntax.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: *syntax.node, v: *syntax.node, unionmod: str) void = {
if (v.op == syntax.tkind.TK_ELLIPSIS) {
let inner: *syntax.node = resolvealias(c, unwrapbang(v));
if (inner != nil) {
if (inner.kind == syntax.nkind.N_TTAGGED) {
let im: str = taggeddefmod(c, v);
let m: *syntax.node = inner.list;
for (m != nil) {
checkvariantcovered(c, n, m, im);
m = m.next;
};
return;
};
};
};
let covered: bool = false;
let cs2: *syntax.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); };
};
// 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: *syntax.node, src: *syntax.node, where: str) void = {
cerr(where);
cerr(": not assignable");
if (src != nil) {
if (src.kind == syntax.nkind.N_TNAME) {
cerr(" (");
cerr(src.str);
cerr(" → ");
if (dst != nil) {
if (dst.kind == syntax.nkind.N_TNAME) {
cerr(dst.str);
};
};
cerr(")");
};
};
cerr("\n");
c.errs += 1;
};
// taggedarrayvariantctor — #5/#60: true when `src` is boxed into the
// tagged union `dst` via a variant that chases to TY_ARRAY. The array-
// payload box is unwired in cgen (cstage zero-filled the slot at box
// materialization — a silent MOVQ $0 payload drop; wwstage only loud at
// the dead match arm, errbadcase:4107). Reject the CONSTRUCT until the
// faithful array-block-store lands (deferred task #6). The tagged TYPE-
// decl with an array variant stays legal — test 944 declares
// (void|size|[5]size) and boxes only the narrow `size` variant — only
// the array-variant box is refused. Mirrors the concrete→tagged variant
// select in isassignable (:3859) and cstage tagged_array_variant so the
// variant chosen here is the one the box would materialize.
fn taggedarrayvariantctor(c: *checker, dst: *syntax.node, src: *syntax.node) bool = {
if (dst == nil) { return false; };
if (src == nil) { return false; };
let du: *syntax.node = resolvealias(c, unwrapbang(dst));
let su: *syntax.node = resolvealias(c, unwrapbang(src));
if (du == nil) { return false; };
if (du.kind != syntax.nkind.N_TTAGGED) { return false; };
if (su != nil) { if (su.kind == syntax.nkind.N_TTAGGED) { return false; }; };
let v: *syntax.node = du.list;
for (v != nil) {
let vspread: bool = (v.op == syntax.tkind.TK_ELLIPSIS);
let vu: *syntax.node = resolvealias(c, unwrapbang(v));
let vtagged: bool = false;
if (vu != nil) { if (vu.kind == syntax.nkind.N_TTAGGED) { vtagged = true; }; };
if (vtagged && !vspread) {
if (typeeqast(c, v, src)) { return false; };
} else {
let innerconf: bool = false;
if (isassignable(c, v, src, &innerconf)) {
if (vu != nil) { if (vu.kind == syntax.nkind.N_TARRAY) { return true; }; };
return false;
};
};
v = v.next;
};
return false;
};
// 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: *syntax.node, rhs: *syntax.node) void = {
if (arrtn == nil) { return; };
if (rhs == nil) { return; };
// #106: chase a TY_NAMED alias (`type A=[2]int`) to the underlying
// [N]T before the kind gate — else an alias declared type bails
// here and the over-fill (#9) never fires (silent DATA-truncate).
// Idempotent on a non-alias (resolvealias returns the node), so a
// direct N_TARRAY is untouched. Makes EVERY caller (decl/let/def/
// struct/return/call-arg) alias-aware by construction.
arrtn = resolvealias(c, unwrapbang(arrtn));
if (arrtn == nil) { return; };
if (arrtn.kind != syntax.nkind.N_TARRAY) { return; };
if (rhs.kind != syntax.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. #141: a def-dim
// child (`[MAX]u8`) now folds via arrayelen and is count-checked
// like a literal (closes #13's def-dim exemption).
// Under-long (count < N, no `...`) stays accepted as before; Hare
// rejects it — task #10.
let declen: u64 = arrayelen(c, arrtn.rhs);
// #9: fire the over-fill whenever the length is EXPLICITLY declared
// (arrtn.rhs present) — incl `[0]`. `[_]` leaves arrtn.rhs nil UNTIL
// inferarraylen stamps it with the real count (runs first), so a
// resolved `[_]` arrives here with cnt == declen (no over-fill). An
// explicit `[0]=[1,2]` keeps arrtn.rhs=N_INTLIT(0) → declen 0, cnt 2 →
// loud. `[0]=[]` → cnt 0, no error. Replaces the `declen > 0` guard,
// the wwstage twin of cstage's dropped `alen > 0`.
if (arrtn.rhs != nil) {
let cnt: u64 = 0u64;
let ce: *syntax.node = rhs.list;
for (ce != nil) {
let cskip: bool = false;
if (ce.kind == syntax.nkind.N_FIELD) {
if (syntax.streq(ce.str, "...")) { cskip = true; };
};
if (!cskip) { cnt += 1u64; };
ce = ce.next;
};
if (cnt > declen) {
cerr("array literal has ");
cerr(strconv.u64tos(cnt, strconv.base.DEC));
cerr(" elements but declared array holds ");
cerr(strconv.u64tos(declen, strconv.base.DEC));
cerr("\n");
c.errs += 1;
return;
};
};
let elemtn: *syntax.node = arrtn.lhs;
let at: *syntax.tinfo = tinfofornode(c, arrtn);
let et: *syntax.tinfo = nil;
if (at != nil) { et = at.sub; };
et = tichase(et);
let e: *syntax.node = rhs.list;
for (e != nil) {
let skip: bool = false;
if (e.kind == syntax.nkind.N_FIELD) {
if (syntax.streq(e.str, "...")) { skip = true; };
};
if (!skip) {
let ev: *syntax.node = e;
for (ev != nil && ev.kind == syntax.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: *syntax.node = resolvealias(c, elemtn);
if (eltr != nil && eltr.kind == syntax.nkind.N_TARRAY
&& ev != nil && ev.kind == syntax.nkind.N_ARRLIT) {
checkarrlitfits(c, eltr, ev);
e = e.next;
continue;
};
let v: u64 = 0u64;
let folded: bool = false;
if (et != nil) {
if (syntax.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: *syntax.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;
};
};
// checktuplearrfits — #20/#25/#26: walk a declared tuple type's
// element types (ttn.list, each N_TPARAM-wrapped → type on .lhs)
// lockstep with an N_TUPLE rhs's values (tup.list, chained directly).
// An array-literal element runs the alias-aware over-fill
// checkarrlitfits; a nested-tuple element (declared N_TTUPLE vs rhs
// N_TUPLE) recurses. Shared by checkletassign (let) and
// checkretassign (return). Mirrors cstage's element-wise
// type_assignable count-reject; no-ops on scalar elements.
fn checktuplearrfits(c: *checker, ttn: *syntax.node, tup: *syntax.node) void = {
if (ttn == nil) { return; };
if (tup == nil) { return; };
// #38/F2 (review item 10): a tuple literal whose arity differs from the
// declared tuple is LOUD. cstage type_assignable's tuple arm requires
// both param chains to end together (cmd/wcc/type.c:416, consumed as
// "init not assignable" at check.c:2386-2391); wwstage's isassignable
// has NO tuple arm, so an over/short literal fell to the lenient
// catch-all and the lockstep walk below silently ignored the leftovers.
// The short direction was the live miscompile (cgen stored a stale,
// never-popped register into the missing slot). Count both chains and
// reject a mismatch before the per-element fits walk. Recursion through
// this one choke point gates every nesting depth.
let dn: u64 = 0u64;
let dc: *syntax.node = ttn.list;
for (dc != nil) { dn += 1u64; dc = dc.next; };
let vn: u64 = 0u64;
let vc: *syntax.node = tup.list;
for (vc != nil) { vn += 1u64; vc = vc.next; };
if (dn != vn) {
cerr("tuple literal has ");
cerr(strconv.u64tos(vn, strconv.base.DEC));
cerr(" elements but declared tuple holds ");
cerr(strconv.u64tos(dn, strconv.base.DEC));
cerr("\n");
c.errs += 1;
return;
};
let dt: *syntax.node = ttn.list;
let vt: *syntax.node = tup.list;
for (dt != nil && vt != nil) {
if (vt.kind == syntax.nkind.N_ARRLIT) {
checkarrlitfits(c, dt.lhs, vt);
} else {
if (vt.kind == syntax.nkind.N_TUPLE) {
let drt: *syntax.node = resolvealias(c, unwrapbang(dt.lhs));
if (drt != nil && drt.kind == syntax.nkind.N_TTUPLE) {
checktuplearrfits(c, drt, vt);
};
};
};
dt = dt.next;
vt = vt.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: *syntax.node, val: *syntax.node) bool = {
if (val == nil) { return false; };
if (val.kind != syntax.nkind.N_ARRLIT) { return false; };
let du: *syntax.node = resolvealias(c, unwrapbang(dsttn));
if (du == nil) { return false; };
// #13: the borrow target may be a SLICE success variant of a tagged-
// union return — same no-outliving-backing dangle, but it slips the
// N_TSLICE gate (the dst node chases to N_TTAGGED). Chase to a slice
// variant so the reject sees through the union. An N_ARRLIT can only
// target a slice variant; an array-typed variant is the #5/#60 reject
// upstream; full non-let support is #33. Twin of cstage #13 arm.
//
// #20 RETAINED DIVERGENCE (benign, rule-7 tracked, NEVER silent): this
// chase is UNGATED — it picks the first slice variant whatever the
// arrlit's element type. cstage's reject_arrlit_borrow gates the same
// chase on type_assignable (cmd/wcc/check.c reject_arrlit_borrow), so
// for an UNTYPED arrlit whose element matches NO variant (#18: [3]int
// into ([]i32|e)) cstage DECLINES the chase → rejects purely at the
// assignability layer ("not assignable"). wwstage emits "not assignable"
// too (errnotassign, checkretassign) and THEN this extra "cannot borrow
// as a slice". Both stages reject the SAME SET — no decision/asm
// divergence (a reject emits no .s; rule-10 is asm-only). The exact-
// parity port (gate this chase on isassignable, mirroring cstage) is
// task #20; it touches this shared #13/#17/#18 chokepoint, so it is its
// own commit.
if (du.kind == syntax.nkind.N_TTAGGED) {
let v: *syntax.node = du.list;
for (v != nil) {
let vu: *syntax.node = resolvealias(c, unwrapbang(v));
if (vu != nil && vu.kind == syntax.nkind.N_TSLICE) {
du = vu;
break;
};
v = v.next;
};
};
if (du.kind != syntax.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: *syntax.node, srctn: *syntax.node, val: *syntax.node) *syntax.node = {
if (dsttn == nil) { return val; };
if (srctn == nil) { return val; };
if (val == nil) { return val; };
let du: *syntax.node = resolvealias(c, unwrapbang(dsttn));
let su: *syntax.node = resolvealias(c, unwrapbang(srctn));
if (du == nil) { return val; };
if (su == nil) { return val; };
// #17: the borrow target may be the SLICE success variant of a tagged
// union (`return/assign/let/f(arr)` into `([]T|e)`). Chase du to that
// variant so an array VARIABLE lowers to a full slice exactly as a bare
// []T dst does — the existing slice cgen then builds the full
// {ptr,len,cap} header and the union widen's slice arm wraps it, closing
// the silent len/cap drop. Mirrors #13's rejectarrlitborrow N_TTAGGED
// chase (an array LITERAL has no outliving backing and is rejected there
// first; a variable has storage, so this borrow is legal).
if (du.kind == syntax.nkind.N_TTAGGED && su.kind == syntax.nkind.N_TARRAY) {
let v: *syntax.node = du.list;
for (v != nil) {
let vu: *syntax.node = resolvealias(c, unwrapbang(v));
if (vu != nil && vu.kind == syntax.nkind.N_TSLICE && typeeqast(c, vu.lhs, su.lhs)) {
du = vu;
break;
};
v = v.next;
};
};
if (du.kind != syntax.nkind.N_TSLICE) { return val; };
if (su.kind != syntax.nkind.N_TARRAY) { return val; };
if (!typeeqast(c, du.lhs, su.lhs)) { return val; };
let sl: *syntax.node = syntax.newnode(syntax.nkind.N_SLICE, val.file, val.line, val.col);
sl.lhs = val; // sliced base; lo (.rhs) / hi (.cond) nil → 0 : len(arr)
let slt: *syntax.node = syntax.newnode(syntax.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: *syntax.node) *syntax.node = {
if (callee == nil) { return nil; };
if (callee.kind == syntax.nkind.N_IDENT) {
let s: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, callee.str);
if (s != nil) {
if (s.skind == syntax.skind.SK_FN) { return s.decl; };
};
return nil;
};
if (callee.kind == syntax.nkind.N_DOT) {
if (callee.lhs != nil) {
if (callee.lhs.kind == syntax.nkind.N_IDENT) {
let ms: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, callee.lhs.str);
if (ms != nil && ms.skind != syntax.skind.SK_USE) {
let mu: *syntax.sym = syntax.scopelookupuselocal(ms.scope, callee.lhs.str);
if (mu != nil) { ms = mu; };
};
if (ms != nil) {
if (ms.skind == syntax.skind.SK_USE || ms.use_alias != 0i32) {
let fs: *syntax.sym = syntax.scopelookupinmodule(c.cur, modkeyfor(c, callee.lhs.str), callee.str);
if (fs != nil) {
if (fs.skind == syntax.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: *syntax.node) void = {
if (n == nil) { return; };
// #24: a 1-arg `free(x)` is the Hare no-op pseudo-builtin (#27), NOT the
// rt 2-arg `free(p: *void, n: u64)` that calleefndecl resolves to in the
// bundle (lib seeds both: the nil-decl builtin at check.ww:137 AND
// rt's @symbol("rt_free") decl). cstage intercepts the builtin by name +
// arity BEFORE call resolution (cmd/wcc/check.c:1650, n->list->next ==
// NULL) and runs NO arg typecheck; exprtype's free arm (:3014) is the
// wwstage twin but runs after this seam. Skip so `free(charset)` /
// `free(slice)` (regex finish #27) isn't checked against rt_free's *void
// param. `free` is the only builtin name with a colliding real decl
// (len/alloc/append/delete/insert keep nil decls → calleefndecl bails).
if (n.lhs != nil) { if (n.lhs.kind == syntax.nkind.N_IDENT) {
if (syntax.streq(n.lhs.str, "free")) {
if (n.list != nil) { if (n.list.next == nil) { return; }; };
};
}; };
let decl: *syntax.node = calleefndecl(c, n.lhs);
// task #51: calleefndecl nils every fn-VALUE callee — a deref `(*fp)(...)`
// (N_UN), an SK_VAR fn-ptr ident, a struct-field fn call — so this bail
// skips the #258 array→slice desugar AND the general arg typecheck for
// them, and cgen then pushes raw array words where the callee reads a 24B
// slice header. cstage drives the same arg loop off the callee TYPE
// (cmd/wcc/check.c:1828 type_chase_named(cexpr(callee))). The type-keyed
// callee path is MECHANISM (not a checker gate) — filed as task #51.
if (decl == nil) { return; };
let param: *syntax.node = decl.list;
let prev: *syntax.node = nil;
let a: *syntax.node = n.list;
for (a != nil) {
let nexta: *syntax.node = a.next;
if (param != nil) {
if (param.kind == syntax.nkind.N_PARAM) {
// C-style FFI variadic (bare `...`): str=="...",
// op != TK_ELLIPSIS, lhs == nil. The `...` absorbs
// every remaining arg untyped — mirror cstage
// check.c:1860 `if (!u->variadic) ...; continue`.
// MANDATORY: this param's lhs is nil, so the per-arg
// coercerunelit / checkarrlitfits / desugararrayslice
// calls below would deref nil on a rune-literal or
// array-literal variadic arg.
if (syntax.streq(param.str, "...")
&& param.op != syntax.tkind.TK_ELLIPSIS) {
break;
};
if (param.op != syntax.tkind.TK_ELLIPSIS) {
let atype: *syntax.node = exprtype(c, a, nil);
// #120: `f(1.0)` narrows the arg literal to the
// param's f32. Mirror cstage cmd/wcc/check.c N_CALL
// coerce_floatlit at the param-typed arg.
coercefloatlit(c, a, param.lhs);
// #29: a rune literal narrowing into an integer param
// (`take('b')` where take(b: u8)). Override atype to the
// integer target so c3's general isassignable accepts,
// mirroring cstage's coarse untyped_rune rule. See
// coercerunelit. cgen is byte-id-neutral (narrows by the
// param/destination width).
let runet: *syntax.node = coercerunelit(c, a, param.lhs);
if (runet != nil) { atype = runet; };
// #24: GENERAL per-arg assignability — align UP to
// cstage check.c:1867-1870, which type_assignables
// every non-variadic call arg (`argument type %s not
// assignable to %s`). wwstage previously ran NO general
// param typecheck (only a narrow #258 array→slice arm),
// so any mistyped scalar call-arg silently miscompiled
// (an int read as a 24B slice header). The shared
// isassignable SUBSUMES that #258 arm: its N_TSLICE/
// N_TARRAY arm is a confident reject on an element
// mismatch and an accept on a match (the desugar below
// then borrows). Conf-gated + UNIONed with
// assignableaddrfn exactly like the let/return sibling
// sites (:5151/5154, :5222/5225); the spread-tagged
// lenient escape (isassignable :4078) keeps conf=false so
// `take(42)` into `(...formattable | bool)` stays accepted.
let conf: bool = false;
let ok: bool = isassignable(c, param.lhs, atype, &conf);
if (!ok) { if (assignableaddrfn(c, param.lhs, a)) { ok = true; }; };
if (conf) { if (!ok) { errnotassign(c, param.lhs, atype, "argument"); }; };
// #12: overlong array-lit CALL-ARG — `g([1,2,3])`.
// Reject at CHECK time (clean over-fill msg) instead
// of falling to cgen #271's late aggregate-arg loud.
// param.lhs is the declared param type (alias-aware
// via the resolvealias chase in checkarrlitfits).
if (a.kind == syntax.nkind.N_ARRLIT) {
checkarrlitfits(c, param.lhs, a);
};
// #31/#33: bare array-literal arg has no backing
// — loud-reject (supported only at a `let`).
if (!rejectarrlitborrow(c, param.lhs, a)) {
let rep: *syntax.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 != syntax.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: *syntax.node) void = {
if (n == nil) { return; };
if (n.lhs == nil) { return; };
if (n.rhs == nil) { return; };
// catB-22: reject reassigning a `const`-flagged binding (the
// is_const bit set at the let-install above). A bare `_` discard
// lvalue carries an empty str and is skipped. Mirror cstage
// cmd/wcc/check.c:1889-1896.
if (n.lhs.kind == syntax.nkind.N_IDENT) {
if (n.lhs.str.len > 0) {
let s: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, n.lhs.str);
if (s != nil) {
if (s.is_const != 0i32) {
cerr("error: cannot assign to const binding\n");
c.errs += 1;
};
};
};
};
let ltn: *syntax.node = exprtype(c, n.lhs, nil);
let rtn: *syntax.node = exprtype(c, n.rhs, nil);
// #120: `w = 1.0` narrows the rhs literal to the lvalue's f32. Mirror
// cstage cmd/wcc/check.c N_ASSIGN coerce_floatlit.
coercefloatlit(c, n.rhs, ltn);
// #29: a rune literal narrowing into an integer assign/index-store
// target (`buf[i] = 'F'`). Override rtn to the integer target so a
// general assign typecheck accepts, mirroring cstage's coarse
// untyped_rune rule. See coercerunelit. cgen narrows by the destination
// width (byte-id-neutral). Removes the getopt `'X': u8` index casts.
let runet: *syntax.node = coercerunelit(c, n.rhs, ltn);
if (runet != nil) { rtn = runet; };
// #24/#36 (rule-7 deferred-divergence, NEVER silent): the ASSIGN seam
// does NOT yet route the general conf-gated UNION (isassignable ||
// assignableaddrfn) that the let / return / call-arg seams run — so a
// mistyped bare-assignment `x = some_slice` (a 24B slice header into an
// 8B int slot) is still silently accepted here, the one remaining
// member of the #24 cat-A. cstage DOES check it (cmd/wcc/check.c:1899,
// `cannot assign %s to %s`, every op incl. compound). The union was
// implemented + reverted: it correctly closed `x = slice` and matched
// cstage on `p += 1`, but surfaced a false over-reject of an EXACT-
// signature bare fn assigned to a fn-pointer struct field (lib/log
// `r.logger.println = stdprintln`) because typeeqast compares fn types
// at the AST level and cannot match a variadic + module-qualified-param
// fn signature (the #178 divergence, self-flagged at the typeeqast
// N_TFN arm). So the assign seam is BLOCKED on #178 and filed as task
// #36 (the bounded #178 typeeqast fn-compare fix, task #35, lands
// first). Until then this seam runs only coercerunelit + the #258
// array→slice desugar below.
// #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: *syntax.node) void = {
if (n == nil) { return; };
if (n.lhs == nil) { return; };
if (n.lhs.kind != syntax.nkind.N_TARRAY) { return; };
if (n.lhs.rhs != nil) { return; }; // explicit [N] or already inferred
if (n.rhs == nil || n.rhs.kind != syntax.nkind.N_ARRLIT) {
cerr("error: [_]T needs an array-literal initialiser\n");
c.errs += 1;
let z: *syntax.node = syntax.newnode(syntax.nkind.N_INTLIT, "", 0, 0);
z.uval = 0u64;
n.lhs.rhs = z; // sentinel: idempotent, error already raised
return;
};
let cnt: u64 = 0u64;
let it: *syntax.node = n.rhs.list;
for (it != nil) {
let skip: bool = false;
if (it.kind == syntax.nkind.N_FIELD) {
if (syntax.streq(it.str, "...")) { skip = true; };
};
if (!skip) { cnt += 1u64; };
it = it.next;
};
let cn: *syntax.node = syntax.newnode(syntax.nkind.N_INTLIT, "", 0, 0);
cn.uval = cnt;
n.lhs.rhs = cn;
};
fn checkletassign(c: *checker, n: *syntax.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: *syntax.node = nil;
if (n.lhs != nil && n.lhs.kind == syntax.nkind.N_TSLICE) {
let inner: *syntax.node = n.rhs;
if (inner.kind == syntax.nkind.N_TRYPROP) {
inner = inner.lhs;
} else { if (inner.kind == syntax.nkind.N_TRYUNW) {
inner = inner.lhs;
}; };
if (inner != nil && inner.kind == syntax.nkind.N_CALL) {
let callee: *syntax.node = inner.lhs;
let a0: *syntax.node = inner.list;
let a1: *syntax.node = nil;
let a2: *syntax.node = nil;
if (a0 != nil) { a1 = a0.next; };
if (a1 != nil) { a2 = a1.next; };
if (callee != nil
&& callee.kind == syntax.nkind.N_IDENT
&& syntax.streq(callee.str, "alloc")
&& a0 != nil && a0.kind == syntax.nkind.N_ARRLIT
&& a0.list == nil
&& a1 != nil && a2 == nil) {
octx = inner;
};
};
};
let savedoctx: *syntax.node = c.allococtx;
c.allococtx = octx;
let src: *syntax.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 == syntax.nkind.N_TSTRUCT
&& n.rhs.kind == syntax.nkind.N_STRUCTLIT
&& n.rhs.lhs != nil && n.rhs.lhs.kind == syntax.nkind.N_IDENT) {
let ltn: *syntax.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 == syntax.nkind.N_TSLICE) {
let wrapped: bool = false;
let inner: *syntax.node = n.rhs;
if (inner.kind == syntax.nkind.N_TRYPROP) {
wrapped = true;
inner = inner.lhs;
} else { if (inner.kind == syntax.nkind.N_TRYUNW) {
wrapped = true;
inner = inner.lhs;
}; };
if (inner != nil && inner.kind == syntax.nkind.N_CALL) {
let callee: *syntax.node = inner.lhs;
let a0: *syntax.node = inner.list;
let a1: *syntax.node = nil;
let a2: *syntax.node = nil;
if (a0 != nil) { a1 = a0.next; };
if (a1 != nil) { a2 = a1.next; };
if (callee != nil
&& callee.kind == syntax.nkind.N_IDENT
&& syntax.streq(callee.str, "alloc")
&& a0 != nil && a0.kind == syntax.nkind.N_ARRLIT
&& a0.list == nil
&& a1 != nil && a2 == nil) {
let shadowed: bool = false;
if (c.curmod.len > 0) {
if (syntax.scopelookupinmodule(c.cur, c.curmod, "alloc") != nil) {
shadowed = true;
};
};
if (!shadowed) {
let sl: *syntax.node = syntax.newnode(syntax.nkind.N_TSLICE, "", 0, 0);
sl.lhs = n.lhs.lhs;
if (wrapped) {
src = sl;
} else {
let nome: *syntax.node = mktname(c, "nomem");
sl.next = nome;
let tt: *syntax.node = syntax.newnode(syntax.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).
// #106: an alias-of-array lhs (`type A=[2]int; let g: A = […]`) arrives
// as N_TNAME, so the bare N_TARRAY gate skipped it and the over-fill
// never fired (silent DATA-truncate). The let path is the one caller
// that pre-gates on the declared node's kind (def/return/call-arg pass
// it straight to the alias-aware checkarrlitfits); resolve the alias
// here too so an alias-of-array routes through the same over-fill. A
// direct N_TARRAY is unchanged (resolvealias is idempotent), and the
// early return matches the direct-array branch's pre-existing return.
let llhs: *syntax.node = resolvealias(c, unwrapbang(n.lhs));
if (llhs != nil && llhs.kind == syntax.nkind.N_TARRAY && n.rhs.kind == syntax.nkind.N_ARRLIT) {
checkarrlitfits(c, n.lhs, n.rhs);
return;
};
// #20: array-typed TUPLE element with an overlong array literal —
// `let t:([2]int,i32) = ([1,2,3],5)` was silently accepted (the tuple
// position wasn't wired to checkarrlitfits, unlike the direct-array
// let above). #26: the walk now lives in checktuplearrfits, which also
// recurses into a nested-tuple element (checkarrlitfits chases aliases
// #106, recurses nested arrays #251; the helper no-ops on non-array,
// non-tuple elements). Mirror cstage's tuple element-wise reject. No
// early return — the rest of checkletassign still runs for the tuple.
// N_TTUPLE elements wrap their type on .lhs (N_TPARAM chain,
// stamptuplebinds:311); the N_TUPLE rhs values chain directly on .list.
if (llhs != nil && llhs.kind == syntax.nkind.N_TTUPLE
&& n.rhs.kind == syntax.nkind.N_TUPLE) {
checktuplearrfits(c, llhs, n.rhs);
};
// #25/#31: an array literal initialising a SLICE binding. 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 off [count]T. Twin of cstage
// arrlit_init_fits' slice arm (cmd/wcc/check.c:519-520). #28/#32: this
// admission runs at BOTH scopes. The matching DATA-vs-borrow split is
// the desugar at the foot of this fn (gated c.cur != c.top): a LOCAL
// `let []u8 = [...]` lowers to a runtime arr[0:len] borrow, while a
// MODULE-level `const/let []u8 = [...]` keeps its raw N_ARRLIT for cgen
// to materialize as DATA (#18). cstage admits both — module-level via
// arrlit_init_fits in check_file pass-2 (check.c:3406-3409), which never
// desugars — so gating the admission local-only made wwstage REJECT
// valid Hare (`const dotdot: []u8 = ['.', '.'];` ref/hare/path/stack.ha:30).
// #28: a TUPLE-element slice global ([](str,*fn) tables) is EXCLUDED at
// module scope — the synthesis delegates per-element validation to
// checkarrlitfits, whose element check is isassignable, which has NO
// strict tuple arm (#38). cstage's arrlit_init_fits uses type_assignable
// (strict on tuples), so routing a tuple-element slice through the
// synthesis would over-accept a sig-mismatched `&fn` element that cstage
// rejects (#124 wrong_sig_table). Tuple-element slices instead stay on
// the natural whole-element typeeqast path below (isassignable's #258
// array→slice arm), which IS strict and reaches cstage's same decision.
let elemtup: bool = false;
if (c.cur == c.top && n.lhs.lhs != nil) {
let etn: *syntax.node = resolvealias(c, unwrapbang(n.lhs.lhs));
if (etn != nil && etn.kind == syntax.nkind.N_TTUPLE) {
elemtup = true;
};
};
if (n.lhs.kind == syntax.nkind.N_TSLICE
&& n.rhs.kind == syntax.nkind.N_ARRLIT && !elemtup) {
let cnt: u64 = 0u64;
let e0: *syntax.node = n.rhs.list;
for (e0 != nil) {
let skip: bool = false;
if (e0.kind == syntax.nkind.N_FIELD) {
if (syntax.streq(e0.str, "...")) { skip = true; };
};
if (!skip) { cnt += 1u64; };
e0 = e0.next;
};
let cn: *syntax.node = syntax.newnode(syntax.nkind.N_INTLIT, "", 0, 0);
cn.uval = cnt;
let arr: *syntax.node = syntax.newnode(syntax.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.
// #28: LOCAL ONLY. The local desugar replaces n.rhs with the N_SLICE
// borrow, so this stash is consumed and then unreachable. A MODULE-
// level decl keeps its raw N_ARRLIT (DATA emit), and its emitslicedata
// sizes off the DECLARED slice tnode (d.lhs), never the stash — so the
// stash would only leave the synthesized, untyped count node (cn) on
// the live tree for the pass-3 asserttyped walker to trip on.
if (c.cur != c.top) { n.rhs.lhs = arr; };
src = arr;
};
// #29: an un-suffixed rune literal narrowing into an integer let target
// (`let b: u8 = 'a'`). Override src to the integer target so isassignable
// accepts, mirroring cstage's coarse untyped_rune rule. See coercerunelit.
let runet: *syntax.node = coercerunelit(c, n.rhs, n.lhs);
if (runet != nil) { src = runet; };
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"); };
// #5/#60: reject boxing an array payload into a tagged variant. cerr
// alone does NOT fail the build — bump c.errs (errnotassign:4245 idiom).
if (taggedarrayvariantctor(c, n.lhs, src)) {
cerr("array-typed tagged-union variant construction unwired — reject (task #5 / #60)\n");
c.errs += 1;
return;
};
// #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: *syntax.node) void = {
if (n == nil) { return; };
if (n.lhs == nil) {
// bare `return;` — mirror cstage cmd/wcc/check.c:2428-2439:
// the value-less return has type void, then type_assignable
// (c.fnret, void). A void fnret or a tagged union carrying a
// void variant accepts; a non-void scalar fnret rejects (the
// value-less return would RET a garbage register). isassignable
// reaches the same verdict (void→void typeeqast; void→tagged via
// the void variant; void→i32 a confident primitive mismatch).
if (c.fnret == nil) { return; };
let vt: *syntax.node = mktname(c, "void");
let vconf: bool = false;
let vok: bool = isassignable(c, c.fnret, vt, &vconf);
if (vconf) { if (!vok) { errnotassign(c, c.fnret, vt, "return"); }; };
return;
};
if (c.fnret == nil) { return; };
// #12: overlong array-lit RETURN — `fn f() [2]int = { return [1,2,3]; }`.
// #9 wired the over-fill at DECL only; the return position was lenient.
// Fire BEFORE the isassignable/conf guards below — a direct [N]T return
// type drives isassignable to conf=false (array-length leniency), which
// would short-circuit the check (the alias path set conf=true, so neg3
// caught it but the direct neg0 slipped). Alias-aware via the
// resolvealias chase at the top of checkarrlitfits.
if (n.lhs.kind == syntax.nkind.N_ARRLIT) {
checkarrlitfits(c, c.fnret, n.lhs);
};
// #25: array-typed element of a TUPLE RETURN with an overlong array
// literal — `fn f() ([2]int,i32) = { return ([1,2,3],5); }`. #20
// wired the tuple over-fill walk at the LET position only; the
// return path runs through checkretassign which never routed tuple
// elements through checkarrlitfits. Reuse the #26 helper. Fire
// BEFORE the isassignable/conf guards (a tuple return drives
// conf=false → short-circuit), same reason as the #12 array arm
// above. Alias/!-aware on the fnret tuple type.
let rrt: *syntax.node = resolvealias(c, unwrapbang(c.fnret));
if (rrt != nil && rrt.kind == syntax.nkind.N_TTUPLE
&& n.lhs.kind == syntax.nkind.N_TUPLE) {
checktuplearrfits(c, rrt, n.lhs);
};
let src: *syntax.node = exprtype(c, n.lhs, nil);
if (src == nil) { return; };
// #29: a rune literal returned into an integer (or integer-variant)
// fnret (`return '\\';` into u8 or (u8 | star | ...)). Override src so
// isassignable accepts; cgen boxes via the shape fallback. See
// coercerunelit. Removes the fnmatch.ww:126 `'\\': u8` workaround cast.
let runet: *syntax.node = coercerunelit(c, n.lhs, c.fnret);
if (runet != nil) { src = runet; };
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"); };
// #5/#60: reject returning an array payload into a tagged variant. cerr
// alone does NOT fail the build — bump c.errs (errnotassign:4245 idiom).
if (taggedarrayvariantctor(c, c.fnret, src)) {
cerr("array-typed tagged-union variant construction unwired — reject (task #5 / #60)\n");
c.errs += 1;
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);
};
};
// `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: *syntax.node) void = {
if (n == nil) { return; };
// e is in n.lhs (value), T is in n.rhs (type expr).
let st: *syntax.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 == syntax.nkind.N_TRYPROP || n.lhs.kind == syntax.nkind.N_TRYUNW) {
st = exprtype(c, n.lhs, nil);
};
// #59.9: an N_BIN operand (`(m.A | m.B) as u32`) was never
// typed, so the OR-fold reached cgen unstamped and
// cgtypeassert's enum-reinterpret gate (#27b) missed —
// lowering a plain enum value as a phantom tagged assert
// (unconditional exit 1). cstage types the lhs
// unconditionally (cmd/wcc/check.c:2220 cexpr(c, n->lhs)).
if (n.lhs.kind == syntax.nkind.N_BIN) {
st = exprtype(c, n.lhs, nil);
};
};
let u: *syntax.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 == syntax.nkind.N_TYPEASSERT) {
let v: *syntax.node = resolvealias(c, unwrapbang(n.rhs));
let lhsenum: bool = false;
let rhsenum: bool = false;
if (u != nil) { if (u.kind == syntax.nkind.N_TENUM) { lhsenum = true; }; };
if (v != nil) { if (v.kind == syntax.nkind.N_TENUM) { rhsenum = true; }; };
if (lhsenum || rhsenum) {
if (isinttypeast(u)) { if (isinttypeast(v)) { return; }; };
};
};
if (u.kind != syntax.nkind.N_TTAGGED) {
cerr("is/as: operand is not a tagged union\n");
c.errs += 1;
return;
};
let want: *syntax.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: *syntax.tinfo = tinfofornode(c, u);
let wanttinfo: *syntax.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 == syntax.nkind.N_TNAME) {
cerr(" (");
cerr(want.str);
cerr(")");
};
cerr("\n");
c.errs += 1;
};
// 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: *syntax.node) *syntax.node = {
if (e == nil) { return nil; };
if (e.kind == syntax.nkind.N_IDENT) {
// #11a: curmod preference (the #53/#55 family). A bare ident
// whose leaf also names a global in a later module otherwise
// binds the foreign decl's type, mis-typing the try operand and
// either spuriously rejecting valid `?` code or skipping the F8
// reject. Mirrors exprtype's own N_IDENT arm (scopelookupprefer
// at :2897); cstage types the operand via cexpr with cur_mod.
let s: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, e.str);
if (s == nil) { return nil; };
if (s.decl == nil) { return nil; };
return s.decl.lhs;
};
if (e.kind == syntax.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: *syntax.node = e.lhs;
if (callee == nil) { return nil; };
let nm: str;
nm.ptr = nil; nm.len = 0;
if (callee.kind == syntax.nkind.N_IDENT) { nm = callee.str; };
// #11b/task #51: an N_DOT callee `mod.f()?` is looked up by BARE
// LEAF here, NOT via the module qualifier (callee.lhs) the way
// exprtype's own N_CALL arm does (:3095 scopelookupinmodule) — a
// cross-module same-leaf `f` misbinds. The mechanism fix (module-
// keyed callee resolution + the deref-callee `(*fp)()?` shape that
// returns nil below) rides task #51 with the fn-ptr-callee desugar.
if (callee.kind == syntax.nkind.N_DOT) { nm = callee.str; };
if (nm.len == 0) { return nil; };
// #11a: curmod preference for the bare-leaf callee. A same-leaf
// `op()?` declared in a later module otherwise binds the foreign
// op's return type — its error subset then spuriously fails (or
// wrongly passes) the enclosing-return check. Mirrors exprtype's
// N_CALL arm (scopelookupprefer at :3336). The N_DOT-callee leaf
// (callee.str above) still resolves bare-leaf, not via the module
// qualifier callee.lhs.str — that module-keyed fix rides task #51.
let s: *syntax.sym = syntax.scopelookupprefer(c.cur, c.curmod, nm);
if (s == nil) { return nil; };
if (s.skind != syntax.skind.SK_FN) { return nil; };
if (s.decl == nil) { return nil; };
return s.decl.lhs;
};
// task #51: a deref callee `(*fp)()?` / SK_VAR fn-ptr ident / struct-
// field fn call returns nil here, so checktryprop silently bails and
// the F8 multi-success reject is skipped for those shapes. The fix
// (type-keyed operand resolution: peel TPTR → TFN.ret) is mechanism,
// filed with the fn-ptr-callee desugar as task #51 — NOT a checker gate.
return nil;
};
// trycountvariants — #38/F3 (review item 8): walk a tagged union's variant
// list FLATTENING `...inner` spreads, accumulating the success count and the
// has-error flag. cstage counts/checks the resolve_type-FLATTENED u->params
// (cmd/wcc/check.c:2160-2165 over the TK_ELLIPSIS-spliced chain); ww's
// checktryprop walked the raw AST, so a `(...inner | e)` counted the spread as
// ONE success and the F8 multi-success gate never fired — the exact silent
// accept the gate exists to catch (a valid bool success then treated as error
// by the single-tag-compare cgen). iserror is judged against the OUTER union
// (`outer`) throughout — taggedhaserr(outer) holds because the spread's sibling
// or a spliced member carries the error, so each flattened member routes
// through varianterr, matching cstage's per-param iserror. Mirrors the #209
// spread recursion tinfofornode already runs over vu.params (check.ww:2275).
fn trycountvariants(c: *checker, outer: *syntax.node, v: *syntax.node, nsuccp: *int,
haserrp: *bool, depth: i32) void = {
for (v != nil) {
if (v.op == syntax.tkind.TK_ELLIPSIS && depth < 8i32) {
let inner: *syntax.node = resolvealias(c, unwrapbang(v));
if (inner != nil && inner.kind == syntax.nkind.N_TTAGGED) {
trycountvariants(c, outer, inner.list, nsuccp,
haserrp, depth + 1i32);
v = v.next;
continue;
};
};
if (iserrvariant(c, outer, v)) { *haserrp = true; }
else { *nsuccp += 1; };
v = v.next;
};
};
fn checktryprop(c: *checker, n: *syntax.node) void = {
if (n == nil) { return; };
let t: *syntax.node = exprtypeoftry(c, n.lhs);
let u: *syntax.node = resolvealias(c, unwrapbang(t));
if (u == nil) { return; };
if (u.kind != syntax.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;
// #38/F3 (review item 8): flatten `...inner` spreads so the F8
// multi-success gate counts the spliced members, not the spread alias.
trycountvariants(c, u, u.list, &nsucc, &haserr, 0i32);
if (nsucc > 1) {
if (n.kind == syntax.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 != syntax.nkind.N_TRYPROP) { return; };
if (!haserr) { return; };
// Enclosing fn must return a tagged union with each operand
// error variant present.
let r: *syntax.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 != syntax.nkind.N_TTAGGED) {
cerr("?: enclosing fn return is not tagged\n");
c.errs += 1;
return;
};
let ev: *syntax.node = u.list;
for (ev != nil) {
if (iserrvariant(c, u, ev)) {
let found: bool = false;
let rv: *syntax.node = r.list;
for (rv != nil) {
if (typeeqast(c, 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;
};
};
// hascvariadic — true iff the param list ends in a bare C-style `...`
// (the param whose str=="...", set by the parser; Hare-style `T...`
// carries a name + op==TK_ELLIPSIS instead). Mirrors cstage
// build_fn_type's `p->str && strcmp(p->str,"...")==0` test
// (check.c:2646).
fn hascvariadic(params: *syntax.node) bool = {
let p: *syntax.node = params;
for (p != nil) {
if (syntax.streq(p.str, "...")) { return true; };
p = p.next;
};
return false;
};
// 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: *syntax.node) void = {
let p: *syntax.node = params;
for (p != nil) {
if (p.kind == syntax.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 == syntax.tkind.TK_ELLIPSIS) {
if (p.lhs != nil && p.lhs.kind != syntax.nkind.N_TSLICE) {
let sl: *syntax.node = syntax.newnode(syntax.nkind.N_TSLICE, "", 0, 0);
sl.lhs = p.lhs;
// op marks the wrapper as THIS normalization, not
// surface syntax, so typeeqast can peel exactly it
// when comparing against an unnormalized fn TYPE
// expr (`let f: fn(args: i64...) void = sum` — the
// decl side reads []i64 here, the let side i64).
// Param-lhs position never carries a tagged spread
// marker, so the op reads stay disjoint.
sl.op = syntax.tkind.TK_ELLIPSIS;
p.lhs = sl;
};
};
let nm: str = p.str;
if (nm.len > 0) {
checkmoduleshadow(c, nm, "param");
syntax.scopedefine(c.cur, nm, syntax.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: *syntax.node) void = {
let outer: *syntax.scope = c.cur;
c.cur = syntax.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: *syntax.node = fnnode.list;
for (p != nil) {
if (p.kind == syntax.nkind.N_PARAM) {
if (p.lhs != nil) { resolvewalk(c, p.lhs); };
};
p = p.next;
};
let prevret: *syntax.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: *syntax.node) bool = {
if (id == nil) { return false; };
if (id.kind != syntax.nkind.N_IDENT) { return false; };
if (!syntax.streq(id.str, "abort") && !syntax.streq(id.str, "assert")) {
return false;
};
return syntax.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: *syntax.node, indot: bool) void = {
if (n == nil) { return; };
let k: syntax.nkind = n.kind;
let isexpr: bool =
k == syntax.nkind.N_INTLIT || k == syntax.nkind.N_FLOATLIT ||
k == syntax.nkind.N_STRLIT || k == syntax.nkind.N_RUNELIT ||
k == syntax.nkind.N_TRUE || k == syntax.nkind.N_FALSE ||
k == syntax.nkind.N_NIL || k == syntax.nkind.N_VOIDLIT ||
k == syntax.nkind.N_IDENT || k == syntax.nkind.N_BIN ||
k == syntax.nkind.N_UN || k == syntax.nkind.N_CALL ||
k == syntax.nkind.N_INDEX || k == syntax.nkind.N_CAST ||
k == syntax.nkind.N_STRUCTLIT || k == syntax.nkind.N_ARRLIT ||
k == syntax.nkind.N_RECV || k == syntax.nkind.N_DOT ||
k == syntax.nkind.N_SLICE || k == syntax.nkind.N_SPREAD ||
k == syntax.nkind.N_TUPLE || k == syntax.nkind.N_TRYPROP ||
k == syntax.nkind.N_TRYUNW || k == syntax.nkind.N_TYPETEST ||
k == syntax.nkind.N_TYPEASSERT || k == syntax.nkind.N_YIELD ||
k == syntax.nkind.N_MATCH;
let skip: bool = false;
if (isexpr && k == syntax.nkind.N_IDENT) {
if (indot) { skip = true; };
if (!skip) {
let s: *syntax.sym = syntax.scopelookup(c.cur, n.str);
if (s != nil) {
if (s.skind == syntax.skind.SK_USE) { skip = true; };
if (s.decl == nil) { skip = true; };
};
};
if (!skip) { if (isassertfam(c, n)) { skip = true; }; };
};
if (isexpr && k == syntax.nkind.N_CALL) {
if (isassertfam(c, n.lhs)) { skip = true; };
};
if (isexpr && !skip) {
if (n.type_ == nil) {
cerr("asserttyped: ");
let kn: str = syntax.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 == syntax.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: *syntax.node = n.list;
for (m != nil) {
asserttyped(c, m, false);
m = m.next;
};
};
export fn checkinit(c: *checker, tc: *syntax.tctx) void = {
c.tc = tc;
c.top = syntax.newscope(nil);
c.cur = c.top;
c.nresolved = 0;
c.nunresolved = 0;
c.errs = 0;
c.istest = 0i32; // #15: caller (w6c main) sets it after init
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: *syntax.node) void = {
if (file == nil) { return; };
if (file.kind != syntax.nkind.N_FILE) { return; };
c.file = file;
// #80: under -T, PREPEND the synth `use test;` BEFORE Pass 1 so declmod
// (called per-decl during install) sees a matching `use test` when it
// keys lib/test's `run` — keying it under mod="test", not "". This is a
// pure ORDER fix: the synth N_USE was appended AFTER install (below),
// too late for declmod, so `run` keyed under "" — leaving the synth
// `test.run` ty_err (the E1 bridge) and colliding with a user root
// `fn run` (also mod=""). Decoupled from cgen: the symbol mangle keys
// off d.module (the //ww:module directive, cgen mod_collect), NOT this
// scope keying — cgen already emits the correct CALL test.run. Twin of
// cstage cmd/wcc/check.c.
if (c.istest != 0) {
let usenode: *syntax.node = syntax.newnode(syntax.nkind.N_USE, file.file, file.line, file.col);
usenode.str = "test";
usenode.usepath = "test";
usenode.next = file.list;
file.list = usenode;
};
// Pass 1: install all top-level names.
let d: *syntax.node = file.list;
for (d != nil) {
installdecl(c, file, d);
d = d.next;
};
// Program-global uniqueness on the ENTRY `main`. M1 #32: the entry is
// the ROOT-unit main (imported==0) — it alone lowers to the bare
// `main` symbol. An IMPORTED package's `main` (imported==1) mangles on
// its path (`foo.bar.main`) and may coexist, closing the old dup-main
// collision by construction (#31). Two ROOT entries still collide on
// the bare symbol → reject loud (rule 7). Walks USER decls only — runs
// before the -T synth main is appended below. Twin of cmd/wcc/check.c.
let firstmain: *syntax.node = nil;
let mm: *syntax.node = file.list;
for (mm != nil) {
let ismain: bool = (mm.kind == syntax.nkind.N_FNDECL
|| mm.kind == syntax.nkind.N_LET || mm.kind == syntax.nkind.N_DEF
|| mm.kind == syntax.nkind.N_TYPEDECL) && syntax.streq(mm.str, "main");
if (ismain && mm.imported == 0) {
if (firstmain == nil) {
firstmain = mm;
} else {
cerr(mm.file);
cerr(": error: duplicate entry main: only one root main may exist (#32)\n");
c.errs += 1;
};
};
mm = mm.next;
};
// #15 @test harness — under `w6c_ww -T`, synthesize the entry the
// driver would otherwise hand-wire. We sit at the seam between
// fn-install (Pass 1, all names now in scope so the synth callees
// resolve) and fn-body-resolve (Pass 2 below, which stamps the
// appended entry for free). Mirrors harec's checker-side is_test
// work — keep @test fns + suppress/own the hosted main
// (ref/harec/src/check.c:3941,4000) — NOT the build driver. cgen is
// untouched: the appended N_FNDECL rides cgfn, byte-id by
// construction (rule 10; cstage twin at cmd/wcc/check.c).
//
// #17 RECORD-AND-CONTINUE (rob ruling 2026-06-10; drew-17-attest-spec
// §a): instead of straight-line `foo(); bar();` calls (which abort the
// whole run on the first failing @test — old D3), synthesize a value
// table `[](str, *fn() void) = {("foo", &foo), ...}` + a single call to
// the lib/test runner. The runner forks per test and reads the child's
// wait-status, so abort/div0/SIGSEGV/nonzero each fail THAT test and the
// run proceeds (lib/test/run.ww). Table = harec __test_array reduced to
// a value table (D1 no section; D2 real symbols). RETAINED reductions
// (user-ratified, reinstatable post-CSP): D4 no sort, no fnmatch filter
// (source/collection order; fnmatch is #17 commit-3); D5 no reflective
// file:line (the runner prints `name ... ok/FAIL` + a count summary).
// Table rides cgen's #117 slice-of-tuple-global path; `run` resolves
// bare against the auto-bundled lib/test (synth runs post-pass-1, so
// run sits in the same flat "" bucket as the @test fns).
if (c.istest != 0) {
let pf: str = file.file;
let pl: i32 = file.line;
let pc: i32 = file.col;
// (b) the synth entry OWNS `main` — loud-reject a user one.
let u: *syntax.node = file.list;
for (u != nil) {
if (u.kind == syntax.nkind.N_FNDECL && u.body != nil
&& syntax.streq(u.str, "main")) {
cerr(u.file);
cerr(": error: test mode: main is synthesized by -T; remove the explicit main\n");
c.errs += 1;
};
// #24(b): the synth table OWNS `__wwtests` — loud-reject a user
// decl of that name (mirror the `main` reservation; cstage
// cmd/wcc/check.c). A user `__wwtests` whose type HAPPENS to
// match run()'s `[](str, *fn()void)` param slips the general
// call-arg check (a) but still silently shadows the synth table,
// so the synth `run(__wwtests)` iterates the user's table, not
// the collected @tests — reserve the NAME so the collision is
// loud regardless of type. Any decl kind (const/let/fn).
if (syntax.streq(u.str, "__wwtests")) {
cerr(u.file);
cerr(": error: test mode: __wwtests is reserved by -T; rename the declaration\n");
c.errs += 1;
};
u = u.next;
};
// (c) collect @test fns in file.list order; build one table row
// `("<name>", &<name>)` per validated @test fn.
let rhead: *syntax.node = nil;
let rtail: *syntax.node = nil;
let ntest: i32 = 0;
let t: *syntax.node = file.list;
for (t != nil) {
if (t.kind == syntax.nkind.N_FNDECL) {
let ntestattr: i32 = 0;
let at: *syntax.node = t.attr;
for (at != nil) {
if (at.kind == syntax.nkind.N_ATTR
&& syntax.streq(at.str, "test")) {
ntestattr += 1;
};
at = at.next;
};
// Attribute-shape rejects, fixed order, first
// failure wins per fn; wording byte-stable with
// the cstage twin (check.c). A silently-dropped
// shape here ships a test that never runs.
if (ntestattr > 1) {
cerr(t.file);
cerr(": error: duplicate @test on fn '");
cerr(t.str);
cerr("'\n");
c.errs += 1;
} else { if (ntestattr == 1 && t.exported != 0) {
cerr(t.file);
cerr(": error: @test fn '");
cerr(t.str);
cerr("' cannot be exported\n");
c.errs += 1;
} else { if (ntestattr == 1 && t.body == nil) {
cerr(t.file);
cerr(": error: @test fn '");
cerr(t.str);
cerr("' needs a body\n");
c.errs += 1;
} else { if (ntestattr == 1) {
// `fn f() void` parses the explicit void
// into t.lhs, so void-returning is lhs==nil
// OR an N_TNAME "void".
let retvoid: bool = t.lhs == nil
|| (t.lhs.kind == syntax.nkind.N_TNAME
&& syntax.streq(t.lhs.str, "void"));
if (t.list != nil || !retvoid) {
cerr(t.file);
cerr(": error: @test fn '");
cerr(t.str);
cerr("' must be fn() void\n");
c.errs += 1;
} else {
let nm: *syntax.node = syntax.newnode(syntax.nkind.N_STRLIT, pf, pl, pc);
nm.str = t.str;
let id: *syntax.node = syntax.newnode(syntax.nkind.N_IDENT, pf, pl, pc);
id.str = t.str;
let amp: *syntax.node = syntax.newnode(syntax.nkind.N_UN, pf, pl, pc);
amp.op = syntax.tkind.TK_AMP;
amp.lhs = id;
let row: *syntax.node = syntax.newnode(syntax.nkind.N_TUPLE, pf, pl, pc);
row.list = nm;
nm.next = amp;
if (rhead == nil) { rhead = row; }
else { rtail.next = row; };
rtail = row;
ntest += 1i32;
};
}; }; }; };
};
t = t.next;
};
let body: *syntax.node = syntax.newnode(syntax.nkind.N_BLOCK, pf, pl, pc);
let tab: *syntax.node = nil;
if (ntest == 0i32) {
// no @test fns in this unit — exit 0, nothing to run.
let ret: *syntax.node = syntax.newnode(syntax.nkind.N_RETURN, pf, pl, pc);
let zero: *syntax.node = syntax.newnode(syntax.nkind.N_INTLIT, pf, pl, pc);
zero.uval = 0u64;
ret.lhs = zero;
body.list = ret;
} else {
// const __wwtests: [](str, *fn() void) = [rows...];
// wwstage tuple-type elements wrap in N_TPARAM (parse.ww:308).
let e0: *syntax.node = syntax.newnode(syntax.nkind.N_TNAME, pf, pl, pc);
e0.str = "str";
let p0: *syntax.node = syntax.newnode(syntax.nkind.N_TPARAM, pf, pl, pc);
p0.lhs = e0;
let vret: *syntax.node = syntax.newnode(syntax.nkind.N_TNAME, pf, pl, pc);
vret.str = "void";
let vfn: *syntax.node = syntax.newnode(syntax.nkind.N_TFN, pf, pl, pc);
vfn.lhs = vret;
let e1: *syntax.node = syntax.newnode(syntax.nkind.N_TPTR, pf, pl, pc);
e1.lhs = vfn;
let p1: *syntax.node = syntax.newnode(syntax.nkind.N_TPARAM, pf, pl, pc);
p1.lhs = e1;
let tup: *syntax.node = syntax.newnode(syntax.nkind.N_TTUPLE, pf, pl, pc);
tup.list = p0;
p0.next = p1;
let tsl: *syntax.node = syntax.newnode(syntax.nkind.N_TSLICE, pf, pl, pc);
tsl.lhs = tup;
let arr: *syntax.node = syntax.newnode(syntax.nkind.N_ARRLIT, pf, pl, pc);
arr.list = rhead;
tab = syntax.newnode(syntax.nkind.N_LET, pf, pl, pc);
tab.op = syntax.tkind.TK_CONST;
tab.str = "__wwtests";
tab.lhs = tsl;
tab.rhs = arr;
// pass 1 already ran; install the table name now so main
// resolves it (declmod returns "" for tab.nmod=""). Goes
// direct to scopedefineinmodule, NOT installdecl: cstage's
// synth install (cmd/wcc/check.c:3079) bypasses the
// duplicate-decl reject (#23) the same way, so a pathological
// user `const __wwtests` stays a downstream type error in
// both stages rather than a dup-diagnostic in only one.
syntax.scopedefineinmodule(c.top, tab.str, "", syntax.skind.SK_VAR, nil, tab);
let arg: *syntax.node = syntax.newnode(syntax.nkind.N_IDENT, pf, pl, pc);
arg.str = "__wwtests";
let call: *syntax.node = syntax.newnode(syntax.nkind.N_CALL, pf, pl, pc);
// QUALIFIED test.run (N_DOT base ident `test`, member `run`):
// the sep producer sees lib/test as a real imported package,
// so the call must carry the module qualifier; the combined
// path tags auto-bundled lib/test `//ww:module test` too, so
// it resolves+mangles identically (test.run). Cstage twin:
// cmd/wcc/check.c synth. The base ident binds through the
// synth N_USE below.
let dot: *syntax.node = syntax.newnode(syntax.nkind.N_DOT, pf, pl, pc);
let did: *syntax.node = syntax.newnode(syntax.nkind.N_IDENT, pf, pl, pc);
did.str = "test";
dot.lhs = did;
dot.str = "run";
call.lhs = dot;
call.list = arg;
let ret: *syntax.node = syntax.newnode(syntax.nkind.N_RETURN, pf, pl, pc);
ret.lhs = call;
body.list = ret;
// #80: the synth `use test;` (the N_DOT base qualifier + the
// usepathfor source mapping `test`→its path) is now PREPENDED
// before Pass 1 at the top of checkfile, so declmod keys
// lib/test's `run` under "test" and the synth `test.run`
// type-resolves. It is installed (SK_USE) by Pass 1's N_USE arm.
};
let m: *syntax.node = syntax.newnode(syntax.nkind.N_FNDECL, pf, pl, pc);
m.str = "main";
m.exported = 1i32;
let rety: *syntax.node = syntax.newnode(syntax.nkind.N_TNAME, pf, pl, pc);
rety.str = "i32";
m.lhs = rety;
m.body = body;
// Append the table const (if any) then main to file.list tail. No
// type_ pre-set on main: installdecl never stamps a fn's type_ on
// the ww side — cgfn reads lhs/list on demand. Pass-2 below
// resolve-walks the appended bodies.
let tl: *syntax.node = file.list;
if (tl == nil) {
if (tab != nil) { file.list = tab; tab.next = m; }
else { file.list = m; };
} else {
for (tl.next != nil) { tl = tl.next; };
if (tab != nil) { tl.next = tab; tab.next = m; }
else { tl.next = m; };
};
};
// 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: syntax.nkind = d.kind;
switch (k) {
case syntax.nkind.N_FNDECL:
// ww restricts C-style ... to bodiless decls pending
// vastart/vaarg/vaend builtins (#16); harec permits bodied
// C-variadic fns (check.c:3656). The bare C-style param is
// the one with str=="..." (Hare-style `T...` carries a name
// + op==TK_ELLIPSIS instead), mirroring cstage build_fn_type
// check.c:2646.
if (d.body != nil && hascvariadic(d.list)) {
cerr(d.file);
cerr(": error: C-style variadic '...' requires a bodiless declaration\n");
c.errs += 1;
};
if (d.lhs != nil) { resolvewalk(c, d.lhs); }; // return type
resolvefnbody(c, d);
case syntax.nkind.N_DEF:
// #11: a module-level `def xs: [_]T = arrlit;` must infer
// its length BEFORE resolvewalk stamps d.lhs's tinfo, the
// def twin of the N_LET arm below. #7 wired only the let
// path, so the def path silently stayed length 0 (no DATA,
// garbage indexed reads). inferarraylen mutates the N_TARRAY
// length child in place (the SSoT all reads resolve through),
// so no Sym re-point is needed on this side.
inferarraylen(c, d);
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 syntax.nkind.N_TYPEDECL:
if (d.lhs != nil) { resolvewalk(c, d.lhs); };
case syntax.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;
};
// #6 harec-fidelity (ref/harec/src/check.c:3941): a @test fn is fully
// checked above (pass 2 + pass 3 walked it like every fn) but is NOT
// emitted in a non-test build. harec skips append_decl for
// FN_TEST && !is_test, so the fn never reaches the codegen decl list;
// the body is still checked, only the emission is dropped. ww shares
// one file.list across check + cgen (no separate checked-decl list),
// so we splice the already-checked @test fns out here, after all
// passes — they stay checked, never reach cgen. The -T path is
// untouched: its synth main calls the @test fns, so they must remain.
// Twin: cmd/wcc/check.c.
if (c.istest == 0) {
let prev: *syntax.node = nil;
let e: *syntax.node = file.list;
for (e != nil) {
let istest: bool = false;
if (e.kind == syntax.nkind.N_FNDECL) {
let at: *syntax.node = e.attr;
for (at != nil) {
if (at.kind == syntax.nkind.N_ATTR
&& syntax.streq(at.str, "test")) {
istest = true;
};
at = at.next;
};
};
let nx: *syntax.node = e.next;
if (istest) {
if (prev == nil) { file.list = nx; }
else { prev.next = nx; };
} else {
prev = e;
};
e = nx;
};
};
let empty: str;
c.curmod = empty;
};