Files
ww/selfhost/cmd/wcc/cgenutil.ww
Hojun-Cho 69a817f0f3 selfhost+test: decompose user-struct by-value params (#11)
wwstage param-slot allocator dispatched isfloat/istagged/isslice/
isstr/catch-all and skipped TY_STRUCT. `fn(a: S, b: S)` where S is
16B emitted $16 frame (DI/SI only); cstage emits $32 (DI/SI/DX/CX)
per SysV ABI.

Two-site fix mirroring cmd/w6c/cgen.c:6820 (callee prologue) and
:4240 (caller push):

- New structparamsize(c, t) helper in cgenutil.ww resolves the
  TY_STRUCT TNAME chain, returns totsize for sizes (0,16], else 0.
  >16B drops to stack — bug-compat with cstage's <=16 gate.
- New struct arm in cgfnparams + matching cgfn pre-scan in
  cgendecl.ww. nw = (size>8) ? 2 : 1; partial-fit stitch (idx=5
  + nw=2) emits one reg + one stack tail.
- New struct branch in pushargsrev N_IDENT arm: MOVQ + PUSHQ
  high→low so cgcall's existing pop drains correctly.

Test 717: 4 rows × {cstage, wwstage, asm-id}. Headline 2×16B,
mixed 16B+8B (caller-side surface), str+struct regression guard,
partial-fit 5×i64+16B stitch.
2026-05-17 00:27:22 +09:00

3518 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, dotinnerstructptr, elemsizeof
// - slot sizing: structlookup, primsize, slotsize, fieldsize,
// registerstruct, collectstructs
// - rhs helpers: rhstargetname, taggedvariantindex
//
// Bundler pulls this in transitively via cgen.ww; consumers don't
// need to `use cgenutil;` directly.
use os;
use mem;
use ast;
use tok;
use typ;
use sym;
use 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(c.a, 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.
fn callee_variadic_param(c: *cgen, callee: *node, nfixed_out: *i32) *node = {
*nfixed_out = 0;
if (callee == nil) { return nil; };
let cnm: str;
cnm.ptr = nil; cnm.len = 0;
if (callee.kind == nkind.N_IDENT) { cnm = callee.str; };
if (callee.kind == nkind.N_DOT) { cnm = callee.str; };
if (cnm.len == 0) { return nil; };
let ps: *node = fnparamslookup(c, cnm);
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)
// where N is recorded on the N_CALL node at scanlocals time so both
// the prologue reservation and the call-site emission agree.
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 = amalloc(c.a, (total: u64) + 1u64): *u8;
let k: i32 = 0;
for (k < total) { p[k] = buf[k]; k += 1; };
p[total] = 0u8;
let r: str;
r.ptr = p;
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);
};
};
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", c.tagscrsz, 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, 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;
};
emitline("\tPUSHQ\tAX\n");
return rest + 1;
};
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); };
// N_DOT to a slice field: resolve the field through the struct
// (or *struct) the base ident / inner chain lands on, then check
// the field tnode. Mirrors nodeisstr's N_DOT branch so call-arg
// push/pop counts 3 words for `p.sl` and `p.inner.sl` shapes.
// `.ptr` / `.len` / `.cap` are pseudo-fields — they yield ptr
// (*u8) and i32, not a slice — so we exclude them up front.
if (k == nkind.N_DOT) {
let base: *node = n.lhs;
let fld: str = n.str;
if (streq(fld, "ptr")) { return false; };
if (streq(fld, "len")) { return false; };
if (streq(fld, "cap")) { return false; };
if (base != nil) {
let sname: str;
sname.ptr = nil; sname.len = 0;
if (base.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, base.str);
if (lc != nil) {
let tn: *node = lc.tnode;
let lkind: nkind = nkind.N_NONE;
if (tn != nil) { lkind = tn.kind; };
if (lkind == nkind.N_TNAME) { sname = tn.str; };
if (lkind == nkind.N_TPTR) {
let inner: *node = tn.lhs;
if (inner != nil) {
if (inner.kind == nkind.N_TNAME) { sname = inner.str; };
};
};
};
};
if (base.kind == nkind.N_DOT) {
let innert: *node = dotinnerstructptr(c, base);
if (innert != nil) {
if (innert.kind == nkind.N_TNAME) { sname = innert.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)) {
return isslicetype(c, fi.tnode);
};
fi = fi.finext;
};
};
};
// Chained dot through value-struct hops (`o.inner.sl`,
// `p.inner.sl`): dotinnerstructptr above only walks
// *struct fields, so a value-struct chain falls through.
// dotchainresolve handles arbitrary depth through value
// struct AND `*T` root, returning the leaf fieldinfo.
let rootnm: str = "";
let rootoff: i32 = 0;
let totaloff: i32 = 0;
let lfi: *fieldinfo = nil;
let sdelta: i32 = -1;
let isglobal: bool = false;
let ptrroot: bool = false;
let ok: bool = dotchainresolve(c, n,
&rootnm, &rootoff, &totaloff,
&lfi, &sdelta, &isglobal, &ptrroot);
if (ok && sdelta < 0 && lfi != nil) {
return isslicetype(c, lfi.tnode);
};
};
return false;
};
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 (struct field / chained / pseudo-fields excluded), 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.
// - N_DOT to a tuple positional `t.1` of a str element — wwstage's
// cgdot loads (AX, BX) but tuple-as-arg has independent issues.
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 cnm: str = callee.str;
let rt: *node = fnretlookup(c, cnm);
return isstrtype(c, rt);
};
};
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;
};
if (k == nkind.N_DOT) {
let base: *node = n.lhs;
let fld: str = n.str;
// `<expr>.ptr` is *u8 not str; `<expr>.len` is i32 not str.
if (streq(fld, "ptr")) { return false; };
if (streq(fld, "len")) { return false; };
if (streq(fld, "cap")) { return false; };
if (base != nil) {
let sname: str;
sname.ptr = nil; sname.len = 0;
if (base.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, base.str);
if (lc != nil) {
let tn: *node = lc.tnode;
let lkind: nkind = nkind.N_NONE;
if (tn != nil) { lkind = tn.kind; };
if (lkind == nkind.N_TNAME) { sname = tn.str; };
if (lkind == nkind.N_TPTR) {
let inner: *node = tn.lhs;
if (inner != nil) {
if (inner.kind == nkind.N_TNAME) { sname = inner.str; };
};
};
};
};
// Chained dot (`p.foo.bar`): use dotinnerstructptr
// to resolve the inner chain to the *struct it lands
// on, then look up `fld` in that struct.
if (base.kind == nkind.N_DOT) {
let innert: *node = dotinnerstructptr(c, base);
if (innert != nil) {
if (innert.kind == nkind.N_TNAME) { sname = innert.str; };
};
};
if (sname.len > 0) {
let si: *structinfo = structlookup(c, sname);
if (si != nil) {
let fi: *fieldinfo = si.fields;
for (fi != nil) {
let fn_: str = fi.fname;
if (streq(fn_, fld)) {
return isstrtype(c, fi.tnode);
};
fi = fi.finext;
};
};
};
// Chained dot through value-struct hops (`p.inner.s`):
// dotinnerstructptr above only walks *struct fields;
// dotchainresolve handles arbitrary depth through
// value struct AND `*T` root. Mirror of the nodeisslice
// fallback so chained str-field args also push 2 words.
let rootnm: str = "";
let rootoff: i32 = 0;
let totaloff: i32 = 0;
let lfi: *fieldinfo = nil;
let sdelta: i32 = -1;
let isglobal: bool = false;
let ptrroot: bool = false;
let ok: bool = dotchainresolve(c, n,
&rootnm, &rootoff, &totaloff,
&lfi, &sdelta, &isglobal, &ptrroot);
if (ok && sdelta < 0 && lfi != nil) {
return isstrtype(c, lfi.tnode);
};
};
return false;
};
if (k == nkind.N_CAST) {
return isstrtype(c, n.rhs);
};
return false;
};
// typenameisunsigned — true for u8/u16/u32/u64/uint/uintptr/rune.
// rune is a Unicode codepoint (0..0x10FFFF); cgen treats it as
// unsigned so narrow-cast / sub-word load paths zero-extend (MOVL,
// not MOVSXD). Mirrors cstage's type_isunsigned post task #5.
fn typenameisunsigned(nm: str) bool = {
if (streq(nm, "u8")) { return true; };
if (streq(nm, "u16")) { return true; };
if (streq(nm, "u32")) { return true; };
if (streq(nm, "u64")) { return true; };
if (streq(nm, "uint")) { return true; };
if (streq(nm, "uintptr")) { return true; };
if (streq(nm, "rune")) { return true; };
return false;
};
// typenodeisunsigned — recurse through TNAME aliases / TBANG / TENUM
// to the resolved primitive. Mirrors cstage's type_isunsigned which
// recurses into TY_NAMED.under and TY_ENUM.sub.
fn typenodeisunsignedc(c: *cgen, t: *node) bool = {
if (t == nil) { return false; };
let k: nkind = t.kind;
if (k == nkind.N_TBANG) { return typenodeisunsignedc(c, t.lhs); };
if (k == nkind.N_TENUM) { return typenodeisunsignedc(c, t.lhs); };
if (k == nkind.N_TNAME) {
let nm: str = t.str;
if (typenameisunsigned(nm)) { return true; };
if (typenameissigned(nm)) { return false; };
// Follow aliases / enum storage.
let al: *node = aliaslookup(c, nm);
if (al != nil) { return typenodeisunsignedc(c, al); };
let en: *enumtype = enumlookup(c, nm);
if (en != nil) {
if (en.storage != nil) {
return typenodeisunsignedc(c, en.storage);
};
return false; // default storage i32 is signed
};
};
return false;
};
// typenodeisunsigned — legacy callers without *cgen context. Only
// resolves primitive TNAMEs (no alias/enum recursion); use the
// _c variant where the cgen registry is in scope.
fn typenodeisunsigned(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind == nkind.N_TNAME) { return typenameisunsigned(t.str); };
return false;
};
// typeis8byteprimitive — does this type take exactly one 8-byte
// slot (pointer / fn-ptr / 64-bit int / chan / scalar primitive
// padded up to 8) rather than a wider aggregate? Used by nkind.N_LET
// zero-init to mirror C cgen's "only zero if sz == 8 at the type
// level" rule. Strings (16), slices (24), tagged unions (>=16),
// tuples (16), structs (varies), arrays — all fall through to
// false here even when their *slot* rounds up to 8.
fn typeis8byteprimitive(c: *cgen, t: *node) bool = {
if (t == nil) { return false; };
let k: nkind = t.kind;
if (k == nkind.N_TPTR) { return true; };
if (k == nkind.N_TFN) { return true; };
if (k == nkind.N_TCHAN) { return true; };
if (k == nkind.N_TSLICE) { return false; };
if (k == nkind.N_TARRAY) {
// C cgen (cmd/w6c/cgen.c:3317) zero-inits TY_ARRAY whenever
// its raw byte size is 8 — e.g. `[8]bool`, `[2]i32`, `[4]i16`,
// `[1]i64`. Mirror that here so the wwstage matches.
let lenn: *node = t.rhs;
let elemn: *node = t.lhs;
if (lenn == nil) { return false; };
if (lenn.kind != nkind.N_INTLIT) { return false; };
let elen: i64 = lenn.uval: i64;
let esz: i32 = 8;
if (elemn != nil) {
if (elemn.kind == nkind.N_TNAME) {
let ps: i32 = primsize(elemn.str);
if (ps > 0) { esz = ps; };
};
};
return (esz: i64 * elen) == 8i64;
};
if (k == nkind.N_TTUPLE) { return false; };
if (k == nkind.N_TTAGGED){ return false; };
if (k == nkind.N_TNAME) {
let nm: str = t.str;
if (streq(nm, "str")) { return false; };
// Struct alias: not a primitive even if the slot is 8B.
if (structlookup(c, nm) != nil) { return false; };
// Primitive (i8/u8/.../i64/u64/bool/rune/f32/f64/int/...).
// All of these get slot-padded to 8 and zero-init in C.
if (primsize(nm) > 0) { return true; };
return false;
};
return false;
};
// elemissigned — given an indexable type (`*T`, `[]T`, `[N]T`), is
// its element a signed narrow primitive (i8/i16/i32)? Used by
// cgindex to pick MOVSXD vs MOVL at esz=4 (and MOVSBQ/MOVSWQ at
// esz=1/2). Mirrors cstage's `signed_elem`. Follows alias/enum
// chains so `[]Alias` arrays resolve to the underlying signedness.
fn elemissignedc(c: *cgen, t: *node) bool = {
if (t == nil) { return false; };
let elem: *node = nil;
let k: nkind = t.kind;
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 false; };
return fieldissignedc(c, elem);
};
fn elemissigned(t: *node) bool = {
if (t == nil) { return false; };
let elem: *node = nil;
let k: nkind = t.kind;
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 false; };
if (elem.kind != nkind.N_TNAME) { return false; };
return typenameissigned(elem.str);
};
// typenameissigned — true for i8/i16/i32/i64/int. rune is excluded
// (it's a non-negative Unicode codepoint, treated as unsigned).
fn typenameissigned(nm: str) bool = {
if (streq(nm, "i8")) { return true; };
if (streq(nm, "i16")) { return true; };
if (streq(nm, "i32")) { return true; };
if (streq(nm, "i64")) { return true; };
if (streq(nm, "int")) { return true; };
return false;
};
// fieldissignedc — does this field/element type need sign-extension
// on a sub-word load? Walks TBANG / TENUM / TNAME-aliases to the
// resolved primitive. Mirrors cstage's fld_issigned: bool is treated
// as unsigned (0/1 ⇒ MOVZBQ); rune is unsigned (codepoint ⇒ MOVL).
fn fieldissignedc(c: *cgen, t: *node) bool = {
if (t == nil) { return false; };
let k: nkind = t.kind;
if (k == nkind.N_TBANG) { return fieldissignedc(c, t.lhs); };
if (k == nkind.N_TENUM) { return fieldissignedc(c, t.lhs); };
if (k == nkind.N_TNAME) {
let nm: str = t.str;
if (streq(nm, "bool")) { return false; };
if (typenameisunsigned(nm)) { return false; };
if (typenameissigned(nm)) { return true; };
let al: *node = aliaslookup(c, nm);
if (al != nil) { return fieldissignedc(c, al); };
let en: *enumtype = enumlookup(c, nm);
if (en != nil) {
if (en.storage != nil) {
return fieldissignedc(c, en.storage);
};
return true; // default i32 storage is signed
};
};
return false;
};
// 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. 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.
// Resolves TBANG / TENUM / TNAME-alias chains so `type err = !i32`
// picks up size 4 the same way the cstage checker pre-computes
// t->size — without this, aliased narrows fall through to MOVQ.
export fn localloadop(c: *cgen, tnode: *node) str = {
let t: *node = tnode;
for (t != nil) {
let k: nkind = t.kind;
if (k == nkind.N_TBANG) { t = t.lhs; }
else { if (k == nkind.N_TENUM) { t = t.lhs; }
else { if (k == nkind.N_TNAME) {
let nm: str = t.str;
if (primsize(nm) > 0) { break; };
let al: *node = aliaslookup(c, nm);
if (al == nil) { break; };
t = al;
}
else { break; }; }; };
};
let sz: i32 = fieldsize(c, t);
if (sz != 1) { if (sz != 2) { if (sz != 4) { return "MOVQ"; }; }; };
let sigd: bool = fieldissignedc(c, tnode);
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.
if (streq(fld, "ptr")) {
let innert: *node = tn;
if (tn.kind == nkind.N_TPTR) { innert = tn.lhs; };
if (innert == nil) { return 8; };
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); };
return 8;
};
// 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 16; };
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;
};
// dotinnerstructptr — for an nkind.N_DOT whose lhs is a chain of dots
// or an nkind.N_IDENT, walk the chain and return the nkind.N_TNAME tnode of the
// struct that the chain dereferences to (i.e., for `r.sym` where
// .sym is *lsym, return nkind.N_TNAME("lsym")). Returns nil if the chain
// doesn't resolve to a *struct.
//
// Used by the chained-DOT cgen path so `r.sym.val` knows the outer
// is a field of `lsym`.
fn dotinnerstructptr(c: *cgen, n: *node) *node = {
if (n == nil) { return nil; };
if (n.kind != nkind.N_DOT) { return nil; };
let base: *node = n.lhs;
let fld: str = n.str;
if (base == nil) { return nil; };
// Resolve base's struct tnode.
let baset: *node = nil;
if (base.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, base.str);
if (lc == nil) { return nil; };
let tn: *node = lc.tnode;
if (tn == nil) { return nil; };
// base could be either struct-by-value (nkind.N_TNAME) or *struct (nkind.N_TPTR).
if (tn.kind == nkind.N_TNAME) { baset = tn; };
if (tn.kind == nkind.N_TPTR) { baset = tn.lhs; };
} else { if (base.kind == nkind.N_DOT) {
baset = dotinnerstructptr(c, base);
};};
if (baset == nil) { return nil; };
if (baset.kind != nkind.N_TNAME) { return nil; };
// Look up the struct, find the field, return the field's *struct.
let si: *structinfo = structlookup(c, baset.str);
if (si == nil) { return nil; };
let fi: *fieldinfo = si.fields;
for (fi != nil) {
if (streq(fi.fname, fld)) {
let ft: *node = fi.tnode;
if (ft == nil) { return nil; };
if (ft.kind != nkind.N_TPTR) { return nil; };
let inner: *node = ft.lhs;
if (inner == nil) { return nil; };
if (inner.kind != nkind.N_TNAME) { return nil; };
return inner;
};
fi = fi.finext;
};
return nil;
};
// 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; };
};
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 16; };
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
// don't have a typed AST yet, so we walk surface nodes:
// nkind.N_INTLIT — never marked unsigned (no tsuffix plumbing yet)
// nkind.N_IDENT — look up the local's declared type
// nkind.N_DOT — look up the field's declared type via struct reg
// 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) { return typenodeisunsigned(lc.tnode); };
return false;
};
if (k == nkind.N_DOT) {
let base: *node = n.lhs;
let fld: str = n.str;
if (base != nil) {
if (base.kind == nkind.N_IDENT) {
let bn: str = base.str;
let lc: *local = localfindnode(c, bn);
if (lc != nil) {
let tn: *node = lc.tnode;
let lkind: nkind = nkind.N_NONE;
if (tn != nil) { lkind = tn.kind; };
let sname: str;
sname.ptr = nil; sname.len = 0;
if (lkind == nkind.N_TPTR) {
let inner: *node = tn.lhs;
if (inner != nil) {
if (inner.kind == nkind.N_TNAME) { sname = inner.str; };
};
};
if (lkind == nkind.N_TNAME) { sname = tn.str; };
if (sname.len > 0) {
let si: *structinfo = structlookup(c, sname);
if (si != nil) {
let fi: *fieldinfo = si.fields;
for (fi != nil) {
let fn_: str = fi.fname;
if (streq(fn_, fld)) {
return typenodeisunsigned(fi.tnode);
};
fi = fi.finext;
};
};
};
};
};
};
return false;
};
if (k == nkind.N_CAST) { return typenodeisunsigned(n.rhs); };
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 typenodeisunsigned(elem);
};
};
};
};
};
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;
};
fn structlookup(c: *cgen, name: str) *structinfo = {
// Exact match first: bare-from-source struct names and already-
// leafed lookups hit here directly.
let s: *structinfo = c.structs;
for (s != nil) {
let sn: str = s.sname;
if (streq(sn, name)) { return s; };
s = s.sinext;
};
// Module-qualified form: `pkg.S` → match the leaf scoped to its
// originating module. Mirrors aliaslookup's mod-filter; the
// `smod == pkg` guard is what prevents two modules with same-
// leaf-name structs from collapsing into whichever entry appears
// first in the chain.
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 = typenameisunsigned(nm);
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 = typenameisunsigned(s);
};
};
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) {
typenodeprimresolved(c, dotfieldtnode(c, n),
sz_out, unsigned_out);
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;
if (callee.kind == nkind.N_IDENT) { cname = callee.str; };
if (callee.kind == nkind.N_DOT) { cname = callee.str; };
if (cname.len == 0) { return nil; };
let rt: *node = fnretlookup(c, cname);
if (rt == nil) { return nil; };
if (unwrap) {
// Strip error variants — success type is the first
// variant of the tagged return.
if (rt.kind != nkind.N_TTAGGED) { return nil; };
return rt.list;
};
// Plain call: declared return type is the local's type.
return rt;
};
// 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. Used by both scanlocals (prologue sizing) and
// cglet (slot alloc) so they agree on the frame layout.
//
// `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 = 16;
} 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;
};
fn slotsize(c: *cgen, typn: *node) i32 = {
if (typn == nil) { return 8; };
let k: nkind = typn.kind;
if (k == nkind.N_TPTR) { return 8; };
if (k == nkind.N_TFN) { return 8; };
if (k == nkind.N_TCHAN) { return 8; };
if (k == nkind.N_TSLICE) { return 24; };
if (k == nkind.N_TTUPLE) {
// Sum element sizes. Mirrors C cgen which uses raw type
// sizes; padding to 8 happens inside slotsize for primitives,
// so a `(i64, str)` resolves to 8 + 16 = 24 (matches the C
// cgen 24B init / positional-access layout).
let total: i32 = 0;
let p: *node = typn.list;
for (p != nil) {
total += slotsize(c, p);
p = p.next;
};
return total;
};
if (k == nkind.N_TTAGGED){
// Nullable `(*T | void)` collapses to a single 8B pointer.
if (isnullabletype(typn)) { return 8; };
// Slot = 8 (tag) + max(variant payload sizes), rounded up
// to an 8-byte multiple so the reg-passing ABI (size/8
// words) doesn't drop the last value register. Mirrors C
// cgen's resolve_type for nkind.N_TTAGGED.
let v: *node = typn.list;
let maxsz: i32 = 0;
for (v != nil) {
let sz: i32 = slotsize(c, v);
if (sz > maxsz) { maxsz = sz; };
v = v.next;
};
let pad: i32 = (maxsz + 7) & ~7;
return 8 + pad;
};
if (k == nkind.N_TNAME) {
let nm: str = typn.str;
if (streq(nm, "str")) { return 16; };
let ps: i32 = primsize(nm);
if (ps > 0) {
// Pad to 8 for stack slots — matches C cgen which spills
// every primitive into an 8-byte slot.
return 8;
};
// Named struct lookup.
let si: *structinfo = structlookup(c, nm);
if (si != nil) { return si.totsize; };
// Type alias (`type foo = !str;` / `type foo = bar;`):
// follow it so a tagged-union variant of a !str-aliased
// error type contributes 16 bytes to the max payload
// rather than 8 (the default).
if (c != nil) {
let aliased: *node = aliaslookup(c, nm);
if (aliased != nil) {
if (aliased.kind == nkind.N_TBANG) {
return slotsize(c, aliased.lhs);
};
return slotsize(c, aliased);
};
};
return 8;
};
if (k == nkind.N_TARRAY) {
let lenn: *node = typn.rhs;
let elemn: *node = typn.lhs;
let elen: i64 = 1i64;
if (lenn != nil) {
if (lenn.kind == nkind.N_INTLIT) { elen = lenn.uval: i64; };
};
let esz: i32 = 8;
if (elemn != nil) {
if (elemn.kind == nkind.N_TNAME) {
let en: str = elemn.str;
// `str` is a composite primitive (ptr+len, 16B);
// primsize returns 0 for it, so without this
// explicit case a `[N]str` would slot 8B/elem,
// collapsing the per-element stride and losing
// every .len half.
if (streq(en, "str")) { esz = 16; };
let ps: i32 = primsize(en);
if (esz == 8) { if (ps > 0) { esz = ps; }
else {
// Named struct / aliased type: size off
// the structinfo if present, else follow
// the alias via aliaslookup so
// `[N]formattable` reads the resolved
// tagged slot (e.g. 24B for
// `(i64|str|bool)`), not the fall-
// through 8B.
let si: *structinfo = structlookup(c, en);
if (si != nil) { esz = si.totsize; }
else { if (c != nil) {
let al: *node = aliaslookup(c, en);
if (al != nil) {
esz = slotsize(c, al);
};
}; };
}; };
} else { if (elemn.kind == nkind.N_TTAGGED) {
// Tagged-union element: full slot (8 tag +
// padded max payload). Matches C cgen's
// resolve_type for `[N]TAGGED`.
esz = slotsize(c, elemn);
} else { if (elemn.kind == nkind.N_TPTR) {
esz = 8;
} else { if (elemn.kind == nkind.N_TSTRUCT) {
esz = slotsize(c, elemn);
}; }; }; };
};
return (esz: i64 * elen): i32;
};
if (k == nkind.N_TSTRUCT) {
// Inline anonymous struct — sum of field sizes.
let f: *node = typn.list;
let total: i32 = 0;
for (f != nil) {
if (f.kind == nkind.N_TFIELD) {
total += slotsize(c, f.lhs);
};
f = f.next;
};
return total;
};
return 8;
};
// registerstruct — compute field offsets + total size for a struct
// type-decl, store in c.structs. Field type sizes use the same
// slotsize logic (with primitives kept at their natural width — we
// only round to 8 for stack slots, not struct interiors).
fn fieldsize(c: *cgen, tnode: *node) i32 = {
if (tnode == nil) { return 8; };
let k: nkind = tnode.kind;
if (k == nkind.N_TTAGGED){ return slotsize(c, tnode); };
if (k == nkind.N_TNAME) {
let nm: str = tnode.str;
if (streq(nm, "str")) { return 16; };
let ps: i32 = primsize(nm);
if (ps > 0) { return ps; };
let si: *structinfo = structlookup(c, nm);
if (si != nil) { return si.totsize; };
// Enum: size of its storage type. Mirrors the C cgen, which
// reads Type.size off the TY_ENUM (which inherits from .sub).
let en: *enumtype = enumlookup(c, nm);
if (en != nil) {
if (en.storage != nil) {
if (en.storage.kind == nkind.N_TNAME) {
let sps: i32 = primsize(en.storage.str);
if (sps > 0) { return sps; };
};
};
return 4; // default storage is i32
};
// Type alias to a tagged-union — recurse through aliaslookup
// so `e: ev` (where `ev = (i64 | i32)`) takes 16B in the
// containing struct rather than the 8B default.
if (c != nil) {
let aliased: *node = aliaslookup(c, nm);
if (aliased != nil) { return fieldsize(c, aliased); };
};
return 8;
};
if (k == nkind.N_TPTR) { return 8; };
if (k == nkind.N_TSLICE) { return 24; };
if (k == nkind.N_TARRAY) {
// Same shape as slotsize's TARRAY branch.
let lenn: *node = tnode.rhs;
let elemn: *node = tnode.lhs;
let elen: i64 = 1i64;
if (lenn != nil) {
if (lenn.kind == nkind.N_INTLIT) { elen = lenn.uval: i64; };
};
let esz: i32 = fieldsize(c, elemn);
return (esz: i64 * elen): i32;
};
return 8;
};
fn registerstruct(c: *cgen, name: str, module: str, tstruct: *node) void = {
let si: *structinfo = amalloc(c.a, 80u64): *structinfo;
si.sname = name;
si.smod = module;
si.fields = nil;
si.totsize = 0;
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 = amalloc(c.a, 48u64): *fieldinfo;
fi.fname = f.str;
fi.foff = off;
fi.fsz = sz;
fi.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.module, body);
};
};
};
d = d.next;
};
};
// `type X = str;` aliases) to `str`. Takes *cgen so it can walk the
// alias chain registered at file load.
fn isstrtyperaw(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind == nkind.N_TNAME) {
let nm: str = t.str;
if (streq(nm, "str")) { return true; };
};
return false;
};
fn isstrtype(c: *cgen, t: *node) bool = {
if (isstrtyperaw(t)) { return true; };
if (c == nil) { return false; };
let r: *node = resolvetype(c, t);
if (isstrtyperaw(r)) { return true; };
// `parserr = !str` — `!T` aliases shouldn't hide their
// underlying type from str-routing. Unwrap and re-check.
if (r != nil) {
if (r.kind == nkind.N_TBANG) {
let inner: *node = r.lhs;
if (isstrtyperaw(inner)) { return true; };
if (inner != nil) {
let r2: *node = resolvetype(c, inner);
if (isstrtyperaw(r2)) { return true; };
};
};
};
return false;
};
fn isslicetyperaw(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind == nkind.N_TSLICE) { return true; };
return false;
};
fn isslicetype(c: *cgen, t: *node) bool = {
if (isslicetyperaw(t)) { return true; };
if (c == nil) { return false; };
let r: *node = resolvetype(c, t);
return isslicetyperaw(r);
};
fn istaggedtyperaw(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind == nkind.N_TTAGGED) { return true; };
return false;
};
// 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). Mirrors cgmatch's inline scrutinee
// type resolution; factored so cgmatch (emit) and scanlocals (count)
// agree on the spill slot's size per the scan+emit lockstep invariant.
// 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;
if (callee.kind == nkind.N_IDENT) { cnm = callee.str; };
if (callee.kind == nkind.N_DOT) { cnm = callee.str; };
if (cnm.len > 0) {
let rt: *node = fnretlookup(c, cnm);
if (rt != nil) { return resolvetagged(c, rt); };
};
};
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) {
let ft: *node = dotfieldtnode(c, scrut);
if (ft == nil) { return nil; };
return resolvetagged(c, ft);
};
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. Used by both scanlocals
// (counting) and cgmatch (emitting) per rule-10 align-to-cstage.
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. Mirrors isstrtype: follow N_TNAME to its
// underlying decl, then unwrap a leading N_TBANG so `type error =
// !(invalid | overflow);` is still recognised as tagged. Without the
// bang unwrap the prologue treats the param as scalar (8B), spilling
// only DI and losing the value-word SI; the match read of slot+8 then
// trails into saved BP.
fn istaggedtype(c: *cgen, t: *node) bool = {
if (istaggedtyperaw(t)) { return true; };
if (c == nil) { return false; };
let r: *node = resolvetype(c, t);
if (istaggedtyperaw(r)) { return true; };
if (r != nil) {
if (r.kind == nkind.N_TBANG) {
let inner: *node = r.lhs;
if (istaggedtyperaw(inner)) { return true; };
if (inner != nil) {
let r2: *node = resolvetype(c, inner);
if (istaggedtyperaw(r2)) { return true; };
};
};
};
return false;
};
// isf32typeraw / isf64typeraw — bare TNAME check, no alias resolution.
fn isf32typeraw(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind != nkind.N_TNAME) { return false; };
return streq(t.str, "f32");
};
fn isf64typeraw(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind != nkind.N_TNAME) { return false; };
return streq(t.str, "f64");
};
// isfloattype — f32 / f64 (and aliases of those). Used by cglet,
// cgident, cgassign, cgbin, cgcast, cgcall, cgreturn, fn-prologue to
// dispatch the MOVSS/MOVSD-shaped paths.
export fn isfloattype(c: *cgen, t: *node) bool = {
if (isf32typeraw(t)) { return true; };
if (isf64typeraw(t)) { return true; };
if (c == nil) { return false; };
let r: *node = resolvetype(c, t);
if (isf32typeraw(r)) { return true; };
if (isf64typeraw(r)) { return true; };
return false;
};
// isf32type — narrower predicate: true only for f32 (after alias
// resolution). f64 returns false. Used to pick MOVSS vs MOVSD and
// the SS-variant arithmetic / cast opcodes.
export fn isf32type(c: *cgen, t: *node) bool = {
if (isf32typeraw(t)) { return true; };
if (c == nil) { return false; };
let r: *node = resolvetype(c, t);
return isf32typeraw(r);
};
// 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 rt: *node = fnretlookup(c, nm);
if (isf32type(c, rt)) { return 1; };
if (isfloattype(c, rt)) { 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.
let base: *node = n.lhs;
let fld: str = n.str;
if (base != nil) {
let sname: str;
sname.ptr = nil; sname.len = 0;
if (base.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, base.str);
if (lc != nil) {
let tn: *node = lc.tnode;
if (tn != nil) {
if (tn.kind == nkind.N_TNAME) { sname = tn.str; };
if (tn.kind == nkind.N_TPTR) {
let pe: *node = tn.lhs;
if (pe != nil) {
if (pe.kind == nkind.N_TNAME) { sname = pe.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)) {
if (isf32type(c, fi.tnode)) { return 1; };
if (isfloattype(c, fi.tnode)) { return 2; };
return 0;
};
fi = fi.finext;
};
};
};
};
return 0;
};
return 0;
};
// isnullabletype — nkind.N_TTAGGED with exactly two children, one *T and
// one `void`. Folds to a single 8-byte pointer slot per Hare's
// `(*T | null)` semantics. Mirrors check.c's resolve_type detection.
export fn isnullabletype(t: *node) bool = {
if (t == nil) { return false; };
if (t.kind != nkind.N_TTAGGED) { return false; };
let a: *node = t.list;
if (a == nil) { return false; };
let b: *node = a.next;
if (b == nil) { return false; };
if (b.next != nil) { return false; };
let aptr: bool = (a.kind == nkind.N_TPTR);
let bptr: bool = (b.kind == nkind.N_TPTR);
let avoid: bool = (a.kind == nkind.N_TNAME);
if (avoid) { avoid = streq(a.str, "void"); };
let bvoid: bool = (b.kind == nkind.N_TNAME);
if (bvoid) { bvoid = streq(b.str, "void"); };
if (aptr) { if (bvoid) { return true; }; };
if (avoid) { if (bptr) { return true; }; };
return false;
};
// nullableptrtag — 0-based index of the *T variant in a nullable
// union. The void variant takes the other slot (0 or 1).
export fn nullableptrtag(t: *node) i32 = {
if (t == nil) { return 0; };
if (t.kind != nkind.N_TTAGGED) { return 0; };
let a: *node = t.list;
if (a != nil) { if (a.kind == nkind.N_TPTR) { return 0; }; };
return 1;
};
// 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;
};
// rhstargetname — for a returned value, what's its declared (or
// surface-inferred) type name? `expr: T` casts dictate T directly;
// bare strlit/intlit fall back to a primitive name.
fn rhstargetname(c: *cgen, rhs: *node) str = {
let nm: str;
nm.ptr = nil; nm.len = 0;
if (rhs == nil) { return nm; };
// Unary `-` / `+` / `~` inherit the inner expression's type:
// cstage's checker stamps N_UN's type from cunop's inner walk,
// so `-42i64` is ty_i64 there. Wwstage has no checker stage —
// peel the operator here so a typed-int literal under a sign
// reaches its tsuffix branch below instead of falling into
// taggedvariantindex's "first non-str variant" fallback. Mirror
// of cmd/wcc/check.c cunop TK_MINUS/PLUS/TILDE returning t.
if (rhs.kind == nkind.N_UN) {
let op: tkind = rhs.op;
if (op == tkind.TK_MINUS || op == tkind.TK_PLUS
|| op == tkind.TK_TILDE) {
if (rhs.lhs != nil) {
return rhstargetname(c, rhs.lhs);
};
};
};
if (rhs.kind == nkind.N_CAST) {
let t: *node = rhs.rhs;
if (t != nil) {
if (t.kind == nkind.N_TNAME) { return t.str; };
};
return nm;
};
if (rhs.kind == nkind.N_STRLIT) { return "str"; };
if (rhs.kind == nkind.N_TRUE) { return "bool"; };
if (rhs.kind == nkind.N_FALSE) { return "bool"; };
if (rhs.kind == nkind.N_RUNELIT) { return "rune"; };
if (rhs.kind == nkind.N_INTLIT) {
// Typed int literal (`42i64`, `3u8`): suffix names the
// concrete variant so flatvariantidx finds it. Untyped
// literals (tsuffix=="") fall through to the isstr scan.
let s: str = rhs.tsuffix;
if (s.len > 0) { return s; };
};
// `T{}` carries its type name on the lhs N_IDENT — the parser
// builds `N_STRUCTLIT{ lhs = N_IDENT("T"), list = fields }`.
// Needed so `return eof{};` (variant of a tagged union) resolves
// to the `eof` variant index rather than falling through to the
// "first non-str variant" fallback in taggedvariantindex.
if (rhs.kind == nkind.N_STRUCTLIT) {
let tref: *node = rhs.lhs;
if (tref != nil) {
if (tref.kind == nkind.N_IDENT) { return tref.str; };
if (tref.kind == nkind.N_TNAME) { return tref.str; };
};
return nm;
};
if (rhs.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, rhs.str);
if (lc != nil) {
let tn: *node = lc.tnode;
if (tn != nil) {
if (tn.kind == nkind.N_TNAME) { return tn.str; };
};
};
};
return nm;
};
// 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; };
if (rhs == nil) { return -1; };
// Alias-unwrap: wwstage has no typed AST, so an aliased tagged
// return (`type ft = (i64|str|bool); fn f() ft = ...`) reaches
// here as N_TNAME("ft"), not N_TTAGGED. flatvariantidx and the
// fallback both gate on N_TTAGGED → -1 → caller maps to 0,
// silently emitting `MOVQ $0, AX` for every non-leading variant.
// Cstage's check.c canonicalizes N_TNAME → underlying upfront;
// every wwstage cgen consumer of a type-bearing node has to
// remember this step itself. TODO(#11): a wwstage check pass
// between parse and cgen would replace the per-site unwrap with
// a single canonicalization. Same shape of fix as nodeisstr.
let resolved: *node = resolvetagged(c, tagged);
if (resolved != nil) { tagged = resolved; };
let wantname: str = rhstargetname(c, rhs);
if (wantname.len > 0) {
let r: i32 = flatvariantidx(c, tagged, wantname);
if (r >= 0) { return r; };
};
// Fallback: by str-shape (resolves aliases). Walks the
// spread-flattened variant list so a `(...inner | str)` outer
// agrees with the (i32 | str) inner's str position.
let wantstr: bool = nodeisstr(c, rhs);
let v: *node = tagged.list;
let idx: i32 = 0;
for (v != nil) {
let isspread: bool = (v.op == tkind.TK_ELLIPSIS);
if (isspread) {
let inner: *node = v;
if (inner.kind == nkind.N_TNAME) {
let a: *node = aliaslookup(c, inner.str);
if (a != nil) { inner = a; };
};
if (inner != nil) {
if (inner.kind == nkind.N_TTAGGED) {
let iv: *node = inner.list;
for (iv != nil) {
let ivisstr: bool = false;
if (iv.kind == nkind.N_TNAME) {
if (isstrtype(c, iv)) { ivisstr = true; };
};
if (ivisstr == wantstr) { return idx; };
iv = iv.next;
idx += 1;
};
v = v.next;
continue;
};
};
};
let visstr: bool = false;
if (v.kind == nkind.N_TNAME) {
if (isstrtype(c, v)) { visstr = true; };
};
if (visstr == wantstr) { return idx; };
v = v.next;
idx += 1;
};
return -1;
};
// flatvariantidx — walk `tagged`'s variant list (with spread `...inner`
// expansion) and return the flat 0-based index where `want` matches.
// Mirrors check.c's spread flatten at type resolution: an outer
// `(...inner | T)` has the inner's variants inlined in declaration
// order, so the tag indices stay in sync between cstage (which
// resolves types upfront) and wwstage (which doesn't). Returns -1 if
// no variant matches.
fn flatvariantidx(c: *cgen, tagged: *node, want: str) i32 = {
if (tagged == nil) { return -1; };
if (tagged.kind != nkind.N_TTAGGED) { return -1; };
if (want.len == 0) { return -1; };
let v: *node = tagged.list;
let idx: i32 = 0;
for (v != nil) {
let isspread: bool = (v.op == tkind.TK_ELLIPSIS);
if (isspread) {
let inner: *node = v;
if (inner.kind == nkind.N_TNAME) {
let a: *node = aliaslookup(c, inner.str);
if (a != nil) { inner = a; };
};
if (inner != nil) {
if (inner.kind == nkind.N_TTAGGED) {
let iv: *node = inner.list;
for (iv != nil) {
if (iv.kind == nkind.N_TNAME) {
if (variantnamematch(iv.str, want)) {
return idx;
};
};
iv = iv.next;
idx += 1;
};
v = v.next;
continue;
};
};
};
if (v.kind == nkind.N_TNAME) {
if (variantnamematch(v.str, want)) { return idx; };
};
v = v.next;
idx += 1;
};
return -1;
};
// 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.
// Mirrors cg_widen_tag_remap in cmd/w6c/cgen.c.
fn cgwidentagremap(c: *cgen, dst: *node, src: *node, slot_off: i32) void = {
if (dst == nil) { return; };
if (src == nil) { return; };
if (dst.kind != nkind.N_TTAGGED) { return; };
if (src.kind != nkind.N_TTAGGED) { return; };
let identity: bool = true;
let v: *node = src.list;
let idx: i32 = 0;
for (v != nil) {
let di: i32 = cgtagvariantidx(c, dst, v);
if (di < 0) { di = 0; };
if (di != idx) { identity = false; v = nil; }
else { v = v.next; idx += 1; };
};
if (identity) { return; };
let done: str = mklabel(c, "remap_done");
emitline("\tMOVQ\t");
emitoff(slot_off: i64);
emitline("(BP), AX\n");
v = src.list;
idx = 0;
for (v != nil) {
let next: str = mklabel(c, "remap_next");
let di: i32 = cgtagvariantidx(c, dst, v);
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);
v = v.next;
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);
};
// dotfieldtnode — for an N_DOT src whose base is a local ident or
// *struct, return the declared type node of the named field, or nil
// if the shape doesn't resolve (e.g. enum-member access, pseudo-
// field `.len`, top-level global). Used by rhstaggedabicall and
// related predicates to walk into the field's tagged type.
fn dotfieldtnode(c: *cgen, n: *node) *node = {
if (n == nil) { return nil; };
if (n.kind != nkind.N_DOT) { return nil; };
let base: *node = n.lhs;
let fld: str = n.str;
if (base == nil) { return nil; };
if (base.kind != nkind.N_IDENT) { return nil; };
let lc: *local = localfindnode(c, base.str);
let btn: *node = nil;
if (lc != nil) { btn = lc.tnode; }
else { btn = letvartnode(c, base.str); };
if (btn == nil) { return nil; };
let bk: nkind = btn.kind;
let sname: str;
sname.ptr = nil; sname.len = 0;
if (bk == nkind.N_TPTR) {
let inner: *node = btn.lhs;
if (inner != nil) {
if (inner.kind == nkind.N_TNAME) { sname = inner.str; };
};
};
if (bk == nkind.N_TNAME) { sname = btn.str; };
if (sname.len == 0) { return nil; };
let si: *structinfo = structlookup(c, sname);
if (si == nil) { return nil; };
let fi: *fieldinfo = si.fields;
for (fi != nil) {
if (streq(fi.fname, fld)) { return fi.tnode; };
fi = fi.finext;
};
return nil;
};
// 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;
if (callee.kind == nkind.N_IDENT) { calleename = callee.str; };
if (callee.kind == nkind.N_DOT) { calleename = callee.str; };
if (calleename.len > 0) {
let rt: *node = fnretlookup(c, calleename);
if (rt != nil) {
if (istaggedtype(c, rt)) { 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.
if (src.kind == nkind.N_DOT) {
let ft: *node = dotfieldtnode(c, src);
if (ft != nil) {
if (istaggedtype(c, ft)) { 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: *node, 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");
// Same shared scratch — c.tagscrsz is the per-fn max across every
// reservation site (scanlocals); pinning to slot_sz here would
// undersize the slot if a sibling site (cgreturn, pushargsrev,
// cgindex) needed a larger one and fired second.
let scr: i32 = localadd(c, "@tagscr", c.tagscrsz, 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: *node, src: *node, slot_off: i32, slot_sz: i32) void = {
let dt: *node = resolvetagged(c, dst);
if (dt == nil) { return; };
// Nullable fold: one 8B word holding the pointer (or 0 for void).
if (isnullabletype(dst)) {
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. Compare nominally via str match on
// the tagged-alias name.
let castisdst: bool = false;
let castrhs: *node = src.rhs;
if (castrhs != nil) {
if (castrhs.kind == nkind.N_TTAGGED) {
castisdst = true;
};
if (castrhs.kind == nkind.N_TNAME) {
if (dst != nil) {
if (dst.kind == nkind.N_TNAME) {
if (streq(castrhs.str, dst.str)) {
castisdst = true;
};
};
};
};
};
if (castisdst && !inneristagged) {
src = inner;
};
};
};
};
// Tagged source ident: byte-copy slot words then tag-remap.
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, st, 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 = taggedvariantindex(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 = taggedvariantindex(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 = taggedvariantindex(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";
let fname: str = "f64";
if (fkind == 1) { fmov = "MOVSS"; fname = "f32"; };
cgexpr(c, src);
emitline("\t");
emitline(fmov);
emitline("\tX0, ");
emitoff((slot_off + 8): i64);
emitline("(BP)\n");
let ftag: i32 = flatvariantidx(c, dt, fname);
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 = taggedvariantindex(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 *outleaffi is the leaf fieldinfo
// 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.
export fn dotchainresolve(c: *cgen, n: *node,
outrootname: *str, outrootoff: *i32, outtotaloff: *i32,
outleaffi: **fieldinfo, outslicedelta: *i32,
outisglobal: *bool, outptrroot: *bool) bool = {
*outrootname = "";
*outrootoff = 0;
*outisglobal = false;
*outptrroot = false;
*outtotaloff = 0;
*outleaffi = 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 rootstruct: str = "";
let lc: *local = localfindnode(c, cur.str);
if (lc != nil) {
if (lc.tnode != nil) {
if (lc.tnode.kind == nkind.N_TNAME) {
rootstruct = lc.tnode.str;
*outrootoff = lc.off;
};
// `*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) {
rootstruct = pe.str;
*outrootoff = lc.off;
*outptrroot = true;
};
};
};
};
};
if (rootstruct.len == 0) {
let gsi: *structinfo = letvarstructinfo(c, cur.str);
if (gsi != nil) {
rootstruct = gsi.sname;
*outisglobal = true;
};
};
if (rootstruct.len == 0) { return false; };
let curstruct: str = rootstruct;
let i: i32 = nsteps - 1;
for (i >= 0) {
let csi: *structinfo = structlookup(c, curstruct);
if (csi == nil) { return false; };
if (stk[i] == nil) { return false; };
let stepnm: str = stk[i].str;
let fi: *fieldinfo = csi.fields;
let found: *fieldinfo = nil;
for (fi != nil) {
if (streq(fi.fname, stepnm)) { found = fi; break; };
fi = fi.finext;
};
if (found == nil) { return false; };
if (i == 0) {
*outtotaloff = *outtotaloff + found.foff;
*outleaffi = found;
return true;
};
let ft: *node = found.tnode;
if (ft == nil) { return false; };
if (ft.kind == nkind.N_TNAME) {
if (streq(ft.str, "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 + found.foff;
*outslicedelta = delta;
return true;
};
if (primsize(ft.str) != 0) { return false; };
*outtotaloff = *outtotaloff + found.foff;
curstruct = ft.str;
i -= 1;
} else { if (ft.kind == nkind.N_TSLICE) {
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 + found.foff;
*outslicedelta = delta;
return true;
} else {
return false;
}; };
};
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,
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);
};