Files
ww/selfhost/cmd/wcc/cgenutil.ww
Hojun-Cho 50d70e74e4 w6c: append() accepts struct CALL rvalue elements (#34)
Evaluate the call pre-grow into a per-site scratch, receiving by the
N_LET matrix (sret / float / odd-tail / GP), then grow, slot, sized
ladder. Both stages, byte-identical. The two reject pins graduate to
16B GP accept rows; three new fixtures cover 24B GP, 3B/4B tails,
16B float, and 40B sret.
2026-08-08 14:10:55 +09:00

6503 lines
262 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// selfhost/cmd/wcc/cgenutil.ww — split out of cgen.ww.
//
// General helpers used across cgenexpr / cgenstmt / cgendecl:
// - pushargsrev: per-call arg pushing
// - type predicates: isstr*/isslice*/istagged*/nodeis* families
// - field ops: fieldloadop, fieldstoreop
// - index helpers: elemsizeof, elemsizeofc
// - slot sizing: structlookup, primsize, slotsize, fieldsize,
// registerstruct, collectstructs
// - rhs helpers: taggedvariantindex
//
// Bundler pulls this in transitively via cgen.ww; consumers don't
// need to `use cgenutil;` directly.
package wcc;
import os;
import syntax;
import strconv;
// ---- variadic-call helpers (Hare-style `T...` param) -----------------
// slicewrap — synthesise an N_TSLICE node wrapping the given element
// type AST. Used by the Hare-style variadic path so the local entry
// for the param (callee side) and the call-site slice descriptor
// (caller side) both advertise their effective type as []ELEM —
// every isslicetype / nodeisslice check then succeeds naturally.
fn slicewrap(c: *cgen, elem: *syntax.node) *syntax.node = {
let s: *syntax.node = syntax.newnode(syntax.nkind.N_TSLICE, "", 0, 0);
s.lhs = elem;
return s;
};
// findvariadicparam — walk a param-list head and return the variadic
// param node (the one with op == TK_ELLIPSIS) plus the count of
// non-variadic params before it. Returns nil/0 when no variadic.
// nfixed_out cannot be nil.
fn findvariadicparam(ps: *syntax.node, nfixed_out: *i32) *syntax.node = {
*nfixed_out = 0;
let p: *syntax.node = ps;
for (p != nil) {
if (p.kind == syntax.nkind.N_PARAM) {
if (p.op == syntax.tkind.TK_ELLIPSIS) {
return p;
};
*nfixed_out += 1;
};
p = p.next;
};
return nil;
};
// fnptrcalleetfn — resolve a callee that names a LOCAL fn pointer
// (bare local `fp(...)`, or field call `w.emit(...)` where w is a
// local struct value / *struct and emit is an N_TFN field) to its
// N_TFN type node. Returns nil for every other callee shape — the
// name registry stays primary for module fns. Extracted from the
// cgcall isfnptrcall detection so the CALL-target choice and the
// arg classification key off ONE resolution and can never disagree
// within a call (#59.8: a local whose name shadows the current
// module made fnparamslookupmod match the module's own same-leaf
// variadic fn — nfixed off by one, the fixed arg boxed into the
// gather and the spread emitted as zeros; with no collision the
// lookup missed entirely and the raw N_SPREAD leaked to pushargsrev
// as a single $0 word). Cstage mirror: cgcall's callee_params from
// the checker-stamped n->lhs->type (cmd/w6c/cgen.c TY_FN cu->params).
// dotbasestructinfo — the struct registry entry for a DOT base: a
// local ident (struct value or *struct), or a nested field chain
// (`a.b.cb(...)` — each link a struct- or *struct-typed field). The
// chained form previously fell to the name registry with an empty
// module hint (the #59.8 family's last open shape) and emitted
// CALL <leaf>(SB).
fn dotbasestructinfo(c: *cgen, base: *syntax.node) *structinfo = {
if (base == nil) { return nil; };
if (base.kind == syntax.nkind.N_IDENT) {
let lc: *local = localfindnode(c, base.str);
if (lc == nil) { return nil; };
let tn: *syntax.node = lc.tnode;
if (tn == nil) { return nil; };
let sname: str;
sname.ptr = nil; sname.len = 0;
if (tn.kind == syntax.nkind.N_TNAME) { sname = tn.str; };
if (tn.kind == syntax.nkind.N_TPTR) {
if (tn.lhs != nil) {
if (tn.lhs.kind == syntax.nkind.N_TNAME) {
sname = tn.lhs.str;
};
};
};
if (sname.len == 0) { return nil; };
return structlookup(c, sname);
};
if (base.kind != syntax.nkind.N_DOT) { return nil; };
let outer: *structinfo = dotbasestructinfo(c, base.lhs);
if (outer == nil) { return nil; };
let fi: *fieldinfo = outer.fields;
for (fi != nil) {
if (syntax.streq(fi.fname, base.str)) {
let ft: *syntax.node = fi.tnode;
if (ft == nil) { return nil; };
if (ft.kind == syntax.nkind.N_TPTR) {
if (ft.lhs != nil) { ft = ft.lhs; };
};
if (ft.kind == syntax.nkind.N_TNAME) {
return structlookup(c, ft.str);
};
return nil;
};
fi = fi.finext;
};
return nil;
};
fn fnptrcalleetfn(c: *cgen, callee: *syntax.node) *syntax.node = {
if (callee == nil) { return nil; };
if (callee.kind == syntax.nkind.N_IDENT) {
let lc0: *local = localfindnode(c, callee.str);
if (lc0 == nil) { return nil; };
let tn0: *syntax.node = lc0.tnode;
if (tn0 == nil) { return nil; };
if (tn0.kind == syntax.nkind.N_TFN) { return tn0; };
if (tn0.kind == syntax.nkind.N_TPTR) {
if (tn0.lhs != nil) {
if (tn0.lhs.kind == syntax.nkind.N_TFN) {
return tn0.lhs;
};
};
};
return nil;
};
if (callee.kind != syntax.nkind.N_DOT) { return nil; };
let si: *structinfo = dotbasestructinfo(c, callee.lhs);
if (si == nil) { return nil; };
let fi: *fieldinfo = si.fields;
for (fi != nil) {
if (syntax.streq(fi.fname, callee.str)) {
let ft: *syntax.node = fi.tnode;
if (ft != nil) {
if (ft.kind == syntax.nkind.N_TFN) { return ft; };
// `cb: *fn(...)` — same TPTR peel as the N_IDENT
// arm above; without it the field-call fell through
// to the name registry and emitted CALL <leaf>(SB)
// (undefined symbol — loud at link, but cstage
// resolves the stamped ptr-to-fn and calls indirect).
if (ft.kind == syntax.nkind.N_TPTR) {
if (ft.lhs != nil) {
if (ft.lhs.kind == syntax.nkind.N_TFN) {
return ft.lhs;
};
};
};
};
return nil;
};
fi = fi.finext;
};
return nil;
};
// callee_variadic_param — convenience wrapper: looks up the callee
// by name and finds its variadic param + nfixed. Returns nil if the
// callee isn't registered or has no variadic param.
//
// N_DOT routes through fnparamslookupmod with the module hint
// (callee.lhs.str) — bare fnparamslookup walks same-module-first
// (#4d) which is wrong for a cross-module N_DOT call into a module
// whose same-leaf fn has divergent variadic-vs-non-variadic shape.
// #4d explicitly deferred this re-routing; surfaced by #16 when
// strings.contains gained a variadic shape and a caller's
// bytes.contains call site picked strings.contains' variadic
// params for arg-prep while emitting CALL bytes.contains.
fn callee_variadic_param(c: *cgen, callee: *syntax.node, nfixed_out: *i32) *syntax.node = {
*nfixed_out = 0;
if (callee == nil) { return nil; };
// #59.8: a LOCAL fn-ptr callee resolves through its own N_TFN,
// never the name registry (which the collision shape mis-hits).
// A raw field-N_TFN variadic param carries the bare element T
// (registry params are installparams-promoted to []T); wrap it
// so the vararg_sl descriptor slot's tnode classifies as a
// slice downstream (mirror check.ww installparams / cstage
// check.c:455).
let ft: *syntax.node = fnptrcalleetfn(c, callee);
if (ft != nil) {
let vp: *syntax.node = findvariadicparam(ft.list, nfixed_out);
if (vp == nil) { return nil; };
if (vp.lhs != nil) {
if (vp.lhs.kind != syntax.nkind.N_TSLICE) {
let w: *syntax.node = syntax.newnode(
syntax.nkind.N_PARAM, "", 0, 0);
w.str = vp.str;
w.op = vp.op;
let sl: *syntax.node = syntax.newnode(
syntax.nkind.N_TSLICE, "", 0, 0);
sl.lhs = vp.lhs;
sl.type_ = syntax.typeslice(
vp.lhs.type_: *syntax.tinfo): *void;
w.lhs = sl;
return w;
};
};
return vp;
};
let ps: *syntax.node = nil;
if (callee.kind == syntax.nkind.N_IDENT) {
if (callee.str.len == 0) { return nil; };
ps = fnparamslookup(c, callee.str);
} else { if (callee.kind == syntax.nkind.N_DOT) {
if (callee.str.len == 0) { return nil; };
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.lhs != nil) {
if (callee.lhs.kind == syntax.nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
ps = fnparamslookupmod(c, callee.str, cmod);
}; };
return findvariadicparam(ps, nfixed_out);
};
// calleecvariadic — true when the callee is a C-style variadic fn: a
// bare `...` param (str == "...", distinct from the Hare-style `T...`
// findvariadicparam keys on op == TK_ELLIPSIS). nfixed_out gets the
// count of fixed params before the `...`. Mirror of cstage's
// `cu->variadic` gate (cmd/w6c/cgen.c cgcall), which check.c:902 sets
// ONLY for the bare-`...` form — the bare `...` adds no Tparam, so
// nfixed is the fixed-param count. Drives the SysV §3.5.7 AL=XMM-count
// emit and the C-default-promotion of an f32 variadic-tail arg to f64
// (#14). nfixed_out cannot be nil.
fn calleecvariadic(c: *cgen, callee: *syntax.node, nfixed_out: *i32) bool = {
*nfixed_out = 0;
if (callee == nil) { return false; };
let ps: *syntax.node = nil;
// #59.8: a LOCAL fn-ptr callee reads its own N_TFN params — the
// name registry mis-hits when the local shadows a module name.
let lft: *syntax.node = fnptrcalleetfn(c, callee);
if (lft != nil) {
ps = lft.list;
} else { if (callee.kind == syntax.nkind.N_IDENT) {
if (callee.str.len == 0) { return false; };
ps = fnparamslookup(c, callee.str);
} else { if (callee.kind == syntax.nkind.N_DOT) {
if (callee.str.len == 0) { return false; };
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.lhs != nil) {
if (callee.lhs.kind == syntax.nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
ps = fnparamslookupmod(c, callee.str, cmod);
}; }; };
let p: *syntax.node = ps;
for (p != nil) {
if (p.kind == syntax.nkind.N_PARAM) {
if (syntax.streq(p.str, "...")) {
return true;
};
*nfixed_out += 1;
};
p = p.next;
};
return false;
};
// ---- expression cgen -------------------------------------------------
// pushargsrev — recursively walks the arg list, evaluates rightmost
// first, and pushes. str args take two slots (ptr in AX, len in BX);
// the order on the stack so a left-to-right pop into argregs lands
// (ptr, len) correctly is: PUSHQ BX (top), PUSHQ AX (above) — the
// pop sequence then yields AX, then BX.
//
// `param` is the corresponding declared parameter for `arg` (N_PARAM
// node from the callee's signature) or nil. When param's type is a
// tagged union and `arg`'s surface type is a concrete variant of it,
// we materialise (tag, value-words, pad) for the parameter slot before
// pushing — mirrors cmd/w6c/cgen.c's call-arg widening.
//
// #38b: cgcall walks the list TWICE — memphase=true first, staging
// every MEMORY-class (>48B tagged) arg below all register-class
// words, then memphase=false for the register classes. Each phase
// skips the other's args; the return value counts only own-phase
// slot words. Mirrors cstage cgcall's mem pre-pass; ABI shape per
// ref/qbe/amd64/sysv.c:80-85 (inmem) / :411-426 (stack blit).
// argtaggedwidensz — the SysV slot width (16/24/32) a CONCRETE arg is
// widened to when passed to a tagged-union PARAM, or 0 when no register-class
// widen happens: param not tagged / not a fixed param, an already-matching-
// slot tagged source (natural push), the nullable 8B fold (handled by the
// scalar path), or a >48B memory-class slot (staged below the register words).
// This is the SSoT the cgcall DRAIN consults for its widen-first POP count;
// it MUST agree word-for-word with pushargsrev's widen-PUSH count below
// (same param-tagged + !aistagged gates, same taggedcastpeel) or the drain
// desyncs — the #30/#48 root was a drain with no widen branch: the widened
// box's words were under-drained (#30: a following float read the leftover
// payload word) or the float-source box was misclassified as a float arg
// (#48: MOVSD ate the tag word into X0). Mirrors cstage's precomputed
// widen[i]/widen_sz[i] (cmd/w6c/cgen.c cgcall).
fn argtaggedwidensz(c: *cgen, arg0: *syntax.node, param: *syntax.node) i32 = {
if (param == nil) { return 0; };
if (param.kind != syntax.nkind.N_PARAM) { return 0; };
if (param.op == syntax.tkind.TK_ELLIPSIS) { return 0; };
let ptype: *syntax.node = param.lhs;
if (ptype == nil) { return 0; };
if (!istaggedtype(c, ptype)) { return 0; };
// >48B slot is memory-class: pushargsrev stages it below the register
// words and the drain skips it (dmemsz) — never a register widen.
if (taggedmemargsize(ptype.type_: *syntax.tinfo) > 0) { return 0; };
let arg: *syntax.node = taggedcastpeel(c, arg0);
let pslot: i32 = slotsize(c, ptype);
// Already a matching-slot tagged source → natural push, no widen
// (mirrors pushargsrev's aistagged gates: ident/call/index/dot/deref).
if (arg.kind == syntax.nkind.N_IDENT) {
let lc: *local = localfindnode(c, arg.str);
if (lc != nil) {
if (istaggedtype(c, lc.tnode)) {
if (slotsize(c, lc.tnode) == pslot) { return 0; };
};
};
};
if (taggedcallslot(c, arg) == pslot) { return 0; };
if (arg.kind == syntax.nkind.N_INDEX) {
if (istaggedtype(c, arg)) { if (slotsize(c, arg) == pslot) { return 0; }; };
};
if (arg.kind == syntax.nkind.N_DOT) {
if (istaggedtype(c, arg)) { if (slotsize(c, arg) == pslot) { return 0; }; };
};
if (arg.kind == syntax.nkind.N_UN && arg.op == syntax.tkind.TK_STAR) {
if (istaggedtype(c, arg)) { if (slotsize(c, arg) == pslot) { return 0; }; };
};
// The 8B nullable fold pushes one pointer word the scalar drain path
// already pops correctly; only the multi-word tagged widen needs the
// drain's dedicated branch.
if (pslot < 16) { return 0; };
return pslot;
};
// argidx is this arg's 0-based position in the call's arg list;
// cvarnfixed is the fixed-param count of a C-variadic callee (-1 when
// the callee is not C-variadic). An f32 arg in the variadic tail
// (argidx >= cvarnfixed) is promoted to f64 here (#14), so its 8B stack
// slot holds a real double for the pop side and the callee's
// va_arg(double).
fn pushargsrev(c: *cgen, arg: *syntax.node, param: *syntax.node, memphase: bool, argidx: i32, cvarnfixed: i32) i32 = {
if (arg == nil) { return 0; };
let nextparam: *syntax.node = nil;
if (param != nil) { nextparam = param.next; };
let rest: i32 = pushargsrev(c, arg.next, nextparam, memphase, argidx + 1, cvarnfixed);
// Family C (#35): peel tagged→tagged casts FIRST so every gate
// below keys on the operand — an identity cast reduces to the
// ident fast path, a widening cast trips the widen branch with
// the operand as source. cgexpr on the cast node collapses to
// one word (silent word0 push pre-#35). Mirrors cstage's
// args[i] = cg_tagged_castpeel(args[i]) pre-pass; the cgcall
// pop side counts via pushargsrev's return, so the drain stays
// balanced.
arg = taggedcastpeel(c, arg);
// #38b MEMORY-class detection: keyed off the declared param's
// type (so widening into a >48B slot is caught), else the arg's
// own stamped type (fn-ptr callee carries no param nodes).
let memsz: i32 = 0;
let memptype: *syntax.node = nil;
if (param != nil) { if (param.kind == syntax.nkind.N_PARAM) {
if (param.op != syntax.tkind.TK_ELLIPSIS) {
memptype = param.lhs;
if (memptype != nil) {
memsz = taggedmemargsize(memptype.type_: *syntax.tinfo);
};
};
}; };
if (memsz == 0) {
memsz = taggedmemargsize(arg.type_: *syntax.tinfo);
};
if (memphase != (memsz > 0)) { return rest; };
if (memsz > 0) {
// same-type check — mirror cstage's `(pu == au) ||
// type_eq(p->type, at)` widen detection.
let same: bool = false;
let at: *syntax.tinfo = arg.type_: *syntax.tinfo;
if (memptype != nil) {
let pt: *syntax.tinfo = memptype.type_: *syntax.tinfo;
let pu: *syntax.tinfo = pt;
pu = tichase(pu);
let au: *syntax.tinfo = at;
au = tichase(au);
if (pu != nil && pu == au) { same = true; };
if (!same && pt != nil && at != nil) {
if (syntax.typeeq(pt, at)) { same = true; };
};
} else {
same = true;
};
if (!same) {
// Widen via the @tagscr scratch for EVERY source
// shape — the direct-push fast arms below stage
// exactly 4 words, short of the memsz/8 the drain
// accounts for (mirrors cstage cg_widen_tagged_push
// dst_is_mem routing).
let scroff: i32 = tagscradd(c, memsz);
emitline("\tXORQ\tAX, AX\n");
let zz: i32 = 0;
for (zz < memsz) {
emitline("\tMOVQ\tAX, ");
emitoff((scroff + zz): i64);
emitline("(BP)\n");
zz += 8;
};
cgwidentaggedstore(c, memptype.type_: *syntax.tinfo, arg,
"BP", scroff, memsz);
let pp: i32 = memsz - 8;
for (pp >= 0) {
emitline("\tMOVQ\t");
emitoff((scroff + pp): i64);
emitline("(BP), AX\n");
emitline("\tPUSHQ\tAX\n");
pp -= 8;
};
return rest + memsz / 8;
};
// Exact type: raw slot words high→low from the value's
// address (local slot, or any aggargsrcaddr-addressable
// source: global let, N_DOT chain, array index, deref).
// An exact-type CALL source is sret-class (>32B tagged
// return) — its result is in memory behind a dest pointer,
// not a register cursor; receive-then-push is the
// #40-family follow-up.
if (arg.kind == syntax.nkind.N_CALL) {
let mc: str = "#38b: sret-class tagged call result as a >48B by-value arg unwired (#40-family follow-up)\n";
os.write(2, mc.ptr, mc.len: u64);
os.exit(1);
};
if (arg.kind == syntax.nkind.N_IDENT) {
let lc: *local = localfindnode(c, arg.str);
if (lc != nil) {
let w: i32 = memsz / 8 - 1;
for (w >= 0) {
emitline("\tMOVQ\t");
emitoff((lc.off + w*8): i64);
emitline("(BP), AX\n");
emitline("\tPUSHQ\tAX\n");
w -= 1;
};
return rest + memsz / 8;
};
};
if (aggargsrcaddr(c, arg, "SI")) {
let w: i32 = memsz / 8 - 1;
for (w >= 0) {
emitline("\tMOVQ\t");
emitoff((w*8): i64);
emitline("(SI), AX\n");
emitline("\tPUSHQ\tAX\n");
w -= 1;
};
return rest + memsz / 8;
};
// #40/FB3: a place the enumerated arms miss — slice
// element, deref-spine element — resolves through the F6
// resolver. AFTER aggargsrcaddr so every pre-#40 shape
// keeps its asm; the resolver balances its own pushes, so
// the words already staged below stay put.
if (cgplaceaddr(c, arg, "SI")) {
let w: i32 = memsz / 8 - 1;
for (w >= 0) {
emitline("\tMOVQ\t");
emitoff((w*8): i64);
emitline("(SI), AX\n");
emitline("\tPUSHQ\tAX\n");
w -= 1;
};
return rest + memsz / 8;
};
let mu: str = "#38b: >48B tagged arg from unsupported source kind (rvalue and unresolvable-place sources unwired)\n";
os.write(2, mu.ptr, mu.len: u64);
os.exit(1);
};
// Implicit widening from a concrete variant to a tagged-union
// parameter slot. Skips when the arg is already a tagged local
// (line 121's slice-or-tagged shortcut handles that).
let widensz: i32 = 0;
let widentag: i32 = 0;
if (param != nil) {
if (param.kind == syntax.nkind.N_PARAM) {
// Hare-style variadic `T...`: effective param type is
// []T (slice). The arg here is the synthesised slice
// descriptor (or a forwarded `xs...` slice), not a
// value of T being widened into a tagged slot — skip
// the widening detection so the slice-ident fast path
// at the bottom of pushargsrev gets the push.
if (param.op == syntax.tkind.TK_ELLIPSIS) {
widensz = 0;
} else {
let ptype: *syntax.node = param.lhs;
if (istaggedtype(c, ptype)) {
let aistagged: bool = false;
if (arg.kind == syntax.nkind.N_IDENT) {
let lc: *local = localfindnode(c, arg.str);
if (lc != nil) {
// #55: only treat a tagged ident as
// "already tagged" (natural push, no remap)
// when its slot MATCHES the param. On slot-
// DIFFER the source is a NARROWER union widened
// into a wider one — fall to the widen scratch
// + tag-remap below (the slot-gated INDEX/DOT/
// STAR arms' twin). Pre-#55 the ungated TRUE
// natural-pushed the narrower box's words with
// no remap (aligned-tag LUCK, misaligned-tag
// wrong). cstage routes every tagged source
// through cg_widen_tagged_store (cmd/w6c/
// cgen.c:2982 src_is_tagged).
if (istaggedtype(c, lc.tnode)) {
if (slotsize(c, lc.tnode) == slotsize(c, ptype)) {
aistagged = true;
};
};
};
};
// #21: a CALL returning a tagged-union must
// skip widening — cgexpr leaves AX=tag,
// DX=word0, CX=word1, R8=word2 per the
// tagged-return ABI; the widening branch would
// treat AX as a concrete payload and silently
// drop DX/CX/R8. Restrict to the matching-slot
// case (mirrors cstage type_eq at
// cmd/w6c/cgen.c:4216-4221); tagged-source
// widening into a wider slot is out of scope.
if (taggedcallslot(c, arg) == slotsize(c, ptype)) {
aistagged = true;
};
// #12: N_INDEX of a sum-typed slice element —
// cgindex emits the same AX/DX/CX/R8 tagged ABI.
// Without this gate the widening scalar branch
// hardcodes the param's first-variant tag and
// the callee reads a fixed arm on garbage.
// #60: arg.type_ is the checker-stamped element
// tinfo (check.ww indexresult); istaggedtype/
// slotsize read .type_, so feed the N_INDEX node
// directly. cstage reads the element via
// base->type->sub (cmd/w6c/cgen.c:3518).
if (arg.kind == syntax.nkind.N_INDEX) {
if (istaggedtype(c, arg)) {
if (slotsize(c, arg) == slotsize(c, ptype)) {
aistagged = true;
};
};
};
// #22a: t.N tuple-element read leaves the
// same AX/DX/CX/R8 box cursor (this arc's
// t.N box load) — without this gate the
// widening scalar branch clamps the
// unresolvable tag to 0 and the callee
// reads variant 0. Stamped-carrier (#67)
// twin of the N_INDEX arm above; cstage
// needs no kind gate (its widen[i] `same`
// check is type-keyed on args[i]->type).
if (arg.kind == syntax.nkind.N_DOT) {
if (istaggedtype(c, arg)) {
if (slotsize(c, arg) == slotsize(c, ptype)) {
aistagged = true;
};
};
};
// Family C (#35): a DEREF source is a
// tagged box too (mem-based, any size)
// — without this gate the widening
// scalar branch boxed the box. Same
// slotsize key as the N_INDEX/N_DOT
// stamped-carrier arms above.
if (arg.kind == syntax.nkind.N_UN && arg.op == syntax.tkind.TK_STAR) {
if (istaggedtype(c, arg)) {
if (slotsize(c, arg) == slotsize(c, ptype)) {
aistagged = true;
};
};
};
if (!aistagged) {
widensz = slotsize(c, ptype);
let tagged: *syntax.node = resolvetagged(c, ptype);
let t: i32 = taggedvariantindex(c, tagged, arg);
if (t < 0) { t = 0; };
widentag = t;
};
};
};
};
};
if (widensz == 8) {
// Nullable fold: pointer value IS the discriminator. No
// separate tag word.
cgexpr(c, arg);
emitline("\tPUSHQ\tAX\n");
return rest + 1;
};
if (widensz > 0) {
// Struct-payload widening into a tagged-union param uses
// @tagscr (zero + cgwidentaggedstore writes fields + tag,
// then push slot words high → low). Scalar / str go via
// the direct push fast path below — keeps wwstage's asm
// byte-identical to cstage for selfhost source.
// #66: a tuple-typed source has no direct-push shape — the
// scalar fast arm would push word 0 only (payload slot 1+
// dropped) and coerce an unresolved tag to 0. Route through
// the scratch store, whose #242/#66 tuple arm handles the
// literal/cast forms and loud-stops the rest (#72). Mirrors
// cstage cg_widen_tagged_push src_is_tuple.
let argtup: *syntax.tinfo = arg.type_: *syntax.tinfo;
argtup = tichase(argtup);
let argistuple: bool = false;
// #55: a tagged SOURCE widened into a wider/reordered tagged param
// (slot-DIFFER — the ident/deref/index/dot legs that fell past the
// slot-gated aistagged arms above) has no direct-push shape: the
// scalar fast arm below would box word0 with a tag clamped to 0,
// dropping the real tag + high words and skipping the variant
// remap. Route through the @tagscr store, whose ident/memread/
// cursor arms copy the box + tag-remap. Mirrors cstage
// cg_widen_tagged_push's unconditional src_is_tagged scratch
// routing (cmd/w6c/cgen.c:2982).
let argistagged: bool = false;
if (argtup != nil) {
if (argtup.kind == syntax.tykind.TY_TUPLE) {
argistuple = true;
};
if (argtup.kind == syntax.tykind.TY_TAGGED) {
argistagged = true;
};
};
let pname: str = rhsstructpayload(c, arg);
if (pname.len > 0 || argistuple || argistagged) {
let ptype: *syntax.node = param.lhs;
let scroff: i32 = tagscradd(c, widensz);
emitline("\tXORQ\tAX, AX\n");
let zz: i32 = 0;
for (zz < widensz) {
emitline("\tMOVQ\tAX, ");
emitoff((scroff + zz): i64);
emitline("(BP)\n");
zz += 8;
};
cgwidentaggedstore(c, ptype.type_: *syntax.tinfo, arg, "BP", scroff, widensz);
let pp: i32 = widensz - 8;
for (pp >= 0) {
emitline("\tMOVQ\t");
emitoff((scroff + pp): i64);
emitline("(BP), AX\n");
emitline("\tPUSHQ\tAX\n");
pp -= 8;
};
return rest + widensz / 8;
};
cgexpr(c, arg);
if (nodeisslice(c, arg)) {
// Slice payload (24B): cgexpr leaves (AX=ptr, BX=len,
// CX=cap). Slot layout: [+0]=tag, [+8]=ptr, [+16]=len,
// [+24]=cap. Push high→low so pop drains tag first.
// Requires widensz >= 32; a smaller slot would mean the
// destination union doesn't list slice as a variant
// (caller should have flagged a type error).
emitline("\tPUSHQ\tCX\n");
emitline("\tPUSHQ\tBX\n");
emitline("\tPUSHQ\tAX\n");
emitline("\tMOVQ\t$");
emitint(widentag: i64);
emitline(", AX\n");
emitline("\tPUSHQ\tAX\n");
} else { if (nodeisstr(c, arg)) {
// str IS []u8: slot 32 [+0]=tag,[+8]=ptr,[+16]=len,
// [+24]=cap — same shape as the slice arm above. Push
// cap, len, ptr, tag high→low so pop drains tag first
// into arg-reg[0] (#1/Phase 3).
emitline("\tPUSHQ\tCX\n");
emitline("\tPUSHQ\tBX\n");
emitline("\tPUSHQ\tAX\n");
emitline("\tMOVQ\t$");
emitint(widentag: i64);
emitline(", AX\n");
emitline("\tPUSHQ\tAX\n");
} else {
// Scalar variant: single value word at +8. Pad a zero
// high word when slot is 24B (some other variant of
// the union is 16B-shaped).
let pp: i32 = widensz - 8;
for (pp > 8) {
emitline("\tXORQ\tDX, DX\n");
emitline("\tPUSHQ\tDX\n");
pp -= 8;
};
// #49: an f64/f32 payload sits in X0 (cgexpr left it
// there), not AX — spill it through the stack so the
// callee reads the real bits. A plain PUSHQ AX pushed
// whatever AX last held (stale for a runtime float
// producer; only a const folder leaves the bits in AX
// — why #48 with a no-payload-read arm passed but #49
// reading `d == 2.5` did not). Both stages (#263);
// cstage cg_widen_tagged_push twin.
if (isfloattype(c, arg)) {
emitline("\tSUBQ\t$8, SP\n");
let fmov: str = "MOVSD";
if (isf32type(c, arg)) { fmov = "MOVSS"; };
emitline("\t");
emitline(fmov);
emitline("\tX0, (SP)\n");
} else {
emitline("\tPUSHQ\tAX\n");
};
emitline("\tMOVQ\t$");
emitint(widentag: i64);
emitline(", AX\n");
emitline("\tPUSHQ\tAX\n");
};};
return rest + widensz / 8;
};
// nkind.N_SLICE expression as arg: `buf[lo:hi]` builds a slice header
// on the stack matching C cgen's sequence — push base, push hi,
// compute lo, pop into BX/CX, derive len/ptr, push (cap, len, ptr).
if (arg.kind == syntax.nkind.N_SLICE) {
let base: *syntax.node = arg.lhs;
let lo: *syntax.node = arg.rhs;
let hi: *syntax.node = arg.cond;
let baselocal: *local = nil;
let globaltn: *syntax.node = nil;
let globalname: str;
globalname.ptr = nil; globalname.len = 0;
if (base != nil) {
if (base.kind == syntax.nkind.N_IDENT) {
let bn: str = base.str;
baselocal = localfindnode(c, bn);
if (baselocal == nil) {
let gt: *syntax.node = letvartnode(c, bn);
if (gt != nil) {
globaltn = gt;
globalname = bn;
};
};
};
};
// #257: an N_DOT `[N]T`-field base (`x.o[lo:hi]` as a call
// arg) carries no tnode — resolve esz / base-address from the
// checker-stamped element tinfo on base.type_ instead. Cstage
// twin reads base->type (cgen.c pushargs N_SLICE esz). Mirror
// of the cgslice #252 site.
let dotbu: *syntax.tinfo = nil;
if (base != nil) { if (base.kind == syntax.nkind.N_DOT) {
dotbu = base.type_: *syntax.tinfo;
dotbu = tichase(dotbu);
};};
// #60 (alias arc #5): alias-NAMED N_IDENT base — the tnode
// reads below see only the N_TNAME leaf (esz 1-sentinel,
// MOVQ base). Re-key off the chased stamped tinfo, the
// pusharg twin of the cgslice fix (cstage pushargs N_SLICE
// reads bu = type_chase_named(base->type) uniformly).
let basealias: bool = false;
let bu60: *syntax.tinfo = nil;
if (base != nil) { if (base.kind == syntax.nkind.N_IDENT) {
let bt60: *syntax.tinfo = base.type_: *syntax.tinfo;
if (bt60 != nil) { if (bt60.kind == syntax.tykind.TY_NAMED) {
basealias = true;
bu60 = tichase(bt60);
};};
};};
// esz from the type table for an N_IDENT base (#76; mirrors
// the cgindex idiom) or an N_DOT array/slice-field base (#257:
// scale by the field's element width, not esz=1 -> silently
// wrong for non-u8). Other non-ident bases stay esz=1.
// (#31: a bare N_ARRLIT arg never reaches here — it loud-rejects
// at the checker, supported only at a `let`; #33.)
let esz: i32 = 1;
if (baselocal != nil) {
esz = elemsizeofc(c, baselocal.tnode);
} else { if (globaltn != nil) {
esz = elemsizeofc(c, globaltn);
} else { if (dotbu != nil && dotbu.sub != nil) {
esz = dotbu.sub.size: i32;
};};};
if (bu60 != nil) {
let es60: *syntax.tinfo = tichase(bu60.sub);
if (es60 != nil) { esz = es60.size: i32; };
};
// base address → push
if (baselocal != nil) {
let tn: *syntax.node = baselocal.tnode;
let isarr60: bool = false;
if (tn != nil) {
if (tn.kind == syntax.nkind.N_TARRAY) { isarr60 = true; };
};
// #60: alias-NAMED base — chased kind (see cgindex twin).
if (bu60 != nil) { isarr60 = bu60.kind == syntax.tykind.TY_ARRAY; };
if (isarr60) {
emitline("\tLEAQ\t");
emitoff(baselocal.off: i64);
emitline("(BP), AX\n");
} else {
emitline("\tMOVQ\t");
emitoff(baselocal.off: i64);
emitline("(BP), AX\n");
};
} else { if (globaltn != nil) {
let gisarr60: bool = globaltn.kind == syntax.nkind.N_TARRAY;
// #60: alias-NAMED base — chased kind; runtime-
// unreachable until #77/#78 global DATA.
if (bu60 != nil) { gisarr60 = bu60.kind == syntax.tykind.TY_ARRAY; };
if (gisarr60) {
emitline("\tLEAQ\t");
emitsymname(c, globalname);
emitline("(SB), AX\n");
} else {
emitline("\tMOVQ\t");
emitsymname(c, globalname);
emitline("(SB), AX\n");
};
} else { if (dotbaseaddr(c, base, "AX")) {
// #257: N_DOT `[N]T`-field base as a call arg → field
// ADDRESS (LEAQ), not the auto-deref VALUE load cgexpr
// emits. Same choke-point as the cgslice #252 site;
// `[]T`/str/`*T` fields fall through to cgexpr.
} else {
cgexpr(c, base);
};};};
emitline("\tPUSHQ\tAX\n");
// hi (default base length) → push
if (hi != nil) {
cgexpr(c, hi);
} else { if (baselocal != nil) {
let tn: *syntax.node = baselocal.tnode;
if (tn != nil) {
if (tn.kind == syntax.nkind.N_TARRAY) {
let lenn: *syntax.node = tn.rhs;
if (lenn != nil && lenn.kind == syntax.nkind.N_INTLIT) {
emitline("\tMOVQ\t$");
emituint(lenn.uval);
emitline(", AX\n");
} else {
// #56: def/const dim — resolve from the stamped
// array tinfo (rule-13). The #60 bu60 arm below
// covers only NAMED-alias bases (tn.kind ==
// N_TNAME); a plain `[MAX]u8` base reaches here
// with a non-N_INTLIT dim and dropped the
// default-hi entirely. cstage reads bu->alen.
let abt: *syntax.tinfo = tichase(tn.type_: *syntax.tinfo);
if (abt != nil && abt.kind == syntax.tykind.TY_ARRAY) {
emitline("\tMOVQ\t$");
emitint(abt.alen: i64);
emitline(", AX\n");
};
};
} else { if (tn.kind == syntax.nkind.N_TSLICE) {
emitline("\tMOVQ\t");
emitoff((baselocal.off + 8): i64);
emitline("(BP), AX\n");
} else { if (tn.kind == syntax.nkind.N_TNAME) {
if (syntax.streq(tn.str, "str")) {
emitline("\tMOVQ\t");
emitoff((baselocal.off + 8): i64);
emitline("(BP), AX\n");
};
};};};
};
// #60: alias-NAMED base default-hi — chased kind
// (see the cgslice twin).
if (bu60 != nil) {
if (bu60.kind == syntax.tykind.TY_ARRAY) {
emitline("\tMOVQ\t$");
emitint(bu60.alen: i64);
emitline(", AX\n");
} else { if (bu60.kind == syntax.tykind.TY_SLICE
|| bu60.kind == syntax.tykind.TY_STR) {
emitline("\tMOVQ\t");
emitoff((baselocal.off + 8): i64);
emitline("(BP), AX\n");
};};
};
} else { if (globaltn != nil) {
if (globaltn.kind == syntax.nkind.N_TARRAY) {
let lenn: *syntax.node = globaltn.rhs;
if (lenn != nil && lenn.kind == syntax.nkind.N_INTLIT) {
emitline("\tMOVQ\t$");
emituint(lenn.uval);
emitline(", AX\n");
} else {
// #56: def/const dim on a global array base —
// resolve from the stamped array tinfo (rule-13),
// twin of the local arg-push arm above.
let abt: *syntax.tinfo = tichase(globaltn.type_: *syntax.tinfo);
if (abt != nil && abt.kind == syntax.tykind.TY_ARRAY) {
emitline("\tMOVQ\t$");
emitint(abt.alen: i64);
emitline(", AX\n");
};
};
} else { if (globaltn.kind == syntax.nkind.N_TSLICE) {
emitline("\tLEAQ\t");
emitsymname(c, globalname);
emitline("(SB), CX\n");
emitline("\tMOVQ\t8(CX), AX\n");
} else { if (globaltn.kind == syntax.nkind.N_TNAME) {
if (syntax.streq(globaltn.str, "str")) {
emitline("\tLEAQ\t");
emitsymname(c, globalname);
emitline("(SB), CX\n");
emitline("\tMOVQ\t8(CX), AX\n");
};
};};};
// #60: alias-NAMED global base default-hi — chased
// kind; runtime-unreachable until #77/#78 global DATA.
if (bu60 != nil) {
if (bu60.kind == syntax.tykind.TY_ARRAY) {
emitline("\tMOVQ\t$");
emitint(bu60.alen: i64);
emitline(", AX\n");
} else { if (bu60.kind == syntax.tykind.TY_SLICE
|| bu60.kind == syntax.tykind.TY_STR) {
emitline("\tLEAQ\t");
emitsymname(c, globalname);
emitline("(SB), CX\n");
emitline("\tMOVQ\t8(CX), AX\n");
};};
};
} else { if (dotbu != nil && dotbu.kind == syntax.tykind.TY_ARRAY) {
// #252 twin: default-hi over an N_DOT `[N]T`-field base —
// element count from the field's array tinfo (cstage reads
// bu->alen uniformly). Without this the #258 array→slice
// borrow desugar handed callees a 0-length slice.
emitline("\tMOVQ\t$");
emitint(dotbu.alen: i64);
emitline(", AX\n");
} else { if (dotbu != nil && (dotbu.kind == syntax.tykind.TY_SLICE
|| dotbu.kind == syntax.tykind.TY_STR)) {
// slice/str FIELD base — the arg twin of the cgslice
// default-hi arm; same re-eval-for-header rationale.
// Cstage twin: cgen.c pushargs N_SLICE.
cgexpr(c, base);
emitline("\tMOVQ\tBX, AX\n");
} else {
emitline("\tMOVQ\t$0, AX\n");
};};};};};
emitline("\tPUSHQ\tAX\n");
// lo (default 0) → AX
if (lo != nil) { cgexpr(c, lo); }
else { emitline("\tMOVQ\t$0, AX\n"); };
emitline("\tPOPQ\tBX\n"); // hi
emitline("\tPOPQ\tCX\n"); // base
emitline("\tMOVQ\tBX, DX\n"); // DX = hi
emitline("\tSUBQ\tAX, DX\n"); // DX = hi - lo = len
// ptr = base + lo*esz (#76; ensure.ha:30 membsz-unit).
// BX=lo*esz; AX=lo PRESERVED for cap. BX (dead hi) reloaded
// by cgbasecap below.
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", BX\n");
emitline("\tIMULQ\tAX, BX\n");
emitline("\tADDQ\tBX, CX\n");
} else {
emitline("\tADDQ\tAX, CX\n"); // CX = base + lo = ptr
};
// cap = base_cap - lo (#20); AX=lo, BX free.
if (cgbasecap(c, base, "BX")) {
emitline("\tSUBQ\tAX, BX\n");
emitline("\tPUSHQ\tBX\n"); // cap
} else {
emitline("\tPUSHQ\tDX\n"); // cap = len
};
emitline("\tPUSHQ\tDX\n"); // len
emitline("\tPUSHQ\tCX\n"); // ptr (top)
return rest + 3;
};
// Slice/tagged ident args: emit per-register MOVQ+PUSHQ pairs in
// reverse order (cap/v1, len/v0, ptr/tag) so a left-to-right pop
// into argregs lands the canonical (ptr/tag, len/v0, cap/v1).
// For tagged ident with a >24B slot (slice-payload variant),
// push a fourth word from off+24.
if (arg.kind == syntax.nkind.N_IDENT) {
let nm: str = arg.str;
let lc: *local = localfindnode(c, nm);
if (lc != nil) {
let off: i32 = lc.off;
if (isslicetype(c, lc.tnode) || istaggedtype(c, lc.tnode)) {
let nwords: i32 = 3;
if (istaggedtype(c, lc.tnode)) {
let ssz: i32 = slotsize(c, lc.tnode);
nwords = ssz / 8;
};
let w: i32 = nwords - 1;
for (w >= 0) {
emitline("\tMOVQ\t");
emitoff((off + w*8): i64);
emitline("(BP), AX\n");
emitline("\tPUSHQ\tAX\n");
w -= 1;
};
return rest + nwords;
};
// By-value struct ident: load qword(s) from the slot
// and push high → low so left-to-right pop on the
// callee side lands word 0 / word 1 into the SysV arg
// register pair. Mirrors cstage cgen.c §4240 (call
// site) so the wwstage prologue's new struct spill arm
// (cgendecl.ww structparamsize branch) sees the same
// reg layout. Pre-#11 the call-site fell through to
// `cgexpr(c, arg)` + scalar PUSHQ AX — only the first
// 8B word made it across, and the callee's second-arg
// slots picked up the wrong neighbour's value.
// #21/#224: COUNT the push off the checker-STAMPED tinfo
// (structabisizetn), not the name-keyed structparamsize(lc.tnode).
// On a cross-module same-leaf collision the inferred-let's tnode
// is a bare leaf that structlookup mis-resolves (returns 0 or a
// foreign struct >16B → 0), so the fast path was skipped and the
// arg dropped to the scalar single-PUSHQ default — word1 lost.
// cstage counts via struct_arg_size(args[i]->type) (TYPE-keyed),
// never colliding; align ww UP. The <=16B gate keeps >16B structs
// + arrays on the #271 aggregate arm below (the fast path emits
// at most 2 words).
let stsz: i32 = structabisizetn(arg.type_: *syntax.tinfo);
if (stsz > 0 && stsz <= 16) {
if (stsz > 8) {
emitline("\tMOVQ\t");
emitoff((off + 8): i64);
emitline("(BP), AX\n");
emitline("\tPUSHQ\tAX\n");
};
emitline("\tMOVQ\t");
emitoff(off: i64);
emitline("(BP), AX\n");
emitline("\tPUSHQ\tAX\n");
let nw: i32 = 1;
if (stsz > 8) { nw = 2; };
return rest + nw;
};
};
// #150: a module-global by-value struct arg. localfindnode
// above misses it (a global is not a frame slot), and the #271
// arm below excludes a ≤16B struct ident (structident=true), so
// pre-fix it fell through to the scalar single-PUSHQ default and
// silently dropped word1. cstage's mirror-opposite bug read the
// FRAME (localfind→0 off==0 footgun); both stages converge here
// on the global-base load: LEAQ name(SB) into BX, then copy ALL
// eightbytes high→low (the #256/#129-A.2 struct-global shape; the
// isletvar||deflookup gate is the let_islet||def_isstructdef twin
// from dotchainaddr). The slot-word count is the stamped tinfo
// size (the type table, byte-id with cstage struct_arg_size).
if (lc == nil) {
let gst: *syntax.tinfo = arg.type_: *syntax.tinfo;
gst = tichase(gst);
if (gst != nil) { if (gst.kind == syntax.tykind.TY_STRUCT) {
let gsz: i32 = gst.size: i32;
if (gsz > 0 && gsz <= 16
&& (isletvar(c, nm) || deflookup(c, nm))) {
emitline("\tLEAQ\t");
emitsymname(c, nm);
emitline("(SB), BX\n");
if (gsz > 8) {
emitline("\tMOVQ\t8(BX), AX\n");
emitline("\tPUSHQ\tAX\n");
};
emitline("\tMOVQ\t(BX), AX\n");
emitline("\tPUSHQ\tAX\n");
let gnw: i32 = 1;
if (gsz > 8) { gnw = 2; };
return rest + gnw;
};
}; };
// #151: a module-global by-value slice/str arg — the
// slice/str twin of the #150 struct arm above. wwstage's
// nodeisslice/nodeisstr (below) are LOCAL-keyed
// (localfindnode→nil for a global) so a global slice/str
// ident returned false there and fell to the scalar
// single-PUSHQ default, dropping len+cap. cstage is
// type-keyed (node_isslice/node_isstr on n->type) so it
// pushed all 3 header words. cstage emits a DIFFERENT
// per-type sequence — mirror EACH for byte-id: a slice via
// its dedicated push arm (BX-base per-word, cgen.c:9124); a
// str falls to cgexpr's cgslicehdr (CX-base AX/BX/CX,
// cgen.c:1866) then the node_isstr triple push.
// LET-only (NOT deflookup, unlike the #150 struct arm):
// cstage's slice arm gates let_islet (cgen.c:1869) and a
// def-str is const-folded by cgexpr (LEAQ _S_0, MOVQ
// $len) — it has no name(SB) holder. A deflookup here
// would LEAQ an undefined main.<def>(SB) (w6l fails); a
// def must fall through to the const-fold path instead.
if (gst != nil && isletvar(c, nm)) {
if (gst.kind == syntax.tykind.TY_SLICE) {
emitline("\tLEAQ\t");
emitsymname(c, nm);
emitline("(SB), BX\n");
emitline("\tMOVQ\t16(BX), AX\n");
emitline("\tPUSHQ\tAX\n");
emitline("\tMOVQ\t8(BX), AX\n");
emitline("\tPUSHQ\tAX\n");
emitline("\tMOVQ\t(BX), AX\n");
emitline("\tPUSHQ\tAX\n");
return rest + 3;
};
if (gst.kind == syntax.tykind.TY_STR) {
emitline("\tLEAQ\t");
emitsymname(c, nm);
emitline("(SB), CX\n");
emitline("\tMOVQ\t(CX), AX\n");
emitline("\tMOVQ\t8(CX), BX\n");
emitline("\tMOVQ\t16(CX), CX\n");
emitline("\tPUSHQ\tCX\n");
emitline("\tPUSHQ\tBX\n");
emitline("\tPUSHQ\tAX\n");
return rest + 3;
};
};
};
};
// #271: aggregate (struct/array) arg from any source the ≤16B
// struct-IDENT fast path above doesn't cover — a 16B struct from a
// non-ident source, OR any array, OR a struct > 16B. The arg twin
// of the #265/#268 let-init copy (mirror of cstage cgen.c #271 push
// arm): materialise the source ADDRESS in SI and push its ceil(sz/8)
// words high→low (the pop drains word0 into the first arg reg). A
// CALL source receives first — ≤24B in AX/DX/CX pushed straight,
// >24B sret'd into @aggargscr then pushed from there. Pre-fix every
// such source fell to the scalar default (one PUSHQ for a multi-word
// aggregate) and stack-imbalanced against the type-based drain.
let aggsz: i32 = aggargsizetn(arg.type_: *syntax.tinfo);
if (aggsz > 0) {
// Exclude a ≤16B-struct IDENT — it owns the structabisizetn
// fast path above. That path is now TYPE-keyed (#21/#224): a
// cross-module same-leaf collision no longer misses (the prior
// name-keyed structparamsize returned 0 → the arg dropped to the
// scalar default → word1 lost; #784/#223). The exclusion is
// TYPE-keyed via the stamped tinfo, mirroring cstage
// node_isstructarg (struct_arg_size on args[i]->type) — a
// name-keyed gate here re-opens the #211/#13 name-keyed
// divergence the cstage type gate doesn't have.
let structident: bool = false;
if (arg.kind == syntax.nkind.N_IDENT) {
let st: *syntax.tinfo = arg.type_: *syntax.tinfo;
st = tichase(st);
if (st != nil) { if (st.kind == syntax.tykind.TY_STRUCT) {
if (st.size: i32 <= 16) { structident = true; };
}; };
};
if (!structident) {
if (aggargfloatstop(arg)) {
let msg: str = "#271/#165: float-bearing struct arg from a non-ident source needs SSE eightbyte transport (out of scope)\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
let nwords: i32 = (aggsz + 7) / 8;
if (arg.kind == syntax.nkind.N_CALL) {
if (callsretsize(c, arg) > 0) {
let scr: i32 = localadd(c, "@aggargscr",
aggsz, nil);
c.sretdestoff = scr;
cgexpr(c, arg);
c.sretdestoff = 0;
let k: i32 = nwords - 1;
for (k >= 0) {
emitline("\tMOVQ\t");
emitoff((scr + k*8): i64);
emitline("(BP), AX\n");
emitline("\tPUSHQ\tAX\n");
k -= 1;
};
} else {
// ≤24B: producer left AX=word0,
// DX=word1, CX=word2. Push high→low so
// the pop drains word0 first.
cgexpr(c, arg);
let k: i32 = nwords - 1;
for (k >= 0) {
if (k == 2) {
emitline("\tPUSHQ\tCX\n");
} else { if (k == 1) {
emitline("\tPUSHQ\tDX\n");
} else {
emitline("\tPUSHQ\tAX\n");
}; };
k -= 1;
};
};
return rest + nwords;
};
if (!aggargsrcaddr(c, arg, "SI")) {
let msg: str = "#271: aggregate arg from unsupported source kind\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
let k: i32 = nwords - 1;
for (k >= 0) {
emitline("\tMOVQ\t");
emitoff((k*8): i64);
emitline("(SI), AX\n");
emitline("\tPUSHQ\tAX\n");
k -= 1;
};
return rest + nwords;
};
};
// Float arg: cgexpr leaves the value in X0. Push 8 bytes from
// X0 via SUBQ+MOVSD so cgcall's pop side can drain into the
// XMM stream (X0..X7). f32 still occupies 8B on the stack —
// the MOVSS load on the pop side touches only the low 4.
let fk: i32 = 0;
if (arg != nil) {
let at: *syntax.tinfo = arg.type_: *syntax.tinfo;
if (syntax.typeisf32(at)) { fk = 1; }
else { if (syntax.typeisfloat(at)) { fk = 2; }; };
};
if (fk != 0) {
cgexpr(c, arg);
let mov: str = "MOVSD";
if (fk == 1) { mov = "MOVSS"; };
// #14: an f32 in the C-variadic tail widens to f64 (C default
// argument promotion) — CVTSS2SD in X0, then MOVSD spills a
// full 8B double; the pop reloads MOVSD and counts it as one
// SSE reg. Mirror cstage cmd/w6c/cgen.c promote_f32 push.
if (cvarnfixed >= 0 && argidx >= cvarnfixed && fk == 1) {
emitline("\tCVTSS2SD\tX0, X0\n");
mov = "MOVSD";
};
emitline("\tSUBQ\t$8, SP\n");
emitline("\t");
emitline(mov);
emitline("\tX0, (SP)\n");
return rest + 1;
};
// #68: a tuple-LITERAL arg derives its DECLARED tuple type from the
// callee PARAM type node (the #57 decl wire, extended to call-arg
// send), so a declared-tagged element's concrete rvalue widens into
// the box cursor; the restage + push then key on the param tuple type
// node (declared element widths), not the literal's element-
// constructed types. Mirrors cstage cgcall paramtup.
let paramtt: *syntax.node = nil;
if (arg.kind == syntax.nkind.N_TUPLE) { if (param != nil) {
if (param.kind == syntax.nkind.N_PARAM && param.lhs != nil) {
let ptn: *syntax.node = param.lhs;
for (ptn != nil && ptn.kind == syntax.nkind.N_TNAME) {
ptn = aliaslookup(c, ptn.str);
};
if (ptn != nil) { if (ptn.kind == syntax.nkind.N_TTUPLE) { paramtt = ptn; }; };
};
}; };
if (paramtt != nil) { cgtuplelittocursor(c, arg, paramtt); }
else { cgexpr(c, arg); };
// #163/#32 (C-t2): tuple ARG (param twin of #164's return). cgexpr /
// the decl-aware fill left the tuple in the return-ABI cursor (AX/DX/
// CX/R8 + X0/X1) for every nodetuplearg producer — call (return ABI),
// ident (cgtupleslottocursor), literal (cgtuplelittocursor), `?`/`!`
// unwrap (payload shift); restage it into @tupargscr by SysV class
// (tupstore, the #164 helper) and push the slot words high->low so
// the pop drains slot+0 first into the SysV ARG cursor. The frame
// slot decouples the return-class regs from the overlapping
// arg-class regs. When the PARAM tuple type is known (#68), walk its
// declared element type nodes (p.lhs); else an N_TUPLE literal's
// elements are VALUE exprs classified the way cgtuplelittocursor does.
let tuparg: *syntax.node = paramtt;
if (tuparg == nil) { tuparg = nodetuplearg(c, arg); };
if (tuparg != nil) {
let tuplit: bool = false;
if (paramtt == nil) { if (tuparg.kind == syntax.nkind.N_TUPLE) { tuplit = true; }; };
let gptot: i32 = 0;
let sstot: i32 = 0;
let tsz: i32 = 0;
let p: *syntax.node = tuparg.list;
for (p != nil) {
let et: *syntax.node = p.lhs;
if (tuplit) { et = p; };
// C-t2 (ken demand 1, rule 7): a COMPOSITE element
// (nested tuple/struct/array/tagged) occupies more
// than the one GP word this walk counts — the checker
// accepts the shape but the cursor transport cannot
// carry it; pre-guard it ran WRONG (inner words
// skewed). The stamped tinfo classifies both type
// nodes and literal value exprs. Mirrors the cstage
// restage guard.
let eti: *syntax.tinfo = et.type_: *syntax.tinfo;
eti = tichase(eti);
if (eti != nil) {
let bad: bool = eti.kind == syntax.tykind.TY_TUPLE
|| eti.kind == syntax.tykind.TY_STRUCT
|| eti.kind == syntax.tykind.TY_ARRAY;
// #68: a declared-tagged element graduates to a real
// widen — the decl-aware send (cgtuplelittocursor over
// the param tuple type) left the box words in the
// cursor, so tupstore below carries them. A tagged
// element with no param decl stays rule-7 loud (the
// cursor was filled stamped-keyed).
if (eti.kind == syntax.tykind.TY_TAGGED && paramtt == nil) { bad = true; };
if (bad) {
let mne: str = "#32: tuple arg element kind unsupported (nested tuple/struct/array/tagged; rule 7)\n";
os.write(2, mne.ptr, mne.len: u64);
os.exit(1);
};
};
// eslot — the full slot stride (str/slice header, tagged
// box, else 8); on the declared (non-tuplit) path tupeslotn
// reads the element TYPE node directly (#68 box-aware,
// mirrors cstage tuple_eslot). A literal element rides the
// wide-vs-scalar split off its VALUE node.
let eslot: i32 = 8;
if (tuplit) {
let wide: bool = nodeisstr(c, et) || nodeisslice(c, et);
if (wide) { eslot = tyslicesize(): i32; };
} else {
eslot = tupeslotn(et);
};
if (isfloattype(c, et)) { sstot += 1; }
else { gptot += eslot / 8; };
tsz += eslot;
p = p.next;
};
// The producing cursor fill already satisfied #164's caps;
// guard anyway (tupstore indexes [AX,DX,CX,R8] / [X0,X1]).
if (gptot > TUPLE_GPCAP || sstot > TUPLE_SSECAP) {
let msg: str = "tuple arg exceeds return-cursor ABI capacity; see #163/#164\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
let scr: i32 = localadd(c, "@tupargscr", tsz, nil);
let gpcur: i32 = 0;
let ssecur: i32 = 0;
let eoff: i32 = 0;
p = tuparg.list;
for (p != nil) {
let et: *syntax.node = p.lhs;
if (tuplit) { et = p; };
let eslot: i32 = 8;
if (tuplit) {
let wide: bool = nodeisstr(c, et) || nodeisslice(c, et);
if (wide) { eslot = tyslicesize(): i32; };
} else {
eslot = tupeslotn(et);
};
tupstore(c, gpcur, ssecur, scr + eoff, eslot, et);
if (isfloattype(c, et)) { ssecur += 1; }
else { gpcur += eslot / 8; };
eoff += eslot;
p = p.next;
};
let w: i32 = tsz - 8;
for (w >= 0) {
emitline("\tMOVQ\t");
emitoff((scr + w): i64);
emitline("(BP), AX\n");
emitline("\tPUSHQ\tAX\n");
w -= 8;
};
return rest + tsz / 8;
};
// #32 (C-t2, rule 7): a tuple-typed arg from a source shape whose
// cgexpr does NOT fill the return cursor (chain reads, match exprs,
// ...) must die loud here — pre-fix it fell to the scalar
// single-PUSHQ default and silently skewed every later arg
// register. Mirrors the cstage cgcall guard.
{
let ati: *syntax.tinfo = arg.type_: *syntax.tinfo;
ati = tichase(ati);
if (ati != nil) {
if (ati.kind == syntax.tykind.TY_TUPLE) {
let m32: str = "#32: tuple arg from unsupported source shape (call/ident/literal/unwrap only; rule 7)\n";
os.write(2, m32.ptr, m32.len: u64);
os.exit(1);
};
};
};
if (nodeisslice(c, arg)) {
emitline("\tPUSHQ\tCX\n");
emitline("\tPUSHQ\tBX\n");
emitline("\tPUSHQ\tAX\n");
return rest + 3;
};
if (nodeisstr(c, arg)) {
// str IS []u8: cgexpr left (AX=ptr, BX=len, CX=cap). Push
// the triple, same as the slice arm above (#1/Phase 3).
emitline("\tPUSHQ\tCX\n");
emitline("\tPUSHQ\tBX\n");
emitline("\tPUSHQ\tAX\n");
return rest + 3;
};
// #21: CALL returning a tagged-union — the aistagged guard
// above kept us out of the widening path. Push the tagged-
// return ABI registers (AX=tag, DX=word0, CX=word1, R8=word2)
// high → low so the left-to-right POPQ into argregs drains the
// tag first. Mirrors cstage at cmd/w6c/cgen.c:4373-4387.
let tcs: i32 = taggedcallslot(c, arg);
if (tcs > 0) {
// #38b residual (rule 7): an sret-class call result is in
// memory, not the cursor — the @aggargscr-style receive-then-
// push is the #40-family follow-up. Mirrors cstage cgen.c
// cgcall tagged arg-push gate.
if (callsretsize(c, arg) > 0) {
let m38r: str = "#38b: >32B tagged call result as a call argument unwired (#40-family follow-up)\n";
os.write(2, m38r.ptr, m38r.len: u64);
os.exit(1);
};
if (tcs > 24) { emitline("\tPUSHQ\tR8\n"); };
if (tcs > 16) { emitline("\tPUSHQ\tCX\n"); };
if (tcs > 8) { emitline("\tPUSHQ\tDX\n"); };
emitline("\tPUSHQ\tAX\n");
return rest + tcs / 8;
};
// #12: N_INDEX of a sum-typed slice element. cgindex above left
// the tagged-CALL ABI in AX/DX/CX/R8; the bare PUSHQ AX below
// would only carry the tag word and drop the payload. #60:
// arg.type_ is the element tinfo (istaggedtype/slotsize read
// .type_) — feed the N_INDEX node directly, dropping the
// indexvaluetnode walk.
// #22a: N_DOT rides the same arm — the t.N tuple-element box load
// (this arc) fills the identical AX/DX/CX/R8 cursor. cstage's twin
// is the generic node_istaggedarg push (cgen.c cgcall); wwstage
// keeps the stamped-carrier kind gate (#67 pattern) — the
// remaining kinds (deref/cast/unwrap) are word0-only reads today,
// filed residual.
if (arg.kind == syntax.nkind.N_INDEX || arg.kind == syntax.nkind.N_DOT
|| (arg.kind == syntax.nkind.N_UN && arg.op == syntax.tkind.TK_STAR)) {
if (istaggedtype(c, arg)) {
let isz: i32 = slotsize(c, arg);
// #35 (Family C): a mem-based read left the box
// ADDRESS in AX — push the words from memory
// high→low, the mem twin of the cursor push below.
// Covers the any-size deref source and the 33-48B
// INDEX/DOT reads that loud-stopped here pre-#35.
// Mirrors cstage.
if (taggedmemread(c, arg)) {
let mk35: i32 = isz - 8;
for (mk35 >= 0) {
emitline("\tMOVQ\t");
emitdispreg(mk35: i64, "AX");
emitline(", DX\n");
emitline("\tPUSHQ\tDX\n");
mk35 -= 8;
};
return rest + isz / 8;
};
if (isz > 24) { emitline("\tPUSHQ\tR8\n"); };
if (isz > 16) { emitline("\tPUSHQ\tCX\n"); };
if (isz > 8) { emitline("\tPUSHQ\tDX\n"); };
emitline("\tPUSHQ\tAX\n");
return rest + isz / 8;
};
};
emitline("\tPUSHQ\tAX\n");
return rest + 1;
};
// taggedcallslot — if `n` is an N_CALL whose callee returns a tagged
// type, returns the slot size in bytes; else 0. Used by pushargsrev's
// aistagged guard and natural-push arm, and by cgcall's pop sizer, to
// route a tagged-return call result through the AX/DX/CX/R8 high→low
// push convention rather than the concrete-variant widening path
// (which drops DX/CX/R8). See task #21.
export fn taggedcallslot(c: *cgen, n: *syntax.node) i32 = {
if (n == nil) { return 0; };
if (n.kind != syntax.nkind.N_CALL) { return 0; };
let callee: *syntax.node = n.lhs;
if (callee == nil) { return 0; };
if (callee.kind != syntax.nkind.N_IDENT) { return 0; };
let rtyp: *syntax.node = fnretlookup(c, callee.str);
if (!istaggedtype(c, rtyp)) { return 0; };
return slotsize(c, rtyp);
};
fn nodeisslice(c: *cgen, n: *syntax.node) bool = {
if (n == nil) { return false; };
let k: syntax.nkind = n.kind;
if (k == syntax.nkind.N_IDENT) {
let nm: str = n.str;
let lc: *local = localfindnode(c, nm);
// F7-c1: the local read collapses onto the checker-stamped
// n.type_, the same shape cstage's node_isslice uses
// (cmd/w6c/cgen.c:182 type_isslice(n->type)). lc.tnode.type_
// (what isslicetype read) and n.type_ are the same tinfo for a
// local ident (exprtype N_IDENT stamps n.type_ off the same
// decl localfindnode keys on); the localfindnode gate stays to
// keep the global-var-not-def case routing to false (out of F7
// scope), so this is a zero-delta mechanic validation.
if (lc != nil) { return syntax.typeisslice(n.type_: *syntax.tinfo); };
// #21: a module-level `def g: []T` ident is not a frame
// slot (localfindnode→nil), so the local arm above misses
// it and it would fall to the scalar single-PUSHQ default,
// dropping len/cap. cstage's node_isslice is type-keyed
// (cmd/w6c/cgen.c:226-229 type_isslice(n->type)); align
// wwstage UP by reading the checker-stamped .type_ (the def
// decl's str/slice type node, check.ww exprtype N_IDENT
// arm). A def has no DATA symbol — it rides cgident's
// const-fold, so the push site and cgcall's pop sizer flip
// together on this shared recognizer (load-bearing).
if (deflookup(c, nm)) {
return syntax.typeisslice(n.type_: *syntax.tinfo);
};
// #142-adjacent: module-global let/const slice ident — the
// str twin's rationale verbatim (see nodeisstr N_IDENT).
if (isletvar(c, nm)) {
return syntax.typeisslice(n.type_: *syntax.tinfo);
};
return false;
};
if (k == syntax.nkind.N_SLICE) { return true; };
if (k == syntax.nkind.N_CAST) { return isslicetype(c, n.rhs); };
// #24: N_CALL returning a slice — cgexpr leaves (AX=ptr,
// BX=len, CX=cap); pushargsrev's slice arm pushes CX/BX/AX
// and cgcall pops 3 words. Without this arm the natural-push
// fallthrough emits one PUSHQ AX (loses .len/.cap) and the pop
// side under-drains by 2 words, leaving R8/R9 unset for the
// receiver. Mirrors nodeisstr's N_CALL arm just below.
// N_DOT (cross-module callee, #34): route through fnretlookupmod
// so a same-leaf caller-module fn with diverging return shape
// doesn't shadow the explicit `mod.f()` qualifier — surfaced by
// strings.slice returning `frombytes(utf8.slice(...))`
// where strings.slice itself returns str.
if (k == syntax.nkind.N_CALL) {
let callee: *syntax.node = n.lhs;
if (callee != nil) {
if (callee.kind == syntax.nkind.N_IDENT) {
let rtyp: *syntax.node = fnretlookupmod(c, callee.str, c.curmod);
return isslicetype(c, rtyp);
};
if (callee.kind == syntax.nkind.N_DOT) {
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.lhs != nil) {
if (callee.lhs.kind == syntax.nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
let rtyp: *syntax.node = fnretlookupmod(c, callee.str, cmod);
return isslicetype(c, rtyp);
};
};
// #59.5: a callee that is neither N_IDENT nor N_DOT — an
// indirect call through a fn-pointer VALUE (`(*f)()`,
// N_UN TK_STAR) — used to fall to false, so a slice return
// pushed one word and cgcall's pop sizer under-drained.
// Read the checker stamp (check.ww fn-VALUE callee arm),
// the callee-shape-blind key cstage node_isslice uses.
return syntax.typeisslice(n.type_: *syntax.tinfo);
};
// N_DOT: read the checker-stamped n.type_. Struct field, nested
// dot, value-struct hops, and pseudo-fields (.ptr/.len/.cap) all
// resolve to the right tinfo via check.ww:1947-1978 (pseudo-field
// + struct-field stamps). Cstage cgen.c:182-184 node_isslice =
// type_isslice(n->type) — same shape. Collapsed per A.6.3h (#56).
if (k == syntax.nkind.N_DOT) {
return syntax.typeisslice(n.type_: *syntax.tinfo);
};
// #45 (F7-c2): `arr[i]` whose element is a slice. cgindex leaves
// (AX=ptr, BX=len, CX=cap) for a 24B element, but with no N_INDEX
// arm here pushargsrev fell to the scalar default — one PUSHQ AX —
// and the cgcall pop under-drained by 2 words (take(rows[1]) pushed
// 1 word, callee read garbage len). Read the checker-stamped element
// type (exprtype N_INDEX stamps n.type_, check.ww:2777), mirroring
// cstage node_isslice = type_isslice(n->type) and the N_INDEX arms of
// nodeisstr (below) + nodeisunsigned (:1798).
if (k == syntax.nkind.N_INDEX) {
return syntax.typeisslice(n.type_: *syntax.tinfo);
};
// #9 (C1c): `*h` where h:*[]T — an N_UN TK_STAR deref whose pointee is
// a slice. C1b (c67f362) fixed the 24B header LOAD; this arm fixes the
// call-arg push COUNT (pushargsrev's slice arm :1194 and cgcall's pop
// sizer cgenexpr.ww:8098 both key off this recognizer). Without it the
// deref fell through to the scalar single-PUSHQ default, marshalling
// .ptr and dropping .len/.cap. Read the checker-stamped n.type_
// (unoptype TK_STAR returns the pointee, check.ww:2967-2981), mirroring
// cstage node_isslice = type_isslice(n->type) (cmd/w6c/cgen.c:226-228)
// and the N_DOT/N_INDEX arms above.
if (k == syntax.nkind.N_UN && n.op == syntax.tkind.TK_STAR) {
return syntax.typeisslice(n.type_: *syntax.tinfo);
};
// #6 (Mech A): an unwrap source (`f()!` N_TRYUNW, `r!`/`r?` N_TRYPROP)
// whose success variant is a slice. Without this arm the unwrap fell
// through to the scalar single-PUSHQ default, marshalling .ptr and
// dropping .len/.cap (cgcall's pop sizer under-drains). n.type_ is
// checker-stamped to the success variant (check.ww N_TRYPROP/N_TRYUNW),
// mirroring cstage node_isslice = type_isslice(n->type). Twin of the
// N_UN(TK_STAR) #9 arm above.
if (k == syntax.nkind.N_TRYUNW || k == syntax.nkind.N_TRYPROP) {
return syntax.typeisslice(n.type_: *syntax.tinfo);
};
return false;
};
// nodeisstr — best-effort surface check: does this expression
// evaluate to a str value? Used to drive the call-arg push convention
// (str args take two slots: ptr + len).
//
// The value-bearing arms (N_IDENT local, N_INDEX, N_DOT) read the
// checker-stamped n.type_ — the typed-AST check the prior TODO(#11)
// wanted, mirroring cstage node_isstr = type_isstr(n->type). The
// remaining N_kind arms (N_STRLIT, N_CALL, N_CAST) carry their own
// recognizer because the stamp is on a sub-node (callee return / cast
// target), not on `n` itself. Covered: N_STRLIT, N_IDENT (local stamp /
// def stamp), N_CALL (return type), N_INDEX (element stamp — #46 F7-c2),
// N_DOT (n.type_ — #56 A.6.3h), N_CAST, N_UN(TK_STAR) (#9 C1c).
fn nodeisstr(c: *cgen, n: *syntax.node) bool = {
if (n == nil) { return false; };
let k: syntax.nkind = n.kind;
if (k == syntax.nkind.N_STRLIT) { return true; };
if (k == syntax.nkind.N_IDENT) {
let nm: str = n.str;
let lc: *local = localfindnode(c, nm);
// F7-c1: the local read collapses onto the checker-stamped
// n.type_, the same shape cstage's node_isstr uses (cmd/w6c/
// cgen.c:213 type_isstr(n->type)). typeisstr chases TY_NAMED so
// `!str` aliases (parserr = !str) and `type foo = str;` chains
// resolve through exactly as isstrtype(lc.tnode) did — n.type_
// and lc.tnode.type_ are the same tinfo for a local ident
// (exprtype N_IDENT stamps n.type_ off the same decl
// localfindnode keys on). The localfindnode gate stays to keep
// the global-var-not-def case routing to false (out of F7
// scope), so this is a zero-delta mechanic validation.
if (lc != nil) { return syntax.typeisstr(n.type_: *syntax.tinfo); };
// #21: a module-level `def s: str` ident is not a frame slot
// (localfindnode→nil), so the local arm above misses it and
// it would fall to the scalar single-PUSHQ default, dropping
// len/cap. cstage's node_isstr is type-keyed (cmd/w6c/
// cgen.c:213-216 type_isstr(n->type)); align wwstage UP by
// reading the checker-stamped .type_ (the def decl's str type
// node, check.ww exprtype N_IDENT arm). A def has no DATA
// symbol — it rides cgident's const-fold (LEAQ _S_n, MOVQ
// $len), so the push site and cgcall's pop sizer flip together
// on this shared recognizer (load-bearing).
if (deflookup(c, nm)) {
return syntax.typeisstr(n.type_: *syntax.tinfo);
};
// #142-adjacent: a module-global let/const str ident (path's
// `const sepstr: str`) is neither a frame slot nor a def; the
// false fall-through sent its union-widen push to the scalar
// arm (len/cap zero-filled). Read the checker stamp, the key
// cstage node_isstr uses unconditionally.
if (isletvar(c, nm)) {
return syntax.typeisstr(n.type_: *syntax.tinfo);
};
return false;
};
if (k == syntax.nkind.N_CALL) {
let callee: *syntax.node = n.lhs;
if (callee != nil) {
if (callee.kind == syntax.nkind.N_IDENT) {
let rtyp: *syntax.node = fnretlookupmod(c, callee.str, c.curmod);
return isstrtype(c, rtyp);
};
// #34: cross-module N_DOT — route through fnretlookupmod
// so a same-leaf caller-module fn (different return shape)
// doesn't shadow the explicit qualifier.
if (callee.kind == syntax.nkind.N_DOT) {
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.lhs != nil) {
if (callee.lhs.kind == syntax.nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
let rtyp: *syntax.node = fnretlookupmod(c, callee.str, cmod);
return isstrtype(c, rtyp);
};
};
// #59.5: indirect fn-pointer callee (`(*f)()`) — read the
// checker stamp; see the nodeisslice N_CALL twin above.
return syntax.typeisstr(n.type_: *syntax.tinfo);
};
// #46 (F7-c2): `arr[i]` whose element is a str. The prior structural
// walk only recognised N_IDENT and N_DOT bases (idxelemtn off the
// base's tnode), so a chained / call / slice base (take(m[1][1]))
// fell through to `return false` → 1-word push, callee read garbage
// .len. Read the checker-stamped element type instead (exprtype
// N_INDEX stamps n.type_, check.ww:2777), mirroring cstage node_isstr
// = type_isstr(n->type) and nodeisunsigned's N_INDEX arm (:1798). The
// base-kind whitelist is gone — every base shape routes through the
// one stamp read.
if (k == syntax.nkind.N_INDEX) {
return syntax.typeisstr(n.type_: *syntax.tinfo);
};
// N_DOT: read the checker-stamped n.type_. Struct field, nested
// dot, value-struct hops, and pseudo-fields (.ptr/.len/.cap) all
// resolve to the right tinfo via check.ww:1947-1978. Cstage
// cgen.c:168-170 node_isstr = type_isstr(n->type) — same shape.
// Collapsed per A.6.3h (#56).
if (k == syntax.nkind.N_DOT) {
return syntax.typeisstr(n.type_: *syntax.tinfo);
};
if (k == syntax.nkind.N_CAST) {
return isstrtype(c, n.rhs);
};
// #9 (C1c): `*sp` where sp:*str — the str twin of the N_UN TK_STAR
// slice arm in nodeisslice. The same recognizer drives the call-arg
// push count and cgcall's pop sizer; reading the checker-stamped
// n.type_ (pointee per unoptype TK_STAR, check.ww:2967-2981) mirrors
// cstage node_isstr = type_isstr(n->type) (cmd/w6c/cgen.c:213-216).
if (k == syntax.nkind.N_UN && n.op == syntax.tkind.TK_STAR) {
return syntax.typeisstr(n.type_: *syntax.tinfo);
};
// #6 (Mech A): str twin of the N_TRYUNW/N_TRYPROP slice arm in
// nodeisslice — an unwrap source whose success variant is a str.
// n.type_ is checker-stamped to the success variant; mirrors cstage
// node_isstr = type_isstr(n->type).
if (k == syntax.nkind.N_TRYUNW || k == syntax.nkind.N_TRYPROP) {
return syntax.typeisstr(n.type_: *syntax.tinfo);
};
return false;
};
// typeis8byteprimitive — does this type take exactly one 8-byte
// slot rather than a wider aggregate? One-liner via typeis8byteprim
// (cstage N_LET sz==8 ladder SSoT). t.type_ is stamped at check.ww
// L426-436 for every type-AST kind callers reach. Collapsed onto the
// tinfo helper per A.6.3b (#46).
fn typeis8byteprimitive(c: *cgen, t: *syntax.node) bool = {
if (t == nil) { return false; };
return syntax.typeis8byteprim(t.type_: *syntax.tinfo);
};
// elemissignedc — given an indexable type-AST (`*T`, `[]T`, `[N]T`),
// is its element a signed narrow primitive? Used by cgindex to pick
// MOVSXD vs MOVL at esz=4 (and MOVSBQ/MOVSWQ at esz=1/2). Mirrors
// cstage's `signed_elem` (cmd/w6c/cgen.c idx_eff path). Reads through
// the stamped tinfo so alias/enum recursion lives in lib/ww/typ.ww.
fn elemissignedc(c: *cgen, t: *syntax.node) bool = {
if (t == nil) { return false; };
// #65 Phase-N step-2 cleanup: peel TY_NAMED before the .sub read.
// #64 now flows per-decl NAMED wrappers, so a NAMED-of-(`*T`/`[]T`/
// `[N]T`) reaching here would read NAMED.sub (nil) instead of the
// element. Mirrors cstage idx_eff's type_unwrap (cmd/w6c/cgen.c:790)
// before the eff->sub read (:3518-3520). Byte-id-neutral: every
// aliased indexable in-tree has a u8 element (typeissigned=false
// either way). Transitive peel matches the #63 idiom.
// idxeffti additionally drills `*[N]T` to the pointee array (#61)
// so a signed-narrow element behind a pointer-to-array still
// sign-extends on load.
let ti: *syntax.tinfo = idxeffti(t.type_: *syntax.tinfo);
if (ti == nil) { return false; };
return syntax.typeissigned(ti.sub);
};
// elemisfloatc — given an indexable type-AST (`*T`, `[]T`, `[N]T`), is
// its element an f32/f64? Used by cgindex to route the element load to
// MOVSS/MOVSD into X0 instead of the integer loadopsz into AX (#119 —
// the array-element twin of the scalar-float global load at cgen.c:
// 2014). Reads through the stamped tinfo, peeling TY_NAMED before the
// .sub read exactly as elemissignedc does (#64/#65). Float-ness comes
// from the SAME tinfo the esz already reads — never a fresh node-stamp
// (the unstamped-base trap that broke the exprfloatkind collapse, #121).
fn elemisfloatc(c: *cgen, t: *syntax.node) bool = {
if (t == nil) { return false; };
// idxeffti = TY_NAMED peel + the `*[N]T` drill (#61).
let ti: *syntax.tinfo = idxeffti(t.type_: *syntax.tinfo);
if (ti == nil) { return false; };
return syntax.typeisfloat(ti.sub);
};
// elemisf32c — narrower elemisfloatc: true only when the element is f32,
// so cgindex picks MOVSS over MOVSD at the #119 element load.
fn elemisf32c(c: *cgen, t: *syntax.node) bool = {
if (t == nil) { return false; };
// idxeffti = TY_NAMED peel + the `*[N]T` drill (#61).
let ti: *syntax.tinfo = idxeffti(t.type_: *syntax.tinfo);
if (ti == nil) { return false; };
return syntax.typeisf32(ti.sub);
};
// elemisarrayc — given an indexable type-AST (`*T` / `[]T` / `[N]T`), is
// its element itself an array (`[N][M]T` → element `[M]T`)? cgindex then
// leaves the sub-array's ADDRESS in the result reg rather than
// dereferencing — a nested index adds its offset and only the final
// scalar element dereferences (#156, sister of #135 N_DOT-base-on-
// array-field). Node-based with the `*[N]T` drill-through, mirroring
// elemsizeof (:920-948) so elem-is-array aligns with the esz this same
// tnode feeds. cstage twin: idx_eff(bt)->sub unwrapped == TY_ARRAY
// (cmd/w6c/cgen.c). `c` kept for signature symmetry with elemisfloatc.
fn elemisarrayc(c: *cgen, t: *syntax.node) bool = {
if (t == nil) { return false; };
let k: syntax.nkind = t.kind;
let elem: *syntax.node = nil;
if (k == syntax.nkind.N_TPTR) { elem = t.lhs; };
if (k == syntax.nkind.N_TSLICE) { elem = t.lhs; };
if (k == syntax.nkind.N_TARRAY) { elem = t.lhs; };
if (elem == nil) { return false; };
if (k == syntax.nkind.N_TPTR) {
if (elem.kind == syntax.nkind.N_TARRAY) {
if (elem.lhs != nil) { elem = elem.lhs; };
};
};
return elem.kind == syntax.nkind.N_TARRAY;
};
// tichase is the wwstage twin of cstage type_chase_named
// (cmd/wcc/type.c:160-162, alias arc #5). Alias chains stack TY_NAMED
// layers, so consumers that need structural shape must resolve the whole
// chain or risk the scalar fallback (#60's esz=1/pointer-base family).
fn tichase(t0: *syntax.tinfo) *syntax.tinfo = {
let t: *syntax.tinfo = t0;
for (t != nil && t.kind == syntax.tykind.TY_NAMED) { t = t.under; };
return t;
};
// tinfoisarray — TY_ARRAY (NAMED-aware), the tinfo-keyed companion to
// elemisarrayc for cgindex's N_DOT/N_INDEX base branches, where the
// element type comes from n.type_ (stamped tinfo) not a tnode. Same
// role as typeisslice/typeisstr in lib/ww/typ.ww; kept cgen-local to
// avoid widening the frontend surface for one #156 read-half check.
fn tinfoisarray(t: *syntax.tinfo) bool = {
let u: *syntax.tinfo = t;
u = tichase(u);
if (u == nil) { return false; };
return u.kind == syntax.tykind.TY_ARRAY;
};
// fieldissignedc — does this field/element type-AST need sign-
// extension on a sub-word load? One-liner via typeissigned (cstage
// cgen.c:240 `fld_issigned` SSoT). t.type_ is stamped at check.ww
// L426-436 for every type-AST kind we see here (TNAME / TPTR /
// TBANG / TENUM / TARRAY / TSLICE — see resolvewalk).
fn fieldissignedc(c: *cgen, t: *syntax.node) bool = {
if (t == nil) { return false; };
return syntax.typeissigned(t.type_: *syntax.tinfo);
};
// fieldloadop — pick the load instruction for a non-str struct
// field by its declared size + signedness. Mirrors cstage's
// fldloadop: MOVZBQ/MOVSBQ for 1B, MOVZWQ/MOVSWQ for 2B,
// MOVL/MOVSXD for 4B, MOVQ for 8B. f might be nil for fields
// outside our struct registry.
fn fieldloadop(c: *cgen, f: *fieldinfo) str = {
if (f == nil) { return "MOVQ"; };
let sz: i32 = f.fsz;
let sigd: bool = fieldissignedc(c, f.tnode);
if (sz == 1) { if (sigd) { return "MOVSBQ"; }; return "MOVZBQ"; };
if (sz == 2) { if (sigd) { return "MOVSWQ"; }; return "MOVZWQ"; };
if (sz == 4) { if (sigd) { return "MOVSXD"; }; return "MOVL"; };
return "MOVQ";
};
// fieldstoreop — pick the store instruction for a non-str struct
// field by its declared size. MOVB for 1, MOVW for 2, MOVL for 4,
// MOVQ for 8. c kept in the signature for symmetry with fieldloadop.
fn fieldstoreop(c: *cgen, f: *fieldinfo) str = {
if (f == nil) { return "MOVQ"; };
let sz: i32 = f.fsz;
if (sz == 1) { return "MOVB"; };
if (sz == 2) { return "MOVW"; };
if (sz == 4) { return "MOVL"; };
return "MOVQ";
};
// tnodeloadop / tnodestoreop — same dispatch as fieldloadop /
// fieldstoreop but keyed on a raw type-AST node (tuple element type,
// pointer-target, slice-element, etc.) rather than a struct fieldinfo.
// Used at the index / tuple / pointer-deref sites where there's no
// fieldinfo entry but the type-node + size are both known.
fn tnodeloadop(c: *cgen, t: *syntax.node, sz: i32) str = {
let sigd: bool = fieldissignedc(c, t);
if (sz == 1) { if (sigd) { return "MOVSBQ"; }; return "MOVZBQ"; };
if (sz == 2) { if (sigd) { return "MOVSWQ"; }; return "MOVZWQ"; };
if (sz == 4) { if (sigd) { return "MOVSXD"; }; return "MOVL"; };
return "MOVQ";
};
fn tnodestoreop(c: *cgen, t: *syntax.node, sz: i32) str = {
if (sz == 1) { return "MOVB"; };
if (sz == 2) { return "MOVW"; };
if (sz == 4) { return "MOVL"; };
return "MOVQ";
};
// loadopsz — load op when the (size, signedness) pair has already
// been resolved upstream and the type-node isn't carried through.
// cgindex precomputes `signed_elem` via elemissignedc; cgforrange
// precomputes `bind_signed[b]` via paramissigned. Same dispatch as
// tnodeloadop's tail; only the keying differs.
fn loadopsz(sigd: bool, sz: i32) str = {
if (sz == 1) { if (sigd) { return "MOVSBQ"; }; return "MOVZBQ"; };
if (sz == 2) { if (sigd) { return "MOVSWQ"; }; return "MOVZWQ"; };
if (sz == 4) { if (sigd) { return "MOVSXD"; }; return "MOVL"; };
return "MOVQ";
};
// storeopsz — store op when the size has already been resolved
// upstream off a stamped tinfo (the #31 receiver-layout store choke-
// point). Size-only twin of fieldstoreop's body {1->MOVB, 2->MOVW,
// 4->MOVL, else MOVQ}; the store side carries no signedness. Preserves
// the #15 sub-8 guard (a 2B tail field stores MOVW, not a slot-wide
// MOVQ into saved BP).
fn storeopsz(sz: i32) str = {
if (sz == 1) { return "MOVB"; };
if (sz == 2) { return "MOVW"; };
if (sz == 4) { return "MOVL"; };
return "MOVQ";
};
// localloadop — read instruction for a scalar local/let load. Same
// dispatch as fieldloadop, but keyed on the value's own tnode.type_.
// Lets the caller emit MOVSXD/MOVSWQ/MOVSBQ on a signed-narrow slot
// instead of a raw MOVQ, so a slot that was last written by a narrow
// deref-store (`*p: *i32 = v` lowers to MOVL, only 4B) reads back as
// a properly-sign-extended i64. The natural N_ASSIGN / N_LET paths
// store the rhs as a sign-extended 8B word, so MOVQ accidentally
// works; deref-stores are the only path that touches fewer bytes
// than MOVQ reads. Mirror of cstage's localloadop in cmd/w6c/cgen.c
// — tinfo.size carries the same numeric width cstage's `t->size`
// reports, with TBANG / TENUM / TNAME-alias chains pre-folded by
// tinfofornode (check.ww:1102-1153 TNAME, 1154-1161 TBANG,
// 1196-1208 TENUM).
export fn localloadop(c: *cgen, tnode: *syntax.node) str = {
if (tnode == nil) { return "MOVQ"; };
let ti: *syntax.tinfo = tnode.type_: *syntax.tinfo;
if (ti == nil) { return "MOVQ"; };
let sz: i32 = ti.size: i32;
if (sz != 1) { if (sz != 2) { if (sz != 4) { return "MOVQ"; }; }; };
let sigd: bool = syntax.typeissigned(ti);
return loadopsz(sigd, sz);
};
// idxeffti — element-effective tinfo for indexing. `*[N]T` auto-derefs
// at an index base, so its esz/element classification must come from
// the pointee ARRAY (stride T), not from the pointer (whose .sub is
// the whole [N]T — the #61 stride bug, N*size(T) off target per index
// step). One peel choke-point: every tinfo-keyed index-element answer
// (elemsizeofc / elemissignedc / elemisfloatc / elemisf32c) routes
// through here. Mirrors cstage idx_eff (cmd/w6c/cgen.c:1163).
fn idxeffti(t0: *syntax.tinfo) *syntax.tinfo = {
let t: *syntax.tinfo = t0;
t = tichase(t);
if (t != nil && t.kind == syntax.tykind.TY_PTR) {
let p: *syntax.tinfo = t.sub;
p = tichase(p);
if (p != nil && p.kind == syntax.tykind.TY_ARRAY) { return p; };
};
return t;
};
// idxelemtn — element type-NODE for an indexable base tnode, the
// node-keyed companion of idxeffti for the cgen arms that classify
// the element structurally (istaggedtype / isstrtype / isslicetype /
// isfloattype / tnodestoreop). Same `*[N]T` drill: the pointee array's
// OWN element, never the array (#61 — an undrilled elemtn made the
// store-width chooser believe the element IS `[N]T` and emit an
// N*8-byte aggregate copy from an 8B source: caller-frame smash).
// nil for non-indexable kinds (str N_TNAME: callers want nil so
// tnodestoreop falls to the u8 byte store).
fn idxelemtn(tn: *syntax.node) *syntax.node = {
if (tn == nil) { return nil; };
let k: syntax.nkind = tn.kind;
if (k != syntax.nkind.N_TARRAY && k != syntax.nkind.N_TSLICE
&& k != syntax.nkind.N_TPTR) { return nil; };
let elem: *syntax.node = tn.lhs;
if (k == syntax.nkind.N_TPTR && elem != nil
&& elem.kind == syntax.nkind.N_TARRAY) {
return elem.lhs;
};
return elem;
};
// elemsizeof — given the type node of an indexable (`*T`, `[]T`,
// `[N]T`, `str`), return the byte size of one element (1 for u8/i8/
// bool/str-byte, 8 otherwise — same shape as C cgen's esz fallback).
// For aliased element types (e.g. `[N]formattable`), callers that
// need the resolved slot size should use elemsizeofc(c, t) which
// follows aliases via slotsize.
fn elemsizeof(t: *syntax.node) i32 = {
if (t == nil) { return 1; };
let k: syntax.nkind = t.kind;
let elem: *syntax.node = nil;
if (k == syntax.nkind.N_TPTR) { elem = t.lhs; };
if (k == syntax.nkind.N_TSLICE) { elem = t.lhs; };
if (k == syntax.nkind.N_TARRAY) { elem = t.lhs; };
if (k == syntax.nkind.N_TNAME) {
let nm: str = t.str;
// str's element is u8 (F1: tystr.sub = tyu8), named directly
// rather than read off str.sub because elemsizeof gets a raw
// N_TNAME at the ident-base index path with no checker-stamped
// tinfo — str.sub lives on .type_.sub, unstamped at this site
// (cf. cgforrange's `if (sti != nil)` guard). This IS the
// str.sub-equivalent; the structural collapse onto str.sub is
// blocked on tinfo-stamping here, not intent (#24). cstage twin
// reads eff->sub->size (cmd/w6c/cgen.c N_INDEX).
if (syntax.streq(nm, "str")) { return primtypesize("u8"): i32; };
// Indexing a primitive name (rare): element size = the prim.
// elemsizeof is the STRUCTURAL (non-
// chasing) sizer by design — its alias-resolving twin elemsizeofc
// owns the chase (routed through aliasprimsize at the :1579 leg).
// A bare primsize here is correct, not the #101 bug shape.
let ps: i32 = primsize(nm);
if (ps > 0) { return ps; };
return 1;
};
if (elem == nil) { return 1; };
// `*[N]T`: drill through the pointer into the array's element so
// indexing scales by T's width, not the whole-array byte size.
// FOOTGUN (#156): this drill ALSO fires for a bare 2D `[N][M]T`
// (elem = the inner `[M]T`), so elemsizeof of a 2D array bottoms
// out at the SCALAR T size, NOT the `[M]T` sub-array stride. 2D
// double-index (cgindex) needs the sub-array stride — call
// elemsizeofc, the 2D-correct entry, which recovers slotsize([M]T)
// when elemsizeof returns 8. Never call elemsizeof for a 2D stride.
if (elem.kind == syntax.nkind.N_TARRAY) {
if (elem.lhs != nil) { elem = elem.lhs; };
};
// `*[]T`: stride is the slice header (24B). Hare-faithful — a
// pointer-to-slice is a 1D array of slices, not of T. Mirrors the
// cstage check.c default `*U → U` path for U=[]T (slice element).
if (elem.kind == syntax.nkind.N_TSLICE) { return tyslicesize(): i32; };
if (elem.kind == syntax.nkind.N_TNAME) {
let nm: str = elem.str;
// str element is 16B (ptr+len). primsize returns 0 for it.
if (syntax.streq(nm, "str")) { return primtypesize("str"): i32; };
// This structural sizer leaves alias resolution
// in elemsizeofc (:1579), not here. See the :1475 leg.
let ps: i32 = primsize(nm);
if (ps > 0) { return ps; };
};
return 8;
};
// elemsizeofc — like elemsizeof but resolves aliased element types
// (struct / tagged / `type foo = bar;`) via slotsize. Used where
// cgindex / cgassign need a correct stride for `[N]Alias` arrays
// whose Alias resolves to a tagged union (e.g. `[N]formattable`).
fn elemsizeofc(c: *cgen, t: *syntax.node) i32 = {
if (t == nil) { return 1; };
// #270-2: a NESTED-array element ([N][M]T) — the OUTER index stride
// is the WHOLE sub-array [M]T, not the scalar T that elemsizeof
// drills down to (the documented elemsizeof FOOTGUN). elemsizeof
// returns the inner prim size (4 for [M]u32), so the `direct != 8`
// short-circuit below would mis-emit esz=$4 where cstage emits the
// sub-array stride $12. Mirror cstage esz = idx_eff(bt)->sub->size
// (cmd/w6c/cgen.c:4923): the element-array tinfo's natural size
// (sub.size*elen, type.c:121) IS the outer stride.
// N_TPTR is excluded (#61): a pointee-array is NOT a nested
// element — `p[i]` on `*[N]T` auto-derefs and strides the array's
// OWN element (idx_eff peels TY_PTR→TY_ARRAY before the .sub
// read). Routing it through this whole-sub-array rule scaled
// every index by N*size(T) — the siphash round() corruption. The
// `*[N][M]T` outer stride still resolves below via idxeffti
// (pointee array's .sub = [M]T, its natural size).
let nk: syntax.nkind = t.kind;
let nest: *syntax.node = nil;
if (nk == syntax.nkind.N_TSLICE) { nest = t.lhs; };
if (nk == syntax.nkind.N_TARRAY) { nest = t.lhs; };
if (nest != nil && nest.kind == syntax.nkind.N_TARRAY) {
let eti: *syntax.tinfo = nest.type_: *syntax.tinfo;
eti = tichase(eti);
if (eti != nil) { return eti.size: i32; };
};
// #83/B3: an alias-NAMED INDEXABLE (`type grid = [3]cell`) arrives
// as a bare N_TNAME — elemsizeof's name arm knows only str + prims
// and answers the 1-sentinel, so the `direct != 8` short-circuit
// below returned 1 and every caller strode by one byte (the #60
// esz-1 family, outer-array leg). Answer from the chased stamped
// tinfo via idxeffti (which also drills an alias-of-`*[N]T`),
// element chased like the #8 leg below. Non-indexable names
// (struct/prim/str aliases) fall through to the old paths —
// byte-id preserved there.
if (nk == syntax.nkind.N_TNAME) {
let eff: *syntax.tinfo = idxeffti(t.type_: *syntax.tinfo);
if (eff != nil) {
if (eff.kind == syntax.tykind.TY_ARRAY
|| eff.kind == syntax.tykind.TY_SLICE) {
let aes: *syntax.tinfo = tichase(eff.sub);
if (aes != nil) { return aes.size: i32; };
};
};
};
let direct: i32 = elemsizeof(t);
if (direct != 8) { return direct; };
// #8: direct==8 is elemsizeof's "unresolved alias/aggregate" sentinel.
// Read the element width off the checker-stamped tinfo, mirroring the
// sibling elem*c helpers (elemissignedc :915, elemisfloatc :939, which
// already read t.type_.sub) and cstage idx_eff(bt)->sub->size
// (cmd/w6c/cgen.c N_INDEX). elemsizeofc was the odd-one-out among the
// elem*c family — it derived size purely structurally, so a named-narrow
// element (`[N]tkind`, tkind = enum i32) slipped through to a raw 8B slot
// instead of its i32 backing (4), wrong-striding both the cgindex READ
// and the local-array-init STORE (frame-smash). Peel TY_NAMED on the
// indexable and on its element, matching the #270-2 nested-array block
// above. Structural slotsize fallback stays for the t.type_==nil case.
// idxeffti folds that peel together with the `*[N]T` TY_PTR→
// TY_ARRAY drill (#61) so .sub is the array's element, never the
// whole pointee array.
let ti: *syntax.tinfo = idxeffti(t.type_: *syntax.tinfo);
if (ti != nil) {
let esub: *syntax.tinfo = ti.sub;
esub = tichase(esub);
if (esub != nil) { return esub.size: i32; };
};
let elem: *syntax.node = idxelemtn(t);
if (elem == nil) { return direct; };
if (elem.kind == syntax.nkind.N_TNAME) {
let ps: i32 = aliasprimsize(c, elem.str);
if (ps > 0) { return ps; };
};
return slotsize(c, elem);
};
// nodeisunsigned — best-effort cgen-time inference from the AST. We
// walk surface nodes (N_DOT now reads n.type_ — #55 A.6.3g):
// nkind.N_INTLIT — never marked unsigned (no tsuffix plumbing yet)
// nkind.N_IDENT — look up the local's declared type
// nkind.N_DOT — read the checker-stamped n.type_ (#55 A.6.3g)
// nkind.N_BIN / nkind.N_UN — recurse: unsigned if either operand is unsigned
// nkind.N_CAST — use the cast target type
//
// Conservative: if we can't tell, return false (signed). The cost of
// being wrong here is byte-different asm vs C, not bad runtime.
fn nodeisunsigned(c: *cgen, n: *syntax.node) bool = {
if (n == nil) { return false; };
let k: syntax.nkind = n.kind;
// #25 (F7-c5): collapse the whole N_IDENT arm onto the checker-stamped
// n.type_, dropping the localfindnode special-case. The prior arm read
// the local's declared tnode and returned `false` (signed) for a
// module-GLOBAL ident (localfindnode→nil) — so a `u64` global counter
// fed to / % >> or a relational got signed IDIV/SAR/JG instead of the
// unsigned DIV/SHR/JA cstage emits (type_isunsigned(n->type), cmd/w6c/
// cgen.c:2541). The N_DOT/N_CAST/N_INDEX/N_CALL arms below already read
// n.type_; this aligns the bare-ident arm to the same stamp. CLASS-M:
// the corpus HAS module-global unsigned counters on the divide/shift
// path, so the self-compile .s MOVES — every move is toward cstage
// (IDIV→DIV where the global's stamp is unsigned) and runtime-correct.
if (k == syntax.nkind.N_IDENT) {
return syntax.typeisunsigned(n.type_: *syntax.tinfo);
};
if (k == syntax.nkind.N_DOT) {
return syntax.typeisunsigned(n.type_: *syntax.tinfo);
};
if (k == syntax.nkind.N_CAST) {
if (n.rhs == nil) { return false; };
return syntax.typeisunsigned(n.rhs.type_: *syntax.tinfo);
};
if (k == syntax.nkind.N_BIN) {
if (nodeisunsigned(c, n.lhs)) { return true; };
return nodeisunsigned(c, n.rhs);
};
if (k == syntax.nkind.N_UN) { return nodeisunsigned(c, n.lhs); };
// nkind.N_INDEX: `p[i]` is unsigned iff its element type is
// unsigned. Read the checker-stamped result type directly,
// mirroring the N_DOT arm above and cstage cgen.c:2541
// (`type_isunsigned(n->lhs->type)` on operand's stamped tinfo).
// Replaces the prior structural base-walk that only fired for
// N_IDENT base — fell through to `return false` for N_DOT base
// (e.g. `d.digits[nd]` where d is a *struct), making
// `d.digits[nd] >= 5u8` pick signed JGE instead of unsigned JAE.
// Embodies the #121 principle (collapse structural onto stamp).
// #134.
if (k == syntax.nkind.N_INDEX) {
return syntax.typeisunsigned(n.type_: *syntax.tinfo);
};
// nkind.N_CALL: a call returning an unsigned type (e.g. `fn f() u64`)
// is unsigned. Read the checker-stamped result type directly — the
// N_CALL twin of the #134 N_INDEX arm above. cstage reads the same
// stamp via `type_isunsigned(n->lhs->type)`, stamped at check.c N_CALL
// `n->type = u->ret`; without this arm the wwstage fell through to
// `return false`, picking signed IDIV/SAR over unsigned DIV/SHR on a
// call-result div/mod/shift operand. #168.
if (k == syntax.nkind.N_CALL) {
return syntax.typeisunsigned(n.type_: *syntax.tinfo);
};
return false;
};
// nodeprimwidth — primitive byte width of an expression, or 0 if not
// statically determinable. Mirrors nodeisunsigned's structural walk.
// Used by cgun TK_TILDE to clamp narrow unsigned ~ results to type
// width (NOTQ inverts the full 64-bit register).
fn nodeprimwidth(c: *cgen, n: *syntax.node) i32 = {
if (n == nil) { return 0; };
let k: syntax.nkind = n.kind;
if (k == syntax.nkind.N_IDENT) {
let lc: *local = localfindnode(c, n.str);
if (lc != nil) {
let tn: *syntax.node = lc.tnode;
if (tn != nil) {
if (tn.kind == syntax.nkind.N_TNAME) { return aliasprimsize(c, tn.str); };
};
};
return 0;
};
if (k == syntax.nkind.N_CAST) {
let tn: *syntax.node = n.rhs;
if (tn != nil) {
if (tn.kind == syntax.nkind.N_TNAME) { return aliasprimsize(c, tn.str); };
};
return 0;
};
if (k == syntax.nkind.N_UN) { return nodeprimwidth(c, n.lhs); };
return 0;
};
// ---- type-driven slot sizing ----------------------------------------
// structnaturalsize — type-natural size of `si`, i.e. max(foff +
// fsz) across declared fields, UNROUNDED. This is the memory-copy
// extent: cstage copies exactly these bytes for the >24B sret
// write-through (cgen.c:8150 `int sz; for fields end=foff+fsz`) and
// struct-to-struct moves, so a trailing narrow field (bool@32 in a
// 33B struct padded to 40) keeps its MOVB tail rather than widening
// to a slot-overrunning MOVQ. #33 fixed cstage to use this; ww
// mirrors it. The ≤24B register RECV/RETURN ABI wants a DIFFERENT
// number — see structabisize.
//
// NOTE: si.totsize is yet a THIRD metric — the slot-padded size
// (rounded up to 8 for stack-slot use; see registerstruct's tail
// `if ((off & 7) != 0) ...`). Frame allocation and [N]foo stride
// want that slot number.
fn structnaturalsize(si: *structinfo) i32 = {
if (si == nil) { return 0; };
let n: i32 = 0;
let fi: *fieldinfo = si.fields;
for (fi != nil) {
let end: i32 = fi.foff + fi.fsz;
if (end > n) { n = end; };
fi = fi.finext;
};
return n;
};
// structabisize — the ≤24B register-return ABI size of `si`: the
// natural extent rounded up to the struct's maxalign. SSoT-equal to
// cstage's `lu->size` (check.c:760 `(off+maxalign-1)&~(maxalign-1)`).
// Distinct from structnaturalsize because the register RECV/RETURN
// ABI packs the value into AX/DX/CX at 8-byte granularity: cstage
// writes/reads the tail at maxalign width (cgen.c:7720 `sz=lu->size`,
// :8230 `sz=rt->size`), so a maxalign==8 struct with a sub-8 tail
// (struct{i64,i32}, natural 12) round-trips as MOVQ+MOVQ (16), not
// MOVQ+MOVL (12). Used ONLY at those register-ABI sites; memory
// copies (sret >24B, struct ident-copy) and field-offset math stay
// on structnaturalsize. #169.
//
// maxalign comes from each field's TRUE alignment (tinfo.align), not
// the slot-padded fsz: a [N]u8 / sub-struct field has slot ≥8 but
// align 1, so an fsz ladder would over-round. Mirrors cstage's
// maxalign = max(ft->align) (check.c:708).
fn structabisize(si: *structinfo) i32 = {
if (si == nil) { return 0; };
let n: i32 = 0;
let maxaln: i32 = 1;
let fi: *fieldinfo = si.fields;
for (fi != nil) {
let end: i32 = fi.foff + fi.fsz;
if (end > n) { n = end; };
if (fi.tnode != nil) {
let ti: *syntax.tinfo = fi.tnode.type_: *syntax.tinfo;
ti = tichase(ti);
if (ti != nil) {
let aln: i32 = ti.align: i32;
if (aln > maxaln) { maxaln = aln; };
};
};
fi = fi.finext;
};
return (n + maxaln - 1) & ~(maxaln - 1);
};
// structabisizetn — the maxalign-rounded ABI size of a by-value STRUCT,
// read off the checker-STAMPED tinfo (.size), else 0. The tinfo twin of
// structabisize(*structinfo): check.ww:2467 computes the struct's
// `r.size = (off+maxalign-1)&~(maxalign-1)` — identical to structabisize's
// formula — so `tichase(t).size` IS the ABI size, byte-id with the
// name-keyed structabisize on a resolving lookup. The #21/#224 choke-point
// for the caller-side register-ABI sites (struct-arg push, inferred-let
// struct call-result recv) that previously keyed the COUNT through a
// name-keyed structlookup/structparamsize: a cross-module same-leaf
// collision makes that lookup return 0 (or a foreign struct's size), so the
// push under-counted and the recv over-copied. The stamped tinfo carries
// the right size regardless of leaf collision (the documented #211/#13/#784
// name-keyed cluster; align wwstage UP to cstage's type-keyed struct_arg_size
// / lu->size). STRUCT-only — arrays own their own #271/#267 arms.
fn structabisizetn(t: *syntax.tinfo) i32 = {
if (t == nil) { return 0; };
let u: *syntax.tinfo = tichase(t);
if (u == nil) { return 0; };
if (u.kind == syntax.tykind.TY_STRUCT) { return u.size: i32; };
return 0;
};
// sretretsize — if `t` ultimately denotes a plain TY_STRUCT > 24B,
// return its natural size; else 0. Tagged unions, tuples, str,
// slices, scalars route through their existing register-return ABIs
// (AX/DX/CX/[R8]) regardless of size. Task #23 mirrors cstage's
// cg_sret_retsize predicate. Resolves N_TNAME → struct via structlookup
// and unwraps one leading N_TBANG so `type box = !big;` still
// triggers sret on the underlying big.
//
// Chain-of-aliases (#22): `type a = struct{...}; type b = a;` registers
// `b → a` in c.aliases (target node = N_TNAME "a"), not `b → struct`.
// When structlookup(c, "b") misses, fall through to aliaslookup and
// recurse on the alias target — mirrors slotsize's N_TNAME arm
// (cgenutil.ww:1955) and the cstage while-loop in cg_sret_retsize.
// structlookupchain — resolve TNAME `tn` to its registered struct,
// chasing alias-of-alias (#22). Returns nil if the chain doesn't
// bottom out at a struct. Mirrors cstage's transitive
// `while (t->kind == TY_NAMED) t = t->under` peel; consumed by
// cgdot / cgassign at every "field-walk on a struct-typed local"
// site so a transitively-aliased struct name resolves to its
// fieldinfo list regardless of chain depth.
export fn structlookupchain(c: *cgen, tn: *syntax.node) *structinfo = {
if (tn == nil) { return nil; };
// #92: a struct LITERAL's type ref parses as N_IDENT (expression
// position, lib/ww/parse/expr.ww) where type specs parse N_TNAME
// — both carry the name in .str. Accept both at the ENTRY only:
// iterations past the first walk aliaslookup results, which are
// N_TNAME by the loop's own reassignment gate. Consumer census
// (commit body): no existing caller can pass N_IDENT.
if (tn.kind != syntax.nkind.N_TNAME && tn.kind != syntax.nkind.N_IDENT) { return nil; };
let si: *structinfo = structlookup(c, tn.str);
if (si != nil) { return si; };
let cur: *syntax.node = tn;
for (cur != nil
&& (cur.kind == syntax.nkind.N_TNAME || cur.kind == syntax.nkind.N_IDENT)
&& si == nil) {
let aliased: *syntax.node = aliaslookup(c, cur.str);
if (aliased == nil) { cur = nil; }
else {
if (aliased.kind == syntax.nkind.N_TNAME) {
si = structlookup(c, aliased.str);
cur = aliased;
} else { cur = nil; };
};
};
return si;
};
export fn sretretsize(c: *cgen, t: *syntax.node) i32 = {
if (t == nil) { return 0; };
let r: *syntax.node = t;
if (r.kind == syntax.nkind.N_TBANG) {
r = r.lhs;
if (r == nil) { return 0; };
};
// #38: a tagged union rides AX(tag)+DX/CX/R8 = TUPLE_GPCAP
// eightbytes; a wider slot was silently truncated (payload word
// 4+ died in the callee frame). The ≤cap boundary is load-bearing:
// (str|nomem)-shaped 32B slots MUST stay register-ABI or every
// such consumer in the tree flips. Nullable folds to one word.
// Mirrors cstage cg_sret_retsize TY_TAGGED arm.
if (istaggedtype(c, r)) {
if (isnullabletype(r)) { return 0; };
let tsz38: i32 = slotsize(c, r);
if (tsz38 <= TUPLE_GPCAP * 8) { return 0; };
return tsz38;
};
if (r.kind == syntax.nkind.N_TTUPLE) {
// #10: over-cap tuple → sret. Walk the element TYPE nodes
// (pt.lhs) over the SAME caps the SEND/receive use; a float =
// 1 SSE eightbyte, a slice/str its 3-word header, a scalar 1
// GP word. Return the tuple's natural total size (tinfo.size,
// the type table) so the callee returns via sret. Mirrors
// cstage cg_sret_retsize TY_TUPLE arm; TUPLE_GPCAP/TUPLE_SSECAP
// are the shared cap SSoT with the cgreturn SEND emitter.
let ssecap: i32 = TUPLE_SSECAP;
let gptotal: i32 = 0;
let ssecount: i32 = 0;
let pt: *syntax.node = r.list;
for (pt != nil) {
let et: *syntax.node = pt.lhs;
if (isfloattype(c, et)) {
ssecount = ssecount + 1;
} else {
gptotal = gptotal + tupeslotn(et) / 8;
};
pt = pt.next;
};
if (gptotal > TUPLE_GPCAP || ssecount > ssecap) {
let rti: *syntax.tinfo = r.type_: *syntax.tinfo;
if (rti != nil) { return rti.size: i32; };
};
return 0;
};
// #267: arrays ride the struct-return ABI — natural size
// (sub.size*len, the type table) gates ≤24 reg / >24 sret, mirroring
// cstage cg_sret_retsize TY_ARRAY arm. Pure-int element arrays only;
// no float-array-return consumer (structfloatclass stays struct-only).
if (r.kind == syntax.nkind.N_TARRAY) {
let ati: *syntax.tinfo = r.type_: *syntax.tinfo;
ati = tichase(ati);
if (ati == nil) { return 0; };
let asz: i32 = ati.size: i32;
if (asz <= 24) { return 0; };
return asz;
};
if (r.kind != syntax.nkind.N_TNAME) { return 0; };
// Primitives / aliased-to-primitives are never sret.
if (aliasprimsize(c, r.str) > 0) { return 0; };
if (syntax.streq(r.str, "str")) { return 0; };
// #129: same-module alias wins over any-module struct hit. Without
// this, `type stream = *vtable` (io) loses to memio.stream (56B
// struct) via structlookup's any-module fallback → spurious sret.
// Mirrors cstage cg_sret_retsize, which sees TY_PTR, not a name.
if (c != nil) {
let al: *syntax.node = aliassamemod(c, r.str);
if (al != nil) {
return sretretsize(c, al);
};
};
let si: *structinfo = structlookup(c, r.str);
if (si == nil) {
if (c != nil) {
let aliased: *syntax.node = aliaslookup(c, r.str);
if (aliased != nil) {
return sretretsize(c, aliased);
};
};
return 0;
};
let n: i32 = structnaturalsize(si);
if (n <= 24) { return 0; };
return n;
};
// tfieldlookup — the named field `fld` in struct tinfo `sti`'s stamped
// field chain, else nil. The #31 receiver-layout store choke-point: the
// tfield twin of the name-keyed `for (fi != nil)` fieldinfo walk. A
// cross-module same-leaf collision that mis-resolved structlookupchain
// cannot reach a stamped tinfo, so every field-OFFSET/width/kind keyed
// off the returned tf is collision-immune.
fn tfieldlookup(sti: *syntax.tinfo, fld: str) *syntax.tfield = {
if (sti == nil) { return nil; };
let tf: *syntax.tfield = sti.fields;
for (tf != nil) {
if (syntax.streq(tf.name, fld)) { return tf; };
tf = tf.tnext;
};
return nil;
};
// sretretsizetn — verbatim port of cstage cg_sret_retsize (cmd/w6c/
// cgen.c:380) keyed on the checker-STAMPED *tinfo. The tinfo twin of
// sretretsize, the #31 store-loop choke-point (the sret hard-stop /
// sret-receive field sub-arms). Walks resolved tinfo, so the #129
// aliassamemod/module-juggling sretretsize carries — pure name-keying
// compensation for structlookup's any-module fallback — is NOT needed
// and absent: tichase(t) peels NAMED then falls through all four kind
// tests to 0, exactly what #129 reconstructed by name (cstage sees a
// resolved Type*, never a name). There is NO TY_BANG tinfo kind — `!T`
// folds into TY_TAGGED's per-variant iserror (typ.ww:82-90), so there
// is no error-peel branch; mirror cstage's four branches exactly. `c`
// kept for call-site symmetry with sretretsize (unused: tinfo carries
// the module-independent layout).
fn sretretsizetn(c: *cgen, t: *syntax.tinfo) i32 = {
let u: *syntax.tinfo = tichase(t);
if (u == nil) { return 0; };
if (u.kind == syntax.tykind.TY_STRUCT) {
if (u.size: i32 <= 24) { return 0; };
return u.size: i32;
};
// #38: a tagged union rides AX(tag)+DX/CX/R8 = TUPLE_GPCAP
// eightbytes; nullable folds to one ptr word. Mirrors cstage
// cg_sret_retsize TY_TAGGED arm.
if (u.kind == syntax.tykind.TY_TAGGED) {
if (u.nullable != 0) { return 0; };
if (u.size: i32 <= TUPLE_GPCAP * 8) { return 0; };
return u.size: i32;
};
// #267: arrays ride the struct-return ABI (≤24 reg / >24 sret).
if (u.kind == syntax.tykind.TY_ARRAY) {
if (u.size: i32 <= 24) { return 0; };
return u.size: i32;
};
// #10: over-cap tuple → sret. Walk the positional element chain
// over the SAME caps the SEND/receive use (a float = 1 SSE
// eightbyte; str/slice/tagged ride their box-rounded GP stride via
// tupeslot; a scalar 1 GP word). Mirrors cstage cg_sret_retsize
// TY_TUPLE arm; tupleelems is the tinfo positional chain.
if (u.kind == syntax.tykind.TY_TUPLE) {
let gptotal: i32 = 0;
let ssecount: i32 = 0;
let te: *syntax.ttupleelem = u.tupleelems;
for (te != nil) {
if (syntax.typeisfloat(te.type_)) {
ssecount = ssecount + 1;
} else {
gptotal = gptotal + tupeslot(te.type_) / 8;
};
te = te.tnext;
};
if (gptotal > TUPLE_GPCAP || ssecount > TUPLE_SSECAP) {
return u.size: i32;
};
return 0;
};
return 0;
};
// callsretsize — if N_CALL `n`'s callee returns a plain TY_STRUCT
// > 24B, return its natural size; else 0. Wraps sretretsize over the
// callee's resolved return type, used by cglet / cgassign receive
// sites and cgcall to detect sret at the receive / emit boundaries.
export fn callsretsize(c: *cgen, n: *syntax.node) i32 = {
if (n == nil) { return 0; };
if (n.kind != syntax.nkind.N_CALL) { return 0; };
let callee: *syntax.node = n.lhs;
if (callee == nil) { return 0; };
let cn: str;
cn.ptr = nil; cn.len = 0;
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.kind == syntax.nkind.N_IDENT) {
cn = callee.str;
cmod = c.curmod;
};
if (callee.kind == syntax.nkind.N_DOT) {
cn = callee.str;
if (callee.lhs != nil) {
if (callee.lhs.kind == syntax.nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
};
if (cn.len == 0) { return 0; };
let rtyp: *syntax.node = fnretlookupmod(c, cn, cmod);
// #129: sretretsize must see the CALLEE's module context so
// aliassamemod resolves aliases from the callee's module (not the
// caller's). Mirrors cstage operating on resolved Type* objects
// (type_chase_named never has this confusion). Swap + restore.
let savedmod: str = c.curmod;
if (cmod.len > 0) { c.curmod = cmod; };
let r: i32 = sretretsize(c, rtyp);
c.curmod = savedmod;
return r;
};
fn structlookup(c: *cgen, name: str) *structinfo = {
// Same-module first, then any. Trio-leaf graduation mirroring
// aliaslookup (#27), fnret/fnparamslookupmod (#28/#31), and
// enumlookup (#4a): without the prefer pass a bare-leaf struct
// name in module M can collapse onto another module's same-leaf
// struct prepended earlier in c.structs, silently picking the
// wrong totsize / field offsets.
let s: *structinfo = c.structs;
for (s != nil) {
if (syntax.streq(s.sname, name)) {
if (syntax.streq(s.smod, c.curmod)) { return s; };
};
s = s.sinext;
};
s = c.structs;
for (s != nil) {
let sn: str = s.sname;
if (syntax.streq(sn, name)) { return s; };
s = s.sinext;
};
// Module-qualified form embedded in name (`pkg.S`): scope the
// leaf to its originating module. The `smod == pkg` guard
// prevents same-leaf structs in two modules from collapsing.
let i: i32 = name.len - 1;
for (i >= 0) {
if (name[i] == '.') {
let pkg: str;
pkg.ptr = name.ptr;
pkg.len = i;
let leaf: str;
leaf.ptr = name.ptr + ((i + 1): u64);
leaf.len = name.len - (i + 1);
// M1 #22: the embedded qualifier (`utf8`) is the use ALIAS;
// the struct's smod is now the dotted import PATH
// (`encoding.utf8`). Map alias→path before comparing.
let pkgmod: str = usehint(c, pkg);
let b: *structinfo = c.structs;
for (b != nil) {
if (syntax.streq(b.sname, leaf)) {
if (syntax.streq(b.smod, pkgmod)) {
return b;
};
};
b = b.sinext;
};
return nil;
};
i -= 1;
};
return nil;
};
// #223: same-module-ONLY struct lookup. structlookup's any-module
// fallback returns a foreign same-leaf struct; the cgdot alias-peel
// needs to break ONLY on a struct that THIS module defines (a genuine
// struct-value receiver), not on a foreign struct that merely shares a
// leaf with a same-module alias (io.stream alias vs memio.stream
// struct). Returns the struct only when it lives in c.curmod.
fn structsamemod(c: *cgen, name: str) *structinfo = {
let s: *structinfo = c.structs;
for (s != nil) {
if (syntax.streq(s.sname, name)) {
if (syntax.streq(s.smod, c.curmod)) { return s; };
};
s = s.sinext;
};
return nil;
};
// primsize — size in bytes of a primitive type name (or 0 if not
// recognised as a primitive — the caller falls back to other paths).
// fldnumidx — parse a tuple field name like "0" / "1" / "12" into an
// index, or -1 if not all-digits. Used by cgdot to dispatch
// `t.0` / `t.1` against an nkind.N_TTUPLE local without pulling in strconv.
fn fldnumidx(s: str) i32 = {
if (s.len == 0) { return -1; };
let r: i32 = 0;
let i: i32 = 0;
for (i < s.len) {
let b: u8 = s[i];
if (b < 48u8) { return -1; };
if (b > 57u8) { return -1; };
r = r * 10 + ((b - 48u8): i32);
i += 1;
};
return r;
};
// This primitive-width oracle is the SSoT table aliasprimsize wraps;
// there is nothing below it to chase (#101/#109).
fn primsize(name: str) i32 = {
if (syntax.streq(name, "u8")) { return 1; };
if (syntax.streq(name, "i8")) { return 1; };
if (syntax.streq(name, "bool")) { return 1; };
if (syntax.streq(name, "u16")) { return 2; };
if (syntax.streq(name, "i16")) { return 2; };
if (syntax.streq(name, "u32")) { return 4; };
if (syntax.streq(name, "i32")) { return 4; };
if (syntax.streq(name, "f32")) { return 4; };
if (syntax.streq(name, "u64")) { return 8; };
if (syntax.streq(name, "i64")) { return 8; };
if (syntax.streq(name, "uint")) { return 8; };
if (syntax.streq(name, "int")) { return 8; };
if (syntax.streq(name, "uintptr")) { return 8; };
if (syntax.streq(name, "size")) { return 8; };
if (syntax.streq(name, "f64")) { return 8; };
if (syntax.streq(name, "rune")) { return 4; };
if (syntax.streq(name, "void")) { return 0; };
return 0;
};
// aliasprimsize — resolved primitive byte width for a type NAME: the
// prim width if `nm` is itself a primitive, else chase the alias chain
// (aliaslookup) to its bottom and take that prim's width. Returns 0
// when the name doesn't reduce to a width-known primitive (struct /
// tagged / `!`/enum-bottom / unresolved). SSoT for the size-use
// primsize() family: a bare primsize(name) is alias-blind — a narrow
// alias (`type my32 = u32`) returns 0, defaulting the stride/width to
// 8 (the #101 struct-fill miscompile: [3]my32 strode 8 not 4, field n
// collided with arr[2]). cstage chases my32→u32→4 via type_chase_named
// at the twin sites; this is the ww align-up. The bare-primsize GUARD
// family (is-primitive dispatch) is the #109 follow-on, NOT routed
// here. #101.
// primsize is the leaf-primitive probe this SSoT helper wraps, then
// aliaslookup chases (#101/#109).
fn aliasprimsize(c: *cgen, nm: str) i32 = {
let ps: i32 = primsize(nm);
if (ps > 0) { return ps; };
let cur: *syntax.node = aliaslookup(c, nm);
for (cur != nil) {
if (cur.kind != syntax.nkind.N_TNAME) { return 0; };
let p: i32 = primsize(cur.str);
if (p > 0) { return p; };
cur = aliaslookup(c, cur.str);
};
return 0;
};
// typenodeprimresolved — walk N_TBANG / N_TENUM / N_TNAME alias
// chains to the underlying primitive, returning its byte size and
// signedness. Sets *sz_out = 0 when the type doesn't reduce to a
// width-known primitive (composite, unresolved name, default-storage
// enum, etc.). Mirrors cstage's `type_isint(t) ? t->size : 0` /
// `type_isunsigned` recursion through TY_NAMED and TY_ENUM. Used by
// cgcast's identity-width identity-sign clamp-skip predicate (#33).
export fn typenodeprimresolved(c: *cgen, t: *syntax.node,
sz_out: *i32, unsigned_out: *bool) void = {
*sz_out = 0;
*unsigned_out = false;
let cur: *syntax.node = t;
for (cur != nil) {
let k: syntax.nkind = cur.kind;
if (k == syntax.nkind.N_TBANG) { cur = cur.lhs; }
else { if (k == syntax.nkind.N_TENUM) { cur = cur.lhs; }
else { if (k == syntax.nkind.N_TNAME) {
let nm: str = cur.str;
// bool is excluded from the int-prim contract: cstage's
// `type_isint(TY_BOOL)` is false, so its identity check
// leaves src_w=0 on a bool source. Match that here so a
// `let y: i8 = b: i8;` (bool b) doesn't fire identity in
// wwstage and skip the MOVSBQ that cstage emits. Other
// call sites (slot sizing, etc.) still want
// primsize("bool")=1, so the exclusion stays local. The
// dedicated `is_bool` path in cgcast owns bool→bool's
// ANDQ $255 on both stages.
if (syntax.streq(nm, "bool")) { return; };
// This function is a primitive resolver
// chaser (#33) — primsize is the leaf-primitive probe;
// aliaslookup below advances the walk on a miss.
let ps: i32 = primsize(nm);
if (ps > 0) {
*sz_out = ps;
*unsigned_out = syntax.typeisunsigned(cur.type_: *syntax.tinfo);
return;
};
let al: *syntax.node = aliaslookup(c, nm);
if (al == nil) { return; };
cur = al;
}
else { return; }; }; };
};
};
// exprprimresolved — best-effort static (primsize, signedness) for an
// expression. Used by cgcast (#33) to derive the source-side primitive
// width and signedness so the identity-width identity-sign clamp-skip
// predicate fires. Sets *sz_out = 0 when the type can't be derived
// (untyped literal, call result with no return-type lookup, etc.);
// caller treats sz=0 as "not identity", which conservatively keeps
// the clamp. Mirror of cstage's `n->lhs->type` lookup with the same
// TY_NAMED / TY_ENUM recursion through type_isint / type_isunsigned.
export fn exprprimresolved(c: *cgen, n: *syntax.node,
sz_out: *i32, unsigned_out: *bool) void = {
*sz_out = 0;
*unsigned_out = false;
if (n == nil) { return; };
let k: syntax.nkind = n.kind;
if (k == syntax.nkind.N_INTLIT) {
// Typed-int literal: `7u32` has tsuffix = "u32". Mirrors
// cstage's `cexpr` which assigns `lookup_builtin(tsuffix)`
// as the node's type — without this, wwstage misses the
// suffix and emits a defensive clamp where cstage skips,
// breaking byte-id on rows like `let y: mymode = 7u32:
// mymode;` (mymode = enum u32).
let s: str = n.tsuffix;
if (s.len > 0) {
// A typed-int literal suffix
// (`7u32`) is a builtin primitive name by grammar — no
// alias can reach here, so there is nothing to chase.
let ps: i32 = primsize(s);
if (ps > 0) {
*sz_out = ps;
*unsigned_out = syntax.typeisunsigned(n.type_: *syntax.tinfo);
};
};
return;
};
if (k == syntax.nkind.N_IDENT) {
let lc: *local = localfindnode(c, n.str);
if (lc != nil) {
typenodeprimresolved(c, lc.tnode,
sz_out, unsigned_out);
};
return;
};
if (k == syntax.nkind.N_CAST) {
typenodeprimresolved(c, n.rhs, sz_out, unsigned_out);
return;
};
if (k == syntax.nkind.N_UN) {
exprprimresolved(c, n.lhs, sz_out, unsigned_out);
return;
};
if (k == syntax.nkind.N_DOT) {
// #59: read the checker-stamped tinfo instead of re-deriving the
// field type via dotfieldtnode's structinfo walk. Mirrors cstage
// castsrcprim N_DOT (cmd/w6c/cgen.c:323-344): the base must
// resolve to a struct (or ptr-to-struct) before the field type
// counts. That guard excludes pseudo-fields .len/.cap/.ptr (the
// checker stamps them i32/*T at check.ww:1987-2004) and tuple
// positionals, keeping them at sz=0 — asymmetry there breaks 995
// byte-id (cgen.c:286-290). The field's own width/sign is the
// N_DOT's stamped type_ (check.ww:2012). typeisint ? size : 0;
// bool falls out because typeisint(bool) is false — the same
// exclusion the old streq("bool") arm encoded.
// B2-c3/B1: the base walk CHASES each hop (was a hand-rolled
// 2-peel that ran out at a 3-level alias base or a ptr-to-
// 2-level base — clamp kept on a knowable identity cast,
// runtime-correct but cs!=ww asm). Aligns up to cstage
// castsrcprim post-F1 (type_chase_named at both hops). The
// field-u chase is asm-neutral: cs keeps its single peel
// there, sound through type_isint's NAMED recursion + the
// NAMED tinfo carrying its underlying's size (probe
// b1c_fld2lvl byte-id).
let bu: *syntax.tinfo = nil;
if (n.lhs != nil) { bu = n.lhs.type_: *syntax.tinfo; };
bu = tichase(bu);
if (bu != nil && bu.kind == syntax.tykind.TY_PTR) { bu = tichase(bu.sub); };
if (bu != nil && bu.kind == syntax.tykind.TY_STRUCT) {
let u: *syntax.tinfo = n.type_: *syntax.tinfo;
u = tichase(u);
if (syntax.typeisint(u)) {
*sz_out = u.size: i32;
*unsigned_out = syntax.typeisunsigned(u);
};
};
return;
};
};
// variantnamematch — tagged-union variant names are compared as if
// they'd been alias-resolved. Pattern names can be module-qualified
// (`strconv.invalid` from a `case let e: strconv.invalid =>`),
// while the variant's declared name inside its own module is bare
// (`invalid`). With no checker the cgen can't follow imports, so we
// accept exact match plus suffix-after-`.` on either side. Mirrors
// the C cgen's type_eq, which goes through resolved Type pointers.
fn variantnamematch(vname: str, pname: str) bool = {
if (syntax.streq(vname, pname)) { return true; };
// `pname` is qualified, `vname` is bare: drop module prefix.
let i: i32 = 0;
for (i < pname.len) {
if (pname[i] == '.': u8) {
let tail: str;
tail.ptr = pname.ptr + i + 1;
tail.len = pname.len - i - 1;
if (syntax.streq(tail, vname)) { return true; };
};
i += 1;
};
// `vname` is qualified, `pname` is bare: same trick in reverse.
let j: i32 = 0;
for (j < vname.len) {
if (vname[j] == '.': u8) {
let tail: str;
tail.ptr = vname.ptr + j + 1;
tail.len = vname.len - j - 1;
if (syntax.streq(tail, pname)) { return true; };
};
j += 1;
};
return false;
};
// inferletcalltype — for an annotation-less `let x = expr;`, return
// a usable tnode for cgen's struct-aware paths. Today: `let x =
// f()?` infers x's type from the success variant of f's tagged
// return; without this, x has tnode = nil and `x.field` falls into
// the SB-symbol fallback (linker reports `undefined reference to
// <fieldname>`). We don't infer for plain `let x = f()` yet —
// non-tagged returns don't carry their type back the same way.
fn inferletcalltype(c: *cgen, rhs: *syntax.node) *syntax.node = {
if (rhs == nil) { return nil; };
// `?` (N_TRYPROP) and `!` (N_TRYUNW) both unwrap a tagged
// return to its success variant; the rhs we want the type of
// is the inner call expression.
let unwrap: bool = false;
let call: *syntax.node = rhs;
if (rhs.kind == syntax.nkind.N_TRYPROP) { call = rhs.lhs; unwrap = true; };
if (rhs.kind == syntax.nkind.N_TRYUNW) { call = rhs.lhs; unwrap = true; };
if (call == nil) { return nil; };
if (call.kind != syntax.nkind.N_CALL) { return nil; };
let callee: *syntax.node = call.lhs;
if (callee == nil) { return nil; };
let cname: str;
cname.ptr = nil; cname.len = 0;
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.kind == syntax.nkind.N_IDENT) {
cname = callee.str;
cmod = c.curmod;
};
if (callee.kind == syntax.nkind.N_DOT) {
cname = callee.str;
if (callee.lhs != nil) {
if (callee.lhs.kind == syntax.nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
};
if (cname.len == 0) { return nil; };
let rtyp: *syntax.node = fnretlookupmod(c, cname, cmod);
if (rtyp == nil) { return nil; };
if (unwrap) {
// Strip error variants — success type is the first
// variant of the tagged return.
if (rtyp.kind != syntax.nkind.N_TTAGGED) { return nil; };
return rtyp.list;
};
// Plain call: declared return type is the local's type.
return rtyp;
};
// letslotsize — slot size for a `let` binding. Like slotsize, but
// detects `[_]T = arrlit;` (the type-AST has rhs == nil as the
// length-inferred sentinel) and computes count × element-size from
// the initialiser. Called from cglet at emit time so the frame
// grows monotonically per first-use (#15).
//
// `let x = f();` (no annotation): infer from `f`'s declared return
// type so a 24B tagged-union return reserves all three spill slots,
// not the default 8B. Without this, the AX:DX:CX spill in cglet's
// tagged-init branch writes past the local and tramples the next
// slot.
export fn letslotsize(c: *cgen, n: *syntax.node) i32 = {
// `[_]T = arrlit;` inferred-length arrays no longer need a slot-size
// intercept here: the checker (inferarraylen, check.ww) stamps the
// real element count onto the array type's length child before cgen
// runs, so slotsize reads it like any explicit `[N]T` (#7).
// Annotation-less init: defer to the call's return type if we can
// infer it. Tagged-union returns need 24B; everything else matches
// slotsize on the inferred type.
let tn: *syntax.node = n.lhs;
if (tn == nil) { tn = inferletcalltype(c, n.rhs); };
if (tn == nil) { return 8; };
let s: i32 = slotsize(c, tn);
// #15: cstage cglet (cmd/w6c/cgen.c:12321) defaults a local's slot
// to 8 — only ARRAY/SLICE/STR/STRUCT/TUPLE/TAGGED take the real
// type size. slotsize returns 0 for a void/`done`-aliased scalar
// local; localreserve no longer applies a sub-8 floor (it mirrors
// cstage's no-floor localslot), so a void slot must be floored here
// instead, else its zero width collides with the next local. A
// genuine empty struct or [0]T array (also slotsize 0) keeps its 0.
if (s == 0) {
let ti: *syntax.tinfo = tn.type_: *syntax.tinfo;
if (ti != nil) { ti = tichase(ti); };
if (ti != nil && ti.kind == syntax.tykind.TY_VOID) { return 8; };
};
return s;
};
// #48 A.6.3d: AST walker retired. resolvewalk (check.ww:426-436) stamps
// `n.type_` on every N_T* kind via tinfofornode, which folds TBANG
// (inner unchanged, check.ww:1154-1161), TNAME alias chains
// (resolvealias, check.ww:1128-1153), TARRAY/TPTR/TSLICE/TCHAN/TFN/
// TENUM/TTUPLE/TSTRUCT/TTAGGED with size + slot-padded slotsize.
//
// cstage SSoT is distributed — there is no single slot_size(Type*).
// Tagged slot follows cstage cmd/wcc/check.c:348 + :998 (tag (8) +
// max variant payload rounded to 8); the wwstage TTAGGED arm at
// check.ww:1296-1346 mirrors that layout. cgen.c:4850 / :5193 use
// the same `(su->kind == TY_TAGGED) ? su->size : 16` pattern for the
// match-spill slot. Pointer-and-narrow → 8 is the local-frame
// convention encoded at every cstage `localoff(…, 8, …)` callsite
// (cmd/w6c/cgen.c throughout); wwstage encodes the same pad-to-8 at
// this read site so `[N]i32` stride stays 4 (natural) — moving it
// into ti.slotsize would lift array stride to 8/elem.
//
// `c: *cgen` retained unused for callsite stability (localloadop
// precedent, 68219a1).
fn slotsize(c: *cgen, typn: *syntax.node) i32 = {
if (typn == nil) { return 8; };
let ti: *syntax.tinfo = typn.type_: *syntax.tinfo;
if (ti == nil) { return 8; };
// #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.
ti = tichase(ti);
if (ti == nil) { return 8; };
let kk: syntax.tykind = ti.kind;
if (kk == syntax.tykind.TY_VOID) { return 0; };
if (kk == syntax.tykind.TY_PTR || kk == syntax.tykind.TY_SLICE ||
kk == syntax.tykind.TY_CHAN || kk == syntax.tykind.TY_FN ||
kk == syntax.tykind.TY_STR || kk == syntax.tykind.TY_TAGGED) {
return ti.size: i32;
};
// #75/#9: a struct OR array LOCAL's stack slot is the type's NATURAL
// size rounded to the 8B slot grain — NOT ti.slotsize, which sums the
// slot-PADDED element/field widths and over-reserves when a nested
// element is a sub-8-tail composite (e.g. [2]outer with
// outer{a:u8, p:inner{x:u8,y:u8}, z:i64}: outer.slotsize 24 != size 16
// → ww frame $48 vs cstage natural $32). cstage's localslot reserves at
// round8(f->type->size) (cmd/w6c/cgen.c, frame=(frame+size+7)&~7 over
// the checker's natural size); ww's localreserve mirrors that round.
// ti.size and ti.slotsize round to the SAME 8-multiple for every array
// of prims/arrays (element slotsize==size) and for an 8-multiple tagged
// element (#48 [N]Alias), so this arm MOVES only the nested-sub-8-struct
// case — the #9 divergence. #75 fixed the TY_STRUCT arm; #9 carries the
// identical transform to its TY_ARRAY sibling (same dual-SSoT slotsize
// leak as #44/#55, one notion over). The per-element STRIDE is
// unaffected: it reads slotsize on the ELEMENT node (struct arm) /
// elemsizeofc's natural sub.size, never this array-total arm. TUPLE
// keeps ti.slotsize (8B/elem slot, USER ruling #60 — untouched).
if (kk == syntax.tykind.TY_STRUCT || kk == syntax.tykind.TY_ARRAY) {
return ((ti.size + 7u64) & ~7u64): i32;
};
if (kk == syntax.tykind.TY_TUPLE) {
return ti.slotsize: i32;
};
return 8;
};
// fieldsize — slot-padded byte width of a struct field's type-AST,
// consumed by registerstruct's alignment + offset math (≥8→8 /
// ≥4→4 / ≥2→2 ladder at L1875-1877) and by the `*p OP=` deref-
// compound at cgenexpr.ww:3594. Mirror of check.ww:1053
// `fieldslotsize` (the same dispatch on the populated tinfo);
// slotsize-template precedent at a828c03. cstage SSoT is
// `f->type->size` (cmd/w6c/cgen.c:1386, :1656, :2515, :2535);
// wwstage routes through ti.slotsize for composites since the
// stack-slot pad rules live on tinfo (#48 verdict), and through
// ti.size for the kinds whose natural size already equals their
// in-struct width.
//
// `c: *cgen` retained unused for callsite stability (slotsize /
// localloadop precedent, a828c03 / 68219a1).
fn fieldsize(c: *cgen, tnode: *syntax.node) i32 = {
if (tnode == nil) { return 8; };
let ti: *syntax.tinfo = tnode.type_: *syntax.tinfo;
if (ti == nil) { return 8; };
// #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.
ti = tichase(ti);
if (ti == nil) { return 8; };
let k: syntax.tykind = ti.kind;
if (k == syntax.tykind.TY_STRUCT) { return ti.slotsize: i32; };
if (k == syntax.tykind.TY_ARRAY) { return ti.slotsize: i32; };
if (k == syntax.tykind.TY_TAGGED) { return ti.size: i32; };
if (k == syntax.tykind.TY_SLICE) { return ti.size: i32; };
if (k == syntax.tykind.TY_PTR || k == syntax.tykind.TY_FN ||
k == syntax.tykind.TY_CHAN) { return 8; };
if (k == syntax.tykind.TY_STR) { return ti.size: i32; };
// Primitives + TY_ENUM keep natural width inside structs
// (cstage parity: cgen.c reads f->type->size directly). TY_TUPLE
// flows here too — pre-collapse fallback was 8, the populated
// ti.size carries the natural sum; ken-thompson 2026-05-23 review:
// keep the corrected behavior, no fixtures in selfhost exercise
// a tuple-typed struct field today (995 byte-id is the gate).
if (ti.size > 0u64) { return ti.size: i32; };
return 8;
};
// #44/#55: fi.foff is a VIEW of the checker's already-built NATURAL
// `tfield.offset` (check.ww N_TSTRUCT L2234), NOT a second slot-padded
// layout recomputed via fieldsize. The two sources diverged iff a struct
// had a nested sub-8 composite field (slotsize != size) plus a successor:
// the WRITE path used this slot-padded foff, the READ path
// (cgplaceaddr/dotbaseaddr) read tfield.offset natural — ww mis-addressed
// its own fields. cstage has no structinfo and reads tfield directly
// (self-consistently natural); this unifies ww's second source onto it,
// preserving cs==ww. Lock-step walk: tstruct.list N_TFIELD AST nodes and
// ti.fields tfields share one head-first declared order (both skip
// non-TFIELD identically), so they advance in exact step. si.totsize keeps
// the slot-padded total (stack-slot allocator's number) from ti.slotsize,
// already 8-rounded at check.ww:2259-2261.
// regfieldwalk — cursor state threaded through regfieldrun's embed-
// descending AST/tfield lock-step walk.
type regfieldwalk = struct {
tf: *syntax.tfield,
head: *fieldinfo,
tail: *fieldinfo,
};
// regfieldrun — walk an AST TFIELD list against the flattened
// tinfo.fields cursor. A named field consumes one tfield; an embed
// (#59.13, f.str == "") descends into the resolved inner struct AST
// the checker planted on f.rhs (check.ww N_TSTRUCT flatten) — its
// fields were promoted into THIS struct's tinfo run in the same
// order, so the one cursor stays in exact step and each promoted
// fieldinfo carries the inner field's own name and type node.
fn regfieldrun(c: *cgen, flist: *syntax.node, w: *regfieldwalk) void = {
let f: *syntax.node = flist;
for (f != nil) {
if (f.kind == syntax.nkind.N_TFIELD) {
if (f.str.len == 0) {
if (f.rhs == nil) {
let msg: str = "#59.13: registerstruct: embed field has no checker-planted inner struct AST\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
regfieldrun(c, f.rhs.list, w);
} else {
if (w.tf == nil) {
let msg: str = "#44/#55: registerstruct: AST/tfield walk desync (more TFIELDs than tinfo.fields)\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
let fi: *fieldinfo = alloc(fieldinfo{
fname = f.str,
foff = w.tf.offset: i32,
fsz = w.tf.type_.size: i32,
tnode = f.lhs,
})!;
if (w.head == nil) { w.head = fi; w.tail = fi; }
else { w.tail.finext = fi; w.tail = fi; };
w.tf = w.tf.tnext;
};
};
f = f.next;
};
};
fn registerstruct(c: *cgen, name: str, srcmod: str, tstruct: *syntax.node) void = {
let si: *structinfo = alloc(structinfo{
sname = name,
smod = srcmod,
})!;
if (tstruct.type_ == nil) {
let msg: str = "#44/#55: registerstruct: struct node has no stamped tinfo\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
let ti: *syntax.tinfo = tichase(tstruct.type_: *syntax.tinfo);
if (ti == nil) {
let msg: str = "#44/#55: registerstruct: tichase yielded nil tinfo\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
let w: regfieldwalk;
w.tf = ti.fields;
w.head = nil;
w.tail = nil;
regfieldrun(c, tstruct.list, &w);
si.fields = w.head;
si.totsize = ti.slotsize: i32;
si.sinext = c.structs;
c.structs = si;
};
fn collectstructs(c: *cgen, file: *syntax.node) void = {
c.structs = nil;
if (file == nil) { return; };
let d: *syntax.node = file.list;
for (d != nil) {
if (d.kind == syntax.nkind.N_TYPEDECL) {
let body: *syntax.node = d.lhs;
// #9: an error-struct (`type X = !struct{...}`) carries an
// N_TBANG-wrapped body; peel it so X registers like any
// struct. cstage is tinfo-based and needs no table, but
// wwstage's name-keyed widen dispatch (cgreturn needswiden
// → cgwidentaggedstore) resolves struct layout through
// c.structs; an unregistered error-struct made the
// struct-variant-of-large-union return silently drop its
// construction (cs!=ww). The strategic fix is #222's sret
// cutover, which deletes this name-keyed dispatch outright;
// until then the table must be complete (errors.opaque_ is
// the first such type). #10 tracks retiring the table.
if (body != nil && body.kind == syntax.nkind.N_TBANG) {
body = body.lhs;
};
if (body != nil) {
if (body.kind == syntax.nkind.N_TSTRUCT) {
registerstruct(c, d.str, d.nmod, body);
};
};
};
d = d.next;
};
};
// isstrtype — alias-aware. Reads the stamped tinfo so `str`,
// `type alias = str`, `!str`, and chained aliases all route to the
// str-shaped slot. Cite cstage cgen.c:159 `type_isstr` SSoT.
// Collapsed onto typeisstr per A.6.3b (#46); the prior AST walker is
// reconstituted by typeisstr's TY_NAMED chase + tinfofornode's
// TBANG-unwrap (check.ww:1145).
fn isstrtype(c: *cgen, t: *syntax.node) bool = {
if (t == nil) { return false; };
return syntax.typeisstr(t.type_: *syntax.tinfo);
};
fn isslicetype(c: *cgen, t: *syntax.node) bool = {
if (t == nil) { return false; };
return syntax.typeisslice(t.type_: *syntax.tinfo);
};
// resolvetagged — return the underlying N_TTAGGED node for `t`, or nil
// if `t` doesn't ultimately denote a tagged union. Follows N_TNAME
// aliases (via resolvetype) and unwraps one leading N_TBANG so
// `type error = !(invalid | overflow);` resolves to its inner
// `(invalid | overflow)` node. Use at sites that read variant lists
// or detect nullable folding off a scrutinee — cgmatch, cgtypetest,
// cgtypeassert — so aliased `!(A|B)` shapes still dispatch.
export fn resolvetagged(c: *cgen, t: *syntax.node) *syntax.node = {
let r: *syntax.node = resolvetype(c, t);
if (r == nil) { return nil; };
if (r.kind == syntax.nkind.N_TBANG) {
let inner: *syntax.node = r.lhs;
if (inner == nil) { return nil; };
r = resolvetype(c, inner);
if (r == nil) { return nil; };
};
if (r.kind == syntax.nkind.N_TTAGGED) { return r; };
return nil;
};
// matchscrutt — resolve a non-ident match scrutinee node to its tagged
// type (or nil if unresolvable). Used by cgmatch to size the
// @match_spill slot at first use (#15 first-use+fail-loud convergence).
// IDENT scrutinees use a different lookup path (read off the local
// directly, no spill) so this returns nil for them too.
fn matchscrutt(c: *cgen, scrut: *syntax.node) *syntax.node = {
if (scrut == nil) { return nil; };
let k: syntax.nkind = scrut.kind;
if (k == syntax.nkind.N_IDENT) { return nil; };
if (k == syntax.nkind.N_CALL) {
let callee: *syntax.node = scrut.lhs;
if (callee != nil) {
let cnm: str;
cnm.ptr = nil; cnm.len = 0;
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.kind == syntax.nkind.N_IDENT) { cnm = callee.str; };
if (callee.kind == syntax.nkind.N_DOT) {
cnm = callee.str;
// Same-module-first disambiguation: a leaf collision
// on `next` (utf8.next + caller-side next) otherwise
// returns the last-declared (caller) rtype and the
// 4-arm match collapses arms 2+ to tag 0. Task #31.
if (callee.lhs != nil) {
if (callee.lhs.kind == syntax.nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
};
if (cnm.len > 0) {
let rtyp: *syntax.node = fnretlookupmod(c, cnm, cmod);
if (rtyp != nil) { return resolvetagged(c, rtyp); };
};
};
return nil;
};
if (k == syntax.nkind.N_INDEX) {
let ibase: *syntax.node = scrut.lhs;
if (ibase == nil) { return nil; };
if (ibase.kind != syntax.nkind.N_IDENT) {
// #48: non-ident index base (s.field[i], call()[i],
// nested). Only idents carry a declared tnode to walk,
// so resolve from the checker-stamped element tinfo
// instead — the #45/#67 stamped-carrier pattern; cgmatch
// gates on istaggedtype and reads scrutt.type_ directly.
// cstage N_MATCH reads s->type for every scrutinee shape
// (cmd/w6c/cgen.c:7510). Pre-#48 this returned nil and
// the variant clamped to 0 + @match_spill mis-sized.
if (istaggedtype(c, scrut)) { return scrut; };
return nil;
};
let bl: *local = localfindnode(c, ibase.str);
let btn: *syntax.node = nil;
if (bl != nil) { btn = bl.tnode; }
else { btn = letvartnode(c, ibase.str); };
if (btn == nil) { return nil; };
// idxelemtn: `*[N]T` drills to the pointee array's element (#61).
let etn: *syntax.node = idxelemtn(btn);
if (etn == nil) { return nil; };
return resolvetagged(c, etn);
};
if (k == syntax.nkind.N_DOT) {
// #67: read the field's stamped tinfo off the N_DOT node
// (post-#66 N_DOT carries the field type) instead of the
// dotfieldtnode AST walk. cgmatch's gate reads scrutt.type_
// via istaggedtype, so the resolved N_TTAGGED node the walk
// produced is no longer the carrier; the istaggedtype guard
// preserves the nil-for-non-tagged contract the sibling
// N_CALL/N_INDEX branches get from resolvetagged.
if (!istaggedtype(c, scrut)) { return nil; };
return scrut;
};
// Family C (#46): a DEREF scrutinee — stamped-carrier like the
// non-ident N_INDEX/N_DOT arms (`match (*p)` spill size + variant
// indices key off scrut.type_; pre-#46 nil here clamped the
// variant to 0 and mis-sized @match_spill).
if (k == syntax.nkind.N_UN && scrut.op == syntax.tkind.TK_STAR) {
if (!istaggedtype(c, scrut)) { return nil; };
return scrut;
};
return nil;
};
// matchspillsz — slot size for the @match_spill scratch a non-ident
// scrutinee lands in. Mirrors cstage's `slot_size = (su->kind ==
// TY_TAGGED) ? su->size : 16` (cmd/w6c/cgen.c cgmatch). 16 default
// when the scrutinee type can't be resolved keeps the historical
// alloc for non-tagged / unresolved cases. Called by cgmatch at first
// use; #15 first-use+fail-loud pins this size per fn.
fn matchspillsz(c: *cgen, scrutt: *syntax.node) i32 = {
if (scrutt == nil) { return 16; };
let sz: i32 = slotsize(c, scrutt);
if (sz <= 0) { return 16; };
return sz;
};
// structparamsize — bytes occupied by a user-defined by-value struct
// param if it fits in 1-2 SysV integer eightbytes (cstage cgen.c
// struct_arg_size mirror; gates on size <= 16). Returns 0 for non-
// struct types or oversized structs so callers can fall through to
// other dispatch arms. Pre-#11 the wwstage prologue had no struct
// branch — user-defined struct params dropped through to the 8B
// scalar catch-all, the second-half value registers (DX/CX) were
// never spilled, and field reads from the under-allocated slot
// trailed into the saved-BP word.
fn structparamsize(c: *cgen, t: *syntax.node) i32 = {
if (c == nil) { return 0; };
let r: *syntax.node = resolvetype(c, t);
if (r == nil) { return 0; };
if (r.kind != syntax.nkind.N_TNAME) { return 0; };
let nm: str = r.str;
if (syntax.streq(nm, "str")) { return 0; };
if (aliasprimsize(c, nm) > 0) { return 0; };
let si: *structinfo = structlookup(c, nm);
if (si == nil) { return 0; };
if (si.totsize <= 0) { return 0; };
if (si.totsize > 16) { return 0; };
return si.totsize;
};
// aggargsize — byte size of a by-value aggregate (struct OR array) call
// arg, else 0 (#271, mirror of cstage aggarg_size). The size axis the
// ≤16B-struct structparamsize carve-out doesn't cover: arrays of any
// size and structs > 16B. Reads the stamped tinfo size (the type table,
// byte-id with cstage Type.size).
fn aggargsizetn(t: *syntax.tinfo) i32 = {
if (t == nil) { return 0; };
let u: *syntax.tinfo = t;
u = tichase(u);
if (u == nil) { return 0; };
if (u.kind == syntax.tykind.TY_STRUCT || u.kind == syntax.tykind.TY_ARRAY) {
return u.size: i32;
};
return 0;
};
// taggedmemargsize — #38b: a tagged-union arg past the 6-reg register
// convention (>48B slot, where the register transport's cap trips) is
// MEMORY-class: the caller stages the whole slot on the outgoing stack
// below every register-class word and the callee reads it in place at
// positive BP offsets. Mirror of cstage tagged_memarg_size; ABI shape
// per ref/qbe/amd64/sysv.c:80-85 (inmem) / :411-426 (stack blit). The
// ≤48B register convention is pinned in-tree (test/926 boundary rows).
fn taggedmemargsize(t: *syntax.tinfo) i32 = {
if (t == nil) { return 0; };
let u: *syntax.tinfo = t;
u = tichase(u);
if (u == nil) { return 0; };
if (u.kind != syntax.tykind.TY_TAGGED) { return 0; };
if (u.nullable != 0) { return 0; };
// SysV supplies 6 integer argument registers (DI..R9); the 8-byte
// words below encode the same capacity as cstage tagged_arg_size.
if (u.size: i32 <= 6 * 8) { return 0; };
return u.size: i32;
};
fn nodeisaggarg(n: *syntax.node) bool = {
if (n == nil) { return false; };
return aggargsizetn(n.type_: *syntax.tinfo) > 0;
};
// aggargfloatstop — true iff the arg is a ≤16B struct with any float
// field (#271/#165). Such a struct from a non-ident source would need
// the SSE eightbyte transport the GP aggregate push/drain can't model;
// both stages loud-stop on it. Same predicate as the cstage Tfield
// fld_isfloat walk (cmd/w6c/cgen.c #271 push arm).
fn aggargfloatstop(n: *syntax.node) bool = {
if (n == nil) { return false; };
let st: *syntax.tinfo = n.type_: *syntax.tinfo;
st = tichase(st);
if (st == nil) { return false; };
if (st.kind != syntax.tykind.TY_STRUCT) { return false; };
if (st.size: i32 > 16) { return false; };
let f: *syntax.tfield = st.fields;
for (f != nil) {
if (syntax.typeisfloat(f.type_)) { return true; };
f = f.tnext;
};
return false;
};
// tinfoaggfloat — does an aggregate tinfo carry ANY float leaf
// (recursively through struct fields / array element / tuple
// positionals)? The #12 producer (cgtrytaggedshift) loud-stops a
// float-bearing aggregate success variant: the union return places a
// float eightbyte in the SSE class (X0/X1) which the GP {AX,DX,CX}
// payload shuffle cannot reach (mirror #11/#165). Conservative — ANY
// float, not a per-eightbyte SSE classify like structfloatclass — a
// loud-stop only needs to refuse, not transport. Mirrors cstage
// agg_has_float (cmd/w6c/cgen.c).
fn tinfoaggfloat(t: *syntax.tinfo) bool = {
if (t == nil) { return false; };
let u: *syntax.tinfo = tichase(t);
if (u == nil) { return false; };
if (syntax.typeisfloat(u)) { return true; };
if (u.kind == syntax.tykind.TY_STRUCT) {
let fi: *syntax.tfield = u.fields;
for (fi != nil) {
if (tinfoaggfloat(fi.type_)) { return true; };
fi = fi.tnext;
};
return false;
};
if (u.kind == syntax.tykind.TY_ARRAY) {
return tinfoaggfloat(u.sub);
};
if (u.kind == syntax.tykind.TY_TUPLE) {
let pe: *syntax.ttupleelem = u.tupleelems;
for (pe != nil) {
if (tinfoaggfloat(pe.type_)) { return true; };
pe = pe.tnext;
};
return false;
};
return false;
};
// structfloatclass — SysV per-eightbyte classification for the #165
// float-bearing-struct param case (param twin of #171's struct return;
// classifies per-eightbyte, not #163's per-element). Returns 0 when the
// struct does NOT qualify — the caller keeps the all-GP transport, which
// is correct + byte-identical there — for: not a <=16B struct; an all-
// integer layout (no float to route); an f32 field; >1 float packed in
// one eightbyte; a float straddling the 8-byte SysV eightbyte boundary;
// or an aggregate field (SysV would recurse, out of scope). Otherwise a
// packed result whose low bits hold the eightbyte count nb (1|2) and bit
// (4+e) marks eightbyte e SSE-class (a lone f64). Qualifies iff every
// eightbyte is pure-INT or a lone f64 AND at least one is f64. f32 /
// sub-eightbyte packing deferred (#165b). Mirrors cstage
// struct_float_class (cmd/w6c/cgen.c).
fn structfloatclass(c: *cgen, t: *syntax.node) i32 = {
if (c == nil) { return 0; };
let r: *syntax.node = resolvetype(c, t);
if (r == nil) { return 0; };
if (r.kind != syntax.nkind.N_TNAME) { return 0; };
let nm: str = r.str;
if (syntax.streq(nm, "str")) { return 0; };
if (aliasprimsize(c, nm) > 0) { return 0; };
let si: *structinfo = structlookup(c, nm);
if (si == nil) { return 0; };
if (si.totsize <= 0) { return 0; };
if (si.totsize > 16) { return 0; };
// SysV classifies aggregates in 8-byte eightbytes; 8 is the
// eightbyte stride, not a type footprint.
let nb: i32 = 1;
if (si.totsize > 8) { nb = 2; };
let nflt0: i32 = 0; let nflt1: i32 = 0;
let nint0: i32 = 0; let nint1: i32 = 0;
let fi: *fieldinfo = si.fields;
for (fi != nil) {
let foff: i32 = fi.foff;
let fsz: i32 = fi.fsz;
let e: i32 = foff / 8;
if (e < 0) { return 0; };
if (e >= nb) { return 0; };
if (isfloattype(c, fi.tnode)) {
if (isf32type(c, fi.tnode)) { return 0; };
if ((foff & 7) != 0) { return 0; };
if (fsz != 8) { return 0; };
if (e == 0) { nflt0 += 1; } else { nflt1 += 1; };
} else {
if (isslicetype(c, fi.tnode)) { return 0; };
if (isstrtype(c, fi.tnode)) { return 0; };
if (istaggedtype(c, fi.tnode)) { return 0; };
if (structparamsize(c, fi.tnode) > 0) { return 0; };
// Alias-aware array/tuple reject, mirroring cstage's
// NAMED-peeled TY_ARRAY/TY_TUPLE (cgen.c struct_float_class).
// A direct-AST-kind N_TARRAY test misses an aliased array
// and every tuple field; the fsz>8 guard below also lets a
// <=8B one slip, so such a struct would wrongly SSE-route on
// this stage but stay GP on cstage (a #165b leak).
let rf: *syntax.node = resolvetype(c, fi.tnode);
if (rf != nil) {
if (rf.kind == syntax.nkind.N_TARRAY) { return 0; };
if (rf.kind == syntax.nkind.N_TTUPLE) { return 0; };
};
if (fsz > 8) { return 0; };
if ((foff + fsz - 1) / 8 != e) { return 0; };
if (e == 0) { nint0 += 1; } else { nint1 += 1; };
};
fi = fi.finext;
};
let enc: i32 = nb;
let hasfloat: bool = false;
if (nflt0 == 1 && nint0 == 0) { enc += 16; hasfloat = true; }
else { if (nflt0 != 0) { return 0; }; };
if (nb == 2) {
if (nflt1 == 1 && nint1 == 0) { enc += 32; hasfloat = true; }
else { if (nflt1 != 0) { return 0; }; };
};
if (!hasfloat) { return 0; };
return enc;
};
// structfloatclassti — tinfo twin of structfloatclass for sites with
// no declared tnode (the stamped element type behind an append CALL
// source, #34). Same gates and encoding as the tnode form: 0, or
// nb + 16/32 per SSE eightbyte. Cite cstage cgen.c struct_float_class
// (Type-keyed there, so the tinfo walk IS the aligned shape).
fn structfloatclassti(t: *syntax.tinfo) i32 = {
t = tichase(t);
if (t == nil || t.kind != syntax.tykind.TY_STRUCT) { return 0; };
let sz: i32 = t.size: i32;
if (sz <= 0 || sz > 16) { return 0; };
// SysV classifies aggregates in 8-byte eightbytes; 8 is the
// eightbyte stride, not a type footprint.
let nb: i32 = 1;
if (sz > 8) { nb = 2; };
let nflt0: i32 = 0; let nflt1: i32 = 0;
let nint0: i32 = 0; let nint1: i32 = 0;
let fi: *syntax.tfield = t.fields;
for (fi != nil) {
let fu: *syntax.tinfo = tichase(fi.type_);
if (fu == nil) { return 0; };
let foff: i32 = fi.offset: i32;
let fsz: i32 = fu.size: i32;
let e: i32 = foff / 8;
if (e < 0 || e >= nb) { return 0; };
if (fu.kind == syntax.tykind.TY_F32) { return 0; };
if (fu.kind == syntax.tykind.TY_F64) {
if (foff - (foff / 8) * 8 != 0) { return 0; };
if (fsz != 8) { return 0; };
if (e == 0) { nflt0 += 1; } else { nflt1 += 1; };
} else {
if (fu.kind == syntax.tykind.TY_STRUCT) { return 0; };
if (fu.kind == syntax.tykind.TY_ARRAY) { return 0; };
if (fu.kind == syntax.tykind.TY_SLICE) { return 0; };
if (fu.kind == syntax.tykind.TY_STR) { return 0; };
if (fu.kind == syntax.tykind.TY_TAGGED) { return 0; };
if (fu.kind == syntax.tykind.TY_TUPLE) { return 0; };
if (fsz > 8) { return 0; };
if ((foff + fsz - 1) / 8 != e) { return 0; };
if (e == 0) { nint0 += 1; } else { nint1 += 1; };
};
fi = fi.tnext;
};
let enc: i32 = nb;
let hasfloat: bool = false;
if (nflt0 == 1 && nint0 == 0) { enc += 16; hasfloat = true; }
else { if (nflt0 != 0) { return 0; }; };
if (nb == 2) {
if (nflt1 == 1 && nint1 == 0) { enc += 32; hasfloat = true; }
else { if (nflt1 != 0) { return 0; }; };
};
if (!hasfloat) { return 0; };
return enc;
};
// istaggedtype — alias-aware. Reads stamped tinfo so `T`,
// `type alias = (A|B)`, `type error = !(invalid|overflow)` all
// resolve to TY_TAGGED — tinfofornode handles the N_TBANG unwrap
// (check.ww:1145) so we don't re-walk it here. Cite cstage cgen.c
// (`type_chase_named` + TY_TAGGED). Collapsed per A.6.3b (#46).
fn istaggedtype(c: *cgen, t: *syntax.node) bool = {
if (t == nil) { return false; };
return syntax.typeistagged(t.type_: *syntax.tinfo);
};
// tnodeisagg — struct/array/tuple kind off the checker-STAMPED tinfo
// (the #49 funnel predicate; #209/#211 discipline — never tnode
// names). Mirror of cstage's chase-then-kind test at the fill /
// assign aggregate arms.
fn tnodeisagg(t: *syntax.node) bool = {
if (t == nil) { return false; };
let u: *syntax.tinfo = t.type_: *syntax.tinfo;
u = tichase(u);
if (u == nil) { return false; };
if (u.kind == syntax.tykind.TY_STRUCT) { return true; };
if (u.kind == syntax.tykind.TY_ARRAY) { return true; };
return u.kind == syntax.tykind.TY_TUPLE;
};
// isfloattype — f32 / f64 / untyped_float (alias-aware). Cite cstage
// cgen.c:117 `cg_isfloat`. Dispatches MOVSS/MOVSD-shaped paths across
// cglet, cgident, cgassign, cgbin, cgcast, cgcall, cgreturn, fn-
// prologue. Collapsed per A.6.3b (#46).
export fn isfloattype(c: *cgen, t: *syntax.node) bool = {
if (t == nil) { return false; };
return syntax.typeisfloat(t.type_: *syntax.tinfo);
};
// isf32type — narrower: true only for f32 (after alias chase). Cite
// cstage cgen.c:188 `type_isf32`. Picks MOVSS vs MOVSD and the SS-
// variant arithmetic / cast opcodes. Collapsed per A.6.3b (#46).
export fn isf32type(c: *cgen, t: *syntax.node) bool = {
if (t == nil) { return false; };
return syntax.typeisf32(t.type_: *syntax.tinfo);
};
// isnullabletype — `(*T | void)` one-word fold per Hare's
// `(*T | null)` semantics. Cite cstage cgen.c:396 `type_isnullable`;
// the .nullable flag lands on tinfo at check.ww:1309-1318 when the
// two-variant shape matches. Collapsed per A.6.3b (#46).
export fn isnullabletype(t: *syntax.node) bool = {
if (t == nil) { return false; };
return syntax.typeisnullable(t.type_: *syntax.tinfo);
};
// nullableptrtag — 0-based index of the *T variant in a nullable
// union. Mirror of cstage cgen.c:404-416 `nullable_ptr_tag`: linear
// scan ti.params, strip TY_NAMED on each variant, return idx of first
// TY_PTR. Phase 1 (26724fe) populated the chain in tinfofornode's
// TTAGGED arm so this walk could retire the AST-keyed predecessor.
export fn nullableptrtag(t: *syntax.node) i32 = {
if (t == nil) { return 0; };
let ti: *syntax.tinfo = t.type_: *syntax.tinfo;
if (ti == nil) { return 0; };
// #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.
ti = tichase(ti);
if (ti == nil) { return 0; };
if (ti.kind != syntax.tykind.TY_TAGGED) { return 0; };
let p: *syntax.tparam = ti.params;
let i: i32 = 0;
for (p != nil) {
let vt: *syntax.tinfo = p.type_;
if (vt != nil) {
// A single-level inspection is sufficient here (batch-2
// c3-B2, 018ef66): constructible variant params never carry
// nested NAMED layers at this scan; the cs twin relies on
// the same invariant.
if (vt.kind == syntax.tykind.TY_NAMED) { vt = vt.under; };
if (vt != nil) {
if (vt.kind == syntax.tykind.TY_PTR) { return i; };
};
};
p = p.tnext;
i += 1;
};
return 0;
};
// voidvariantindex — find the 0-based index of the `void` variant in a
// tagged-union type, -1 if absent. Used by cgreturn to map bare `return;`
// in a tagged-union-returning fn to the void variant's tag.
//
// #1/#3 (F1 fold): reads the NORMALIZED variant chain (ti.params, which
// tinfofornode now never-drops + dedups), NOT the raw AST tagged.list. The
// construct/match tag numbering already rides ti.params, so once F1 dedups
// it, a deduped union with a void variant (e.g. `(i32|i32|void)`) would
// desync its bare-`return;` void tag from match's if this stayed AST-keyed.
// Mirrors cstage cg_tag_for_variant(rt, ty_void) (cmd/w6c/cgen.c:900-911):
// chase NAMED, scan params, match bare TY_VOID (not `!void`, carried by the
// tparam iserror flag).
fn voidvariantindex(tagged: *syntax.node) i32 = {
if (tagged == nil) { return -1; };
let ti: *syntax.tinfo = tagged.type_: *syntax.tinfo;
if (ti == nil) { return -1; };
ti = tichase(ti);
if (ti == nil) { return -1; };
if (ti.kind != syntax.tykind.TY_TAGGED) { return -1; };
let p: *syntax.tparam = ti.params;
let idx: i32 = 0;
for (p != nil) {
let vt: *syntax.tinfo = p.type_;
if (vt != nil && vt.kind == syntax.tykind.TY_VOID && !p.iserror) {
return idx;
};
p = p.tnext;
idx += 1;
};
return -1;
};
// taggedvariantindex — given the tagged-union type expr and the
// returned value's surface type, find the matching variant's 0-based
// index. Compare by exact type name first; if no match, fall back to
// "any str-shape variant matches an str-typed value".
fn taggedvariantindex(c: *cgen, tagged: *syntax.node, rhs: *syntax.node) i32 = {
if (tagged == nil) { return -1; };
return taggedvariantindext(c, tagged.type_: *syntax.tinfo, rhs);
};
// taggedvariantindext — tinfo-keyed core of taggedvariantindex. Given
// the dst tagged tinfo `du` (NAMED-peeled internally, gated TY_TAGGED)
// and the source value node, returns the 0-based variant index. #68: the
// tagged-store machinery reads tinfo directly (no type node), so the
// node-keyed taggedvariantindex delegates here off `tagged.type_`.
//
// #66 Phase-N step 3: match the value's stamped type against the variant
// types by typeeq (flatvariantidxt), replacing the rhstargetname surface-
// name compare. #33: untyped values now resolve in flatvariantidxt's
// pass 1 (cgvariantmatch's tyassignableuntyped arm, the cg_variant_match
// :801 mirror); the str/slice shape scan below covers the remaining
// LOOSE sources (concrete types that typeeq no variant).
fn taggedvariantindext(c: *cgen, du: *syntax.tinfo, rhs: *syntax.node) i32 = {
if (du == nil) { return -1; };
if (rhs == nil) { return -1; };
let ti: *syntax.tinfo = du;
ti = tichase(ti);
if (ti == nil) { return -1; };
if (ti.kind != syntax.tykind.TY_TAGGED) { return -1; };
let r: i32 = flatvariantidxt(ti, rhs.type_: *syntax.tinfo, false);
if (r >= 0) { return r; };
// Shape fallback: classify rhs as (str, slice, scalar/other) and
// pick the first variant of matching shape. Covers LOOSE concrete
// sources that typeeq no variant (untyped sources resolve above
// via tyassignableuntyped since #33); the slice axis keeps a
// (u8 | []u8) widen off the leading scalar variant (task #19).
// tinfo.params is already spread-flattened (#61a — `...inner`
// inlined in declaration order), so the old N_TTAGGED.list spread-
// walk collapses to a flat scan over p.type_.
let wantstr: bool = nodeisstr(c, rhs);
let wantslice: bool = nodeisslice(c, rhs);
let p: *syntax.tparam = ti.params;
let idx: i32 = 0;
for (p != nil) {
let vt: *syntax.tinfo = p.type_;
let visstr: bool = syntax.typeisstr(vt);
let visslice: bool = syntax.typeisslice(vt);
if (visstr == wantstr && visslice == wantslice) { return idx; };
p = p.tnext;
idx += 1;
};
return -1;
};
// flatvariantidx — flat 0-based index of the variant whose type matches
// the pattern node `pat`, by typeeq on the stamped tinfos. Reads the
// pre-flattened variant chain off tinfo.params (#61a — `...inner`
// spreads already inlined in declaration order); peels TY_NAMED then
// gates TY_TAGGED.
//
// #66 Phase-N step 3 (THE FLIP, user-ruled B-full): match by
// typeeq(p.type_, pat.type_) — nominal identity carried by the per-decl
// TY_NAMED ptr — instead of the surface-name compare. So `type linerr =
// !str` ≠ str and a cross-module `a.T` ≠ `b.T` are now distinguished.
// Mirrors cstage cg_variant_match (cmd/w6c/cgen.c:451): both-NAMED →
// ptr-id (typeeq, typ.ww:514), one-NAMED → kind mismatch → false. ww has
// no type_assignable, so the untyped/loose arm (cg_variant_match's first
// branch) lives in the caller's str/slice shape fallback, not here.
fn flatvariantidx(c: *cgen, tagged: *syntax.node, pat: *syntax.node) i32 = {
if (tagged == nil) { return -1; };
if (pat == nil) { return -1; };
return flatvariantidxt(tagged.type_: *syntax.tinfo, pat.type_: *syntax.tinfo, false);
};
// flatvariantidxt — tinfo-keyed core of flatvariantidx: flat 0-based
// index of the variant whose type typeeq's `want`, over the pre-
// flattened tinfo.params chain (#61a). Peels TY_NAMED then gates
// TY_TAGGED. #68: the tagged-store machinery reads tinfo directly and
// has no type node to hand the node-keyed flatvariantidx, so the typeeq
// core lives here; flatvariantidx + taggedvariantindext + cgwidentagremap
// all funnel through it. Mirrors cstage cg_tag_for_variant
// (cmd/w6c/cgen.c:503) + cg_variant_match's both-NAMED ptr-id / typeeq
// arm (:451).
// exactonly (#95-c3): the is/as ACCEPTANCE gate (check.ww route) wants
// only nominal variant membership — pass 1. The chain/structural
// tag-synthesis arms below and their >=2 ambiguity os.exit belong to the
// cgen WIDEN consumer; routing the checker through them widened is/as
// acceptance (cs!=ww) and surfaced a cgen fatal mid-check (#107). Two
// consumers, two modes — not a wrapper. cstage has no twin: its is/as
// gate (check.c:2036) never calls cg_tag_for_variant, so cg_tag_for_variant
// stays full-only there.
fn flatvariantidxt(tagged: *syntax.tinfo, want: *syntax.tinfo, exactonly: bool) i32 = {
if (want == nil) { return -1; };
let ti: *syntax.tinfo = tagged;
ti = tichase(ti);
if (ti == nil) { return -1; };
if (ti.kind != syntax.tykind.TY_TAGGED) { return -1; };
// Pass 1: exact match (NAMED-vs-NAMED typeeq, tagged-vs-tagged, bare
// typeeq). Exact matches take precedence and need no guard — distinct
// variants don't exact-match the same source.
let p: *syntax.tparam = ti.params;
let idx: i32 = 0;
for (p != nil) {
if (cgvariantmatch(p.type_, want)) { return idx; };
p = p.tnext;
idx += 1;
};
if (exactonly) { return -1; };
// Pass 1b (#95): NAMED source, no exact variant — chain membership.
// An alias IS-A every type on its NAMED chain (ali2 is-a ali is-a
// base), so declaring the variant as `ali` admits any source whose
// chain shares a node with ali's. Two linear NAMED chains intersect
// iff they share their chased bottom node (.ai/ken-95-oracle.md §1),
// so membership reduces to pointer identity of the chased ends —
// tichase is the blessed chase, no raw hops. Variants are counted
// UNGATED (bare prims are type-table singletons, so a bare variant
// node can BE the source's bottom): that keeps the guard ≡ harec's
// nassign>=2 → NULL (ref/harec/src/types.c:734-738; the P1-exact
// short-circuit is pass 1 above). >=2 chain hits cannot be
// disambiguated once the nominal-lossy model collapses the chain —
// hard-error (drew's ambiguity proviso extended to the chained
// set). cs twin cg_tag_for_variant fused in this commit.
if (want.kind == syntax.tykind.TY_NAMED) {
let sb: *syntax.tinfo = tichase(want);
if (sb == nil) { return -1; };
let q: *syntax.tparam = ti.params;
let qi: i32 = 0;
let found: i32 = -1;
let n: i32 = 0;
for (q != nil) {
if (q.type_ != nil && tichase(q.type_) == sb) {
if (found < 0) { found = qi; };
n += 1;
};
q = q.tnext;
qi += 1;
};
if (n >= 2) {
let msg: str = "flatvariantidxt: source alias chain reaches >=2 variants — ambiguous without nominal layout (#95)\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
if (found >= 0) { return found; };
// c2 (#95): no chain hit (found/n are still -1/0 here) —
// structural fallback on the chased ends. harec interns bare
// composites structurally (type_hash, ref/harec/src/
// types.c:72-81), so a nominally-unrelated structurally-
// equal decl DEALIASES TO THE SAME NODE there and the
// assignability arm accepts via `to == from`
// (types.c:1000-1002) — acceptance is definitional, not an
// arm we could misread. Our store does not intern, so the
// pointer compare of pass 1b misses it; chased typeeq is the
// non-interned rendering of the same rule. EQUALITY only —
// no type_is_assignable scalar import. The >=2 hard-error is
// the nominal-lossy-model rendering of a case harec cannot
// represent (two structurally-identical variants intern to
// ONE type — a union cannot contain it twice), not a harec
// deviation. cs twin cg_tag_for_variant fused in this commit.
q = ti.params;
qi = 0;
for (q != nil) {
if (q.type_ != nil && syntax.typeeq(tichase(q.type_), sb)) {
if (found < 0) { found = qi; };
n += 1;
};
q = q.tnext;
qi += 1;
};
if (n >= 2) {
let msg2: str = "flatvariantidxt: source structurally matches >=2 variants — ambiguous without nominal layout (#95)\n";
os.write(2, msg2.ptr, msg2.len: u64);
os.exit(1);
};
return found;
};
// Pass 2 (#15): no exact variant matched — structurally match a BARE
// source against a NAMED-alias variant (bare *vtable into the
// `stream` (= *vtable) variant of `(file | stream)`). The bare side
// has no nominal identity, so structure is the only discriminator;
// without this the widen found no variant and defaulted to tag 0,
// miscompiling emitbytes' io.write(&cgoutstream.vt). Exact-first
// (pass 1) keeps a bare `i64` into `(i64 | oserror)` binding the
// exact `i64`. drew's proviso: guard the structural fallback like the
// #218 nested-widen site — a bare source matching >=2 NAMED variants
// needs nominal layout to disambiguate, so hard-error.
if (want.kind != syntax.tykind.TY_NAMED) {
let q: *syntax.tparam = ti.params;
let qi: i32 = 0;
let found: i32 = -1;
let n: i32 = 0;
for (q != nil) {
// Full chase (F2a batch-4 c2): the old one-level
// unwrap missed a chained ptr-alias variant
// (type a=*X; type b=a) — every pass fell through
// and the widen defaulted to tag 0, SILENT. The
// TY_NAMED gate keeps bare variants in pass-1's
// exact domain; drew's >=2-candidate hard-error
// below now guards the CHASED match set. cs twin
// cg_tag_for_variant fused in this commit (probe:
// both-wrong-identical pre-fix).
let pu: *syntax.tinfo = q.type_;
if (pu != nil && pu.kind == syntax.tykind.TY_NAMED
&& syntax.typeeq(tichase(pu), want)) {
if (found < 0) { found = qi; };
n += 1;
};
q = q.tnext;
qi += 1;
};
if (n >= 2) {
let msg: str = "flatvariantidxt: bare source structurally matches >=2 NAMED variants — ambiguous without nominal layout (#15/#218/#199b/#10)\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
return found;
};
return -1;
};
// tyassignableuntyped — can a value slot of type `dst` HOLD an untyped
// source value? The untyped→typed subset of cstage type_assignable
// (cmd/wcc/type.c:355-370), plus its concrete→tagged variant drill
// (type.c:316-324) for a variant that is itself a union. ww's checker
// isassignable is node-keyed (AST type exprs), so the tinfo-keyed
// widen/match funnel needs this focused mirror (#33).
fn tyassignableuntyped(dst: *syntax.tinfo, want: *syntax.tinfo) bool = {
if (dst == nil) { return false; };
if (want == nil) { return false; };
// c4 (rob RE-RULE 2026-06-05): FULL chase, not the one-level unwrap
// — harec type_is_assignable dealiases the dst transitively when it
// is not tagged and keeps the ORIGINAL dst for the tagged variant
// drill, taggedness detected via the full chase (ref/harec/src/
// types.c:989-996). The one-level unwrap left a 2-level named bool
// alias variant silently mis-tagged 0 (the str twin was masked by
// taggedvariantindext's shape fallback). Aligns ww UP to the cs
// twin cmd/wcc/type.c:369-385, harec-shaped since F1 9bd0d8b.
let du: *syntax.tinfo = tichase(dst);
if (du != nil && du.kind == syntax.tykind.TY_TAGGED) {
// concrete→tagged drill (type.c:316-324): a NESTED tagged
// variant compares by type_eq there — never equal to an
// untyped source — so only non-tagged variants recurse, on
// the variant's ORIGINAL type; the variant's taggedness is
// detected via the full chase (was one-level: a 2-level
// alias-tagged variant slipped INTO the recursion, diverging
// from cs's checker-level #199α reject — see the c4 finding).
let p: *syntax.tparam = du.params;
for (p != nil) {
let pu: *syntax.tinfo = tichase(p.type_);
let nestedtagged: bool = false;
if (pu != nil) {
if (pu.kind == syntax.tykind.TY_TAGGED) { nestedtagged = true; };
};
if (!nestedtagged) {
if (tyassignableuntyped(p.type_, want)) { return true; };
};
p = p.tnext;
};
return false;
};
// type.c:369-385 — INT/FLOAT/RUNE run on the UNPEELED dst exactly
// like cs (typeisnum/typeisfloat/typeisint self-recurse TY_NAMED);
// STR/BOOL/NIL read the chased du like cs reads its chased du.
if (want.kind == syntax.tykind.TY_UNTYPED_INT) { return syntax.typeisnum(dst); };
if (want.kind == syntax.tykind.TY_UNTYPED_FLOAT) { return syntax.typeisfloat(dst); };
if (want.kind == syntax.tykind.TY_UNTYPED_STR) {
if (du == nil) { return false; };
return du.kind == syntax.tykind.TY_STR;
};
if (want.kind == syntax.tykind.TY_UNTYPED_RUNE) {
if (syntax.typeisint(dst)) { return true; };
return dst.kind == syntax.tykind.TY_RUNE;
};
if (want.kind == syntax.tykind.TY_UNTYPED_BOOL) {
if (du == nil) { return false; };
return du.kind == syntax.tykind.TY_BOOL;
};
if (want.kind == syntax.tykind.TY_UNTYPED_NIL) {
if (du == nil) { return false; };
if (du.kind == syntax.tykind.TY_PTR) { return true; };
if (du.kind == syntax.tykind.TY_SLICE) { return true; };
if (du.kind == syntax.tykind.TY_CHAN) { return true; };
return du.kind == syntax.tykind.TY_FN;
};
return false;
};
// cgvariantmatch — does a source value of type `want` tag as variant
// `vt` in a tagged-union dispatch? Mirrors cstage cg_variant_match
// (cmd/w6c/cgen.c):
// - untyped source → first variant that can HOLD it (cgen.c:801 →
// type_assignable; #33 — see tyassignableuntyped)
// - both NAMED → nominal ptr-id (typeeq line 545 = same ptr only)
// - exactly one NAMED → #218 nominal lost: fall back to structural
// equality of the two unwrapped tagged unions, so an outer widen of
// a NAMED multi-variant union into an enclosing union computes its
// tag (project tinfo_lossy_nominal). Sound only while the model is
// nominal-lossy; the collision guard in cgwidentaggedstorebp enforces
// the invariant for when #199b/B-full lands true nominal layout.
// - neither NAMED → structural typeeq
fn cgvariantmatch(vt: *syntax.tinfo, want: *syntax.tinfo) bool = {
if (vt == nil) { return false; };
if (want == nil) { return false; };
// #33: pre-fix an untyped scalar fell past the typeeq passes to
// taggedvariantindext's str/slice SHAPE fallback, whose first
// non-str/slice variant can be `void` — a bare
// `let e: (void | size) = 5` stored tag 0 while the is/as side
// resolved `size` to 1 (wwstage-only; cstage resolves here).
if (syntax.typeisuntyped(want)) { return tyassignableuntyped(vt, want); };
if (vt.kind == syntax.tykind.TY_NAMED && want.kind == syntax.tykind.TY_NAMED) {
return syntax.typeeq(vt, want);
};
if (vt.kind == syntax.tykind.TY_NAMED || want.kind == syntax.tykind.TY_NAMED) {
let vu: *syntax.tinfo = vt;
vu = tichase(vu);
let wu: *syntax.tinfo = want;
wu = tichase(wu);
if (vu != nil && wu != nil
&& vu.kind == syntax.tykind.TY_TAGGED
&& wu.kind == syntax.tykind.TY_TAGGED) {
return syntax.typeeq(vu, wu);
};
return false;
};
return syntax.typeeq(vt, want);
};
// cgvariantstructmatch — structural equality ignoring nominal identity
// (peel NAMED, then typeeq). #218 collision guard: counts how many dst
// variants share the source's *shape*; ≥2 means the structural fallback
// could not disambiguate them once nominal identity is lost. Mirrors
// cstage cg_variant_struct_match (cmd/w6c/cgen.c).
fn cgvariantstructmatch(vt: *syntax.tinfo, want: *syntax.tinfo) bool = {
let vu: *syntax.tinfo = vt;
vu = tichase(vu);
let wu: *syntax.tinfo = want;
wu = tichase(wu);
if (vu == nil) { return false; };
if (wu == nil) { return false; };
return syntax.typeeq(vu, wu);
};
// flatslicevariantidx — flat 0-based index of a slice-shape variant in
// `tagged`. Prefers the variant whose element typeeq's the pattern
// element `elem`; falls back to the first slice-shape slot when no exact
// element match is found (the untyped/loose arm — ww has no
// type_assignable). Reads the flattened tinfo.params chain (#61a); peels
// TY_NAMED then gates TY_TAGGED. The slice axis exists because a scalar-
// vs-`[]T` distinction has no surface name to key on (task #19).
//
// #66 Phase-N step 3: element compare flips from surface-name to
// typeeq(p.type_.sub, elem.type_). Mirrors cstage cg_tag_for_variant
// over Type->params. Returns -1 when no slice variant exists.
fn flatslicevariantidx(c: *cgen, tagged: *syntax.node, elem: *syntax.node) i32 = {
if (tagged == nil) { return -1; };
let ti: *syntax.tinfo = tagged.type_: *syntax.tinfo;
ti = tichase(ti);
if (ti == nil) { return -1; };
if (ti.kind != syntax.tykind.TY_TAGGED) { return -1; };
let want: *syntax.tinfo = nil;
if (elem != nil) { want = elem.type_: *syntax.tinfo; };
let fallback: i32 = -1;
let p: *syntax.tparam = ti.params;
let idx: i32 = 0;
for (p != nil) {
let vt: *syntax.tinfo = p.type_;
if (vt != nil) {
if (syntax.typeisslice(vt)) {
if (fallback < 0) { fallback = idx; };
if (want != nil) {
let su: *syntax.tinfo = vt;
su = tichase(su);
if (su != nil) {
if (syntax.typeeq(su.sub, want)) { return idx; };
};
};
};
};
p = p.tnext;
idx += 1;
};
return fallback;
};
// cgwidentagremap — when widening from one tagged union to a wider one,
// rewrite the source's variant tag at slot_off+0 to use the destination's
// variant indices. No-op when src and dst index orders coincide.
//
// #68: both `du` (dst) and `su` (src) are now the tagged tinfos — peel
// TY_NAMED then walk su.params, mapping each source variant to its dst
// index by typeeq (flatvariantidxt). Mirrors cg_widen_tag_remap
// (cmd/w6c/cgen.c:1177) over su->params + cg_tag_for_variant (:503).
fn cgwidentagremap(c: *cgen, du: *syntax.tinfo, su: *syntax.tinfo, slot_off: i32) void = {
let dt: *syntax.tinfo = du;
dt = tichase(dt);
if (dt == nil) { return; };
if (dt.kind != syntax.tykind.TY_TAGGED) { return; };
let st: *syntax.tinfo = su;
st = tichase(st);
if (st == nil) { return; };
if (st.kind != syntax.tykind.TY_TAGGED) { return; };
let identity: bool = true;
let p: *syntax.tparam = st.params;
let idx: i32 = 0;
for (p != nil) {
let di: i32 = flatvariantidxt(dt, p.type_, false);
if (di < 0) { di = 0; };
if (di != idx) { identity = false; p = nil; }
else { p = p.tnext; idx += 1; };
};
if (identity) { return; };
let done: str = mklabel(c, "remap_done");
emitline("\tMOVQ\t");
emitoff(slot_off: i64);
emitline("(BP), AX\n");
p = st.params;
idx = 0;
for (p != nil) {
let next: str = mklabel(c, "remap_next");
let di: i32 = flatvariantidxt(dt, p.type_, false);
if (di < 0) { di = 0; };
emitline("\tCMPQ\t$");
emitint(idx: i64);
emitline(", AX\n");
emitline("\tJNE\t");
emitline(next);
emitline("\n");
emitline("\tMOVQ\t$");
emitint(di: i64);
emitline(", AX\n");
emitline("\tMOVQ\tAX, ");
emitoff(slot_off: i64);
emitline("(BP)\n");
emitline("\tJMP\t");
emitline(done);
emitline("\n");
emitlabel(next);
p = p.tnext;
idx += 1;
};
emitlabel(done);
return;
};
// rhsisstructpayload — is `src` a struct value (literal or local ident
// of a struct type)? Returns the struct name, or empty str. Only true
// when the name is registered in c.structs — `!void` / `!i32` aliases
// share the N_STRUCTLIT / N_TNAME shape but aren't structs, and must
// fall through to the scalar/str/tagged-source paths instead.
fn rhsstructpayload(c: *cgen, src: *syntax.node) str = {
let empty: str;
empty.ptr = nil; empty.len = 0;
if (src == nil) { return empty; };
if (src.kind == syntax.nkind.N_STRUCTLIT) {
let trefn: *syntax.node = src.lhs;
if (trefn != nil) {
// #92: bare structlookup missed an alias-named literal
// — `ali{...}` into a union fell to the scalar widen
// arm, word0-only payload (the class #62 L2 closed for
// the N_IDENT-local arm below). Same funnel: the
// REGISTERED name keeps every consumer's re-lookup
// hitting. Base spellings: structlookup hits at the
// chain entry and si.sname == the literal's own name —
// same string out, same asm.
let si: *structinfo = structlookupchain(c, trefn);
if (si != nil) { return si.sname; };
};
return empty;
};
if (src.kind == syntax.nkind.N_IDENT) {
let lc: *local = localfindnode(c, src.str);
if (lc != nil) {
let tn: *syntax.node = lc.tnode;
if (tn != nil) {
if (tn.kind == syntax.nkind.N_TNAME) {
// #62 Layer-2 (F2 ww half): bare structlookup
// missed an alias name (ali->base), so the
// struct value fell to the SCALAR widen arm —
// word0-only box payload, words 1+ zero-filled
// (both-wrong-identical with cstage pre-F1,
// gate-blind). structlookupchain is the name-
// domain twin of cstage's su=type_chase_named
// (cg_widen_tagged_store, c138605); returning
// the REGISTERED name keeps every consumer's
// re-lookup hitting. The variant TAG still
// keys on the un-chased stamped type — the
// member's nominal identity is the alias.
let si: *structinfo = structlookupchain(c, tn);
if (si != nil) { return si.sname; };
};
};
};
};
return empty;
};
// rhstaggedsource — return the tagged-type node for `src` when src is a
// tagged-typed local ident; nil otherwise. The slot-copy path uses this
// to walk variants for tag remap.
fn rhstaggedident(c: *cgen, src: *syntax.node) *syntax.node = {
if (src == nil) { return nil; };
if (src.kind != syntax.nkind.N_IDENT) { return nil; };
let lc: *local = localfindnode(c, src.str);
if (lc == nil) { return nil; };
let tn: *syntax.node = lc.tnode;
if (!istaggedtype(c, tn)) { return nil; };
return resolvetagged(c, tn);
};
// rhstaggedabicall — does `src` produce a tagged value via the AX/DX/CX
// return ABI? True for N_CALL of a tagged-returning fn, N_INDEX of a
// tagged-element base, and N_DOT of a tagged-typed struct field (after
// #28's cgdot fix loads AX/DX/CX/R8 from the field's slot). Used to
// decide whether cgexpr/spill works for the tagged-source branch of
// cgwidentaggedstore.
fn rhstaggedabicall(c: *cgen, src: *syntax.node) bool = {
if (src == nil) { return false; };
if (src.kind == syntax.nkind.N_CALL) {
// #211: a call's result type is the CALLEE's fn-type ret, which
// the checker stamps onto this N_CALL node (check.ww N_CALL: both
// the SK_FN path and the fn-VALUE/field path set e.type_ = the
// result tinfo). Read it directly — a value-receiver fn-ptr FIELD
// call `s.f(...)` keyed by the field leaf `f` with the receiver
// VARIABLE name as the "module" otherwise mis-binds a same-named
// GLOBAL fn of different register shape (silent cs≠ww). cstage's
// sister reads u->ret off the callee type (cmd/wcc/check.c:1433,
// :1490); harec selects by interned type id, not name (ref/harec/
// src/types.c:714). Mirrors the N_DOT branch below.
if (syntax.typeistagged(src.type_: *syntax.tinfo)) { return true; };
return false;
};
if (src.kind == syntax.nkind.N_INDEX) {
// The checker stamps every N_INDEX node's type_ to the element
// tinfo (check.ww:2334-2337 indexresult) for ANY base shape —
// N_IDENT, N_DOT (`x.o[i]`), or chained N_INDEX (`m[i][j]`). Read
// it directly, mirroring cstage cg_widen_tagged_store keying on
// src->type (cmd/w6c/cgen.c:2020-2023). #261: the prior
// N_IDENT-base-only structural lookup missed N_DOT/N_INDEX bases,
// so a tagged element materialized via `x.o[i]` (correct AX/DX
// slot from cgindex) was then spilled by the scalar-widen arm,
// dropping the tag/payload-high word — silent cs≠ww.
if (syntax.typeistagged(src.type_: *syntax.tinfo)) { return true; };
return false;
};
// N_DOT of a tagged-typed struct field — cgdot loads
// AX=tag, DX=word0, CX=word1[, R8=word2], so downstream
// spill matches the call/index shapes. #58 A.6.3i-phase-2:
// read the checker-stamped n.type_ (check.ww N_DOT struct-field
// stamp) instead of re-deriving via dotfieldtnode — matches
// cstage cg_widen_tagged_store reading src->type directly
// (cmd/w6c/cgen.c:1302-1305). typeistagged(nil) is false.
if (src.kind == syntax.nkind.N_DOT) {
if (syntax.typeistagged(src.type_: *syntax.tinfo)) { return true; };
};
// Family C (#35/#46): a DEREF source is mem-based (taggedmemread,
// any size) — the widen-store's memread arm copies the box from
// the address cgexpr leaves in AX. An unwrap (`?`/`!`) source
// fills the cursor after the tagged-success payload shift (the
// cgtryprop/cgtryunw twin of cstage's kind-blind su-tagged arm).
if (src.kind == syntax.nkind.N_UN && src.op == syntax.tkind.TK_STAR) {
if (syntax.typeistagged(src.type_: *syntax.tinfo)) { return true; };
};
if (src.kind == syntax.nkind.N_TRYPROP || src.kind == syntax.nkind.N_TRYUNW) {
if (syntax.typeistagged(src.type_: *syntax.tinfo)) { return true; };
};
return false;
};
// taggedmemread — #37: does cgexpr leave this tagged expr's box in
// MEMORY (AX = box address) instead of the AX/DX/CX/R8 cursor? True
// for an N_INDEX/N_DOT read whose box exceeds the 4-reg cursor — the
// same mem-based class as an sret-classified call (which the #38b
// gates key separately on callsretsize). Every cursor-spill consumer
// must branch on this before reading AX as the tag. Mirrors cstage
// cg_tagged_memread.
// Family C (#35/#46): a DEREF source is mem-based at ANY size — the
// pointer value IS the box address, so cgun skips the scalar load
// (which carried only the tag) and consumers copy from memory. ≤32B
// INDEX/DOT keep the cursor byte-for-byte (the #37 no-drift bar);
// the nullable one-word fold stays a scalar deref.
fn taggedmemread(c: *cgen, e: *syntax.node) bool = {
if (e == nil) { return false; };
if (e.kind == syntax.nkind.N_UN && e.op == syntax.tkind.TK_STAR) {
let du: *syntax.tinfo = e.type_: *syntax.tinfo;
du = tichase(du);
if (du == nil) { return false; };
if (du.kind != syntax.tykind.TY_TAGGED) { return false; };
if (du.nullable != 0) { return false; };
return du.size: i32 > 8;
};
if (e.kind != syntax.nkind.N_INDEX && e.kind != syntax.nkind.N_DOT) { return false; };
let u: *syntax.tinfo = e.type_: *syntax.tinfo;
u = tichase(u);
if (u == nil) { return false; };
if (u.kind != syntax.tykind.TY_TAGGED) { return false; };
return u.size: i32 > TUPLE_GPCAP * 8;
};
// taggedcastpeel — Family C (#35): a tagged→tagged cast is transport-
// transparent — the operand's box IS the value; transport consumers
// (widen-store, arg push) derive the remap from the operand's type.
// Peeling exposes the ident/deref carrier their source arms key on;
// cgexpr on the cast node itself collapses to one word. Concrete-
// variant casts (`7: size`) keep their node for variant-tag lookup;
// the nullable one-word fold never spills a cursor — excluded.
// Mirrors cstage cg_tagged_castpeel.
fn taggedcastpeel(c: *cgen, e: *syntax.node) *syntax.node = {
for (e != nil && e.kind == syntax.nkind.N_CAST && e.lhs != nil) {
let cu: *syntax.tinfo = e.type_: *syntax.tinfo;
cu = tichase(cu);
if (cu == nil) { return e; };
if (cu.kind != syntax.tykind.TY_TAGGED) { return e; };
if (cu.nullable != 0) { return e; };
let iu: *syntax.tinfo = e.lhs.type_: *syntax.tinfo;
iu = tichase(iu);
if (iu == nil) { return e; };
if (iu.kind != syntax.tykind.TY_TAGGED) { return e; };
if (iu.nullable != 0) { return e; };
e = e.lhs;
};
return e;
};
// taggedidcastpeel — the IDENTITY-only subset of the peel for
// consumers that key variant indices on the scrutinee's own type
// (is/as/match): same-type casts are no-ops there, but a WIDENING
// cast changes the tag numbering and must NOT be peeled — those die
// loud at the consumer's cast catch-all instead. Mirrors cstage
// cg_tagged_idcastpeel.
fn taggedidcastpeel(c: *cgen, e: *syntax.node) *syntax.node = {
for (e != nil && e.kind == syntax.nkind.N_CAST && e.lhs != nil) {
if (!syntax.typeeq(e.type_: *syntax.tinfo, e.lhs.type_: *syntax.tinfo)) { return e; };
let cu: *syntax.tinfo = e.type_: *syntax.tinfo;
cu = tichase(cu);
if (cu == nil) { return e; };
if (cu.kind != syntax.tykind.TY_TAGGED) { return e; };
e = e.lhs;
};
return e;
};
// cgloadtaggedfield — load a tagged-union slot at `basereg`+foff
// into the tagged-return ABI registers (AX=tag, DX=word0, CX=word1,
// R8=word2). Slot sizes: 16B = (tag, word0), 24B = + word1, 32B
// = + word2 (slice variant). Mirrors the cstage tagged-field load
// in cmd/w6c/cgen.c (N_DOT TY_STRUCT/TY_PTR branches).
//
// Load order is fixed regardless of basereg: tag, word0, word2,
// word1. CX (word1 target) goes LAST because basereg may itself
// be CX — top-level globals address via LEAQ name(SB), CX — and
// overwriting it earlier would trash the base address for the
// remaining loads. For BP / BX bases the order is harmless.
// Callers must guarantee basereg is one of "BP", "BX", "CX"; the
// only register loaded into that is NOT a target is BX, so AX-
// or DX-rooted callers must spill first.
fn cgloadtaggedfield(c: *cgen, basereg: str, foff: i32, slot_sz: i32,
cxlast: bool) void = {
// #37: >32B box — leave its ADDRESS in AX (taggedmemread, the
// sret-receive convention); the 4-reg cursor walk below would
// truncate past payload word 2. Mirrors cstage's N_DOT
// TY_STRUCT/TY_PTR tagged arms.
if (slot_sz > TUPLE_GPCAP * 8) {
emitline("\tLEAQ\t");
emitdispreg(foff: i64, basereg);
emitline(", AX\n");
return;
};
// tag → AX
emitline("\tMOVQ\t");
emitdispreg(foff: i64, basereg);
emitline(", AX\n");
// word0 → DX
emitline("\tMOVQ\t");
emitdispreg((foff + 8): i64, basereg);
emitline(", DX\n");
// word1 → CX, word2 → R8. cstage's direct *struct-ptr arm (cgen.c
// N_DOT TY_PTR→TY_STRUCT, ~11926) loads them in strict offset order
// (CX@+16 then R8@+24, BX is not a cursor target); every other arm —
// local/global struct (CX may be the base addr) and the chained-
// *struct twin (~12021) — loads R8 BEFORE CX. cxlast selects: true =
// R8 then CX, false = offset order.
if (cxlast) {
if (slot_sz > 24) {
emitline("\tMOVQ\t");
emitdispreg((foff + 24): i64, basereg);
emitline(", R8\n");
};
if (slot_sz > 16) {
emitline("\tMOVQ\t");
emitdispreg((foff + 16): i64, basereg);
emitline(", CX\n");
};
} else {
if (slot_sz > 16) {
emitline("\tMOVQ\t");
emitdispreg((foff + 16): i64, basereg);
emitline(", CX\n");
};
if (slot_sz > 24) {
emitline("\tMOVQ\t");
emitdispreg((foff + 24): i64, basereg);
emitline(", R8\n");
};
};
};
// cgwidentaggedstore — write tagged-union slot bytes for `src` into
// the slot at `basereg`+slot_off, sized to slot_sz. Mirrors
// cg_widen_tagged_store in cmd/w6c/cgen.c.
//
// `basereg` selects the addressing root:
// - "BP": function-frame slot (let / assign / return / structlit /
// array-elem scratch). Body writes straight to slot_off(BP).
// - else (e.g. "BX" for *struct field, top-level struct LEAQ
// base): pointer-rooted dst. cgexpr inside trashes every GPR,
// so we route through a fresh BP-rooted scratch slot, spill
// basereg before the body, reload after, then word-copy
// scratch → (basereg, slot_off).
//
// Branches by source shape:
// - nullable dst (8B slot): cgexpr → AX → slot+0.
// - tagged src ident: copy slot words, zero-pad, tag-remap.
// - tagged src via AX/DX/CX ABI (call / tagged-arr index): cgexpr,
// spill words; no remap (callee already speaks dst tag order — or
// it doesn't, in which case the source is the wider one and remap
// would need a reversed direction we don't currently emit).
// - struct src (literal or ident): zero slot, write fields at +8+foff,
// tag last.
// - str src: tag@+0, ptr@+8, len@+16, cap@+24 (str IS []u8, #1/Phase 3).
// - scalar src: tag@+0, value@+8.
fn cgwidentaggedstore(c: *cgen, dst: *syntax.tinfo, src: *syntax.node,
basereg: str, slot_off: i32, slot_sz: i32) void = {
if (syntax.streq(basereg, "BP")) {
cgwidentaggedstorebp(c, dst, src, slot_off, slot_sz);
return;
};
// Pointer-rooted dst: spill basereg (cgexpr will trash it),
// materialise into a BP-rooted scratch via the BP path, then
// reload basereg and word-copy scratch → caller's slot.
let bspill: i32 = localadd(c, "@tagbase", 8, nil);
emitline("\tMOVQ\t");
emitline(basereg);
emitline(", ");
emitoff(bspill: i64);
emitline("(BP)\n");
// Shared per-size scratch sized at first use (#15/#26c, size-keyed
// by #44): sibling sites (cgreturn, pushargsrev, cgindex) hitting
// the same slot size reuse the slot; a different size pins its own.
let scr: i32 = tagscradd(c, slot_sz);
emitline("\tXORQ\tAX, AX\n");
let z: i32 = 0;
for (z < slot_sz) {
emitline("\tMOVQ\tAX, ");
emitoff((scr + z): i64);
emitline("(BP)\n");
z += 8;
};
cgwidentaggedstorebp(c, dst, src, scr, slot_sz);
emitline("\tMOVQ\t");
emitoff(bspill: i64);
emitline("(BP), ");
emitline(basereg);
emitline("\n");
let k: i32 = 0;
for (k < slot_sz) {
emitline("\tMOVQ\t");
emitoff((scr + k): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg((slot_off + k): i64, basereg);
emitline("\n");
k += 8;
};
};
// cgwidentaggedstorebp — BP-rooted body. Called via cgwidentaggedstore
// for the natural "BP" case and via the wrapper's scratch path for
// pointer-rooted dst. Direct callers exist only in case of future
// inlined uses inside this file; new code should call the wrapper.
fn cgwidentaggedstorebp(c: *cgen, dst: *syntax.tinfo, src: *syntax.node, slot_off: i32, slot_sz: i32) void = {
// #68: dst is the tagged tinfo. Peel TY_NAMED → du and gate
// TY_TAGGED, mirroring cstage cg_widen_tagged_store's
// `du = (dst->kind==TY_NAMED)?dst->under:dst` + TY_TAGGED guard
// (cmd/w6c/cgen.c:1273). resolvetagged's N_TBANG unwrap is already
// handled upstream by tinfofornode (check.ww:1203-1210).
let dt: *syntax.tinfo = dst;
dt = tichase(dt);
if (dt == nil) { return; };
if (dt.kind != syntax.tykind.TY_TAGGED) { return; };
// Nullable fold: one 8B word holding the pointer (or 0 for void).
if (dt.nullable != 0) {
cgexpr(c, src);
emitline("\tMOVQ\tAX, ");
emitoff(slot_off: i64);
emitline("(BP)\n");
return;
};
// Family C (#35): a tagged→tagged cast is transport-transparent
// — peel it so the ident/deref/memread source arms below see the
// carrier and the remap keys on the operand's type. Pre-#35 the
// cast node fell to the scalar arm (`let w: un3 = (v: un3)`
// stored tag 0 + word0). Mirrors cstage cg_widen_tagged_store.
src = taggedcastpeel(c, src);
// `expr: TaggedAlias` where the cast's destination IS the union
// itself is a widening, not a re-interpret. cgexpr on a CAST
// produces the inner's register shape (str: AX=ptr, BX=len), not
// the tagged AX/DX/CX triple — so peel to the inner and route
// through the matching concrete-variant branch below. A cast to
// a concrete variant (`7: i32`) is left intact so the existing
// scalar / str / slice branches pick the right variant tag.
if (src != nil) {
if (src.kind == syntax.nkind.N_CAST) {
if (src.lhs != nil) {
let inner: *syntax.node = src.lhs;
let inneristagged: bool = false;
if (inner.kind == syntax.nkind.N_IDENT) {
let lc: *local = localfindnode(c, inner.str);
if (lc != nil) {
inneristagged = istaggedtype(c, lc.tnode);
};
};
if (rhstaggedabicall(c, inner)) {
inneristagged = true;
};
// Cast's destination = the dst tagged union
// itself? The rhs of N_CAST holds the target
// type. #68: compare on the stamped tinfos —
// `castu == dt` (peeled-underlying ptr-id) ||
// (castu tagged && typeeq(castt, dst)) — mirroring
// cstage cg_widen_tagged_store's
// `cast_is_widen = (castu==du) || (castu->kind==
// TY_TAGGED && type_eq(castt, dst))`
// (cmd/w6c/cgen.c:1295-1296). Replaces the prior
// surface-name streq; same-alias TNAMEs share one
// NAMED tinfo (check.ww:1160-1173) so typeeq hits
// the a==b fast path.
let castisdst: bool = false;
let castsubset: bool = false;
let castrhs: *syntax.node = src.rhs;
if (castrhs != nil) {
let castt: *syntax.tinfo = castrhs.type_: *syntax.tinfo;
let castu: *syntax.tinfo = castt;
castu = tichase(castu);
if (castu != nil) {
if (castu == dt) {
castisdst = true;
} else { if (castu.kind == syntax.tykind.TY_TAGGED) {
if (syntax.typeeq(castt, dst)) {
castisdst = true;
} else {
castsubset = true;
};
}; };
};
};
// S1/#35 (rule 7): a widen-SUBSET cast — the cast target
// is a TAGGED union that is NOT dst (castu tagged &&
// !typeeq(castt, dst)). The inner-variant tag is never
// remapped to dst's index, so the scalar arm below would
// emit tag=0: a SILENT mis-tag on any 2nd-variant value
// (census S1: `return true: inner`, inner=(int|bool) into
// (int|bool|str), ran the int arm not the bool arm). Loud-
// align to cstage cgen.c:2721-2723. The !inneristagged gate
// matches the src=inner collapse below — a tagged inner is
// handled by the #218 nested arm. Faithful remap (read inner
// tag, inner-idx→outer-idx) = the #23/#40 widen-subset
// feature, deferred post-CSP (needs the nominal variant-
// remap table).
if (castsubset && !inneristagged) {
let m35: str = "#35: tagged cast source shape unwired at the widen subset arm (rule 7)\n";
os.write(2, m35.ptr, m35.len: u64);
os.exit(1);
};
if (castisdst && !inneristagged) {
src = inner;
};
};
};
};
// #62 Layer-2: ONE source-classify chase at entry (post cast peels,
// where src is final) — the per-arm single reads it replaces all
// chased the same src.type_. Mirrors cstage cg_widen_tagged_store's
// `su = type_chase_named(st)` position (c138605). Tag lookups keep
// the un-chased src.type_ (nominal identity is the alias).
let su: *syntax.tinfo = src.type_: *syntax.tinfo;
su = tichase(su);
// #218: is the source itself a single NESTED variant of dt (its
// whole tagged type matches one dt variant), rather than a flattened
// SUBSET whose members spread into dt? If so, the inner tagged value
// is the payload: store it at slot_off+8 with the outer tag at
// slot_off+0, mirroring the scalar/struct/str single-variant arms —
// NOT a copy-to-+0 + sub-variant remap. flatvariantidxt's structural
// fallback (cgvariantmatch) recovers the index after the nominal-
// lossy collapse. Gated on a tagged source so scalar/str/struct
// sources keep their existing arms. Mirrors cstage
// cg_widen_tagged_store's nested arm (cmd/w6c/cgen.c).
let srctagged: bool = (rhstaggedident(c, src) != nil)
|| rhstaggedabicall(c, src);
// #51 (#263): a module-global tagged ident is also a tagged source
// (no local slot — handled by the global branch in the nested copy).
if (!srctagged) { if (src.kind == syntax.nkind.N_IDENT) {
if (localfindnode(c, src.str) == nil) {
let gtn51: *syntax.node = letvartnode(c, src.str);
if (gtn51 != nil) { if (istaggedtype(c, gtn51)) { srctagged = true; }; };
};
}; };
if (srctagged) {
let nested: i32 = flatvariantidxt(dt, src.type_: *syntax.tinfo, false);
if (nested >= 0) {
// drew collision guard: the structural fallback over-
// matches if ≥2 nominally-distinct dt variants share the
// source's shape. Unreachable under today's nominal-lossy
// model, but INVERTS when #199b/B-full lands the nominal
// layer — hard-error NOW so a future collision STOPS the
// compiler instead of silently mis-tagging.
let nmatch: i32 = 0;
let gp: *syntax.tparam = dt.params;
for (gp != nil) {
if (cgvariantstructmatch(gp.type_,
src.type_: *syntax.tinfo)) {
nmatch += 1;
};
gp = gp.tnext;
};
if (nmatch >= 2) {
let msg: str = "cgwidentaggedstore: structural fallback cannot disambiguate nominally-distinct same-shape variants without nominal layout (#218/#199b/B-full)\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
let ssz: i32 = su.size: i32;
emitline("\tXORQ\tAX, AX\n");
let zk: i32 = 0;
for (zk < slot_sz) {
emitline("\tMOVQ\tAX, ");
emitoff((slot_off + zk): i64);
emitline("(BP)\n");
zk += 8;
};
if (src.kind == syntax.nkind.N_IDENT) {
let lc: *local = localfindnode(c, src.str);
if (lc != nil) {
let soff: i32 = lc.off;
let ck: i32 = 0;
for (ck < ssz) {
emitline("\tMOVQ\t");
emitoff((soff + ck): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((slot_off + 8 + ck): i64);
emitline("(BP)\n");
ck += 8;
};
} else {
// #51 (#263 ww-runtime-correct): a module-global tagged
// ident source — no BP slot. Land g(SB) in SI
// (aggargsrcaddr) and copy the inner box to slot+8.
// Pre-fix rhstaggedident returned nil for a global, so
// srctagged was false and the value fell to the scalar
// word0 arm — gi's TAG landed as the payload. cstage
// copies frame garbage (cstage half #44).
if (aggargsrcaddr(c, src, "SI")) {
let ck2: i32 = 0;
for (ck2 < ssz) {
emitline("\tMOVQ\t");
emitoff(ck2: i64);
emitline("(SI), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((slot_off + 8 + ck2): i64);
emitline("(BP)\n");
ck2 += 8;
};
};
};
} else {
// non-nested SUBSET-widen of a GLOBAL tagged
// source still mis-copies (both stages) — task #49.
// #38b: an sret-classified call result is in
// memory (AX = dest pointer), not the cursor —
// the spill below would store the pointer as
// the payload. Mem-to-mem widen is #40.
if (src.kind == syntax.nkind.N_CALL) {
if (callsretsize(c, src) > 0) {
let m40a: str = "#40: sret-class call result cannot be cursor-widened into a tagged slot (mem-to-mem widen unwired)\n";
os.write(2, m40a.ptr, m40a.len: u64);
os.exit(1);
};
};
if (taggedmemread(c, src)) {
// #37: >32B box read — ADDRESS in AX;
// copy the inner box from memory into
// the payload area. Mirrors cstage.
cgexpr(c, src);
let mk: i32 = 0;
for (mk < ssz) {
emitline("\tMOVQ\t");
emitdispreg(mk: i64, "AX");
emitline(", DX\n");
emitline("\tMOVQ\tDX, ");
emitoff((slot_off + 8 + mk): i64);
emitline("(BP)\n");
mk += 8;
};
} else {
// #37 (rule 7): >32B from a non-mem-based
// kind would spill an unfilled cursor.
if (ssz > TUPLE_GPCAP * 8) {
let m37a: str = "#37: >32B tagged payload from a non-mem-based source unwired (rule 7)\n";
os.write(2, m37a.ptr, m37a.len: u64);
os.exit(1);
};
// Family C catch-all (rule 7): a tagged cast
// surviving taggedcastpeel (cast to a THIRD
// union) has no cursor — loud, not word0
// garbage. Mirrors cstage.
if (src.kind == syntax.nkind.N_CAST) {
let m35a: str = "#35: tagged cast source shape unwired at the widen nested arm (rule 7)\n";
os.write(2, m35a.ptr, m35a.len: u64);
os.exit(1);
};
cgexpr(c, src);
emitline("\tMOVQ\tAX, ");
emitoff((slot_off + 8): i64);
emitline("(BP)\n");
if (ssz > 8) {
emitline("\tMOVQ\tDX, ");
emitoff((slot_off + 16): i64);
emitline("(BP)\n");
};
if (ssz > 16) {
emitline("\tMOVQ\tCX, ");
emitoff((slot_off + 24): i64);
emitline("(BP)\n");
};
if (ssz > 24) {
emitline("\tMOVQ\tR8, ");
emitoff((slot_off + 32): i64);
emitline("(BP)\n");
};
};
};
emitline("\tMOVQ\t$");
emitint(nested: i64);
emitline(", ");
emitoff(slot_off: i64);
emitline("(BP)\n");
return;
};
};
// Tagged source ident: byte-copy slot words then tag-remap.
// rhstaggedident gates "src is a tagged-typed local ident"; the
// remap reads the source tagged tinfo off the local's tnode (#68).
let st: *syntax.node = rhstaggedident(c, src);
if (st != nil) {
let lc: *local = localfindnode(c, src.str);
let ssz: i32 = slotsize(c, lc.tnode);
let soff: i32 = lc.off;
let k: i32 = 0;
for (k < ssz) {
emitline("\tMOVQ\t");
emitoff((soff + k): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((slot_off + k): i64);
emitline("(BP)\n");
k += 8;
};
if (ssz < slot_sz) {
emitline("\tXORQ\tAX, AX\n");
let p: i32 = ssz;
for (p < slot_sz) {
emitline("\tMOVQ\tAX, ");
emitoff((slot_off + p): i64);
emitline("(BP)\n");
p += 8;
};
};
cgwidentagremap(c, dt, lc.tnode.type_: *syntax.tinfo, slot_off);
return;
};
// Tagged source via AX/DX/CX/R8 register ABI (N_CALL, N_INDEX
// of tagged element). R8 carries the 4th word for slice-payload
// variants (slot 32B).
if (rhstaggedabicall(c, src)) {
// #38b: an sret-classified call result is in memory (AX =
// dest pointer), not the cursor. #40.
if (src.kind == syntax.nkind.N_CALL) {
if (callsretsize(c, src) > 0) {
let m40b: str = "#40: sret-class call result cannot be cursor-widened into a tagged slot (mem-to-mem widen unwired)\n";
os.write(2, m40b.ptr, m40b.len: u64);
os.exit(1);
};
};
// #37: >32B box read (insts[pc], t.N, s.f) — cgexpr left
// its ADDRESS in AX; copy the whole box from memory, pad,
// tag-remap — the mem-based twin of the ident arm above.
// Mirrors cstage cg_widen_tagged_store's memread arm.
if (taggedmemread(c, src)) {
let ssz37: i32 = su.size: i32;
cgexpr(c, src);
let mk37: i32 = 0;
for (mk37 < ssz37) {
emitline("\tMOVQ\t");
emitdispreg(mk37: i64, "AX");
emitline(", DX\n");
emitline("\tMOVQ\tDX, ");
emitoff((slot_off + mk37): i64);
emitline("(BP)\n");
mk37 += 8;
};
if (ssz37 < slot_sz) {
emitline("\tXORQ\tAX, AX\n");
let pp37: i32 = ssz37;
for (pp37 < slot_sz) {
emitline("\tMOVQ\tAX, ");
emitoff((slot_off + pp37): i64);
emitline("(BP)\n");
pp37 += 8;
};
};
cgwidentagremap(c, dt, src.type_: *syntax.tinfo, slot_off);
return;
};
// #55: spill the AX/DX/CX/R8 cursor by the SOURCE box width
// (su.size), zero-pad to the dst slot, then tag-remap — the
// cursor twin of the ident / memread arms above. Pre-#55 it
// spilled by slot_sz (reading STALE high regs when the source is
// narrower) and skipped the pad + remap, so an INDEX/DOT tagged
// element widened into a wider/reordered union pushed a stale word
// and the un-remapped source tag (silent cs!=ww). Mirrors cstage
// cg_widen_tagged_store's cursor arm + shared pad/remap tail
// (cmd/w6c/cgen.c:2695-2714). Same-slot source (ssz==slot_sz)
// leaves the pad empty + an identity remap, so the emission is
// byte-identical to the prior arm for the exact-slot callers.
cgexpr(c, src);
let ssz: i32 = su.size: i32;
emitline("\tMOVQ\tAX, ");
emitoff(slot_off: i64);
emitline("(BP)\n");
if (ssz > 8) {
emitline("\tMOVQ\tDX, ");
emitoff((slot_off + 8): i64);
emitline("(BP)\n");
};
if (ssz > 16) {
emitline("\tMOVQ\tCX, ");
emitoff((slot_off + 16): i64);
emitline("(BP)\n");
};
if (ssz > 24) {
emitline("\tMOVQ\tR8, ");
emitoff((slot_off + 24): i64);
emitline("(BP)\n");
};
if (ssz < slot_sz) {
emitline("\tXORQ\tAX, AX\n");
let pp: i32 = ssz;
for (pp < slot_sz) {
emitline("\tMOVQ\tAX, ");
emitoff((slot_off + pp): i64);
emitline("(BP)\n");
pp += 8;
};
};
cgwidentagremap(c, dt, src.type_: *syntax.tinfo, slot_off);
return;
};
// #37 (rule 7): a >32B TAGGED source of a kind the resolver arms
// above don't carry (deref/cast/unwrap/...) would fall to the
// scalar word0 arm below and silently truncate — keyed on the
// stamped src.type_ (kind-blind), the twin of cstage
// cg_widen_tagged_store's generic-else bound. Surfaced by
// reviewer-37's `let w = *p` probe on a 56B box: cstage loud,
// wwstage silent (rule-10 break).
if (su != nil && su.kind == syntax.tykind.TY_TAGGED
&& su.size: i32 > TUPLE_GPCAP * 8) {
let m37f: str = "#37: >32B tagged source of a non-mem-based kind unwired (rule 7)\n";
os.write(2, m37f.ptr, m37f.len: u64);
os.exit(1);
};
// Family C catch-all (rule 7): a tagged cast surviving
// taggedcastpeel (cast to a THIRD union) would fall to the
// scalar arm below and silently truncate — loud. Mirrors
// cstage's widen subset-arm cast bound.
if (src.kind == syntax.nkind.N_CAST && su != nil
&& su.kind == syntax.tykind.TY_TAGGED && su.nullable == 0) {
let m35b: str = "#35: tagged cast source shape unwired at the widen subset arm (rule 7)\n";
os.write(2, m35b.ptr, m35b.len: u64);
os.exit(1);
};
// #242: tuple payload. Each element rides ONE register-ABI
// eightbyte — scalar/float a single 8B word, a slice/str its 3-word
// {ptr,len,cap} header (24B) — matching the tagged-return load
// (AX=tag, DX=word0, CX=word1, R8=word2) and the cgmlet receive
// cursor. NOT the packed-by-size t.N field layout (#238). Mirror of
// cstage cg_widen_tagged_store's TY_TUPLE arm.
//
// #66: the cast-wrapped tuple literal `((a, b): range_alias)` is
// the spelling real code uses (regex.ha:213) — the cast targets the
// CONCRETE variant, so the widen-cast peel above leaves it intact
// and pre-#66 it fell to the scalar arm, silently dropping payload
// slot 1+. Peel to the inner tuple here; src.type_ stays the CAST's
// type, which resolves the variant tag by exact named match, so the
// #241 untyped-element un-matchability does not arise for this form.
let tupsrc: *syntax.node = nil;
let tupcast: bool = false;
if (src != nil) {
if (src.kind == syntax.nkind.N_TUPLE) { tupsrc = src; };
if (src.kind == syntax.nkind.N_CAST) {
if (src.lhs != nil) {
if (src.lhs.kind == syntax.nkind.N_TUPLE) {
tupsrc = src.lhs;
tupcast = true;
};
};
};
// #116: a NON-LITERAL tuple-typed source (ident, index,
// deref) is no longer loud here — it routes to the
// addressable block-copy arm just below the literal arm.
// Mirrors cstage cg_widen_tagged_store.
};
if (tupsrc != nil) {
if (su != nil) { if (su.kind == syntax.tykind.TY_TUPLE) {
// #242/#241: mirror cstage's loud-stop CONDITION, not its
// -1 mechanism (rule 10, align the RICHER side DOWN).
// wwstage types `true`/`false` as bool and a suffix-less `7`
// as untyped_int, so flatvariantidxt below DOES resolve the
// variant — but cstage's cg_tag_for_variant can't type a bare
// literal element (#241), returns -1, and loud-stops. A
// program cstage rejects, wwstage must also reject. The shape
// cstage can't type: a bool literal (N_TRUE/N_FALSE) or a
// suffix-less numeric literal (untyped_int/untyped_float).
// LIFT BOTH stage guards together when #241 fixes cstage
// literal typing -> symmetric accept. BARE form only (#66):
// the cast form resolves its tag from the cast's type on
// BOTH stages, so bare elements are fine there.
let bl: *syntax.node = tupsrc.list;
for (bl != nil && !tupcast) {
let bare: bool = false;
if (bl.kind == syntax.nkind.N_TRUE) { bare = true; };
if (bl.kind == syntax.nkind.N_FALSE) { bare = true; };
if (bl.kind == syntax.nkind.N_INTLIT && bl.tsuffix.len == 0) {
bare = true;
};
if (bl.kind == syntax.nkind.N_FLOATLIT && bl.tsuffix.len == 0) {
bare = true;
};
if (bare) {
let ml: str = "cgwidentaggedstore: tuple-in-union variant tag unresolved (untyped/literal tuple element; see #242 / #241)\n";
os.write(2, ml.ptr, ml.len: u64);
os.exit(1);
};
bl = bl.next;
};
// #242: resolve the variant tag via the typeeq core
// (flatvariantidxt) — NOT taggedvariantindext, whose
// str/slice shape fallback would silently pick tag 0 for an
// unmatched tuple, diverging from cstage cg_tag_for_variant
// (which returns -1) and masking the loud-stop below.
let ttag: i32 = flatvariantidxt(dt, src.type_: *syntax.tinfo, false);
// #242: an untyped/literal tuple element (`(true,7)`) leaves
// the src tuple un-matchable, so the variant tag can't
// resolve — the supported shape is a tuple of TYPED exprs
// (strconv parseint `(neg, n)`). Loud-stop rather than
// silently mis-tag (rule 7); #241 literal-init family.
if (ttag < 0) {
let m1: str = "cgwidentaggedstore: tuple-in-union variant tag unresolved (untyped/literal tuple element; see #242 / #241)\n";
os.write(2, m1.ptr, m1.len: u64);
os.exit(1);
};
// #242: this 8B-per-eightbyte packing is correct only when
// no two scalar elements share a SysV eightbyte — e.g.
// (bool,u64). A (i32,i32,u64) would overflow the union
// payload the slotted write assumes. Loud-stop (rule 7);
// SysV eightbyte tuple classification is a deferred
// follow-up. Symmetric with cstage cg_widen_tagged_store.
let ttotal: i32 = 0;
let ce: *syntax.node = tupsrc.list;
for (ce != nil) {
// #47 gap-A: a tagged element rides its OWN
// box (tag + payload, roundup8) per tuple slot
// — NOT one 8B word. Mirror the checker's
// N_TTUPLE accumulation (check.ww:1640-1646);
// the store loop below boxes it recursively
// (two-level widen). Symmetric with cstage.
let ceti: *syntax.tinfo = ce.type_: *syntax.tinfo;
ceti = tichase(ceti);
if (ceti != nil && ceti.kind == syntax.tykind.TY_TAGGED) {
ttotal += (ceti.size: i32 + 7) & ~7;
} else { if (nodeisstr(c, ce) || nodeisslice(c, ce)) {
ttotal += 24;
} else { ttotal += 8; }; };
ce = ce.next;
};
if (8 + ttotal > slot_sz) {
let m2: str = "cgwidentaggedstore: tuple-in-union payload needs SysV eightbyte packing (narrow elements share an eightbyte; see #242 follow-up)\n";
os.write(2, m2.ptr, m2.len: u64);
os.exit(1);
};
emitline("\tXORQ\tAX, AX\n");
let tzk: i32 = 0;
for (tzk < slot_sz) {
emitline("\tMOVQ\tAX, ");
emitoff((slot_off + tzk): i64);
emitline("(BP)\n");
tzk += 8;
};
let tfoff: i32 = 0;
let te: *syntax.node = tupsrc.list;
for (te != nil) {
// #47 gap-A: a tagged element is its own
// tag+payload box. Recurse so the inner box
// (tag@slot+0, payload@slot+8) is built at the
// element's tuple-payload offset, exactly as a
// top-level tagged-store does — two-level widen
// (inner boxes here, outer tuple tag stamped
// below). slot stride = roundup8(box). Symmetric
// with cstage cg_widen_tagged_store.
let teti: *syntax.tinfo = te.type_: *syntax.tinfo;
teti = tichase(teti);
if (teti != nil && teti.kind == syntax.tykind.TY_TAGGED) {
let ebox: i32 = (teti.size: i32 + 7) & ~7;
cgwidentaggedstore(c, te.type_: *syntax.tinfo, te,
"BP", slot_off + 8 + tfoff, ebox);
tfoff += ebox;
te = te.next;
continue;
};
let isflt: bool = isfloattype(c, te);
let wide: bool = nodeisstr(c, te) || nodeisslice(c, te);
let esz: i32 = 8;
let eti: *syntax.tinfo = te.type_: *syntax.tinfo;
if (eti != nil) { esz = eti.size: i32; };
cgexpr(c, te);
if (isflt) {
let mov: str = "MOVSD";
if (isf32type(c, te)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff((slot_off + 8 + tfoff): i64);
emitline("(BP)\n");
} else { if (wide) {
emitline("\tMOVQ\tAX, ");
emitoff((slot_off + 8 + tfoff): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tBX, ");
emitoff((slot_off + 8 + tfoff + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((slot_off + 8 + tfoff + 16): i64);
emitline("(BP)\n");
} else {
let sop: str = tnodestoreop(c, te, esz);
emitline("\t");
emitline(sop);
emitline("\tAX, ");
emitoff((slot_off + 8 + tfoff): i64);
emitline("(BP)\n");
}; };
if (wide) { tfoff += 24; } else { tfoff += 8; };
te = te.next;
};
emitline("\tMOVQ\t$");
emitint(ttag: i64);
emitline(", ");
emitoff(slot_off: i64);
emitline("(BP)\n");
return;
}; };
};
// #116: a NON-LITERAL but ADDRESSABLE tuple source — a tuple
// IDENT var, a slice/array INDEX (tbl[i]), or a DEREF (*p). The
// tuple in memory uses the SAME tupeslot strides as the box
// payload the literal loop above fills, so the source-in-memory
// layout already equals the box payload layout — the fill is a
// flat block-copy of sum(tupeslot) bytes from the source address
// into slot_off+8, no re-slotting (a tagged element rides over
// as its already-built box). Kept loud (out of scope): a
// CALL/sret result (#40-kin) and struct-field / array-literal-
// element sources (loud EARLIER at construction, #49 / #270-1c).
// A cast wrapping a concrete-variant tuple (`(tbl[i]: ci)`)
// survived the widen-cast peel above; its operand is the
// addressable expr. Mirror of cstage cg_widen_tagged_store.
if (su != nil) { if (su.kind == syntax.tykind.TY_TUPLE) {
let addrsrc: *syntax.node = src;
if (addrsrc.kind == syntax.nkind.N_CAST) {
if (addrsrc.lhs != nil) { addrsrc = addrsrc.lhs; };
};
let okkind: bool = false;
if (addrsrc.kind == syntax.nkind.N_IDENT) { okkind = true; };
if (addrsrc.kind == syntax.nkind.N_INDEX) { okkind = true; };
if (addrsrc.kind == syntax.nkind.N_UN) {
if (addrsrc.op == syntax.tkind.TK_STAR) { okkind = true; };
};
if (!okkind) {
let mk: str = "cgwidentaggedstore: tuple-typed source shape unwired (only the bare/cast tuple literal and the addressable ident/index/deref trio carry a full payload; see #116)\n";
os.write(2, mk.ptr, mk.len: u64);
os.exit(1);
};
let ntag: i32 = flatvariantidxt(dt, src.type_: *syntax.tinfo, false);
if (ntag < 0) {
let mt: str = "cgwidentaggedstore: tuple-in-union variant tag unresolved (untyped/literal tuple element; see #242 / #241)\n";
os.write(2, mt.ptr, mt.len: u64);
os.exit(1);
};
// The tuple's type-table size IS sum(tupeslot) under the 8B-slot
// tuple layout (every walk takes its stride from tupeslot; the
// type's size is their sum), so the payload byte-count routes
// through the type table (rule 13) without re-walking the
// elements — and a wwstage tuple tinfo carries no per-element
// params list anyway. Mirror of cstage cg_widen_tagged_store.
let ntotal: i32 = su.size: i32;
if (8 + ntotal > slot_sz) {
let mp: str = "cgwidentaggedstore: tuple-in-union payload needs SysV eightbyte packing (narrow elements share an eightbyte; see #242 follow-up)\n";
os.write(2, mp.ptr, mp.len: u64);
os.exit(1);
};
if (!cgplaceaddr(c, addrsrc, "SI")) {
let ma: str = "cgwidentaggedstore: addressable tuple source address unresolved (see #116)\n";
os.write(2, ma.ptr, ma.len: u64);
os.exit(1);
};
emitline("\tXORQ\tAX, AX\n");
let nzk: i32 = 0;
for (nzk < slot_sz) {
emitline("\tMOVQ\tAX, ");
emitoff((slot_off + nzk): i64);
emitline("(BP)\n");
nzk += 8;
};
let nck: i32 = 0;
for (nck < ntotal) {
emitline("\tMOVQ\t");
emitoff(nck: i64);
emitline("(SI), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((slot_off + 8 + nck): i64);
emitline("(BP)\n");
nck += 8;
};
emitline("\tMOVQ\t$");
emitint(ntag: i64);
emitline(", ");
emitoff(slot_off: i64);
emitline("(BP)\n");
return;
}; };
// #50 (#263 ww-runtime-correct): a module-GLOBAL struct ident source.
// rhsstructpayload below is local/literal-only, so a global struct value
// fell to the scalar word0 arm — payload truncated. Land g(SB) in SI and
// byte-copy the struct words into slot+8; cstage zero-fills the payload
// (cstage half #43). Local/literal sources keep the existing arms (byte-id).
if (src.kind == syntax.nkind.N_IDENT) {
if (localfindnode(c, src.str) == nil) {
let gtn: *syntax.node = letvartnode(c, src.str);
if (gtn != nil) { if (gtn.kind == syntax.nkind.N_TNAME) {
let gsi: *structinfo = structlookupchain(c, gtn);
if (gsi != nil) {
emitline("\tXORQ\tAX, AX\n");
let gz: i32 = 0;
for (gz < slot_sz) {
emitline("\tMOVQ\tAX, ");
emitoff((slot_off + gz): i64);
emitline("(BP)\n");
gz += 8;
};
let gtag: i32 = taggedvariantindext(c, dt, src);
if (gtag < 0) { gtag = 0; };
if (aggargsrcaddr(c, src, "SI")) {
// Natural size, not slot-padded (see the
// local-ident twin below; cstage su->size).
let gtot: i32 = copysrcnatsize(c, src);
let gk: i32 = 0;
for (gk + 8 <= gtot) {
emitline("\tMOVQ\t");
emitoff(gk: i64);
emitline("(SI), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((slot_off + 8 + gk): i64);
emitline("(BP)\n");
gk += 8;
};
if (gk < gtot) {
let gtail: i32 = gtot - gk;
let glop: str = "MOVQ";
if (gtail == 4) { glop = "MOVL"; }
else { if (gtail == 1) { glop = "MOVB"; }; };
emitline("\t"); emitline(glop); emitline("\t");
emitoff(gk: i64); emitline("(SI), AX\n");
emitline("\t"); emitline(glop); emitline("\tAX, ");
emitoff((slot_off + 8 + gk): i64); emitline("(BP)\n");
};
emitline("\tMOVQ\t$");
emitint(gtag: i64);
emitline(", ");
emitoff(slot_off: i64);
emitline("(BP)\n");
return;
};
};
}; };
};
};
// Struct payload (literal or ident).
let sname: str = rhsstructpayload(c, src);
if (sname.len > 0) {
let si: *structinfo = structlookup(c, sname);
if (si != nil) {
emitline("\tXORQ\tAX, AX\n");
let zoff: i32 = 0;
for (zoff < slot_sz) {
emitline("\tMOVQ\tAX, ");
emitoff((slot_off + zoff): i64);
emitline("(BP)\n");
zoff += 8;
};
let tag: i32 = taggedvariantindext(c, dt, src);
if (tag < 0) { tag = 0; };
if (src.kind == syntax.nkind.N_STRUCTLIT) {
// #23: delegate to the single fill path. The
// inline field loop this replaces was a
// parallel fill that drifted: it lacked the
// tagged-field widen arm, so a (void|T)-typed
// field's raw scalar landed in the field's
// TAG word (silent truncation past the first
// tagged field, both stages). Delegation also
// inherits the nested-struct / call / array-
// lit field arms; float / str / slice /
// scalar fields emit byte-identically to the
// old loop EXCEPT fsz==2 scalars, where the
// old loop's fieldstoreop emitted MOVW
// against cstage's MOVQ — a latent cs≠ww the
// fill's #13-pinned dispatch closes. Mirror
// of cstage cg_widen_tagged_store's
// N_STRUCTLIT arm.
cgstructlitfill(c, si, src, 0, 0, "",
slot_off + 8);
} else {
// Struct ident source: byte-copy struct words to slot+8+k.
let lc: *local = localfindnode(c, src.str);
let soff: i32 = 0;
if (lc != nil) { soff = lc.off; };
// Copy the NATURAL size (cstage su->size): the slot-
// padded totsize would MOVQ the source slot's dirty
// pad bytes over the zero-fill just established.
let stotal: i32 = copysrcnatsize(c, src);
let ki: i32 = 0;
for (ki + 8 <= stotal) {
emitline("\tMOVQ\t");
emitoff((soff + ki): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((slot_off + 8 + ki): i64);
emitline("(BP)\n");
ki += 8;
};
if (ki < stotal) {
let tail: i32 = stotal - ki;
let lop: str = "MOVQ";
if (tail == 4) { lop = "MOVL"; }
else { if (tail == 1) { lop = "MOVB"; }; };
emitline("\t");
emitline(lop);
emitline("\t");
emitoff((soff + ki): i64);
emitline("(BP), AX\n");
emitline("\t");
emitline(lop);
emitline("\tAX, ");
emitoff((slot_off + 8 + ki): i64);
emitline("(BP)\n");
};
};
emitline("\tMOVQ\t$");
emitint(tag: i64);
emitline(", ");
emitoff(slot_off: i64);
emitline("(BP)\n");
return;
};
};
// str IS []u8 — same 32B payload as a slice: cgexpr leaves
// (AX=ptr, BX=len, CX=cap); slot layout [+0]=tag, [+8]=ptr,
// [+16]=len, [+24]=cap. str folds onto the slice arm (#1/Phase 3
// collapse; cite cstage cg_widen_tagged_store).
if (nodeisslice(c, src) || nodeisstr(c, src)) {
cgexpr(c, src);
emitline("\tMOVQ\tAX, ");
emitoff((slot_off + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tBX, ");
emitoff((slot_off + 16): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((slot_off + 24): i64);
emitline("(BP)\n");
let tag: i32 = taggedvariantindext(c, dt, src);
if (tag < 0) { tag = 0; };
emitline("\tMOVQ\t$");
emitint(tag: i64);
emitline(", ");
emitoff(slot_off: i64);
emitline("(BP)\n");
return;
};
// Float arm: cgexpr on an f64/f32 source leaves the bit pattern in
// X0 only — the AX-store fallback below would silently write whatever
// was loaded into AX before the SSE conversion. Literal `1.0` works
// by coincidence (TK_FLOAT lowering loads the f64 bit pattern into AX
// before MOVSD'ing into X0); every runtime f64 shape (cast, call,
// unary, ident, struct-field load) needs the explicit MOVSD path.
// Mirror of cstage cg_widen_tagged_store's float arm. Classify off
// the checker stamp (src.type_) — the SSoT cstage reads via
// node_isfloat / type_isf32 — and resolve the variant tag by name
// directly: rhstargetname has no N_FLOATLIT / N_CALL / N_DOT branch
// and would fall through to the str-shape fallback that picks tag 0
// for an `(i64 | f64)` union. The armed asserttyped bail (check.ww)
// guarantees src carries a non-nil stamp, so the sibling-evidence
// loud-abort that used to pin "the float arm requires a stamped
// value" is dead and removed.
let fkind: i32 = 0;
if (src != nil) {
let srct: *syntax.tinfo = src.type_: *syntax.tinfo;
if (syntax.typeisf32(srct)) { fkind = 1; }
else { if (syntax.typeisfloat(srct)) { fkind = 2; }; };
};
if (fkind != 0) {
let fmov: str = "MOVSD";
if (fkind == 1) { fmov = "MOVSS"; };
cgexpr(c, src);
emitline("\t");
emitline(fmov);
emitline("\tX0, ");
emitoff((slot_off + 8): i64);
emitline("(BP)\n");
// #227: zero pad words (+16..slot_sz) so a >16B union slot
// carries the full dst payload width, not just the 1-word float
// value (the BP path never pre-zeroes; a passthrough return or
// *u8 reinterpret otherwise reads stack garbage at slot+16/+24).
// Symmetric with cstage cg_widen_tagged_store float arm.
if (slot_sz > 16) {
emitline("\tXORQ\tAX, AX\n");
let zp: i32 = 16;
for (zp < slot_sz) {
emitline("\tMOVQ\tAX, ");
emitoff((slot_off + zp): i64);
emitline("(BP)\n");
zp += 8;
};
};
// #66 Phase-N step 3: the float arm has no pattern node to ride
// the typeeq flatvariantidx path, so pick the variant by float
// kind (f32 vs f64) over tinfo.params — a shape classification
// like the slice axis, not nominal identity.
let wantf32: bool = (fkind == 1);
let ftag: i32 = -1;
let fti: *syntax.tinfo = dt;
fti = tichase(fti);
if (fti != nil) { if (fti.kind == syntax.tykind.TY_TAGGED) {
let fp: *syntax.tparam = fti.params;
let fidx: i32 = 0;
for (fp != nil) {
let fvt: *syntax.tinfo = fp.type_;
fvt = tichase(fvt);
if (fvt != nil) {
if (syntax.typeisfloat(fvt)) {
if (syntax.typeisf32(fvt) == wantf32) { ftag = fidx; break; };
};
};
fp = fp.tnext;
fidx += 1;
};
}; };
if (ftag < 0) { ftag = 0; };
emitline("\tMOVQ\t$");
emitint(ftag: i64);
emitline(", ");
emitoff(slot_off: i64);
emitline("(BP)\n");
return;
};
// Scalar payload. #227: zero pad words (+16..slot_sz) — see float
// arm above. The BP path never pre-zeroes, so a passthrough return /
// *u8 reinterpret of the narrow-tagged value otherwise reads stack
// garbage in slot+16/+24. Symmetric with cstage scalar arm.
cgexpr(c, src);
emitline("\tMOVQ\tAX, ");
emitoff((slot_off + 8): i64);
emitline("(BP)\n");
if (slot_sz > 16) {
emitline("\tXORQ\tAX, AX\n");
let zp: i32 = 16;
for (zp < slot_sz) {
emitline("\tMOVQ\tAX, ");
emitoff((slot_off + zp): i64);
emitline("(BP)\n");
zp += 8;
};
};
let tag: i32 = taggedvariantindext(c, dt, src);
if (tag < 0) { tag = 0; };
emitline("\tMOVQ\t$");
emitint(tag: i64);
emitline(", ");
emitoff(slot_off: i64);
emitline("(BP)\n");
return;
};
// Spine-walk a chained N_DOT (n) inward to a root ident, summing field
// offsets through value-struct intermediates. Optional slice/str leaf
// pseudo-field (.ptr / .len / .cap) on the last segment is folded into
// *outslicedelta (0/8/16); otherwise *outleaftype is the leaf *tinfo
// and *outslicedelta stays -1. Returns true on success; on false the
// caller falls through to other branches.
//
// Mirrors cmd/w6c/cgen.c's N_DOT chained walker; both stages must agree
// on the same shapes so the bootstrap fixed-point holds. The chain
// depth is capped at 16 — deeper chains are vanishingly rare and fall
// through.
//
// On success the caller emits one load/store at root_base + *outtotaloff
// (+ slicedelta for pseudo leaf). Root resolves as: local frame slot
// (*outisglobal false, base = *outrootoff(BP)) or top-level let
// (*outisglobal true, base reached via LEAQ *outrootname(SB), CX).
//
// Numeric out-params are i32 — offsets fit naturally and the post-#19
// localloadop sign-extends i32 deref-stored slots on read, so negative
// frame offsets round-trip intact.
// #71 (A.6.3j): the spine offset-sum + leaf-type now read off
// tinfo.fields (natural layout) instead of the structinfo/fieldinfo
// walk. Mirror of cstage cmd/w6c/cgen.c:3156-3216 which walks
// `cur->lhs->type` fields. Root resolution (the local/ptr/global split
// and the rootoff/ptrroot/isglobal flags) stays on localfindnode/
// letvarstructinfo unchanged, so the firing set + addressing mode are
// byte-identical to the structinfo era; only the layout SOURCE moves.
// The slot-padded foff and the natural tfield.offset coincide for every
// shape the byte-id gate exercises (cstage already reads the natural
// offset), so this is offset-preserving. Leaf out-param is the field's
// stamped *tinfo (was *fieldinfo); the str/slice pseudo-leaf leaves it
// nil and the callers gate on slicedelta>=0 first.
export fn dotchainresolve(c: *cgen, n: *syntax.node,
outrootname: *str, outrootoff: *i32, outtotaloff: *i32,
outleaftype: **syntax.tinfo, outslicedelta: *i32,
outisglobal: *bool, outptrroot: *bool) bool = {
*outrootname = "";
*outrootoff = 0;
*outisglobal = false;
*outptrroot = false;
*outtotaloff = 0;
*outleaftype = nil;
*outslicedelta = -1;
if (n == nil) { return false; };
if (n.kind != syntax.nkind.N_DOT) { return false; };
let stk: [16]*syntax.node;
let nsteps: i32 = 0;
let cur: *syntax.node = n;
for (cur != nil) {
if (cur.kind != syntax.nkind.N_DOT) { break; };
if (nsteps >= 16) { return false; };
stk[nsteps] = cur;
nsteps += 1;
cur = cur.lhs;
};
if (nsteps < 2) { return false; };
if (cur == nil) { return false; };
if (cur.kind != syntax.nkind.N_IDENT) { return false; };
*outrootname = cur.str;
let resolved: bool = false;
let lc: *local = localfindnode(c, cur.str);
if (lc != nil) {
if (lc.tnode != nil) {
if (lc.tnode.kind == syntax.nkind.N_TNAME) {
*outrootoff = lc.off;
resolved = true;
};
// `*T` root (param/local): dereference at emit time;
// pointee struct supplies the field layout. Callers
// that opt in via *outptrroot emit a MOVQ load of the
// slot before indexing.
if (lc.tnode.kind == syntax.nkind.N_TPTR) {
let pe: *syntax.node = lc.tnode.lhs;
if (pe != nil) {
if (pe.kind == syntax.nkind.N_TNAME) {
*outrootoff = lc.off;
*outptrroot = true;
resolved = true;
};
};
};
};
};
// Global `*struct` root (`let gp: *S`): localfindnode is nil and
// letvarstructinfo below only matches a VALUE-struct global, so a
// `*struct` global root needs its own arm. Gate on N_TPTR→N_TNAME
// exactly as the local `*T` arm above; the post-resolution loop peels
// the TY_PTR off cur.type_ and validates the pointee is a struct.
// Mirrors cstage cgen.c's ptr_root + let_islet base resolution
// (LEAQ name(SB),CX; MOVQ (CX),CX). (#16, chained twin of #15.)
if (!resolved && lc == nil) {
let gtn: *syntax.node = letvartnode(c, cur.str);
if (gtn != nil) {
if (gtn.kind == syntax.nkind.N_TPTR) {
let pe: *syntax.node = gtn.lhs;
if (pe != nil) {
if (pe.kind == syntax.nkind.N_TNAME) {
*outisglobal = true;
*outptrroot = true;
resolved = true;
};
};
};
};
};
if (!resolved) {
let gsi: *structinfo = letvarstructinfo(c, cur.str);
if (gsi != nil) {
*outisglobal = true;
resolved = true;
};
};
if (!resolved) { return false; };
// Root struct layout = the stamped root-ident type_, peeled NAMED
// (plus one TY_PTR hop for a `*struct` root). tfield.type_ then
// supplies each nested struct directly, so no name re-lookup.
let curstruct: *syntax.tinfo = cur.type_: *syntax.tinfo;
curstruct = tichase(curstruct);
if (*outptrroot) {
if (curstruct == nil) { return false; };
if (curstruct.kind != syntax.tykind.TY_PTR) { return false; };
curstruct = curstruct.sub;
curstruct = tichase(curstruct);
};
let i: i32 = nsteps - 1;
for (i >= 0) {
if (curstruct == nil) { return false; };
if (curstruct.kind != syntax.tykind.TY_STRUCT) { return false; };
if (stk[i] == nil) { return false; };
let stepnm: str = stk[i].str;
let tf: *syntax.tfield = curstruct.fields;
let found: *syntax.tfield = nil;
for (tf != nil) {
if (syntax.streq(tf.name, stepnm)) { found = tf; break; };
tf = tf.tnext;
};
if (found == nil) { return false; };
let foff: i32 = found.offset: i32;
if (i == 0) {
*outtotaloff = *outtotaloff + foff;
*outleaftype = found.type_;
return true;
};
let ft: *syntax.tinfo = found.type_;
ft = tichase(ft);
if (ft == nil) { return false; };
if (ft.kind == syntax.tykind.TY_STR) {
// str IS []u8: .cap is the third header word, same as
// the TY_SLICE leaf below — cstage treats str≡slice for
// .ptr/.len/.cap (cmd/w6c/cgen.c:2478) (#1/Phase 3, #11).
if (i != 1) { return false; };
let pseudo: str = stk[0].str;
let delta: i32 = -1;
if (syntax.streq(pseudo, "ptr")) { delta = 0; }
else { if (syntax.streq(pseudo, "len")) { delta = 8; }
else { if (syntax.streq(pseudo, "cap")) { delta = 16; }; }; };
if (delta < 0) { return false; };
*outtotaloff = *outtotaloff + foff;
*outslicedelta = delta;
return true;
};
if (ft.kind == syntax.tykind.TY_SLICE) {
if (i != 1) { return false; };
let pseudo: str = stk[0].str;
let delta: i32 = -1;
if (syntax.streq(pseudo, "ptr")) { delta = 0; }
else { if (syntax.streq(pseudo, "len")) { delta = 8; }
else { if (syntax.streq(pseudo, "cap")) { delta = 16; }; }; };
if (delta < 0) { return false; };
*outtotaloff = *outtotaloff + foff;
*outslicedelta = delta;
return true;
};
if (ft.kind != syntax.tykind.TY_STRUCT) { return false; };
*outtotaloff = *outtotaloff + foff;
curstruct = ft;
i -= 1;
};
return false;
};
// cgstructlitfill — fill a struct-typed slot from an N_STRUCTLIT
// value into one of three destination flavors. Mirror of cstage
// cgen.c's cg_structlit_fill. Used by cglet, cgreturn N_STRUCTLIT,
// cgassign N_IDENT-lhs N_STRUCTLIT (BP-rel) AND cgassign N_DOT-lhs
// N_STRUCTLIT (BP-rel / via *struct local / via struct global) at
// single-dot and chained-dot sites.
//
// Destination modes:
// 0 = DST_BP — base = BP, no reload. Stores at disp+i(BP).
// srcoff/srcname unused.
// 1 = DST_PTR_LOCAL — base = BX, reloaded from srcoff(BP) before
// the ELLIPSIS zero-fill loop and before EVERY
// field store (cgexpr clobbers BX between
// fields). Stores at disp+i(BX). srcname
// unused.
// 2 = DST_GLOBAL — base = BX, reloaded via `LEAQ srcname(SB),
// BX` with the same cadence as DST_PTR_LOCAL.
// srcoff unused.
// 3 = DST_PTR_SP — base = BX, reloaded via `MOVQ (SP), BX` with
// the same cadence as DST_PTR_LOCAL. dst is the
// alloc-heap base saved by cgalloc's `PUSHQ AX`
// (top-of-stack); cgexpr is stack-balanced so
// (SP) keeps pointing at it across the walk.
// srcoff/srcname unused. C7c: nested struct/
// array/tuple field VALUE in alloc(Outer{x =
// Inner{..}}) now writes the inner leaves
// instead of storing AX=0 over the inner slot.
//
// Param semantics (locked in here so the recursion contract is
// clear):
// - `disp` is the per-recursion accumulator — grows by `fi.foff`
// as we descend into a nested struct-typed structlit field.
// - `srcoff` (DST_PTR_LOCAL) and `srcname` (DST_GLOBAL) are
// *constant* across the whole call tree — they identify the
// root dst, which doesn't change with depth.
// - the ELLIPSIS zero-fill extent is read internally as
// structabisize(si) — cstage's cg_structlit_fill computes
// `sz = lu->size` (cgen.c:2085), the maxalign-rounded ABI size
// (check.c:760 lu->size = (off+maxalign-1)&~(maxalign-1)). The
// pre-#169 callers passed two different sizes (natural at DOT
// sites, slot-padded at BP-rel sites); neither matched cstage
// for maxalign<8 structs (the zero-fill ran MOVQ where cstage
// ran MOVL — value-correct, asm-divergent).
//
// Why a helper? The inline field-walk previously did
// `cgexpr(field.lhs); store AX sized`. For struct-typed fields whose
// value is itself a nested N_STRUCTLIT, cgexpr has no whole-struct-
// in-register convention — it lands AX = first qword and the
// trailing bytes silently stay zero. #17 fixed the BP-rel sites;
// #18 extends the same recursion to the four cgassign N_DOT-lhs
// structlit walks (single-dot via_ptr/global/local + chained
// depth>=2).
//
// The non-BP modes emit a redundant BX reload at the start of each
// recursive nested zero-fill / each recursive scalar store — this is
// correctness-by-construction (BX is always freshly loaded right
// before use), and the redundancy only fires on the nested-STRUCTLIT
// shapes that didn't compile before. Byte-identity for the no-
// nested case (the only shape selfhost source uses today) is
// preserved because the existing inline code's reload-before-each-
// store pattern matches the helper's per-store reload exactly.
//
// The scalar store routes through fieldstoreop (full field-width
// {1→MOVB, 2→MOVW, 4→MOVL, else MOVQ}); the missing MOVW for fsz==2
// over-stored a 2-byte tail field past its slot into saved BP on a
// union-return success variant (#15). Symmetric with cstage fldstoreop.
fn cgstructlitfill(c: *cgen, si: *structinfo, lit: *syntax.node,
mode: i32, srcoff: i32, srcname: str,
disp: i32) void = {
if (si == nil) { return; };
let basereg: str = "BP";
if (mode != 0) { basereg = "BX"; };
let totsize: i32 = structabisize(si);
if (lit.op == syntax.tkind.TK_ELLIPSIS) {
// `..., ...` autofill — zero the entire slot first so
// unmentioned fields read as 0. Sized stores: 8/4/1. For
// non-BP modes, reload BX once before the loop (cgexpr-free
// region between iterations, so one reload is enough).
emitline("\tXORQ\tAX, AX\n");
if (mode == 1) {
emitline("\tMOVQ\t");
emitoff(srcoff: i64);
emitline("(BP), BX\n");
};
if (mode == 2) {
emitline("\tLEAQ\t");
emitsymname(c, srcname);
emitline("(SB), BX\n");
};
if (mode == 3) {
emitline("\tMOVQ\t(SP), BX\n");
};
let zi: i32 = 0;
for (zi + 8 <= totsize) {
emitline("\tMOVQ\tAX, ");
if (mode == 0) {
emitoff((disp + zi): i64);
emitline("(BP)\n");
} else {
emitdispreg((disp + zi): i64, basereg);
emitline("\n");
};
zi += 8;
};
for (zi + 4 <= totsize) {
emitline("\tMOVL\tAX, ");
if (mode == 0) {
emitoff((disp + zi): i64);
emitline("(BP)\n");
} else {
emitdispreg((disp + zi): i64, basereg);
emitline("\n");
};
zi += 4;
};
for (zi < totsize) {
emitline("\tMOVB\tAX, ");
if (mode == 0) {
emitoff((disp + zi): i64);
emitline("(BP)\n");
} else {
emitdispreg((disp + zi): i64, basereg);
emitline("\n");
};
zi += 1;
};
};
let fieldnode: *syntax.node = lit.list;
for (fieldnode != nil) {
if (fieldnode.kind == syntax.nkind.N_FIELD) {
let fname: str = fieldnode.str;
let fi: *fieldinfo = si.fields;
for (fi != nil) {
let fn_: str = fi.fname;
if (syntax.streq(fn_, fname)) {
// Tagged-union field: delegate to the shared
// widening writer (handles str/scalar/struct
// literal/ident payload + tagged-subset tag
// remap). For non-BP modes, reload BX first so
// the widener sees a valid base reg.
if (istaggedtype(c, fi.tnode)) {
if (mode == 1) {
emitline("\tMOVQ\t");
emitoff(srcoff: i64);
emitline("(BP), BX\n");
};
if (mode == 2) {
emitline("\tLEAQ\t");
emitsymname(c, srcname);
emitline("(SB), BX\n");
};
if (mode == 3) {
emitline("\tMOVQ\t(SP), BX\n");
};
cgwidentaggedstore(c, fi.tnode.type_: *syntax.tinfo,
fieldnode.lhs, basereg,
disp + fi.foff, fi.fsz);
fi = nil;
} else {
// Nested struct-typed structlit value: look up
// the inner struct's metadata and recurse at the
// field's offset. Pre-#17/#18 the cgexpr-then-
// store below would land AX = first qword and
// the rest silently stayed zero.
let nested: bool = false;
if (fieldnode.lhs != nil) {
if (fieldnode.lhs.kind == syntax.nkind.N_STRUCTLIT) {
if (fi.tnode != nil) {
if (fi.tnode.kind == syntax.nkind.N_TNAME) {
if (aliasprimsize(c, fi.tnode.str) == 0) {
let isi: *structinfo = structlookup(c, fi.tnode.str);
if (isi != nil) {
cgstructlitfill(c, isi,
fieldnode.lhs,
mode, srcoff, srcname,
disp + fi.foff);
nested = true;
};
};
};
};
};
};
// Nested struct-typed CALL value (#20). cgexpr
// leaves AX=bytes[0..7], DX=bytes[8..15], CX=
// bytes[16..23] per #4's cgreturn ABI. Pre-#20
// the cgexpr-then-AX-store fallthrough below
// silently dropped past the first qword for any
// fsz > 8 (only AX got stored).
//
// The choke-point stores MOVQ for full 8B chunks
// plus a sized tail (MOVL/MOVW/MOVB) by `tail =
// fsz%8`. Mirror of cstage cg_structlit_fill's #20
// branch.
//
// Guard `cfsz <= 24` (#14 widened from {0,1,2,4}):
// the choke-point handles every in-cap tail incl.
// 3/5/6/7 via its non-padded scratch detour
// (dest_padded=false — a struct-lit field is
// packed); >24B falls through (sret deferred).
//
// INVARIANT: between cgexpr(N_CALL) and the
// AX/DX/CX stores below, NO instruction may touch
// AX/DX/CX. The BX reload is safe; any other
// emission added here will silently corrupt the
// return value.
let callwhole: bool = false;
if (!nested) {
if (fieldnode.lhs != nil) {
if (fieldnode.lhs.kind == syntax.nkind.N_CALL) {
if (fi.tnode != nil) {
if (fi.tnode.kind == syntax.nkind.N_TNAME) {
if (aliasprimsize(c, fi.tnode.str) == 0) {
let csi: *structinfo = structlookup(c, fi.tnode.str);
if (csi != nil) {
// Inner struct's ABI size
// (maxalign-rounded) — cstage
// reads fl->type->size at the
// nested-call branch
// (cgen.c:2121); check.c:760
// sets that to the
// maxalign-rounded ABI extent
// (NOT natural). fi.fsz is
// wwstage's slot-padded
// totsize (round-to-8); the
// pre-#169 structnaturalsize
// shorts struct{i64,i32}
// (natural 12, ABI 16) to
// MOVQ+MOVL where cstage
// writes MOVQ+MOVQ.
let cfsz: i32 = structabisize(csi);
if (cfsz <= 24) {
// #14: choke-point now stores every in-cap tail; 3/5/6/7 no longer dropped to a lone narrow MOV.
cgexpr(c, fieldnode.lhs);
if (mode == 1) {
emitline("\tMOVQ\t");
emitoff(srcoff: i64);
emitline("(BP), BX\n");
};
if (mode == 2) {
emitline("\tLEAQ\t");
emitsymname(c, srcname);
emitline("(SB), BX\n");
};
if (mode == 3) {
emitline("\tMOVQ\t(SP), BX\n");
};
cgaggregstore(c, basereg, disp + fi.foff, cfsz, false);
callwhole = true;
};
};
};
};
};
};
};
};
if (nested) {
fi = nil;
} else if (callwhole) {
fi = nil;
} else if (isstrtype(c, fi.tnode) || isslicetype(c, fi.tnode)) {
// str IS []u8: 3-word field (ptr,len,cap).
// cgexpr leaves AX/BX/CX; for non-BP modes
// the dst base goes in DX to dodge BX=len /
// CX=cap (the generic store reloads BX, which
// would clobber len) (#1/Phase 3). A slice is
// the same 24B {ptr,len,cap} shape, so it rides
// this arm; without it the generic scalar tail
// stored only the ptr word (#24).
cgexpr(c, fieldnode.lhs);
if (mode == 0) {
emitline("\tMOVQ\tAX, ");
emitoff((disp + fi.foff): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tBX, ");
emitoff((disp + fi.foff + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((disp + fi.foff + 16): i64);
emitline("(BP)\n");
} else {
if (mode == 1) {
emitline("\tMOVQ\t");
emitoff(srcoff: i64);
emitline("(BP), DX\n");
} else { if (mode == 3) {
emitline("\tMOVQ\t(SP), DX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, srcname);
emitline("(SB), DX\n");
}; };
emitline("\tMOVQ\tAX, ");
emitdispreg((disp + fi.foff): i64, "DX");
emitline("\n");
emitline("\tMOVQ\tBX, ");
emitdispreg((disp + fi.foff + 8): i64, "DX");
emitline("\n");
emitline("\tMOVQ\tCX, ");
emitdispreg((disp + fi.foff + 16): i64, "DX");
emitline("\n");
};
fi = nil;
} else if (fi.tnode != nil
&& fi.tnode.kind == syntax.nkind.N_TARRAY
&& fieldnode.lhs != nil
&& fieldnode.lhs.kind == syntax.nkind.N_ARRLIT) {
// #249: array field from an N_ARRLIT. Without this
// the generic tail below cgexprs the N_ARRLIT (→ AX)
// and stores one sized word, silently DROPPING every
// element. Store element-wise at disp+foff+i*esz,
// mirroring the N_LET array-init path
// (cgenstmt.ww:1393). int/float elements only;
// str/slice/struct/tagged elements are the N_LET
// multi-word gap — loud rule-7 error (cstage
// cg_structlit_fill twin).
let elemn: *syntax.node = fi.tnode.lhs;
let isstrel: bool = isstrtype(c, elemn);
let istagel: bool = istaggedtype(c, elemn);
let issliceel: bool = isslicetype(c, elemn);
// #100: key the loud gate off the CHASED stamped
// tinfo (tichase, the tnodeisagg discipline), not
// the raw tnode — a bare N_TSLICE kind test /
// structlookup leaf-name probe is alias-blind, so
// `type el = el0;` bypassed the gate and fell to
// the scalar tail (silent wrong, ken kb5_fill2).
// Twin of cstage cg_structlit_fill's
// type_chase_named key (cgen.c:3179, B5-c1).
let isstructel: bool = false;
if (elemn != nil) {
let eu: *syntax.tinfo = tichase(elemn.type_: *syntax.tinfo);
if (eu != nil) {
if (eu.kind == syntax.tykind.TY_STRUCT) {
isstructel = true;
};
};
};
if (isstrel || issliceel || isstructel || istagel) {
let e1: str = "ww: struct-literal array field '";
os.write(2, e1.ptr, e1.len: u64);
os.write(2, fname.ptr, fname.len: u64);
let e2: str = "' has a str/slice/struct/tagged element — multi-word element store out of #249 scope (N_LET array-init gap)\n";
os.write(2, e2.ptr, e2.len: u64);
os.exit(1);
};
let esz: i32 = 8;
if (elemn != nil) {
if (elemn.kind == syntax.nkind.N_TNAME) {
let ps: i32 = aliasprimsize(c, elemn.str);
if (ps > 0) { esz = ps; };
};
};
let mop: str = tnodestoreop(c, elemn, esz);
let isfloatel: bool = isfloattype(c, elemn);
let fmov: str = "MOVSD";
if (isf32type(c, elemn)) { fmov = "MOVSS"; };
let idx: i32 = 0;
let repeat: bool = false;
let e: *syntax.node = fieldnode.lhs.list;
for (e != nil) {
let isellip: bool = false;
if (e.kind == syntax.nkind.N_FIELD) {
if (syntax.streq(e.str, "...")) {
repeat = true;
isellip = true;
};
};
if (isellip) {
e = nil;
} else {
cgexpr(c, e);
if (mode == 1) {
emitline("\tMOVQ\t");
emitoff(srcoff: i64);
emitline("(BP), BX\n");
};
if (mode == 2) {
emitline("\tLEAQ\t");
emitsymname(c, srcname);
emitline("(SB), BX\n");
};
if (mode == 3) {
emitline("\tMOVQ\t(SP), BX\n");
};
let eoff: i32 = disp + fi.foff + idx * esz;
if (isfloatel) {
emitline("\t");
emitline(fmov);
emitline("\tX0, ");
} else {
emitline("\t");
emitline(mop);
emitline("\tAX, ");
};
if (mode == 0) {
emitoff(eoff: i64);
emitline("(BP)\n");
} else {
emitdispreg(eoff: i64, basereg);
emitline("\n");
};
idx += 1;
e = e.next;
};
};
if (repeat) {
let total: i32 = idx;
if (fi.tnode.rhs != nil) {
if (fi.tnode.rhs.kind == syntax.nkind.N_INTLIT) {
total = fi.tnode.rhs.uval: i32;
};
};
for (idx < total) {
if (mode == 1) {
emitline("\tMOVQ\t");
emitoff(srcoff: i64);
emitline("(BP), BX\n");
};
if (mode == 2) {
emitline("\tLEAQ\t");
emitsymname(c, srcname);
emitline("(SB), BX\n");
};
if (mode == 3) {
emitline("\tMOVQ\t(SP), BX\n");
};
let eoff: i32 = disp + fi.foff + idx * esz;
if (isfloatel) {
emitline("\t");
emitline(fmov);
emitline("\tX0, ");
} else {
emitline("\t");
emitline(mop);
emitline("\tAX, ");
};
if (mode == 0) {
emitoff(eoff: i64);
emitline("(BP)\n");
} else {
emitdispreg(eoff: i64, basereg);
emitline("\n");
};
idx += 1;
};
};
fi = nil;
} else if (tnodeisagg(fi.tnode)) {
// #49 (f38b/x5f-h): an aggregate (struct/array/
// tuple) field from an ADDRESSABLE source expr —
// `outer{.., r = r}` — fell to the scalar tail
// below and stored word0 only. Funnel: source
// address via aggargsrcaddr (SI), field address
// via LEAQ/ADDQ (BX — loaded AFTER the source
// walk, which clobbers BX/AX), then aggcopy.
// Width = the checker-STAMPED tinfo size (the
// cstage fl->type->size SSoT; fi.fsz is the
// slot-padded extent and skews on maxalign<8).
// Non-addressable aggregate sources (tuple-lit,
// >24B/odd-tail call) die loud — pre-#49 the
// same silent word0 (rule 7). Mirror of cstage
// cg_structlit_fill #49 arm.
if (!aggargsrcaddr(c, fieldnode.lhs, "SI")) {
let m49g: str = "structlit fill: aggregate field '";
os.write(2, m49g.ptr, m49g.len: u64);
os.write(2, fname.ptr, fname.len: u64);
let m49h: str = "' from a non-addressable source unwired (task #49/rule-7)\n";
os.write(2, m49h.ptr, m49h.len: u64);
os.exit(1);
};
if (mode == 0) {
emitline("\tLEAQ\t");
emitoff((disp + fi.foff): i64);
emitline("(BP), BX\n");
} else {
if (mode == 1) {
emitline("\tMOVQ\t");
emitoff(srcoff: i64);
emitline("(BP), BX\n");
} else { if (mode == 3) {
emitline("\tMOVQ\t(SP), BX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, srcname);
emitline("(SB), BX\n");
}; };
if (disp + fi.foff != 0) {
emitline("\tADDQ\t$");
emitint((disp + fi.foff): i64);
emitline(", BX\n");
};
};
let agsz49: i32 = 0;
if (fi.tnode != nil) {
let agti49: *syntax.tinfo = fi.tnode.type_: *syntax.tinfo;
agti49 = tichase(agti49);
if (agti49 != nil) { agsz49 = agti49.size: i32; };
};
aggcopy(c, agsz49);
fi = nil;
} else {
cgexpr(c, fieldnode.lhs);
// For non-BP modes, cgexpr just clobbered
// BX; reload it before the store.
if (mode == 1) {
emitline("\tMOVQ\t");
emitoff(srcoff: i64);
emitline("(BP), BX\n");
};
if (mode == 2) {
emitline("\tLEAQ\t");
emitsymname(c, srcname);
emitline("(SB), BX\n");
};
if (mode == 3) {
emitline("\tMOVQ\t(SP), BX\n");
};
if (isfloattype(c, fi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, fi.tnode)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\tX0, ");
if (mode == 0) {
emitoff((disp + fi.foff): i64);
emitline("(BP)\n");
} else {
emitdispreg((disp + fi.foff): i64, basereg);
emitline("\n");
};
fi = nil;
} else {
// Full field-width store via fieldstoreop
// {1→MOVB, 2→MOVW, 4→MOVL, else MOVQ}: the
// missing MOVW for fsz==2 over-stored a
// 2-byte tail field past its slot into
// saved BP on a union-return success
// variant (#15). Symmetric with cstage.
let op: str = fieldstoreop(c, fi);
emitline("\t");
emitline(op);
emitline("\tAX, ");
if (mode == 0) {
emitoff((disp + fi.foff): i64);
emitline("(BP)\n");
} else {
emitdispreg((disp + fi.foff): i64, basereg);
emitline("\n");
};
fi = nil;
};
};
};
} else {
fi = fi.finext;
};
};
};
fieldnode = fieldnode.next;
};
};
// Thin wrapper preserving the BP-rel call shape used by cglet,
// cgreturn, cgassign N_IDENT-lhs N_STRUCTLIT, and the C1.25
// @placescr materialise (cg_structlit_fill_bp's named twin).
fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *syntax.node, bpoff: i32) void = {
if (si == nil) { return; };
cgstructlitfill(c, si, lit, 0, 0, "", bpoff);
};
// cgstructlitfilltn — tinfo-native twin of cgstructlitfill, the #31
// receiver-layout fill choke-point. Walks the struct's checker-STAMPED
// tfield chain (lu.fields) and keys every per-field decision off the
// resolved tf.type_ (kind / size / signedness) — NO structlookup, NO
// fi.tnode names. Nested struct fields recurse on tichase(tf.type_), so
// a cross-module same-leaf collision (the #224 cluster) cannot reach a
// name lookup at any nesting level. Faithful mirror of cstage
// cg_structlit_fill, which recurses on type_chase_named(fl->type)
// (cgen.c:3179) over resolved Type*. Emission (mode reloads, register
// staging, store ordering) is byte-identical to cgstructlitfill; only
// the LAYOUT source flips from si/fieldinfo to lu/tfield. tinfo.size
// equals structabisize/fi.fsz for a struct field (check.ww:2468 ABI
// formula; NAMED.size==under.size at :2234), so the size-keyed stores
// match. Modes: 0=DST_BP, 1=DST_PTR_LOCAL(srcoff), 2=DST_GLOBAL(srcname),
// 3=DST_PTR_SP. Routed by the W1/W2/W5 N_STRUCTLIT sub-arms + W4b.
fn cgstructlitfilltn(c: *cgen, lu: *syntax.tinfo, lit: *syntax.node,
mode: i32, srcoff: i32, srcname: str,
disp: i32) void = {
let s: *syntax.tinfo = tichase(lu);
if (s == nil) { return; };
if (s.kind != syntax.tykind.TY_STRUCT) { return; };
let basereg: str = "BP";
if (mode != 0) { basereg = "BX"; };
let totsize: i32 = s.size: i32;
if (lit.op == syntax.tkind.TK_ELLIPSIS) {
// `..., ...` autofill — zero the entire slot first so
// unmentioned fields read as 0. Sized stores: 8/4/1. For
// non-BP modes, reload BX once before the loop.
emitline("\tXORQ\tAX, AX\n");
if (mode == 1) {
emitline("\tMOVQ\t");
emitoff(srcoff: i64);
emitline("(BP), BX\n");
};
if (mode == 2) {
emitline("\tLEAQ\t");
emitsymname(c, srcname);
emitline("(SB), BX\n");
};
if (mode == 3) {
emitline("\tMOVQ\t(SP), BX\n");
};
let zi: i32 = 0;
for (zi + 8 <= totsize) {
emitline("\tMOVQ\tAX, ");
if (mode == 0) {
emitoff((disp + zi): i64);
emitline("(BP)\n");
} else {
emitdispreg((disp + zi): i64, basereg);
emitline("\n");
};
zi += 8;
};
for (zi + 4 <= totsize) {
emitline("\tMOVL\tAX, ");
if (mode == 0) {
emitoff((disp + zi): i64);
emitline("(BP)\n");
} else {
emitdispreg((disp + zi): i64, basereg);
emitline("\n");
};
zi += 4;
};
for (zi < totsize) {
emitline("\tMOVB\tAX, ");
if (mode == 0) {
emitoff((disp + zi): i64);
emitline("(BP)\n");
} else {
emitdispreg((disp + zi): i64, basereg);
emitline("\n");
};
zi += 1;
};
};
let fieldnode: *syntax.node = lit.list;
for (fieldnode != nil) {
if (fieldnode.kind == syntax.nkind.N_FIELD) {
let fname: str = fieldnode.str;
let tf: *syntax.tfield = tfieldlookup(s, fname);
if (tf != nil) {
let foff: i32 = tf.offset: i32;
let ftraw: *syntax.tinfo = tf.type_;
let fu: *syntax.tinfo = tichase(ftraw);
let fsz: i32 = 0;
if (fu != nil) { fsz = fu.size: i32; };
// Tagged-union field: delegate to the shared
// widening writer (str/scalar/struct-lit/ident
// payload + tagged-subset tag remap). For non-BP
// modes, reload BX first.
if (syntax.typeistagged(ftraw)) {
if (mode == 1) {
emitline("\tMOVQ\t");
emitoff(srcoff: i64);
emitline("(BP), BX\n");
};
if (mode == 2) {
emitline("\tLEAQ\t");
emitsymname(c, srcname);
emitline("(SB), BX\n");
};
if (mode == 3) {
emitline("\tMOVQ\t(SP), BX\n");
};
cgwidentaggedstore(c, ftraw,
fieldnode.lhs, basereg,
disp + foff, fsz);
} else {
// Nested struct-typed structlit value: recurse at
// the field's offset off the resolved field tinfo
// (NO structlookup). Pre-#17/#18 the cgexpr-then-
// store tail would land AX = first qword and the
// rest silently stayed zero.
let nested: bool = false;
if (fieldnode.lhs != nil) {
if (fieldnode.lhs.kind == syntax.nkind.N_STRUCTLIT) {
if (fu != nil) {
if (fu.kind == syntax.tykind.TY_STRUCT) {
cgstructlitfilltn(c, fu,
fieldnode.lhs,
mode, srcoff, srcname,
disp + foff);
nested = true;
};
};
};
};
// Nested struct-typed CALL value (#20): cgexpr
// leaves AX/DX/CX per #4's cgreturn ABI; the
// choke-point stores full 8B chunks + a sized tail.
// Guard fu.size <= 24 (#14: every in-cap tail incl.
// 3/5/6/7); >24B falls through (sret deferred).
// INVARIANT: between cgexpr(N_CALL) and the AX/DX/CX
// stores, only the BX reload may intervene.
let callwhole: bool = false;
if (!nested) {
if (fieldnode.lhs != nil) {
if (fieldnode.lhs.kind == syntax.nkind.N_CALL) {
if (fu != nil) {
if (fu.kind == syntax.tykind.TY_STRUCT) {
let cfsz: i32 = fu.size: i32;
if (cfsz <= 24) {
cgexpr(c, fieldnode.lhs);
if (mode == 1) {
emitline("\tMOVQ\t");
emitoff(srcoff: i64);
emitline("(BP), BX\n");
};
if (mode == 2) {
emitline("\tLEAQ\t");
emitsymname(c, srcname);
emitline("(SB), BX\n");
};
if (mode == 3) {
emitline("\tMOVQ\t(SP), BX\n");
};
cgaggregstore(c, basereg, disp + foff, cfsz, false);
callwhole = true;
};
};
};
};
};
};
if (nested) {
// recursed above
} else if (callwhole) {
// stored above
} else if (syntax.typeisstr(ftraw) || syntax.typeisslice(ftraw)) {
// str IS []u8: 3-word {ptr,len,cap}. cgexpr
// leaves AX/BX/CX; for non-BP modes the dst
// base goes in DX to dodge BX=len / CX=cap.
cgexpr(c, fieldnode.lhs);
if (mode == 0) {
emitline("\tMOVQ\tAX, ");
emitoff((disp + foff): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tBX, ");
emitoff((disp + foff + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((disp + foff + 16): i64);
emitline("(BP)\n");
} else {
if (mode == 1) {
emitline("\tMOVQ\t");
emitoff(srcoff: i64);
emitline("(BP), DX\n");
} else { if (mode == 3) {
emitline("\tMOVQ\t(SP), DX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, srcname);
emitline("(SB), DX\n");
}; };
emitline("\tMOVQ\tAX, ");
emitdispreg((disp + foff): i64, "DX");
emitline("\n");
emitline("\tMOVQ\tBX, ");
emitdispreg((disp + foff + 8): i64, "DX");
emitline("\n");
emitline("\tMOVQ\tCX, ");
emitdispreg((disp + foff + 16): i64, "DX");
emitline("\n");
};
} else if (fu != nil && fu.kind == syntax.tykind.TY_ARRAY
&& fieldnode.lhs != nil
&& fieldnode.lhs.kind == syntax.nkind.N_ARRLIT) {
// #249: array field from an N_ARRLIT. element-
// wise store at disp+foff+i*esz off the array
// element tinfo (fu.sub). int/float elements
// only; str/slice/struct/tagged elements are the
// N_LET multi-word gap — loud rule-7 (cstage
// cg_structlit_fill array arm twin).
let eti: *syntax.tinfo = tichase(fu.sub);
let isstructel: bool = false;
if (eti != nil) {
if (eti.kind == syntax.tykind.TY_STRUCT) { isstructel = true; };
};
if (syntax.typeisstr(fu.sub) || syntax.typeisslice(fu.sub)
|| isstructel || syntax.typeistagged(fu.sub)) {
let e1: str = "ww: struct-literal array field '";
os.write(2, e1.ptr, e1.len: u64);
os.write(2, fname.ptr, fname.len: u64);
let e2: str = "' has a str/slice/struct/tagged element — multi-word element store out of #249 scope (N_LET array-init gap)\n";
os.write(2, e2.ptr, e2.len: u64);
os.exit(1);
};
let esz: i32 = 8;
if (eti != nil) { esz = eti.size: i32; };
let mop: str = storeopsz(esz);
let isfloatel: bool = syntax.typeisfloat(fu.sub);
let fmov: str = "MOVSD";
if (syntax.typeisf32(fu.sub)) { fmov = "MOVSS"; };
let idx: i32 = 0;
let repeat: bool = false;
let e: *syntax.node = fieldnode.lhs.list;
for (e != nil) {
let isellip: bool = false;
if (e.kind == syntax.nkind.N_FIELD) {
if (syntax.streq(e.str, "...")) {
repeat = true;
isellip = true;
};
};
if (isellip) {
e = nil;
} else {
cgexpr(c, e);
if (mode == 1) {
emitline("\tMOVQ\t");
emitoff(srcoff: i64);
emitline("(BP), BX\n");
};
if (mode == 2) {
emitline("\tLEAQ\t");
emitsymname(c, srcname);
emitline("(SB), BX\n");
};
if (mode == 3) {
emitline("\tMOVQ\t(SP), BX\n");
};
let eoff: i32 = disp + foff + idx * esz;
if (isfloatel) {
emitline("\t");
emitline(fmov);
emitline("\tX0, ");
} else {
emitline("\t");
emitline(mop);
emitline("\tAX, ");
};
if (mode == 0) {
emitoff(eoff: i64);
emitline("(BP)\n");
} else {
emitdispreg(eoff: i64, basereg);
emitline("\n");
};
idx += 1;
e = e.next;
};
};
if (repeat) {
let total: i32 = fu.alen: i32;
for (idx < total) {
if (mode == 1) {
emitline("\tMOVQ\t");
emitoff(srcoff: i64);
emitline("(BP), BX\n");
};
if (mode == 2) {
emitline("\tLEAQ\t");
emitsymname(c, srcname);
emitline("(SB), BX\n");
};
if (mode == 3) {
emitline("\tMOVQ\t(SP), BX\n");
};
let eoff: i32 = disp + foff + idx * esz;
if (isfloatel) {
emitline("\t");
emitline(fmov);
emitline("\tX0, ");
} else {
emitline("\t");
emitline(mop);
emitline("\tAX, ");
};
if (mode == 0) {
emitoff(eoff: i64);
emitline("(BP)\n");
} else {
emitdispreg(eoff: i64, basereg);
emitline("\n");
};
idx += 1;
};
};
} else if (fu != nil
&& (fu.kind == syntax.tykind.TY_STRUCT
|| fu.kind == syntax.tykind.TY_ARRAY
|| fu.kind == syntax.tykind.TY_TUPLE)) {
// #49: aggregate (struct/array/tuple) field from
// an ADDRESSABLE source expr — `outer{.., r = r}`.
// Funnel: source addr via aggargsrcaddr (SI),
// field addr via LEAQ/ADDQ (BX, AFTER the source
// walk clobbers BX/AX), then aggcopy. Width = the
// resolved field tinfo size. Non-addressable
// sources die loud (rule 7). cstage #49 arm twin.
if (!aggargsrcaddr(c, fieldnode.lhs, "SI")) {
let m49g: str = "structlit fill: aggregate field '";
os.write(2, m49g.ptr, m49g.len: u64);
os.write(2, fname.ptr, fname.len: u64);
let m49h: str = "' from a non-addressable source unwired (task #49/rule-7)\n";
os.write(2, m49h.ptr, m49h.len: u64);
os.exit(1);
};
if (mode == 0) {
emitline("\tLEAQ\t");
emitoff((disp + foff): i64);
emitline("(BP), BX\n");
} else {
if (mode == 1) {
emitline("\tMOVQ\t");
emitoff(srcoff: i64);
emitline("(BP), BX\n");
} else { if (mode == 3) {
emitline("\tMOVQ\t(SP), BX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, srcname);
emitline("(SB), BX\n");
}; };
if (disp + foff != 0) {
emitline("\tADDQ\t$");
emitint((disp + foff): i64);
emitline(", BX\n");
};
};
aggcopy(c, fsz);
} else {
cgexpr(c, fieldnode.lhs);
// For non-BP modes, cgexpr just clobbered BX;
// reload it before the store.
if (mode == 1) {
emitline("\tMOVQ\t");
emitoff(srcoff: i64);
emitline("(BP), BX\n");
};
if (mode == 2) {
emitline("\tLEAQ\t");
emitsymname(c, srcname);
emitline("(SB), BX\n");
};
if (mode == 3) {
emitline("\tMOVQ\t(SP), BX\n");
};
if (syntax.typeisfloat(ftraw)) {
let mov: str = "MOVSD";
if (syntax.typeisf32(ftraw)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\tX0, ");
if (mode == 0) {
emitoff((disp + foff): i64);
emitline("(BP)\n");
} else {
emitdispreg((disp + foff): i64, basereg);
emitline("\n");
};
} else {
// Full field-width store {1->MOVB, 2->MOVW,
// 4->MOVL, else MOVQ}; the #15 sub-8 guard.
let op: str = storeopsz(fsz);
emitline("\t");
emitline(op);
emitline("\tAX, ");
if (mode == 0) {
emitoff((disp + foff): i64);
emitline("(BP)\n");
} else {
emitdispreg((disp + foff): i64, basereg);
emitline("\n");
};
};
};
};
};
};
fieldnode = fieldnode.next;
};
};