Files
ww/selfhost/cmd/wcc/cgenutil.ww
Hojun-Cho 0763377806 selfhost/cmd/wcc: dotchainresolve walks tinfo.fields (#71, A.6.3j)
The chained value-struct spine-walker resolved root + total offset + leaf
type via structinfo/fieldinfo (slot-padded foff). Swap its guts onto the
stamped root-ident type_: peel TY_NAMED to TY_STRUCT (one TY_PTR hop for a
*struct root) and accumulate tfield.offset down the spine, mirroring cstage's
cur->lhs->type fields walk (cmd/w6c/cgen.c:3156-3216 write, :1955-2030 AMP/
read). Leaf out-param becomes *tinfo; the three callers read leaf-ness via
typeis*/loadopsz, and the cgassign struct-terminal recovers the struct name
from the leaf tinfo's NAMED wrapper for the still-structinfo cgstructlitfill.

Root classification (local/ptr/global) is unchanged, so the firing set and
addressing mode stay byte-identical; the global-root path remains its pre-
existing shared breakage (#27), untouched. Natural tfield.offset equals the
old slot-padded foff for every shape the byte-id gate exercises -- and since
cstage already reads the natural offset and ww-old==cstage held, the flip is
structurally identical, not coincidental.

Latent (off-corpus, byte-id-neutral): the tinfo-peel now fires the str/slice
pseudo-leaf on an aliased-str/slice field where the old structinfo path
bailed -- a faithful-toward-cstage gap closure, filed for a probe + sentinel.

make test 134/134, byte-id 950+990-997 hold.
2026-05-24 02:35:53 +09:00

3323 lines
123 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: indexbaseesz, elemsizeof
// - 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 ast;
import tok;
import typ;
import sym;
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: *node) *node = {
let s: *node = newnode(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: *node, nfixed_out: *i32) *node = {
*nfixed_out = 0;
let p: *node = ps;
for (p != nil) {
if (p.kind == nkind.N_PARAM) {
if (p.op == tkind.TK_ELLIPSIS) {
return p;
};
*nfixed_out += 1;
};
p = p.next;
};
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: *node, nfixed_out: *i32) *node = {
*nfixed_out = 0;
if (callee == nil) { return nil; };
let ps: *node = nil;
if (callee.kind == nkind.N_IDENT) {
if (callee.str.len == 0) { return nil; };
ps = fnparamslookup(c, callee.str);
} else { if (callee.kind == 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 == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
ps = fnparamslookupmod(c, callee.str, cmod);
}; };
return findvariadicparam(ps, nfixed_out);
};
// mkvarargname — fresh local-slot name "<prefix><seq>". Used for
// the per-variadic-call scratch buffers (`@vararg_d_N` for the
// element-data buffer, `@vararg_sl_N` for the 24B slice descriptor).
// N is recorded on the N_CALL node at first emit so re-entry into
// cgcall picks the same names regardless of walk order.
fn mkvarargname(c: *cgen, prefix: str, seq: i32) str = {
let buf: [128]u8;
let i: i32 = 0;
let j: i32 = 0;
for (j < prefix.len) {
buf[i] = prefix[j];
i += 1; j += 1;
};
let ns: str = strconv.i64tos(seq: i64, strconv.base.DEC);
let n: i32 = ns.len;
let dk: i32 = 0;
for (dk < n) { buf[i + dk] = ns.ptr[dk]; dk += 1; };
let total: i32 = i + n;
let p: []u8 = alloc([], (total: u64) + 1u64)!;
let k: i32 = 0;
for (k < total) { p[k] = buf[k]; k += 1; };
p[total] = 0u8;
let r: str;
r.ptr = p.ptr;
r.len = total;
return r;
};
// ---- 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.
fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = {
if (arg == nil) { return 0; };
let nextparam: *node = nil;
if (param != nil) { nextparam = param.next; };
let rest: i32 = pushargsrev(c, arg.next, nextparam);
// 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 == 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 == tkind.TK_ELLIPSIS) {
widensz = 0;
} else {
let ptype: *node = param.lhs;
if (istaggedtype(c, ptype)) {
let aistagged: bool = false;
if (arg.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, arg.str);
if (lc != nil) {
aistagged = istaggedtype(c, lc.tnode);
};
};
// #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 == nkind.N_INDEX) {
if (istaggedtype(c, arg)) {
if (slotsize(c, arg) == slotsize(c, ptype)) {
aistagged = true;
};
};
};
if (!aistagged) {
widensz = slotsize(c, ptype);
let tagged: *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.
let pname: str = rhsstructpayload(c, arg);
if (pname.len > 0) {
let ptype: *node = param.lhs;
let scroff: i32 = localadd(c, "@tagscr", widensz, nil);
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_: *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)) {
// slot 24: [+0]=tag,[+8]=ptr,[+16]=len. Push high→low
// so pop drains tag first into arg-reg[0].
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;
};
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 == nkind.N_SLICE) {
let base: *node = arg.lhs;
let lo: *node = arg.rhs;
let hi: *node = arg.cond;
let baselocal: *local = nil;
let globaltn: *node = nil;
let globalname: str;
globalname.ptr = nil; globalname.len = 0;
if (base != nil) {
if (base.kind == nkind.N_IDENT) {
let bn: str = base.str;
baselocal = localfindnode(c, bn);
if (baselocal == nil) {
let gt: *node = letvartnode(c, bn);
if (gt != nil) {
globaltn = gt;
globalname = bn;
};
};
};
};
// base address → push
if (baselocal != nil) {
let tn: *node = baselocal.tnode;
if (tn != nil) {
if (tn.kind == nkind.N_TARRAY) {
emitline("\tLEAQ\t");
emitoff(baselocal.off: i64);
emitline("(BP), AX\n");
} else {
emitline("\tMOVQ\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) {
if (globaltn.kind == nkind.N_TARRAY) {
emitline("\tLEAQ\t");
emitsymname(c, globalname);
emitline("(SB), AX\n");
} else {
emitline("\tMOVQ\t");
emitsymname(c, globalname);
emitline("(SB), AX\n");
};
} 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: *node = baselocal.tnode;
if (tn != nil) {
if (tn.kind == nkind.N_TARRAY) {
let lenn: *node = tn.rhs;
if (lenn != nil) {
if (lenn.kind == nkind.N_INTLIT) {
emitline("\tMOVQ\t$");
emituint(lenn.uval);
emitline(", AX\n");
};
};
} else { if (tn.kind == nkind.N_TSLICE) {
emitline("\tMOVQ\t");
emitoff((baselocal.off + 8): i64);
emitline("(BP), AX\n");
} else { if (tn.kind == nkind.N_TNAME) {
if (streq(tn.str, "str")) {
emitline("\tMOVQ\t");
emitoff((baselocal.off + 8): i64);
emitline("(BP), AX\n");
};
};};};
};
} else { if (globaltn != nil) {
if (globaltn.kind == nkind.N_TARRAY) {
let lenn: *node = globaltn.rhs;
if (lenn != nil) {
if (lenn.kind == nkind.N_INTLIT) {
emitline("\tMOVQ\t$");
emituint(lenn.uval);
emitline(", AX\n");
};
};
} else { if (globaltn.kind == nkind.N_TSLICE) {
emitline("\tLEAQ\t");
emitsymname(c, globalname);
emitline("(SB), CX\n");
emitline("\tMOVQ\t8(CX), 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
emitline("\tADDQ\tAX, CX\n"); // CX = base + lo = ptr
emitline("\tPUSHQ\tDX\n"); // cap
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 == 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.
let stsz: i32 = structparamsize(c, lc.tnode);
if (stsz > 0) {
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;
};
};
};
// 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 = exprfloatkind(c, arg);
if (fk != 0) {
cgexpr(c, arg);
let mov: str = "MOVSD";
if (fk == 1) { mov = "MOVSS"; };
emitline("\tSUBQ\t$8, SP\n");
emitline("\t");
emitline(mov);
emitline("\tX0, (SP)\n");
return rest + 1;
};
cgexpr(c, arg);
if (nodeisslice(c, arg)) {
emitline("\tPUSHQ\tCX\n");
emitline("\tPUSHQ\tBX\n");
emitline("\tPUSHQ\tAX\n");
return rest + 3;
};
if (nodeisstr(c, arg)) {
emitline("\tPUSHQ\tBX\n");
emitline("\tPUSHQ\tAX\n");
return rest + 2;
};
// #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) {
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.
if (arg.kind == nkind.N_INDEX) {
if (istaggedtype(c, arg)) {
let isz: i32 = slotsize(c, arg);
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: *node) i32 = {
if (n == nil) { return 0; };
if (n.kind != nkind.N_CALL) { return 0; };
let callee: *node = n.lhs;
if (callee == nil) { return 0; };
if (callee.kind != nkind.N_IDENT) { return 0; };
let rtyp: *node = fnretlookup(c, callee.str);
if (!istaggedtype(c, rtyp)) { return 0; };
return slotsize(c, rtyp);
};
fn nodeisslice(c: *cgen, n: *node) bool = {
if (n == nil) { return false; };
let k: nkind = n.kind;
if (k == nkind.N_IDENT) {
let nm: str = n.str;
let lc: *local = localfindnode(c, nm);
if (lc != nil) { return isslicetype(c, lc.tnode); };
return false;
};
if (k == nkind.N_SLICE) { return true; };
if (k == 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 == nkind.N_CALL) {
let callee: *node = n.lhs;
if (callee != nil) {
if (callee.kind == nkind.N_IDENT) {
let rtyp: *node = fnretlookupmod(c, callee.str, c.curmod);
return isslicetype(c, rtyp);
};
if (callee.kind == nkind.N_DOT) {
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
let rtyp: *node = fnretlookupmod(c, callee.str, cmod);
return isslicetype(c, rtyp);
};
};
return false;
};
// 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 == nkind.N_DOT) {
return typeisslice(n.type_: *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).
//
// TODO(#11): every consumer of "is-str" here reconstructs the answer
// from raw N_kind because wwstage has no typed AST. Each new expression
// shape needs an explicit arm or it silently falls through to false,
// which downstream drops the second slot (BX/len) at the call site.
// A typed AST check (cstage reads n->type) would replace this whole
// function. Covered arms below: N_STRLIT, N_IDENT (local/let-typed),
// N_CALL (return type), N_INDEX (element type of [N]T / []T / *T base),
// N_DOT (reads checker-stamped n.type_ — #56 A.6.3h), N_CAST.
// Not covered (separate bugs / out of scope):
// - N_UN(TK_STAR) of `*str` — cgun itself emits only `MOVQ (AX), AX`
// and never loads .len into BX; fixing the recognizer alone won't
// help. Tracked alongside the broader cgun-load-shape gap.
fn nodeisstr(c: *cgen, n: *node) bool = {
if (n == nil) { return false; };
let k: nkind = n.kind;
if (k == nkind.N_STRLIT) { return true; };
if (k == nkind.N_IDENT) {
let nm: str = n.str;
let lc: *local = localfindnode(c, nm);
if (lc != nil) {
// Use isstrtype so `!str` aliases (parserr = !str) and
// `type foo = str;` chains resolve through. The bare
// `streq("str", ...)` test missed them and dropped the
// MOVQ BX,CX shuffle on returns of str-aliased locals.
if (isstrtype(c, lc.tnode)) { return true; };
};
return false;
};
if (k == nkind.N_CALL) {
let callee: *node = n.lhs;
if (callee != nil) {
if (callee.kind == nkind.N_IDENT) {
let rtyp: *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 == nkind.N_DOT) {
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
let rtyp: *node = fnretlookupmod(c, callee.str, cmod);
return isstrtype(c, rtyp);
};
};
return false;
};
// N_INDEX: `arr[i]` whose base is an indexable type carrying a
// str element. cgindex correctly loads (AX=ptr, BX=len) for a
// 16B element; without this arm pushargsrev only pushes AX and
// the call-arg pop reads .len from stack residue. Mirror of
// cstage's node_isstr → type_isstr(n->type), where n->type is
// the resolved element type after check.
if (k == nkind.N_INDEX) {
let base: *node = n.lhs;
if (base != nil) {
if (base.kind == nkind.N_IDENT) {
let bt: *node = nil;
let lc: *local = localfindnode(c, base.str);
if (lc != nil) { bt = lc.tnode; }
else { bt = letvartnode(c, base.str); };
if (bt != nil) {
let elem: *node = nil;
let bk: nkind = bt.kind;
if (bk == nkind.N_TARRAY) { elem = bt.lhs; };
if (bk == nkind.N_TSLICE) { elem = bt.lhs; };
if (bk == nkind.N_TPTR) { elem = bt.lhs; };
if (elem != nil) {
return isstrtype(c, elem);
};
};
};
// N_INDEX through a struct field: e.g. cmd.argsptr[i]
// where argsptr: *str. cgindex correctly loads the
// (ptr, len) pair via indexbaseesz; without this arm
// pushargsrev would only push AX and lose the .len.
if (base.kind == nkind.N_DOT) {
let fld: str = base.str;
if (streq(fld, "ptr")) { return false; };
if (streq(fld, "len")) { return false; };
if (streq(fld, "cap")) { return false; };
let inner: *node = base.lhs;
if (inner != nil) {
if (inner.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, inner.str);
if (lc != nil) {
let tn: *node = lc.tnode;
let sname: str;
sname.ptr = nil; sname.len = 0;
if (tn != nil) {
if (tn.kind == nkind.N_TNAME) { sname = tn.str; };
if (tn.kind == nkind.N_TPTR) {
let pinner: *node = tn.lhs;
if (pinner != nil) {
if (pinner.kind == nkind.N_TNAME) {
sname = pinner.str;
};
};
};
};
if (sname.len > 0) {
let si: *structinfo = structlookup(c, sname);
if (si != nil) {
let fi: *fieldinfo = si.fields;
for (fi != nil) {
if (streq(fi.fname, fld)) {
let ft: *node = fi.tnode;
if (ft != nil) {
let elem: *node = nil;
let fk: nkind = ft.kind;
if (fk == nkind.N_TPTR) { elem = ft.lhs; };
if (fk == nkind.N_TSLICE) { elem = ft.lhs; };
if (fk == nkind.N_TARRAY) { elem = ft.lhs; };
if (elem != nil) {
return isstrtype(c, elem);
};
};
};
fi = fi.finext;
};
};
};
};
};
};
};
};
return false;
};
// 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 == nkind.N_DOT) {
return typeisstr(n.type_: *tinfo);
};
if (k == nkind.N_CAST) {
return isstrtype(c, n.rhs);
};
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: *node) bool = {
if (t == nil) { return false; };
return typeis8byteprim(t.type_: *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: *node) bool = {
if (t == nil) { return false; };
let ti: *tinfo = t.type_: *tinfo;
if (ti == 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.
for (ti != nil && ti.kind == tykind.TY_NAMED) { ti = ti.under; };
if (ti == nil) { return false; };
return typeissigned(ti.sub);
};
// 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: *node) bool = {
if (t == nil) { return false; };
return typeissigned(t.type_: *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: *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: *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";
};
// 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: *node) str = {
if (tnode == nil) { return "MOVQ"; };
let ti: *tinfo = tnode.type_: *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 = typeissigned(ti);
return loadopsz(sigd, sz);
};
// indexbaseesz — element size for `arr[i]` where the base is a
// chained-dot pseudo-field `s.ptr` (s being str/*str/slice/*slice).
// For str the element is one byte; for `[]T` / `*[]T` we drill into
// the slice element type.
fn indexbaseesz(c: *cgen, base: *node) i32 = {
if (base == nil) { return 8; };
if (base.kind != nkind.N_DOT) { return 8; };
let fld: str = base.str;
let inner: *node = base.lhs;
if (inner == nil) { return 8; };
if (inner.kind != nkind.N_IDENT) { return 8; };
let nm: str = inner.str;
let lc: *local = localfindnode(c, nm);
if (lc == nil) { return 8; };
let tn: *node = lc.tnode;
if (tn == nil) { return 8; };
// `.ptr` pseudo-field on str/slice → element of the str/slice.
// Gated on inner kind, NOT on the field name alone: a struct with
// a literal `ptr: *T` field (lib/memio.state, lib/bufio.state) must
// route through the generic struct-field arm below so the stride
// comes from primsize/structlookup, not the str/slice default. The
// over-broad pre-#21 shortcut hard-coded esz=8 and silently
// miscompiled `m.ptr[i]` for `*u8` callers (also widened the load
// op MOVZBQ → MOVQ in cgindex). Mirrors cstage which routes every
// base through `base->type->sub->size` (cmd/w6c/cgen.c idx_eff).
if (streq(fld, "ptr")) {
let innert: *node = tn;
if (tn.kind == nkind.N_TPTR) { innert = tn.lhs; };
if (innert != nil) {
if (innert.kind == nkind.N_TNAME) {
if (streq(innert.str, "str")) { return 1; };
};
// Slice element: resolve through elemsizeofc so a
// slice of a named struct (e.g. *[]option) returns
// the struct stride instead of falling through to
// elemsizeof's default 8.
if (innert.kind == nkind.N_TSLICE) {
return elemsizeofc(c, innert);
};
};
// Inner is a struct N_TNAME (or unresolved) — fall through
// to the generic struct-field arm below.
};
// Generic struct field: if it's *T, element size is T's size.
let lkind: nkind = tn.kind;
let sname: str;
sname.ptr = nil; sname.len = 0;
if (lkind == nkind.N_TNAME) { sname = tn.str; };
if (lkind == nkind.N_TPTR) {
let pinner: *node = tn.lhs;
if (pinner != nil) {
if (pinner.kind == nkind.N_TNAME) { sname = pinner.str; };
};
};
if (sname.len == 0) { return 8; };
let si: *structinfo = structlookup(c, sname);
if (si == nil) { return 8; };
let fi: *fieldinfo = si.fields;
for (fi != nil) {
let fn_: str = fi.fname;
if (streq(fn_, fld)) {
let ft: *node = fi.tnode;
if (ft == nil) { return 8; };
if (ft.kind == nkind.N_TPTR) {
let elem: *node = ft.lhs;
if (elem != nil) {
if (elem.kind == nkind.N_TNAME) {
if (streq(elem.str, "str")) { return primtypesize("str"): i32; };
let ps: i32 = primsize(elem.str);
if (ps > 0) { return ps; };
// Pointer to named struct: indexing
// stride is the struct slot size.
// Without this, &p.ptr[i] for p.ptr:
// *S falls through to 8 and reads
// the wrong element.
let si: *structinfo = structlookup(c, elem.str);
if (si != nil) { return si.totsize; };
};
};
return 8;
};
if (ft.kind == nkind.N_TSLICE) { return elemsizeof(ft); };
// str-typed field: indexing yields one byte
// (`n.s[i]` where .s is str — matches C cgen's
// MOVZBQ for byte indexing).
if (ft.kind == nkind.N_TNAME) {
if (streq(ft.str, "str")) { return 1; };
};
return 8;
};
fi = fi.finext;
};
return 8;
};
// 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: *node) i32 = {
if (t == nil) { return 1; };
let k: nkind = t.kind;
let elem: *node = nil;
if (k == nkind.N_TPTR) { elem = t.lhs; };
if (k == nkind.N_TSLICE) { elem = t.lhs; };
if (k == nkind.N_TARRAY) { elem = t.lhs; };
if (k == nkind.N_TNAME) {
let nm: str = t.str;
if (streq(nm, "str")) { return 1; };
// Indexing a primitive name (rare): element size = the prim.
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.
if (elem.kind == 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 == nkind.N_TSLICE) { return tyslicesize(): i32; };
if (elem.kind == nkind.N_TNAME) {
let nm: str = elem.str;
// str element is 16B (ptr+len). primsize returns 0 for it.
if (streq(nm, "str")) { return primtypesize("str"): i32; };
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: *node) i32 = {
if (t == nil) { return 1; };
let direct: i32 = elemsizeof(t);
if (direct != 8) { return direct; };
let k: nkind = t.kind;
let elem: *node = nil;
if (k == nkind.N_TPTR) { elem = t.lhs; };
if (k == nkind.N_TSLICE) { elem = t.lhs; };
if (k == nkind.N_TARRAY) { elem = t.lhs; };
if (elem == nil) { return direct; };
if (elem.kind == nkind.N_TNAME) {
let ps: i32 = primsize(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: *node) bool = {
if (n == nil) { return false; };
let k: nkind = n.kind;
if (k == nkind.N_IDENT) {
let nm: str = n.str;
let lc: *local = localfindnode(c, nm);
if (lc != nil) {
if (lc.tnode == nil) { return false; };
return typeisunsigned(lc.tnode.type_: *tinfo);
};
return false;
};
if (k == nkind.N_DOT) {
return typeisunsigned(n.type_: *tinfo);
};
if (k == nkind.N_CAST) {
if (n.rhs == nil) { return false; };
return typeisunsigned(n.rhs.type_: *tinfo);
};
if (k == nkind.N_BIN) {
if (nodeisunsigned(c, n.lhs)) { return true; };
return nodeisunsigned(c, n.rhs);
};
if (k == nkind.N_UN) { return nodeisunsigned(c, n.lhs); };
// nkind.N_INDEX: `p[i]` is unsigned iff p's element type is unsigned.
// Walks the base local's declared type and pulls the element
// out — *u8 → u8, [N]u32 → u32, []u64 → u64. Without this the
// compare-codegen for `p[i] >= 48u8` falls back to signed JGE
// instead of JAE, diverging from C w6c on byte indexing.
if (k == nkind.N_INDEX) {
let base: *node = n.lhs;
if (base != nil) {
if (base.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, base.str);
if (lc != nil) {
let tn: *node = lc.tnode;
if (tn != nil) {
let elem: *node = nil;
if (tn.kind == nkind.N_TPTR) { elem = tn.lhs; };
if (tn.kind == nkind.N_TARRAY) { elem = tn.lhs; };
if (tn.kind == nkind.N_TSLICE) { elem = tn.lhs; };
if (elem != nil) {
return typeisunsigned(elem.type_: *tinfo);
};
};
};
};
};
return false;
};
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: *node) i32 = {
if (n == nil) { return 0; };
let k: nkind = n.kind;
if (k == nkind.N_IDENT) {
let lc: *local = localfindnode(c, n.str);
if (lc != nil) {
let tn: *node = lc.tnode;
if (tn != nil) {
if (tn.kind == nkind.N_TNAME) { return primsize(tn.str); };
};
};
return 0;
};
if (k == nkind.N_CAST) {
let tn: *node = n.rhs;
if (tn != nil) {
if (tn.kind == nkind.N_TNAME) { return primsize(tn.str); };
};
return 0;
};
if (k == 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. Mirrors cstage's `lu->size` for a
// TY_STRUCT (rounded only to the struct's maxalign).
//
// NOTE: si.totsize is mis-named — it's actually the *slot-padded*
// size (rounded up to 8 for stack-slot use; see registerstruct's
// tail `if ((off & 7) != 0) ...`). Frame allocation, [N]foo stride,
// and similar consumers want that slot-padded number. The
// receive-side ABI (#5) and any future "TYPE size, not slot size"
// query wants the natural size. Until si.totsize is split into
// si.naturalsize + si.slotsize (tracked as the wwstage-sizing
// follow-up task), recover the type-natural size from the field
// chain here.
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;
};
// 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: *node) *structinfo = {
if (tn == nil) { return nil; };
if (tn.kind != nkind.N_TNAME) { return nil; };
let si: *structinfo = structlookup(c, tn.str);
if (si != nil) { return si; };
let cur: *node = tn;
for (cur != nil && cur.kind == nkind.N_TNAME && si == nil) {
let aliased: *node = aliaslookup(c, cur.str);
if (aliased == nil) { cur = nil; }
else {
if (aliased.kind == nkind.N_TNAME) {
si = structlookup(c, aliased.str);
cur = aliased;
} else { cur = nil; };
};
};
return si;
};
export fn sretretsize(c: *cgen, t: *node) i32 = {
if (t == nil) { return 0; };
let r: *node = t;
if (r.kind == nkind.N_TBANG) {
r = r.lhs;
if (r == nil) { return 0; };
};
if (r.kind != nkind.N_TNAME) { return 0; };
// Primitives / aliased-to-primitives are never sret.
if (primsize(r.str) > 0) { return 0; };
if (streq(r.str, "str")) { return 0; };
let si: *structinfo = structlookup(c, r.str);
if (si == nil) {
if (c != nil) {
let aliased: *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;
};
// 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: *node) i32 = {
if (n == nil) { return 0; };
if (n.kind != nkind.N_CALL) { return 0; };
let callee: *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 == nkind.N_IDENT) {
cn = callee.str;
cmod = c.curmod;
};
if (callee.kind == nkind.N_DOT) {
cn = callee.str;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
};
if (cn.len == 0) { return 0; };
let rtyp: *node = fnretlookupmod(c, cn, cmod);
return sretretsize(c, rtyp);
};
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 (streq(s.sname, name)) {
if (streq(s.smod, c.curmod)) { return s; };
};
s = s.sinext;
};
s = c.structs;
for (s != nil) {
let sn: str = s.sname;
if (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] == 46u8) { // '.'
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);
let b: *structinfo = c.structs;
for (b != nil) {
if (streq(b.sname, leaf)) {
if (streq(b.smod, pkg)) {
return b;
};
};
b = b.sinext;
};
return nil;
};
i -= 1;
};
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;
};
fn primsize(name: str) i32 = {
if (streq(name, "u8")) { return 1; };
if (streq(name, "i8")) { return 1; };
if (streq(name, "bool")) { return 1; };
if (streq(name, "u16")) { return 2; };
if (streq(name, "i16")) { return 2; };
if (streq(name, "u32")) { return 4; };
if (streq(name, "i32")) { return 4; };
if (streq(name, "f32")) { return 4; };
if (streq(name, "u64")) { return 8; };
if (streq(name, "i64")) { return 8; };
if (streq(name, "uint")) { return 8; };
if (streq(name, "int")) { return 8; };
if (streq(name, "uintptr")) { return 8; };
if (streq(name, "f64")) { return 8; };
if (streq(name, "rune")) { return 4; };
if (streq(name, "void")) { return 0; };
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: *node,
sz_out: *i32, unsigned_out: *bool) void = {
*sz_out = 0;
*unsigned_out = false;
let cur: *node = t;
for (cur != nil) {
let k: nkind = cur.kind;
if (k == nkind.N_TBANG) { cur = cur.lhs; }
else { if (k == nkind.N_TENUM) { cur = cur.lhs; }
else { if (k == 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 (streq(nm, "bool")) { return; };
let ps: i32 = primsize(nm);
if (ps > 0) {
*sz_out = ps;
*unsigned_out = typeisunsigned(cur.type_: *tinfo);
return;
};
let al: *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: *node,
sz_out: *i32, unsigned_out: *bool) void = {
*sz_out = 0;
*unsigned_out = false;
if (n == nil) { return; };
let k: nkind = n.kind;
if (k == 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) {
let ps: i32 = primsize(s);
if (ps > 0) {
*sz_out = ps;
*unsigned_out = typeisunsigned(n.type_: *tinfo);
};
};
return;
};
if (k == nkind.N_IDENT) {
let lc: *local = localfindnode(c, n.str);
if (lc != nil) {
typenodeprimresolved(c, lc.tnode,
sz_out, unsigned_out);
};
return;
};
if (k == nkind.N_CAST) {
typenodeprimresolved(c, n.rhs, sz_out, unsigned_out);
return;
};
if (k == nkind.N_UN) {
exprprimresolved(c, n.lhs, sz_out, unsigned_out);
return;
};
if (k == 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). One TY_NAMED peel, then
// typeisint ? size : 0; bool falls out because typeisint(bool) is
// false — the same exclusion the old streq("bool") arm encoded.
let bu: *tinfo = nil;
if (n.lhs != nil) { bu = n.lhs.type_: *tinfo; };
if (bu != nil && bu.kind == tykind.TY_NAMED) { bu = bu.under; };
if (bu != nil && bu.kind == tykind.TY_PTR) { bu = bu.sub; };
if (bu != nil && bu.kind == tykind.TY_NAMED) { bu = bu.under; };
if (bu != nil && bu.kind == tykind.TY_STRUCT) {
let u: *tinfo = n.type_: *tinfo;
if (u != nil && u.kind == tykind.TY_NAMED) { u = u.under; };
if (typeisint(u)) {
*sz_out = u.size: i32;
*unsigned_out = 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 (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 (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 (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: *node) *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: *node = rhs;
if (rhs.kind == nkind.N_TRYPROP) { call = rhs.lhs; unwrap = true; };
if (rhs.kind == nkind.N_TRYUNW) { call = rhs.lhs; unwrap = true; };
if (call == nil) { return nil; };
if (call.kind != nkind.N_CALL) { return nil; };
let callee: *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 == nkind.N_IDENT) {
cname = callee.str;
cmod = c.curmod;
};
if (callee.kind == nkind.N_DOT) {
cname = callee.str;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
};
if (cname.len == 0) { return nil; };
let rtyp: *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 != 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: *node) i32 = {
// `[_]T = arrlit;` — inferred-length array. slotsize would
// return elem_size * 1 (treating missing length as 1); intercept
// and compute the real count first.
if (n.lhs != nil) {
if (n.lhs.kind == nkind.N_TARRAY) {
if (n.lhs.rhs == nil) {
if (n.rhs != nil) {
if (n.rhs.kind == nkind.N_ARRLIT) {
let elemn: *node = n.lhs.lhs;
let esz: i32 = 8;
if (elemn != nil) {
if (elemn.kind == nkind.N_TNAME) {
// Composite primitive: `str` is 16B
// (ptr+len) — primsize returns 0 for
// it, so it'd slot 8B without this.
if (streq(elemn.str, "str")) {
esz = primtypesize("str"): i32;
} else {
let ps: i32 = primsize(elemn.str);
if (ps > 0) { esz = ps; };
};
};
};
let cnt: i32 = 0;
let e: *node = n.rhs.list;
for (e != nil) {
let adv: bool = true;
if (e.kind == nkind.N_FIELD) {
if (streq(e.str, "...")) {
e = nil;
adv = false;
};
};
if (adv) {
cnt += 1;
e = e.next;
};
};
return esz * cnt;
};
};
};
};
};
if (n.lhs != nil) { return slotsize(c, n.lhs); };
// 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 inferred: *node = inferletcalltype(c, n.rhs);
if (inferred != nil) { return slotsize(c, inferred); };
return 8;
};
// #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: *node) i32 = {
if (typn == nil) { return 8; };
let ti: *tinfo = typn.type_: *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.
for (ti != nil && ti.kind == tykind.TY_NAMED) { ti = ti.under; };
if (ti == nil) { return 8; };
let kk: tykind = ti.kind;
if (kk == tykind.TY_VOID) { return 0; };
if (kk == tykind.TY_PTR || kk == tykind.TY_SLICE ||
kk == tykind.TY_CHAN || kk == tykind.TY_FN ||
kk == tykind.TY_STR || kk == tykind.TY_TAGGED) {
return ti.size: i32;
};
if (kk == tykind.TY_STRUCT || kk == tykind.TY_TUPLE ||
kk == tykind.TY_ARRAY) {
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: *node) i32 = {
if (tnode == nil) { return 8; };
let ti: *tinfo = tnode.type_: *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.
for (ti != nil && ti.kind == tykind.TY_NAMED) { ti = ti.under; };
if (ti == nil) { return 8; };
let k: tykind = ti.kind;
if (k == tykind.TY_STRUCT) { return ti.slotsize: i32; };
if (k == tykind.TY_ARRAY) { return ti.slotsize: i32; };
if (k == tykind.TY_TAGGED) { return ti.size: i32; };
if (k == tykind.TY_SLICE) { return ti.size: i32; };
if (k == tykind.TY_PTR || k == tykind.TY_FN ||
k == tykind.TY_CHAN) { return 8; };
if (k == 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;
};
fn registerstruct(c: *cgen, name: str, srcmod: str, tstruct: *node) void = {
let si: *structinfo = alloc(structinfo{
sname = name,
smod = srcmod,
})!;
let head: *fieldinfo = nil;
let tail: *fieldinfo = nil;
let off: i32 = 0;
let f: *node = tstruct.list;
for (f != nil) {
if (f.kind == nkind.N_TFIELD) {
let sz: i32 = fieldsize(c, f.lhs);
// Align to 8 for any field >= 4 bytes (matches our other
// cgen choices). i8/u8/bool may sit on odd byte offsets;
// the C cgen does similar best-effort packing.
let aln: i32 = 1;
if (sz >= 8) { aln = 8; }
else { if (sz >= 4) { aln = 4; }
else { if (sz >= 2) { aln = 2; }; }; };
if ((off & (aln - 1)) != 0) {
off = (off + aln - 1) & ~(aln - 1);
};
let fi: *fieldinfo = alloc(fieldinfo{
fname = f.str,
foff = off,
fsz = sz,
tnode = f.lhs,
})!;
if (head == nil) { head = fi; tail = fi; }
else { tail.finext = fi; tail = fi; };
off += sz;
};
f = f.next;
};
// Round total to 8 for stack-slot use.
if ((off & 7) != 0) { off = (off + 7) & ~7; };
si.fields = head;
si.totsize = off;
si.sinext = c.structs;
c.structs = si;
};
fn collectstructs(c: *cgen, file: *node) void = {
c.structs = nil;
if (file == nil) { return; };
let d: *node = file.list;
for (d != nil) {
if (d.kind == nkind.N_TYPEDECL) {
let body: *node = d.lhs;
if (body != nil) {
if (body.kind == 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: *node) bool = {
if (t == nil) { return false; };
return typeisstr(t.type_: *tinfo);
};
fn isslicetype(c: *cgen, t: *node) bool = {
if (t == nil) { return false; };
return typeisslice(t.type_: *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: *node) *node = {
let r: *node = resolvetype(c, t);
if (r == nil) { return nil; };
if (r.kind == nkind.N_TBANG) {
let inner: *node = r.lhs;
if (inner == nil) { return nil; };
r = resolvetype(c, inner);
if (r == nil) { return nil; };
};
if (r.kind == 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: *node) *node = {
if (scrut == nil) { return nil; };
let k: nkind = scrut.kind;
if (k == nkind.N_IDENT) { return nil; };
if (k == nkind.N_CALL) {
let callee: *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 == nkind.N_IDENT) { cnm = callee.str; };
if (callee.kind == 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 == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
};
if (cnm.len > 0) {
let rtyp: *node = fnretlookupmod(c, cnm, cmod);
if (rtyp != nil) { return resolvetagged(c, rtyp); };
};
};
return nil;
};
if (k == nkind.N_INDEX) {
let ibase: *node = scrut.lhs;
if (ibase == nil) { return nil; };
if (ibase.kind != nkind.N_IDENT) { return nil; };
let bl: *local = localfindnode(c, ibase.str);
let btn: *node = nil;
if (bl != nil) { btn = bl.tnode; }
else { btn = letvartnode(c, ibase.str); };
if (btn == nil) { return nil; };
let bk: nkind = btn.kind;
let etn: *node = nil;
if (bk == nkind.N_TARRAY) { etn = btn.lhs; };
if (bk == nkind.N_TSLICE) { etn = btn.lhs; };
if (bk == nkind.N_TPTR) { etn = btn.lhs; };
if (etn == nil) { return nil; };
return resolvetagged(c, etn);
};
if (k == 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;
};
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: *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: *node) i32 = {
if (c == nil) { return 0; };
let r: *node = resolvetype(c, t);
if (r == nil) { return 0; };
if (r.kind != nkind.N_TNAME) { return 0; };
let nm: str = r.str;
if (streq(nm, "str")) { return 0; };
if (primsize(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;
};
// 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: *node) bool = {
if (t == nil) { return false; };
return typeistagged(t.type_: *tinfo);
};
// 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: *node) bool = {
if (t == nil) { return false; };
return typeisfloat(t.type_: *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: *node) bool = {
if (t == nil) { return false; };
return typeisf32(t.type_: *tinfo);
};
// exprfloatkind — classify an expression's value-class so callers can
// pick float vs integer codegen without a full type system. Returns:
// 0 — integer-like (or unknown — same fallback the existing cgen
// takes today)
// 1 — f32
// 2 — f64
// Recognises: float literals, idents bound to float lets/locals,
// chained casts whose target is float, and (recursively) the inner
// expr of a non-narrowing wrapping construct. Anything we can't
// pin down conservatively reports integer — the worst case is that
// CVT* is skipped for an exotic case the user can still spell with
// an explicit local.
export fn exprfloatkind(c: *cgen, n: *node) i32 = {
if (n == nil) { return 0; };
let k: nkind = n.kind;
if (k == nkind.N_FLOATLIT) { return 2; };
if (k == nkind.N_CAST) {
if (isf32type(c, n.rhs)) { return 1; };
if (isfloattype(c, n.rhs)) { return 2; };
return 0;
};
if (k == nkind.N_IDENT) {
let lc: *local = localfindnode(c, n.str);
if (lc != nil) {
if (isf32type(c, lc.tnode)) { return 1; };
if (isfloattype(c, lc.tnode)) { return 2; };
return 0;
};
let lv: *letvar = c.lets;
for (lv != nil) {
if (streq(lv.name, n.str)) {
if (isf32type(c, lv.tnode)) { return 1; };
if (isfloattype(c, lv.tnode)) { return 2; };
return 0;
};
lv = lv.lvnext;
};
return 0;
};
if (k == nkind.N_UN) {
// Unary on a float (TK_MINUS) returns float; everything
// else is integer-coded.
if (n.op == tkind.TK_MINUS) {
return exprfloatkind(c, n.lhs);
};
return 0;
};
if (k == nkind.N_BIN) {
// Arithmetic binops inherit the operands' kind. Comparison
// (eq/ne/lt/...) returns bool — integer.
let op: tkind = n.op;
if (op == tkind.TK_PLUS) { return exprfloatkind(c, n.lhs); };
if (op == tkind.TK_MINUS) { return exprfloatkind(c, n.lhs); };
if (op == tkind.TK_STAR) { return exprfloatkind(c, n.lhs); };
if (op == tkind.TK_SLASH) { return exprfloatkind(c, n.lhs); };
return 0;
};
if (k == nkind.N_CALL) {
// Look up the callee's declared return type — fnretlookup
// returns the type-AST. Routes float-returning fns through
// the X0 ABI so cglet / cgassign know to spill from X0.
let nm: str;
nm.ptr = nil; nm.len = 0;
if (n.lhs != nil) {
if (n.lhs.kind == nkind.N_IDENT) { nm = n.lhs.str; };
};
if (nm.len > 0) {
let rtyp: *node = fnretlookup(c, nm);
if (isf32type(c, rtyp)) { return 1; };
if (isfloattype(c, rtyp)) { return 2; };
};
return 0;
};
if (k == nkind.N_DOT) {
// `p.field` where the struct field is f64/f32. Without this,
// `v.fval: i64` lowers to CVTSI on an integer-load value
// instead of CVTTSD2SI on the X0 the cgdot path actually
// emits for an f64 field. Read the checker-stamped tinfo on
// the N_DOT itself (check.ww:1973 stamps the field type);
// cstage cgen.c:2128 reads node_isfloat(n) the same way.
// Collapsed per A.6.3g (#55).
if (isf32type(c, n)) { return 1; };
if (isfloattype(c, n)) { return 2; };
return 0;
};
return 0;
};
// 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: *node) bool = {
if (t == nil) { return false; };
return typeisnullable(t.type_: *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: *node) i32 = {
if (t == nil) { return 0; };
let ti: *tinfo = t.type_: *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.
for (ti != nil && ti.kind == tykind.TY_NAMED) { ti = ti.under; };
if (ti == nil) { return 0; };
if (ti.kind != tykind.TY_TAGGED) { return 0; };
let p: *tparam = ti.params;
let i: i32 = 0;
for (p != nil) {
let vt: *tinfo = p.type_;
if (vt != nil) {
if (vt.kind == tykind.TY_NAMED) { vt = vt.under; };
if (vt != nil) {
if (vt.kind == 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 expr, -1 if absent. Used by cgreturn to map bare
// `return;` in a tagged-union-returning fn to the void variant's tag.
fn voidvariantindex(tagged: *node) i32 = {
if (tagged == nil) { return -1; };
if (tagged.kind != nkind.N_TTAGGED) { return -1; };
let v: *node = tagged.list;
let idx: i32 = 0;
for (v != nil) {
if (v.kind == nkind.N_TNAME) {
if (streq(v.str, "void")) { return idx; };
};
v = v.next;
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: *node, rhs: *node) i32 = {
if (tagged == nil) { return -1; };
return taggedvariantindext(c, tagged.type_: *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. Untyped/loose values (whose .type_ is untyped_* and can't
// typeeq a concrete variant) return -1 there and drop to the str/slice
// shape scan below — ww has no type_assignable to mirror cg_variant_match's
// untyped-src arm.
fn taggedvariantindext(c: *cgen, du: *tinfo, rhs: *node) i32 = {
if (du == nil) { return -1; };
if (rhs == nil) { return -1; };
let ti: *tinfo = du;
for (ti != nil && ti.kind == tykind.TY_NAMED) { ti = ti.under; };
if (ti == nil) { return -1; };
if (ti.kind != tykind.TY_TAGGED) { return -1; };
let r: i32 = flatvariantidxt(ti, rhs.type_: *tinfo);
if (r >= 0) { return r; };
// Shape fallback: classify rhs as (str, slice, scalar/other) and
// pick the first variant of matching shape. Stands in for cstage's
// type_assignable on an untyped src — the typeeq pass above can't
// match untyped_* against a concrete variant, and 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: *tparam = ti.params;
let idx: i32 = 0;
for (p != nil) {
let vt: *tinfo = p.type_;
let visstr: bool = typeisstr(vt);
let visslice: bool = 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: *node, pat: *node) i32 = {
if (tagged == nil) { return -1; };
if (pat == nil) { return -1; };
return flatvariantidxt(tagged.type_: *tinfo, pat.type_: *tinfo);
};
// 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).
fn flatvariantidxt(tagged: *tinfo, want: *tinfo) i32 = {
if (want == nil) { return -1; };
let ti: *tinfo = tagged;
for (ti != nil && ti.kind == tykind.TY_NAMED) { ti = ti.under; };
if (ti == nil) { return -1; };
if (ti.kind != tykind.TY_TAGGED) { return -1; };
let p: *tparam = ti.params;
let idx: i32 = 0;
for (p != nil) {
if (typeeq(p.type_, want)) { return idx; };
p = p.tnext;
idx += 1;
};
return -1;
};
// 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: *node, elem: *node) i32 = {
if (tagged == nil) { return -1; };
let ti: *tinfo = tagged.type_: *tinfo;
for (ti != nil && ti.kind == tykind.TY_NAMED) { ti = ti.under; };
if (ti == nil) { return -1; };
if (ti.kind != tykind.TY_TAGGED) { return -1; };
let want: *tinfo = nil;
if (elem != nil) { want = elem.type_: *tinfo; };
let fallback: i32 = -1;
let p: *tparam = ti.params;
let idx: i32 = 0;
for (p != nil) {
let vt: *tinfo = p.type_;
if (vt != nil) {
if (typeisslice(vt)) {
if (fallback < 0) { fallback = idx; };
if (want != nil) {
let su: *tinfo = vt;
for (su != nil && su.kind == tykind.TY_NAMED) { su = su.under; };
if (su != nil) {
if (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: *tinfo, su: *tinfo, slot_off: i32) void = {
let dt: *tinfo = du;
for (dt != nil && dt.kind == tykind.TY_NAMED) { dt = dt.under; };
if (dt == nil) { return; };
if (dt.kind != tykind.TY_TAGGED) { return; };
let st: *tinfo = su;
for (st != nil && st.kind == tykind.TY_NAMED) { st = st.under; };
if (st == nil) { return; };
if (st.kind != tykind.TY_TAGGED) { return; };
let identity: bool = true;
let p: *tparam = st.params;
let idx: i32 = 0;
for (p != nil) {
let di: i32 = flatvariantidxt(dt, p.type_);
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_);
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: *node) str = {
let empty: str;
empty.ptr = nil; empty.len = 0;
if (src == nil) { return empty; };
if (src.kind == nkind.N_STRUCTLIT) {
let trefn: *node = src.lhs;
if (trefn != nil) {
let nm: str;
nm.ptr = nil; nm.len = 0;
if (trefn.kind == nkind.N_IDENT) { nm = trefn.str; };
if (trefn.kind == nkind.N_TNAME) { nm = trefn.str; };
if (nm.len > 0) {
if (structlookup(c, nm) != nil) { return nm; };
};
};
return empty;
};
if (src.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, src.str);
if (lc != nil) {
let tn: *node = lc.tnode;
if (tn != nil) {
if (tn.kind == nkind.N_TNAME) {
if (structlookup(c, tn.str) != nil) {
return tn.str;
};
};
};
};
};
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: *node) *node = {
if (src == nil) { return nil; };
if (src.kind != nkind.N_IDENT) { return nil; };
let lc: *local = localfindnode(c, src.str);
if (lc == nil) { return nil; };
let tn: *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: *node) bool = {
if (src == nil) { return false; };
if (src.kind == nkind.N_CALL) {
let callee: *node = src.lhs;
if (callee != nil) {
let calleename: str;
calleename.ptr = nil; calleename.len = 0;
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.kind == nkind.N_IDENT) {
calleename = callee.str;
cmod = c.curmod;
};
if (callee.kind == nkind.N_DOT) {
calleename = callee.str;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
};
if (calleename.len > 0) {
let rtyp: *node = fnretlookupmod(c, calleename, cmod);
if (rtyp != nil) {
if (istaggedtype(c, rtyp)) { return true; };
};
};
};
return false;
};
if (src.kind == nkind.N_INDEX) {
let base: *node = src.lhs;
if (base != nil) {
if (base.kind == nkind.N_IDENT) {
let bl: *local = localfindnode(c, base.str);
if (bl != nil) {
let btn: *node = bl.tnode;
if (btn != nil) {
let bk: nkind = btn.kind;
let elemt: *node = nil;
if (bk == nkind.N_TARRAY) { elemt = btn.lhs; };
if (bk == nkind.N_TSLICE) { elemt = btn.lhs; };
if (bk == nkind.N_TPTR) { elemt = btn.lhs; };
if (elemt != nil) {
if (istaggedtype(c, elemt)) {
return true;
};
};
};
};
};
};
};
// 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 == nkind.N_DOT) {
if (typeistagged(src.type_: *tinfo)) { return true; };
};
return false;
};
// 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) void = {
// 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");
// word2 → R8 (slice variant: slot = 8 tag + 24 payload = 32).
if (slot_sz > 24) {
emitline("\tMOVQ\t");
emitdispreg((foff + 24): i64, basereg);
emitline(", R8\n");
};
// word1 → CX (load LAST; conflicts with CX-base globals).
if (slot_sz > 16) {
emitline("\tMOVQ\t");
emitdispreg((foff + 16): i64, basereg);
emitline(", CX\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.
// - scalar src: tag@+0, value@+8.
fn cgwidentaggedstore(c: *cgen, dst: *tinfo, src: *node,
basereg: str, slot_off: i32, slot_sz: i32) void = {
if (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 scratch sized at first use per #15/#26c. A sibling
// site (cgreturn, pushargsrev, cgindex) hitting @tagscr later
// with a larger size fatals (rule 7) — pinned offset can't
// grow in place.
let scr: i32 = localadd(c, "@tagscr", slot_sz, nil);
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: *tinfo, src: *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: *tinfo = dst;
for (dt != nil && dt.kind == tykind.TY_NAMED) { dt = dt.under; };
if (dt == nil) { return; };
if (dt.kind != 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;
};
// `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 == nkind.N_CAST) {
if (src.lhs != nil) {
let inner: *node = src.lhs;
let inneristagged: bool = false;
if (inner.kind == 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 castrhs: *node = src.rhs;
if (castrhs != nil) {
let castt: *tinfo = castrhs.type_: *tinfo;
let castu: *tinfo = castt;
for (castu != nil && castu.kind == tykind.TY_NAMED) {
castu = castu.under;
};
if (castu != nil) {
if (castu == dt) {
castisdst = true;
} else { if (castu.kind == tykind.TY_TAGGED) {
if (typeeq(castt, dst)) {
castisdst = true;
};
}; };
};
};
if (castisdst && !inneristagged) {
src = inner;
};
};
};
};
// 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: *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_: *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)) {
cgexpr(c, src);
emitline("\tMOVQ\tAX, ");
emitoff(slot_off: i64);
emitline("(BP)\n");
if (slot_sz > 8) {
emitline("\tMOVQ\tDX, ");
emitoff((slot_off + 8): i64);
emitline("(BP)\n");
};
if (slot_sz > 16) {
emitline("\tMOVQ\tCX, ");
emitoff((slot_off + 16): i64);
emitline("(BP)\n");
};
if (slot_sz > 24) {
emitline("\tMOVQ\tR8, ");
emitoff((slot_off + 24): 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 == nkind.N_STRUCTLIT) {
let fnode: *node = src.list;
for (fnode != nil) {
if (fnode.kind == nkind.N_FIELD) {
let fname: str = fnode.str;
let fi: *fieldinfo = si.fields;
for (fi != nil) {
if (streq(fi.fname, fname)) {
cgexpr(c, fnode.lhs);
if (isfloattype(c, fi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, fi.tnode)) {
mov = "MOVSS";
};
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff((slot_off + 8 + fi.foff): i64);
emitline("(BP)\n");
} else { if (isstrtype(c, fi.tnode)) {
emitline("\tMOVQ\tAX, ");
emitoff((slot_off + 8 + fi.foff): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tBX, ");
emitoff((slot_off + 8 + fi.foff + 8): i64);
emitline("(BP)\n");
} else {
let sop: str = fieldstoreop(c, fi);
emitline("\t");
emitline(sop);
emitline("\tAX, ");
emitoff((slot_off + 8 + fi.foff): i64);
emitline("(BP)\n");
}; };
fi = nil;
} else {
fi = fi.finext;
};
};
};
fnode = fnode.next;
};
} 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; };
let stotal: i32 = si.totsize;
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 payload.
if (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");
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;
};
// Slice payload (24B): cgexpr leaves (AX=ptr, BX=len, CX=cap).
// Slot layout: [+0]=tag, [+8]=ptr, [+16]=len, [+24]=cap.
if (nodeisslice(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. Wwstage has no
// checker so we classify via exprfloatkind (same shape used by cgcast)
// 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.
let fkind: i32 = exprfloatkind(c, src);
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");
// #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: *tinfo = dt;
for (fti != nil && fti.kind == tykind.TY_NAMED) { fti = fti.under; };
if (fti != nil) { if (fti.kind == tykind.TY_TAGGED) {
let fp: *tparam = fti.params;
let fidx: i32 = 0;
for (fp != nil) {
let fvt: *tinfo = fp.type_;
for (fvt != nil && fvt.kind == tykind.TY_NAMED) { fvt = fvt.under; };
if (fvt != nil) {
if (typeisfloat(fvt)) {
if (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.
cgexpr(c, src);
emitline("\tMOVQ\tAX, ");
emitoff((slot_off + 8): 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;
};
// 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: *node,
outrootname: *str, outrootoff: *i32, outtotaloff: *i32,
outleaftype: **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 != nkind.N_DOT) { return false; };
let stk: [16]*node;
let nsteps: i32 = 0;
let cur: *node = n;
for (cur != nil) {
if (cur.kind != 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 != 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 == 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 == nkind.N_TPTR) {
let pe: *node = lc.tnode.lhs;
if (pe != nil) {
if (pe.kind == nkind.N_TNAME) {
*outrootoff = lc.off;
*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: *tinfo = cur.type_: *tinfo;
for (curstruct != nil && curstruct.kind == tykind.TY_NAMED) {
curstruct = curstruct.under;
};
if (*outptrroot) {
if (curstruct == nil) { return false; };
if (curstruct.kind != tykind.TY_PTR) { return false; };
curstruct = curstruct.sub;
for (curstruct != nil && curstruct.kind == tykind.TY_NAMED) {
curstruct = curstruct.under;
};
};
let i: i32 = nsteps - 1;
for (i >= 0) {
if (curstruct == nil) { return false; };
if (curstruct.kind != tykind.TY_STRUCT) { return false; };
if (stk[i] == nil) { return false; };
let stepnm: str = stk[i].str;
let tf: *tfield = curstruct.fields;
let found: *tfield = nil;
for (tf != nil) {
if (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: *tinfo = found.type_;
for (ft != nil && ft.kind == tykind.TY_NAMED) { ft = ft.under; };
if (ft == nil) { return false; };
if (ft.kind == tykind.TY_STR) {
if (i != 1) { return false; };
let pseudo: str = stk[0].str;
let delta: i32 = -1;
if (streq(pseudo, "ptr")) { delta = 0; }
else { if (streq(pseudo, "len")) { delta = 8; }; };
if (delta < 0) { return false; };
*outtotaloff = *outtotaloff + foff;
*outslicedelta = delta;
return true;
};
if (ft.kind == tykind.TY_SLICE) {
if (i != 1) { return false; };
let pseudo: str = stk[0].str;
let delta: i32 = -1;
if (streq(pseudo, "ptr")) { delta = 0; }
else { if (streq(pseudo, "len")) { delta = 8; }
else { if (streq(pseudo, "cap")) { delta = 16; }; }; };
if (delta < 0) { return false; };
*outtotaloff = *outtotaloff + foff;
*outslicedelta = delta;
return true;
};
if (ft.kind != 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.
//
// 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.
// - `totsize` is also constant; pass the natural size for dot
// sites (structnaturalsize) and si.totsize for BP-rel sites,
// matching each site's pre-#18 zero-fill bound.
//
// 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.
//
// Graduation note (task #13): the scalar store currently uses the
// explicit {1→MOVB, 4→MOVL, else MOVQ} dispatch to match cstage
// byte-identically — cstage hasn't yet learned MOVW for fsz==2. Once
// #13 aligns both stages, the dispatch can switch to fieldstoreop
// which already returns MOVW where appropriate.
fn cgstructlitfill(c: *cgen, si: *structinfo, lit: *node,
mode: i32, srcoff: i32, srcname: str,
disp: i32, totsize: i32) void = {
if (si == nil) { return; };
let basereg: str = "BP";
if (mode != 0) { basereg = "BX"; };
if (lit.op == 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");
};
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: *node = lit.list;
for (fieldnode != nil) {
if (fieldnode.kind == nkind.N_FIELD) {
let fname: str = fieldnode.str;
let fi: *fieldinfo = si.fields;
for (fi != nil) {
let fn_: str = fi.fname;
if (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");
};
cgwidentaggedstore(c, fi.tnode.type_: *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 == nkind.N_STRUCTLIT) {
if (fi.tnode != nil) {
if (fi.tnode.kind == nkind.N_TNAME) {
if (primsize(fi.tnode.str) == 0) {
let isi: *structinfo = structlookup(c, fi.tnode.str);
if (isi != nil) {
// Nested fill: pick the size
// discipline matching the outer
// site — dot sites pass natural
// size, BP-rel sites pass
// totsize. Mirror it.
let inner_tot: i32 = isi.totsize;
if (mode != 0) { inner_tot = structnaturalsize(isi); };
cgstructlitfill(c, isi,
fieldnode.lhs,
mode, srcoff, srcname,
disp + fi.foff, inner_tot);
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).
//
// Sized 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.
// MOVW-for-tail==2 only fires on shapes that
// didn't compile before, so no #13 byte-identity
// concern.
//
// Guard `fsz <= 24 && fsz%8 ∈ {0,1,2,4}` matches
// #4's cgreturn ABI: >24B falls through (sret
// deferred); fsz%8 ∈ {3,5,6,7} would need shift-
// store and is also unsupported by #4 — falls
// through to the existing AX-only wrongness
// (consistent, tracked as follow-up).
//
// 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 == nkind.N_CALL) {
if (fi.tnode != nil) {
if (fi.tnode.kind == nkind.N_TNAME) {
if (primsize(fi.tnode.str) == 0) {
let csi: *structinfo = structlookup(c, fi.tnode.str);
if (csi != nil) {
// Use the inner struct's
// NATURAL size (no 8B slot
// rounding) so MOVL/MOVW/
// MOVB tail dispatch matches
// cstage's fl->type->size
// (which is natural per
// check.c). fi.fsz here is
// wwstage's slot-padded
// totsize — using it would
// emit 2× MOVQ where cstage
// emits MOVQ+MOVL for a
// 12B inner, etc. (task #15
// territory; sidestepped
// locally.)
let cfsz: i32 = structnaturalsize(csi);
let crem: i32 = cfsz - (cfsz / 8) * 8;
if (cfsz <= 24) {
if (crem == 0 || crem == 1
|| crem == 2 || crem == 4) {
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");
};
let full: i32 = cfsz / 8;
let ci: i32 = 0;
for (ci < full) {
let r: str = "AX";
if (ci == 1) { r = "DX"; };
if (ci == 2) { r = "CX"; };
emitline("\tMOVQ\t");
emitline(r);
emitline(", ");
if (mode == 0) {
emitoff((disp + fi.foff + ci * 8): i64);
emitline("(BP)\n");
} else {
emitdispreg((disp + fi.foff + ci * 8): i64, basereg);
emitline("\n");
};
ci += 1;
};
if (crem > 0) {
let top: str = "MOVB";
if (crem == 4) { top = "MOVL"; };
if (crem == 2) { top = "MOVW"; };
let tr: str = "AX";
if (full == 1) { tr = "DX"; };
if (full == 2) { tr = "CX"; };
emitline("\t");
emitline(top);
emitline("\t");
emitline(tr);
emitline(", ");
if (mode == 0) {
emitoff((disp + fi.foff + full * 8): i64);
emitline("(BP)\n");
} else {
emitdispreg((disp + fi.foff + full * 8): i64, basereg);
emitline("\n");
};
};
callwhole = true;
};
};
};
};
};
};
};
};
};
if (nested) {
fi = nil;
} else if (callwhole) {
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 (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 {
// Explicit {1→MOVB, 4→MOVL, else MOVQ}
// dispatch (not fieldstoreop) to match
// cstage byte-identically. wwstage's
// fieldstoreop would return MOVW for
// fsz==2 which cstage doesn't emit —
// tracked as task #13.
let fsz: i32 = fi.fsz;
let op: str = "MOVQ";
if (fsz == 1) { op = "MOVB"; };
if (fsz == 4) { op = "MOVL"; };
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, and cgassign N_IDENT-lhs N_STRUCTLIT. Byte-identical to
// the pre-#18 cgstructlitfillbp.
fn cgstructlitfillbp(c: *cgen, si: *structinfo, lit: *node, bpoff: i32) void = {
if (si == nil) { return; };
cgstructlitfill(c, si, lit, 0, 0, "", bpoff, si.totsize);
};