Files
ww/selfhost/cmd/wcc/cgenexpr.ww
Hojun-Cho 47d75d9b59 w6c+selfhost: widen concrete variant to tagged-union call arg
Tagged-union widening already fired for `let r: (str|rune) = "...";`,
`r = "...";`, and `return "..."` from a tagged-returning fn — but not
at call sites, so `fn f(x: (str|rune))` couldn't be called with a bare
str or rune. The arg was pushed as its own static type (2 words for
str, 1 for rune) while the callee's slot expected 3 (tag + payload).

C cgen: at the call boundary, look up the callee's declared param
type per arg. When the param is TY_TAGGED and the arg is a concrete
variant, materialise (tag, value-words, padding) sized to the param's
tagged_arg_size — then the existing pop-into-arg-regs logic picks it
up. Nullable `(*T | void)` collapses to a single 8B push.

selfhost: fnret now carries the params head alongside rtype (amalloc
bumped to 48); pushargsrev takes the matching param node and runs the
same widening sequence per arg. The pop drain in cgcall already
handled extra slot words, so no change needed on that side.

Verified with a smoke covering str/rune literals, typed locals,
pre-existing tagged-local pass-through, and nullable widening from a
raw pointer. Selfhost emits byte-identical asm to C cgen on the test.
2026-05-13 04:44:03 +09:00

3051 lines
93 KiB
Plaintext

// selfhost/cmd/wcc/cgenexpr.ww — split out of cgen.ww.
//
// cgexpr is a thin dispatcher over n.kind; each non-trivial branch
// lives in a per-kind helper (cgstrlit, cgident, cgindex, cgmatch,
// cgdot, cgun, cgbin, cgcall, cgassign). Trivial literal loads
// (nkind.N_INTLIT, nkind.N_RUNELIT, nkind.N_TRUE/FALSE/NIL, nkind.N_CAST) stay inline.
//
// The remainder of cgen lives in cgen.ww (foundation: types, emit
// primitives, the collect* tables, FFI/module maps) and cgenstmt.ww
// (cgstmt).
//
// `use cgenexpr;` is unnecessary at consumer sites — cgen.ww imports
// this file, so any caller of cgen transitively gets cgexpr.
use os;
use mem;
use ast;
use tok;
use typ;
use sym;
use strconv;
fn cgexpr(c: *cgen, n: *node) void = {
if (n == nil) { return; };
let k: nkind = n.kind;
if (k == nkind.N_INTLIT) {
// Print signed (i64), not unsigned (u64). C cgen uses
// `$%lld` so 64-bit constants with bit 63 set show up as
// negative — e.g. FNV-1a's offset basis prints as
// $-3750763034362895579, not $14695981039346656037.
emitline("\tMOVQ\t$");
emitint(n.uval: i64);
emitline(", AX\n");
return;
};
if (k == nkind.N_FLOATLIT) {
// Materialise the f64 bit pattern in AX, push, then MOVSD it
// into X0. The bits come from n.uval — the parser populates
// it from the lexer's bitcast of t.fval, so this path stays
// integer-only (no SSE in the cgen source). The f32
// narrowing is handled at the consumer site, not here — the
// literal always carries the full double precision until
// typed by context.
emitline("\tMOVQ\t$");
emitint(n.uval: i64);
emitline(", AX\n");
emitline("\tPUSHQ\tAX\n");
emitline("\tMOVSD\t(SP), X0\n");
emitline("\tADDQ\t$8, SP\n");
return;
};
if (k == nkind.N_RUNELIT) {
emitline("\tMOVQ\t$");
emitint(n.uval: i64);
emitline(", AX\n");
return;
};
if (k == nkind.N_STRLIT) { cgstrlit(c, n); return; };
if (k == nkind.N_TRUE) {
emitline("\tMOVQ\t$1, AX\n");
return;
};
if (k == nkind.N_FALSE) {
emitline("\tMOVQ\t$0, AX\n");
return;
};
if (k == nkind.N_NIL) {
emitline("\tMOVQ\t$0, AX\n");
return;
};
if (k == nkind.N_VOIDLIT) {
// void value: zero-size, but the consumer's ABI expects a
// deterministic AX. Emit 0 like nil/false do.
emitline("\tMOVQ\t$0, AX\n");
return;
};
if (k == nkind.N_IDENT) { cgident(c, n); return; };
if (k == nkind.N_INDEX) { cgindex(c, n); return; };
if (k == nkind.N_SLICE) { cgslice(c, n); return; };
if (k == nkind.N_MATCH) { cgmatch(c, n); return; };
if (k == nkind.N_CAST) { cgcast(c, n); return; };
if (k == nkind.N_DOT) { cgdot(c, n); return; };
if (k == nkind.N_UN) { cgun(c, n); return; };
if (k == nkind.N_BIN) { cgbin(c, n); return; };
if (k == nkind.N_CALL) { cgcall(c, n); return; };
if (k == nkind.N_ASSIGN) { cgassign(c, n); return; };
if (k == nkind.N_TRYPROP) { cgtryprop(c, n); return; };
if (k == nkind.N_TRYUNW) { cgtryunw(c, n); return; };
if (k == nkind.N_TYPETEST) { cgtypetest(c, n); return; };
if (k == nkind.N_TYPEASSERT) { cgtypeassert(c, n); return; };
// Default fallback: produce a deterministic AX = 0. Mirrors
// the C cgen's `default: cgexpr_int(c, 0)` branch, which is
// what `return eof{};` (N_STRUCTLIT with an empty !void
// variant) silently relies on — without this AX carries a
// stale value into the tagged-union return shuffle.
emitline("\tMOVQ\t$0, AX\n");
};
// cgtagvariantidx — find the 0-based variant index of `vt` inside the
// tagged-union type expression `tagged`. -1 if `tagged` isn't an
// nkind.N_TTAGGED or no variant matches. Mirrors the lookup that cgmatch
// does inline; pulled out so `is` / `as` can reuse it.
fn cgtagvariantidx(tagged: *node, vt: *node) i32 = {
if (tagged == nil) { return -1; };
if (vt == nil) { return -1; };
if (tagged.kind != nkind.N_TTAGGED) { return -1; };
let want: str;
want.ptr = nil; want.len = 0;
if (vt.kind == nkind.N_TNAME) { want = vt.str; };
if (want.len == 0) { return -1; };
let v: *node = tagged.list;
let idx: i32 = 0;
for (v != nil) {
if (v.kind == nkind.N_TNAME) {
if (streq(v.str, want)) { return idx; };
};
v = v.next;
idx += 1;
};
return -1;
};
// cgtryprop — `e?` propagates the error variant up the stack.
// Legacy semantics only (success tag = 0). No tag remap; the
// selfhost code that uses ? today has the same variant order in
// operand and enclosing fn.
fn cgtryprop(c: *cgen, n: *node) void = {
cgexpr(c, n.lhs);
// AX = tag. If non-zero, this is an error; pop frame and RET.
let cl: str = mklabel(c, "tryprop_ok");
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJE\t");
emitline(cl);
emitline("\n");
emitline("\tMOVQ\tBP, SP\n\tPOPQ\tBP\n\tRET\n");
emitlabel(cl);
// Success: unwrap value. Tag-only result was AX; the rest of
// the codegen expects the success value in AX (and BX for str).
// AX=tag, DX=val0, CX=val1 from the call ABI. For str success,
// shuffle (DX,CX) → (AX,BX); else move DX → AX.
let succisstr: bool = false;
if (n.lhs != nil) {
if (n.lhs.kind == nkind.N_CALL) {
let callee: *node = n.lhs.lhs;
if (callee != 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) {
let rt: *node = fnretlookup(c, cname);
if (rt != nil) {
if (rt.kind == nkind.N_TTAGGED) {
let first: *node = rt.list;
if (first != nil) {
if (isstrtype(c, first)) {
succisstr = true;
};
};
};
};
};
};
};
};
if (succisstr) {
emitline("\tMOVQ\tCX, BX\n");
};
emitline("\tMOVQ\tDX, AX\n");
return;
};
// cgtryunw — `e!` aborts on the error variant via exit(1). Legacy
// semantics (success tag = 0).
fn cgtryunw(c: *cgen, n: *node) void = {
cgexpr(c, n.lhs);
let cl: str = mklabel(c, "tryunw_ok");
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJE\t");
emitline(cl);
emitline("\n");
emitline("\tMOVQ\t$1, DI\n\tMOVQ\t$60, AX\n\tSYSCALL\n");
emitlabel(cl);
// Unwrap success value. (Same shuffle pattern as cgtryprop.)
let succisstr: bool = false;
if (n.lhs != nil) {
if (n.lhs.kind == nkind.N_CALL) {
let callee: *node = n.lhs.lhs;
if (callee != 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) {
let rt: *node = fnretlookup(c, cname);
if (rt != nil) {
if (rt.kind == nkind.N_TTAGGED) {
let first: *node = rt.list;
if (first != nil) {
if (isstrtype(c, first)) {
succisstr = true;
};
};
};
};
};
};
};
};
if (succisstr) {
emitline("\tMOVQ\tCX, BX\n");
};
emitline("\tMOVQ\tDX, AX\n");
return;
};
fn cgtypetest(c: *cgen, n: *node) void = {
// `e is T` — load the lhs's tag, compare against T's variant
// index, set AX = (tag == idx). Result type is bool.
//
// Slot resolution is inlined (rather than factored into a helper
// with output parameters): wwstage cgen has a trap with i32
// stored via *i32 in this context — direct assignment of the
// local works, indirection through &scrutoff drops sign bits.
let lhs: *node = n.lhs;
let scrutoff: i32 = 0;
let scrutt: *node = nil;
if (lhs != nil) {
if (lhs.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, lhs.str);
if (lc != nil) {
scrutoff = lc.off;
scrutt = resolvetagged(c, lc.tnode);
};
};
};
let want: i32 = cgtagvariantidx(scrutt, n.rhs);
if (want < 0) { want = 0; };
emitline("\tMOVQ\t");
emitoff(scrutoff: i64);
emitline("(BP), AX\n");
let nel: str = mklabel(c, "is_ne");
let dnl: str = mklabel(c, "is_done");
emitline("\tCMPQ\t$");
emitint(want: i64);
emitline(", AX\n");
emitline("\tJNE\t");
emitline(nel);
emitline("\n\tMOVQ\t$1, AX\n\tJMP\t");
emitline(dnl);
emitline("\n");
emitlabel(nel);
emitline("\tMOVQ\t$0, AX\n");
emitlabel(dnl);
return;
};
// isenumexpr — does this expression's static type resolve to an enum?
// Recognises enum-member access (`Foo.MEMBER`), enum-typed local
// idents, and nkind.N_BIN whose either operand is enum (so `R | W` flows
// through the cast pass-through too).
fn isenumexpr(c: *cgen, e: *node) bool = {
if (e == nil) { return false; };
let k: nkind = e.kind;
if (k == nkind.N_DOT) {
if (e.lhs != nil) {
if (e.lhs.kind == nkind.N_IDENT) {
if (enumlookup(c, e.lhs.str) != nil) { return true; };
};
};
};
if (k == nkind.N_IDENT) {
let lc: *local = localfindnode(c, e.str);
if (lc != nil) {
if (lc.tnode != nil) {
if (lc.tnode.kind == nkind.N_TNAME) {
if (enumlookup(c, lc.tnode.str) != nil) { return true; };
};
};
};
};
if (k == nkind.N_BIN) {
if (isenumexpr(c, e.lhs)) { return true; };
if (isenumexpr(c, e.rhs)) { return true; };
};
if (k == nkind.N_UN) {
if (isenumexpr(c, e.lhs)) { return true; };
};
return false;
};
fn isenumtype(c: *cgen, t: *node) bool = {
if (t == nil) { return false; };
if (t.kind == nkind.N_TENUM) { return true; };
if (t.kind == nkind.N_TNAME) {
if (enumlookup(c, t.str) != nil) { return true; };
};
return false;
};
fn cgtypeassert(c: *cgen, n: *node) void = {
// Enum ↔ integer: reinterpret-only. The LHS value already
// occupies AX (or AX:BX for str variants, irrelevant here);
// no tag/unwrap. Matches cmd/w6c/cgen.c's same short-circuit.
if (isenumexpr(c, n.lhs) || isenumtype(c, n.rhs)) {
cgexpr(c, n.lhs);
return;
};
// `e as T` — load tag, abort (exit 1) if tag != T's variant
// index, otherwise unwrap to T's ABI: scalar/ptr → AX, 16B
// str → (AX, BX). Mirrors cgmatch's slot-based value load.
// Slot resolution inlined; see cgtypetest comment.
let lhs: *node = n.lhs;
let scrutoff: i32 = 0;
let scrutt: *node = nil;
if (lhs != nil) {
if (lhs.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, lhs.str);
if (lc != nil) {
scrutoff = lc.off;
scrutt = resolvetagged(c, lc.tnode);
};
};
};
let want: i32 = cgtagvariantidx(scrutt, n.rhs);
if (want < 0) { want = 0; };
let okl: str = mklabel(c, "asrt_ok");
emitline("\tMOVQ\t");
emitoff(scrutoff: i64);
emitline("(BP), AX\n");
emitline("\tCMPQ\t$");
emitint(want: i64);
emitline(", AX\n");
emitline("\tJE\t");
emitline(okl);
emitline("\n\tMOVQ\t$1, DI\n\tMOVQ\t$60, AX\n\tSYSCALL\n");
emitlabel(okl);
emitline("\tMOVQ\t");
emitoff((scrutoff + 8): i64);
emitline("(BP), AX\n");
if (isstrtype(c, n.rhs)) {
emitline("\tMOVQ\t");
emitoff((scrutoff + 16): i64);
emitline("(BP), BX\n");
};
return;
};
fn cgcast(c: *cgen, n: *node) void = {
let srcfk: i32 = exprfloatkind(c, n.lhs);
let dstf64: bool = isfloattype(c, n.rhs);
let dstf32: bool = isf32type(c, n.rhs);
let dstfk: i32 = 0;
if (dstf32) { dstfk = 1; }
else { if (dstf64) { dstfk = 2; }; };
cgexpr(c, n.lhs);
// 0=int, 1=f32, 2=f64. CVT picks one direction per combo;
// same-kind casts (int↔int with widening differences,
// f64→f64 etc.) stay no-ops at the asm level, matching the
// pre-port behaviour for integer casts.
if (srcfk == 0 && dstfk == 0) { return; };
if (srcfk == 0 && dstfk == 2) {
emitline("\tCVTSI2SD\tAX, X0\n");
return;
};
if (srcfk == 0 && dstfk == 1) {
emitline("\tCVTSI2SS\tAX, X0\n");
return;
};
if (srcfk == 2 && dstfk == 0) {
emitline("\tCVTTSD2SI\tX0, AX\n");
return;
};
if (srcfk == 1 && dstfk == 0) {
emitline("\tCVTTSS2SI\tX0, AX\n");
return;
};
if (srcfk == 2 && dstfk == 1) {
emitline("\tCVTSD2SS\tX0, X0\n");
return;
};
if (srcfk == 1 && dstfk == 2) {
emitline("\tCVTSS2SD\tX0, X0\n");
return;
};
// Same-kind float→float: nothing to emit.
};
fn cgstrlit(c: *cgen, n: *node) void = {
// Result is the (ptr, len) pair: ptr in AX, len in BX. Call
// sites that expect a str arg pick these up directly.
let nstr: str = n.str;
let lab: str = internstrlit(c, nstr);
emitline("\tLEAQ\t");
os.write(1, lab.ptr, lab.len: u64);
emitline("(SB), AX\n");
emitline("\tMOVQ\t$");
emitint(nstr.len: i64);
emitline(", BX\n");
return;
};
fn cgident(c: *cgen, n: *node) void = {
let nm: str = n.str;
let lc: *local = localfindnode(c, nm);
if (lc != nil) {
let off: i32 = lc.off;
// Float local: MOVSS / MOVSD into X0. Skips the AX shuffle
// so consumers (cgbin, cgcast, return) pick up the SSE value
// directly.
if (isfloattype(c, lc.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, lc.tnode)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\t");
emitoff(off: i64);
emitline("(BP), X0\n");
return;
};
emitline("\tMOVQ\t");
emitoff(off: i64);
emitline("(BP), AX\n");
// str local: also load the len half into BX.
if (isstrtype(c, lc.tnode)) {
emitline("\tMOVQ\t");
emitoff((off + 8): i64);
emitline("(BP), BX\n");
};
// slice local: load (ptr, len, cap) into (AX, BX, CX).
if (isslicetype(c, lc.tnode)) {
emitline("\tMOVQ\t");
emitoff((off + 8): i64);
emitline("(BP), BX\n");
emitline("\tMOVQ\t");
emitoff((off + 16): i64);
emitline("(BP), CX\n");
};
return;
};
// Top-level `def` constant — load from its DATA symbol.
if (deflookup(c, nm)) {
emitline("\tMOVQ\t");
emitsymname(c, nm);
emitline("(SB), AX\n");
return;
};
// Fn-name used as a value (e.g. `let f = some_fn;` or
// `... = some_fn;`). LEAQ the symbol address into AX. The
// emitsymname helper handles ffiresolve and module-mangling
// in one go, so a body-less FFI binding emits the C symbol
// it was declared with via @symbol(), not the ww-side ident.
let rt: *node = fnretlookup(c, nm);
if (rt != nil) {
emitline("\tLEAQ\t");
emitsymname(c, nm);
emitline("(SB), AX\n");
return;
};
// Top-level mutable `let` — RIP-relative load from its DATAW
// slot. Mirrors C cgen's catch-all `MOVQ masym(s), AX` for
// scalar lets, plus the (LEAQ, MOVQ, MOVQ[, MOVQ]) sequence
// for str / slice globals so the ABI pair / triple lands in
// (AX, BX[, CX]). Names that aren't lets either (typos,
// never-defined) drop through to the silent return.
if (isletvar(c, nm)) {
let isstr: bool = letvarisstr(c, nm);
let issl: bool = letvarisslice(c, nm);
if (isstr || issl) {
emitline("\tLEAQ\t");
emitsymname(c, nm);
emitline("(SB), CX\n");
emitline("\tMOVQ\t(CX), AX\n");
emitline("\tMOVQ\t8(CX), BX\n");
if (issl) {
// Overwrites the address holder with the
// cap as the last step — CX is no longer
// needed once both ptr/len are loaded.
emitline("\tMOVQ\t16(CX), CX\n");
};
return;
};
// Float global: same LEAQ-indirect shape, since MOVSS/
// MOVSD have no D_EXTERN operand form in w6a.
let lv: *letvar = c.lets;
for (lv != nil) {
if (streq(lv.name, nm)) {
if (isfloattype(c, lv.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, lv.tnode)) { mov = "MOVSS"; };
emitline("\tLEAQ\t");
emitsymname(c, nm);
emitline("(SB), CX\n");
emitline("\t");
emitline(mov);
emitline("\t(CX), X0\n");
return;
};
lv = nil;
} else {
lv = lv.lvnext;
};
};
emitline("\tMOVQ\t");
emitsymname(c, nm);
emitline("(SB), AX\n");
return;
};
return;
};
fn cgindex(c: *cgen, n: *node) void = {
// Element-size-aware load: u8 → MOVZBQ, i32 → MOVSXD, u32 → MOVL,
// str → (ptr, len) into (AX, BX), everything else → MOVQ. Fast
// path when the base is a bare ident (mem.ww shape).
let base: *node = n.lhs;
let idx: *node = n.rhs;
let esz: i32 = 8;
let signed_elem: bool = false;
let baselocal: *local = nil;
// Global `[N]T` array or `*T` pointer used as an index base.
// The local-ident lookup above misses it; we need LEAQ name(SB)
// (array, the symbol IS the storage) or MOVQ name(SB) (pointer,
// the symbol holds the address) to feed the addend.
let isglobalarr: bool = false;
let isglobalptr: bool = false;
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) {
esz = elemsizeof(baselocal.tnode);
signed_elem = elemissigned(baselocal.tnode);
} else {
let tn: *node = letvartnode(c, bn);
if (tn != nil) {
if (tn.kind == nkind.N_TARRAY) {
isglobalarr = true;
globalname = bn;
esz = elemsizeof(tn);
signed_elem = elemissigned(tn);
};
if (tn.kind == nkind.N_TPTR) {
isglobalptr = true;
globalname = bn;
esz = elemsizeof(tn);
signed_elem = elemissigned(tn);
};
};
};
} else { if (base.kind == nkind.N_DOT) {
esz = indexbaseesz(c, base);
};};
};
cgexpr(c, idx);
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", CX\n");
emitline("\tIMULQ\tCX, AX\n");
};
if (isglobalarr || isglobalptr) {
if (isglobalarr) {
emitline("\tLEAQ\t");
emitsymname(c, globalname);
emitline("(SB), BX\n");
} else {
emitline("\tMOVQ\t");
emitsymname(c, globalname);
emitline("(SB), BX\n");
};
emitline("\tADDQ\tAX, BX\n");
if (esz == 16) {
emitline("\tMOVQ\t8(BX), CX\n");
emitline("\tMOVQ\t(BX), AX\n");
emitline("\tMOVQ\tCX, BX\n");
return;
};
if (esz == 1) { emitline("\tMOVZBQ\t(BX), AX\n"); }
else { if (esz == 4) {
if (signed_elem) { emitline("\tMOVSXD\t(BX), AX\n"); }
else { emitline("\tMOVL\t(BX), AX\n"); };
}
else { emitline("\tMOVQ\t(BX), AX\n"); };};
return;
};
if (baselocal != nil) {
let tn: *node = baselocal.tnode;
let isarray: bool = false;
if (tn != nil) { if (tn.kind == nkind.N_TARRAY) { isarray = true; }; };
if (isarray) {
emitline("\tLEAQ\t");
emitoff(baselocal.off: i64);
emitline("(BP), BX\n");
} else {
emitline("\tMOVQ\t");
emitoff(baselocal.off: i64);
emitline("(BP), BX\n");
};
emitline("\tADDQ\tAX, BX\n");
// str element (16B): load (ptr, len) into (AX, BX) so
// the value flows through the str-rhs convention.
if (esz == 16) {
emitline("\tMOVQ\t8(BX), CX\n");
emitline("\tMOVQ\t(BX), AX\n");
emitline("\tMOVQ\tCX, BX\n");
return;
};
if (esz == 1) { emitline("\tMOVZBQ\t(BX), AX\n"); }
else { if (esz == 4) {
if (signed_elem) { emitline("\tMOVSXD\t(BX), AX\n"); }
else { emitline("\tMOVL\t(BX), AX\n"); };
}
else { emitline("\tMOVQ\t(BX), AX\n"); };};
return;
};
// Generic fallback when base isn't a plain ident.
emitline("\tPUSHQ\tAX\n");
cgexpr(c, base);
emitline("\tPOPQ\tBX\n");
emitline("\tADDQ\tBX, AX\n");
if (esz == 16) {
emitline("\tMOVQ\t8(AX), BX\n");
emitline("\tMOVQ\t(AX), AX\n");
return;
};
if (esz == 1) { emitline("\tMOVZBQ\t(AX), AX\n"); }
else { if (esz == 4) {
if (signed_elem) { emitline("\tMOVSXD\t(AX), AX\n"); }
else { emitline("\tMOVL\t(AX), AX\n"); };
}
else { emitline("\tMOVQ\t(AX), AX\n"); };};
return;
};
// cgslice — `base[lo:hi]` as a slice value. Leaves (AX=base+lo,
// BX=hi-lo, CX=hi-lo) so callers can route to a slice slot,
// return, or arg with the same triple ABI. Cap defaults to the
// new length; no syntax for a wider cap yet. Element scaling
// on the ptr isn't wired — non-u8 slices need a follow-up audit.
fn cgslice(c: *cgen, n: *node) void = {
let base: *node = n.lhs;
let lo: *node = n.rhs;
let hi: *node = n.cond;
let baselocal: *local = nil;
if (base != nil) {
if (base.kind == nkind.N_IDENT) {
baselocal = localfindnode(c, base.str);
};
};
// base address
if (baselocal != nil) {
let tn: *node = baselocal.tnode;
let isarray: bool = false;
if (tn != nil) {
if (tn.kind == nkind.N_TARRAY) { isarray = true; };
};
if (isarray) {
emitline("\tLEAQ\t");
emitoff(baselocal.off: i64);
emitline("(BP), AX\n");
} else {
emitline("\tMOVQ\t");
emitoff(baselocal.off: i64);
emitline("(BP), AX\n");
};
} else { if (base != nil) {
cgexpr(c, base);
};};
emitline("\tPUSHQ\tAX\n");
// lo (default 0)
if (lo != nil) { cgexpr(c, lo); }
else { emitline("\tMOVQ\t$0, AX\n"); };
emitline("\tPUSHQ\tAX\n");
// hi (default base length)
if (hi != nil) {
cgexpr(c, hi);
} else { if (baselocal != nil) {
let tn: *node = baselocal.tnode;
let handled: bool = false;
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");
handled = true;
};
};
} else { if (tn.kind == nkind.N_TSLICE) {
emitline("\tMOVQ\t");
emitoff((baselocal.off + 8): i64);
emitline("(BP), AX\n");
handled = true;
} else { if (tn.kind == nkind.N_TNAME) {
if (streq(tn.str, "str")) {
emitline("\tMOVQ\t");
emitoff((baselocal.off + 8): i64);
emitline("(BP), AX\n");
handled = true;
};
};};};
};
if (!handled) { emitline("\tMOVQ\t$0, AX\n"); };
} else {
emitline("\tMOVQ\t$0, AX\n");
};};
emitline("\tMOVQ\tAX, BX\n");
emitline("\tPOPQ\tCX\n");
emitline("\tPOPQ\tAX\n");
emitline("\tADDQ\tCX, AX\n");
emitline("\tSUBQ\tCX, BX\n");
emitline("\tMOVQ\tBX, CX\n");
};
fn cgmatch(c: *cgen, n: *node) void = {
// match (e) { case let v: T => stmt; ... }
//
// Read the tagged-union slot and dispatch by tag. Slot
// layout: [+0]=tag, [+8]=value0, [+16]=value1. Bindings
// (`case let v: T =>`) get a fresh local slot loaded from
// slot+8 (and slot+16 for str-typed payload).
let scrut: *node = n.lhs;
let scrutoff: i32 = 0;
let scrutt: *node = nil;
if (scrut != nil) {
if (scrut.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, scrut.str);
if (lc != nil) {
scrutoff = lc.off;
scrutt = resolvetagged(c, lc.tnode);
};
} else {
// Non-ident scrutinee (call result, ?, etc.). Spill into a
// 24B `@match_spill` scratch slot and dispatch off it.
// Tagged returns follow the AX:DX:CX convention, so store
// all three words at +0/+8/+16; nullable returns are
// single-word (AX = ptr) and only read +0, so the extra
// stores are harmless. For N_CALL we recover the return
// type via fnretlookup so nullable dispatch can pick the
// pointer-vs-null discriminator. Mirrors C cgen's
// @match_spill path in cmd/w6c/cgen.c N_MATCH.
scrutoff = localalloc(c, "@match_spill", 24, nil);
if (scrut.kind == 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) { scrutt = resolvetagged(c, rt); };
};
};
};
cgexpr(c, scrut);
emitline("\tMOVQ\tAX, ");
emitoff(scrutoff: i64);
emitline("(BP)\n");
if (!isnullabletype(scrutt)) {
emitline("\tMOVQ\tDX, ");
emitoff((scrutoff + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((scrutoff + 16): i64);
emitline("(BP)\n");
};
};
};
let endl: str = mklabel(c, "match_end");
// Push end label as the yield target for this match's arm bodies.
if (c.yieldtop < LOOP_MAX) {
c.yieldbuf[c.yieldtop] = endl;
c.yieldtop += 1;
};
let cs: *node = n.list;
for (cs != nil) {
let nxt: str = mklabel(c, "match_next");
let pat: *node = cs.lhs;
let nullable: bool = isnullabletype(scrutt);
// Per-arm scope: save c.locals before allocating the bind
// and restore after the body runs, so the arm's bind (and
// any nested lets) don't leak past the arm. Matches the
// checker's newscope/restore around N_MCASE. Without this,
// `let e: *T = ...; match (r) { case let e: str => ... };
// use e` would resolve `e` after the match to the inner
// str slot instead of the outer ptr.
let arm_locals_saved: *local = c.locals;
// Compute the variant tag for this arm. Default arm
// (no pattern) skips the tag check.
if (pat != nil) {
if (nullable) {
// Discriminator = pointer-vs-null.
// *T arm: skip if ptr == 0.
// void arm: skip if ptr != 0.
let ptr_tag: i32 = nullableptrtag(scrutt);
let cur_tag: i32 = 0;
if (pat.kind == nkind.N_TPTR) { cur_tag = ptr_tag; }
else { if (ptr_tag == 0) { cur_tag = 1; }; };
emitline("\tMOVQ\t");
emitoff(scrutoff: i64);
emitline("(BP), AX\n");
emitline("\tCMPQ\t$0, AX\n");
if (cur_tag == ptr_tag) {
emitline("\tJE\t");
} else {
emitline("\tJNE\t");
};
emitline(nxt);
emitline("\n");
} else {
let want: i32 = 0;
if (scrutt != nil) {
if (scrutt.kind == nkind.N_TTAGGED) {
let patname: str;
patname.ptr = nil; patname.len = 0;
if (pat.kind == nkind.N_TNAME) { patname = pat.str; };
let v: *node = scrutt.list;
let idx: i32 = 0;
let found: bool = false;
for (v != nil) {
if (v.kind == nkind.N_TNAME) {
if (variantnamematch(v.str, patname)) {
want = idx;
found = true;
v = nil;
};
};
if (v != nil) {
v = v.next;
idx += 1;
};
};
if (!found) { want = 0; };
};
};
emitline("\tMOVQ\t");
emitoff(scrutoff: i64);
emitline("(BP), AX\n");
emitline("\tCMPQ\t$");
emitint(want: i64);
emitline(", AX\n");
emitline("\tJNE\t");
emitline(nxt);
emitline("\n");
};
};
// Bind `let v: T` from the slot, if requested.
let bn: str = cs.str;
if (bn.len > 0) {
if (pat != nil) {
if (nullable) {
// Bind the pointer (or skip for the
// void arm, which has zero-size). The
// value IS slot+0.
if (pat.kind == nkind.N_TPTR) {
let voff: i32 = localalloc(c, bn, 8, pat);
emitline("\tMOVQ\t");
emitoff(scrutoff: i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff(voff: i64);
emitline("(BP)\n");
};
} else {
let bsz: i32 = 8;
if (isstrtype(c, pat)) { bsz = 16; };
// localalloc (not localadd): match-arm
// binds don't dedup with same-named binds
// in *other* matches, since C's cgexpr
// allocates a fresh slot per match expr.
let voff: i32 = localalloc(c, bn, bsz, pat);
emitline("\tMOVQ\t");
emitoff((scrutoff + 8): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff(voff: i64);
emitline("(BP)\n");
if (bsz == 16) {
emitline("\tMOVQ\t");
emitoff((scrutoff + 16): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((voff + 8): i64);
emitline("(BP)\n");
};
};
};
};
// Body. Match arms are statements; we cgstmt them.
if (cs.body != nil) { cgstmt(c, cs.body); };
// Restore the locals head — pop everything the arm pushed
// so post-match code resolves names to their original (outer)
// bindings.
c.locals = arm_locals_saved;
emitline("\tJMP\t");
emitline(endl);
emitline("\n");
emitlabel(nxt);
cs = cs.next;
};
emitlabel(endl);
if (c.yieldtop > 0) { c.yieldtop -= 1; };
return;
};
fn cgdot(c: *cgen, n: *node) void = {
let lhs: *node = n.lhs;
let fld: str = n.str;
// Enum member access: `EnumName.MEMBER` or `pkg.EnumName.MEMBER`
// → inline the pre-computed constant. With driver-side
// concatenation, both forms key off the leaf type name.
if (lhs != nil) {
let etname: str;
etname.ptr = nil; etname.len = 0;
if (lhs.kind == nkind.N_IDENT) {
etname = lhs.str;
};
if (lhs.kind == nkind.N_DOT) {
if (lhs.lhs != nil) {
if (lhs.lhs.kind == nkind.N_IDENT) {
etname = lhs.str;
};
};
};
if (etname.len > 0) {
let en: *enumtype = enumlookup(c, etname);
if (en != nil) {
let v: u64;
if (enummemberval(en, fld, &v)) {
emitline("\tMOVQ\t$");
emitint(v: i64);
emitline(", AX\n");
return;
};
};
};
};
if (lhs != nil) {
if (lhs.kind == nkind.N_IDENT) {
let nm: str = lhs.str;
let lc: *local = localfindnode(c, nm);
if (lc != nil) {
let tn: *node = lc.tnode;
let lkind: nkind = nkind.N_NONE;
if (tn != nil) { lkind = tn.kind; };
// Pointer-to-struct: deref then field load.
if (lkind == nkind.N_TPTR) {
let inner: *node = tn.lhs;
let sname: str;
sname.ptr = nil; sname.len = 0;
if (inner != nil) {
if (inner.kind == nkind.N_TNAME) {
sname = inner.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)) {
// str field via *struct: load len into a
// scratch first (so loading ptr into AX
// last leaves (AX=ptr, BX=len)).
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
if (isstrtype(c, fi.tnode)) {
emitline("\tMOVQ\t");
emitdispreg((fi.foff + 8): i64, "BX");
emitline(", CX\n");
emitline("\tMOVQ\t");
emitdispreg(fi.foff: i64, "BX");
emitline(", AX\n");
emitline("\tMOVQ\tCX, BX\n");
} else { if (isfloattype(c, fi.tnode)) {
// f64/f32 via *struct: route through X0.
// MOVQ into AX leaves the SSE reg stale
// and any downstream consumer (arg
// pass, return, arithmetic) reads
// garbage.
let mov: str = "MOVSD";
if (isf32type(c, fi.tnode)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\t");
emitdispreg(fi.foff: i64, "BX");
emitline(", X0\n");
} else {
let op: str = fieldloadop(fi);
emitline("\t");
emitline(op);
emitline("\t");
emitdispreg(fi.foff: i64, "BX");
emitline(", AX\n");
}; };
return;
};
fi = fi.finext;
};
};
};
};
// Direct struct local: field load at off+foff.
if (lkind == nkind.N_TNAME) {
let sname: str = tn.str;
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)) {
// str field: load both halves so chained
// `.ptr` / `.len` see (AX=ptr, BX=len).
if (isstrtype(c, fi.tnode)) {
emitline("\tMOVQ\t");
emitoff((lc.off + fi.foff): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\t");
emitoff((lc.off + fi.foff + 8): i64);
emitline("(BP), BX\n");
} else { if (isfloattype(c, fi.tnode)) {
// f64/f32 field: route through X0.
let mov: str = "MOVSD";
if (isf32type(c, fi.tnode)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\t");
emitoff((lc.off + fi.foff): i64);
emitline("(BP), X0\n");
} else {
let op: str = fieldloadop(fi);
emitline("\t");
emitline(op);
emitline("\t");
emitoff((lc.off + fi.foff): i64);
emitline("(BP), AX\n");
}; };
return;
};
fi = fi.finext;
};
};
};
// Array pseudo-fields: `.ptr` is the array's
// address (LEAQ); `.len` is the static element
// count (immediate).
if (lkind == nkind.N_TARRAY) {
if (streq(fld, "ptr")) {
emitline("\tLEAQ\t");
emitoff(lc.off: i64);
emitline("(BP), AX\n");
return;
};
if (streq(fld, "len")) {
let lenn: *node = tn.rhs;
let alen: i64 = 0i64;
if (lenn != nil) {
if (lenn.kind == nkind.N_INTLIT) { alen = lenn.uval: i64; };
};
emitline("\tMOVQ\t$");
emitint(alen);
emitline(", AX\n");
return;
};
};
// Hare-style tuple positional access: `t.0`, `t.1`.
// Walk the tuple element type list summing slotsize
// (matches the (scalar, str) init layout which puts
// the scalar in an 8B slot and the str in 16B). For
// a str element, load both halves into (AX, BX) so
// chains like `t.1.len` propagate correctly.
if (lkind == nkind.N_TTUPLE) {
let idx: i32 = fldnumidx(fld);
if (idx >= 0) {
let tp: *node = tn.list;
let foff: i32 = 0;
let i: i32 = 0;
for (i < idx) {
if (tp == nil) { i = idx; }
else {
foff += slotsize(c, tp);
tp = tp.next;
i += 1;
};
};
if (tp != nil) {
if (isstrtyperaw(tp)) {
emitline("\tMOVQ\t");
emitoff((lc.off + foff + 0): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\t");
emitoff((lc.off + foff + 8): i64);
emitline("(BP), BX\n");
return;
};
let sz: i32 = slotsize(c, tp);
let op: str = "MOVQ";
if (sz == 1) { op = "MOVZBQ"; }
else { if (sz == 4) { op = "MOVL"; }; };
emitline("\t");
emitline(op);
emitline("\t");
emitoff((lc.off + foff): i64);
emitline("(BP), AX\n");
return;
};
};
};
// str/slice pseudo-fields .ptr/.len/.cap on a
// direct local: load at slot+delta.
let delta: i32 = -1;
if (streq(fld, "ptr")) { delta = 0; };
if (streq(fld, "len")) { delta = 8; };
if (streq(fld, "cap")) { delta = 16; };
if (delta >= 0) {
// Pointer to str/slice (`*[]u8`, `*str`):
// deref, then load at delta within the
// pointed-to header. C cgen does the same.
if (lkind == nkind.N_TPTR) {
let inner: *node = tn.lhs;
let innerkind: nkind = nkind.N_NONE;
if (inner != nil) { innerkind = inner.kind; };
let innerstr: bool = false;
if (innerkind == nkind.N_TNAME) {
if (streq(inner.str, "str")) { innerstr = true; };
};
if (innerkind == nkind.N_TSLICE) { innerstr = true; };
if (innerstr) {
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
emitline("\tMOVQ\t");
emitdispreg(delta: i64, "BX");
emitline(", AX\n");
return;
};
};
emitline("\tMOVQ\t");
emitoff((lc.off + delta): i64);
emitline("(BP), AX\n");
return;
};
};
};
};
// `def NAME: str = "..."` field access — inline the literal.
// Sdef-backed strs aren't laid out in memory, so falling
// through to the SB-load fallback below would mis-emit
// `MOVQ <field>(SB), AX` (looking up the field name as a
// symbol). Mirrors cmd/w6c/cgen.c nkind.N_DOT off==0 / Sdef branch.
if (lhs != nil) {
if (lhs.kind == nkind.N_IDENT) {
let drhs: *node = deflookuprhs(c, lhs.str);
if (drhs != nil) {
if (drhs.kind == nkind.N_STRLIT) {
let bytes: str = drhs.str;
if (streq(fld, "ptr")) {
let lab: str = internstrlit(c, bytes);
emitline("\tLEAQ\t");
os.write(1, lab.ptr, lab.len: u64);
emitline("(SB), AX\n");
return;
};
if (streq(fld, "len")) {
emitline("\tMOVQ\t$");
emitint(bytes.len: i64);
emitline(", AX\n");
return;
};
};
};
};
};
// Top-level str/slice global field access — load .ptr / .len
// (and .cap for slices) via &name(SB) into CX, then MOVQ
// delta(CX), AX. Without this the module-qualified fallback
// below would mis-emit `MOVQ <field>(SB), AX`.
if (lhs != nil) {
if (lhs.kind == nkind.N_IDENT) {
if (isletvar(c, lhs.str)) {
let isstr: bool = letvarisstr(c, lhs.str);
let issl: bool = letvarisslice(c, lhs.str);
if (isstr || issl) {
let delta: i32 = -1;
if (streq(fld, "ptr")) { delta = 0; };
if (streq(fld, "len")) { delta = 8; };
if (issl) {
if (streq(fld, "cap")) { delta = 16; };
};
if (delta >= 0) {
emitline("\tLEAQ\t");
emitsymname(c, lhs.str);
emitline("(SB), CX\n");
emitline("\tMOVQ\t");
emitdispreg(delta: i64, "CX");
emitline(", AX\n");
return;
};
};
};
};
};
// Top-level struct global field read — LEAQ name(SB), CX then
// load at fi.foff(CX). Mirrors the local "Direct struct local"
// branch above, swapping the BP frame slot for the global VA.
// Field-width-aware op handles MOVQ / MOVL / MOVZBQ / MOVSXD.
if (lhs != nil) {
if (lhs.kind == nkind.N_IDENT) {
let si: *structinfo = letvarstructinfo(c, lhs.str);
if (si != nil) {
let fi: *fieldinfo = si.fields;
for (fi != nil) {
if (streq(fi.fname, fld)) {
emitline("\tLEAQ\t");
emitsymname(c, lhs.str);
emitline("(SB), CX\n");
if (isstrtype(c, fi.tnode)) {
emitline("\tMOVQ\t");
emitdispreg(fi.foff: i64, "CX");
emitline(", AX\n");
emitline("\tMOVQ\t");
emitdispreg((fi.foff + 8): i64, "CX");
emitline(", BX\n");
} else { if (isfloattype(c, fi.tnode)) {
// f64/f32 global field: route through X0.
let mov: str = "MOVSD";
if (isf32type(c, fi.tnode)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\t");
emitdispreg(fi.foff: i64, "CX");
emitline(", X0\n");
} else {
let op: str = fieldloadop(fi);
emitline("\t");
emitline(op);
emitline("\t");
emitdispreg(fi.foff: i64, "CX");
emitline(", AX\n");
}; };
return;
};
fi = fi.finext;
};
};
};
};
// `xs[i].field` — slice/array/ptr-of-struct element field access.
// Without this the cgen falls through to the module-qualified
// SB fallback below and emits `MOVQ <fld>(SB), AX` (linker
// reports `undefined reference to <fld>`). cgexpr(c, lhs)
// dispatches to cgindex which leaves the element value in AX
// — for a []*T element that's the *T pointer, so we just chain
// the field load through (AX).
if (lhs != nil) {
if (lhs.kind == nkind.N_INDEX) {
let idxbase: *node = lhs.lhs;
if (idxbase != nil) { if (idxbase.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, idxbase.str);
if (lc != nil) { if (lc.tnode != nil) {
let tn: *node = lc.tnode;
let elemt: *node = nil;
let tk: nkind = tn.kind;
if (tk == nkind.N_TSLICE) { elemt = tn.lhs; };
if (tk == nkind.N_TARRAY) { elemt = tn.lhs; };
if (tk == nkind.N_TPTR) { elemt = tn.lhs; };
if (elemt != nil) { if (elemt.kind == nkind.N_TPTR) {
let inner: *node = elemt.lhs;
if (inner != nil) { if (inner.kind == nkind.N_TNAME) {
let sname: str = inner.str;
let si: *structinfo = structlookup(c, sname);
if (si != nil) {
let fi: *fieldinfo = si.fields;
for (fi != nil) {
if (streq(fi.fname, fld)) {
cgexpr(c, lhs); // AX = *Struct
if (isstrtype(c, fi.tnode)) {
emitline("\tMOVQ\t");
emitdispreg((fi.foff + 8): i64, "AX");
emitline(", BX\n");
emitline("\tMOVQ\t");
emitdispreg(fi.foff: i64, "AX");
emitline(", AX\n");
return;
};
if (isfloattype(c, fi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, fi.tnode)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\t");
emitdispreg(fi.foff: i64, "AX");
emitline(", X0\n");
return;
};
let lop: str = fieldloadop(fi);
emitline("\t");
emitline(lop);
emitline("\t");
emitdispreg(fi.foff: i64, "AX");
emitline(", AX\n");
return;
};
fi = fi.finext;
};
};
};};
};};
};};
};};
};
};
// Module-qualified value reference: `mod.name` where `mod`
// is nkind.N_IDENT bound as skind.SK_USE and the leaf isn't a local.
// Treat as a SB symbol — `MOVQ leaf(SB), AX`. Same fallback
// the C cgen takes when bt is NULL/tyerr.
if (lhs != nil) {
if (lhs.kind == nkind.N_IDENT) {
emitline("\tMOVQ\t");
emitsymname(c, fld);
emitline("(SB), AX\n");
return;
};
};
// Non-ident base pseudo-field: e.g. `"abc".ptr` / `"abc".len`.
// Evaluate the str-producing expression — that leaves
// (AX=ptr, BX=len). Then `.ptr` returns AX as is; `.len`
// shuffles BX→AX. Mirrors what C cgen does (it just evaluates
// the literal and picks the half it wants).
if (streq(fld, "ptr")) { cgexpr(c, lhs); return; };
if (streq(fld, "len")) {
cgexpr(c, lhs);
emitline("\tMOVQ\tBX, AX\n");
return;
};
// Chained struct-field-via-ptr-via-ptr access:
// r.sym.val where r: *lrel, .sym: *lsym, .val: u64
// Inner DOT (`r.sym`) returns a *struct (a pointer-to-struct
// field). Outer DOT dereferences and reads `val`. Without this
// path the cgen falls through and AX retains whatever the
// inner expression left there — typically the *struct pointer
// itself, so reads silently get the pointer value instead of
// the field. (Showed up porting w6l/pass.ww.)
if (lhs != nil) {
if (lhs.kind == nkind.N_DOT) {
let innert: *node = dotinnerstructptr(c, lhs);
if (innert != nil) {
let sname: str = innert.str;
let si: *structinfo = structlookup(c, sname);
if (si != nil) {
let fi: *fieldinfo = si.fields;
for (fi != nil) {
if (streq(fi.fname, fld)) {
cgexpr(c, lhs); // AX = ptr to inner struct
// str field: load both halves.
if (isstrtype(c, fi.tnode)) {
emitline("\tMOVQ\t");
emitdispreg((fi.foff + 8): i64, "AX");
emitline(", BX\n");
emitline("\tMOVQ\t");
emitdispreg(fi.foff: i64, "AX");
emitline(", AX\n");
return;
};
// f64/f32 chained field: route through X0.
if (isfloattype(c, fi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, fi.tnode)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\t");
emitdispreg(fi.foff: i64, "AX");
emitline(", X0\n");
return;
};
let lop: str = fieldloadop(fi);
emitline("\t");
emitline(lop);
emitline("\t");
emitdispreg(fi.foff: i64, "AX");
emitline(", AX\n");
return;
};
fi = fi.finext;
};
};
};
};
};
// Chained `(ident).f1.f2` read where f1 is a struct-by-value
// field. Mirror of the cgassign branch added for the same shape.
// Without this, `L.cur.kind` (cur a by-value struct of *L)
// falls into the SB-fallback and emits `MOVQ kind(SB), AX`.
if (lhs != nil) {
if (lhs.kind == nkind.N_DOT) {
let inner: *node = lhs.lhs;
let innerfld: str = lhs.str;
if (inner != nil) { if (inner.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, inner.str);
if (lc != nil) { if (lc.tnode != nil) {
let tn: *node = lc.tnode;
let lkind: nkind = tn.kind;
let outname: str;
outname.ptr = nil; outname.len = 0;
let isptr: bool = false;
if (lkind == nkind.N_TNAME) { outname = tn.str; };
if (lkind == nkind.N_TPTR) {
let pe: *node = tn.lhs;
if (pe != nil) { if (pe.kind == nkind.N_TNAME) {
outname = pe.str;
isptr = true;
};};
};
if (outname.len > 0) {
let osi: *structinfo = structlookup(c, outname);
if (osi != nil) {
let ofi: *fieldinfo = osi.fields;
for (ofi != nil) {
if (streq(ofi.fname, innerfld)) {
let oft: *node = ofi.tnode;
if (oft != nil) { if (oft.kind == nkind.N_TNAME) {
if (primsize(oft.str) == 0) {
let isi: *structinfo = structlookup(c, oft.str);
if (isi != nil) {
let ffi: *fieldinfo = isi.fields;
for (ffi != nil) {
if (streq(ffi.fname, fld)) {
let totoff: i32 = ofi.foff + ffi.foff;
if (isstrtype(c, ffi.tnode)) {
if (isptr) {
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), CX\n");
emitline("\tMOVQ\t");
emitdispreg((totoff + 8): i64, "CX");
emitline(", BX\n");
emitline("\tMOVQ\t");
emitdispreg(totoff: i64, "CX");
emitline(", AX\n");
} else {
emitline("\tMOVQ\t");
emitoff((lc.off + totoff): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\t");
emitoff((lc.off + totoff + 8): i64);
emitline("(BP), BX\n");
};
return;
};
if (isfloattype(c, ffi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, ffi.tnode)) { mov = "MOVSS"; };
if (isptr) {
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
emitline("\t");
emitline(mov);
emitline("\t");
emitdispreg(totoff: i64, "BX");
emitline(", X0\n");
} else {
emitline("\t");
emitline(mov);
emitline("\t");
emitoff((lc.off + totoff): i64);
emitline("(BP), X0\n");
};
return;
};
let lop: str = fieldloadop(ffi);
if (isptr) {
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
emitline("\t");
emitline(lop);
emitline("\t");
emitdispreg(totoff: i64, "BX");
emitline(", AX\n");
} else {
emitline("\t");
emitline(lop);
emitline("\t");
emitoff((lc.off + totoff): i64);
emitline("(BP), AX\n");
};
return;
};
ffi = ffi.finext;
};
};
};
};};
};
ofi = ofi.finext;
};
};
};
};};
};};
};
};
return;
};
fn cgun(c: *cgen, n: *node) void = {
// Match C cgen ordering: evaluate operand first (load into AX),
// then apply the unary op. AMP / STAR override AX with the
// address / deref. The wasted load before AMP keeps our asm
// byte-identical to the C version.
let fk: i32 = exprfloatkind(c, n.lhs);
if (n.op == tkind.TK_MINUS && fk != 0) {
// Float negate: X0 = 0 - X0. Stash orig, load 0.0, subtract.
// Zero bit pattern equals 0.0 for both f32 and f64 so we
// reuse the integer-zero materialisation.
let mov: str = "MOVSD";
let sub: str = "SUBSD";
if (fk == 1) { mov = "MOVSS"; sub = "SUBSS"; };
cgexpr(c, n.lhs);
emitline("\tSUBQ\t$8, SP\n");
emitline("\t"); emitline(mov); emitline("\tX0, (SP)\n");
emitline("\tMOVQ\t$0, AX\n");
emitline("\tPUSHQ\tAX\n");
emitline("\t"); emitline(mov); emitline("\t(SP), X0\n");
emitline("\tADDQ\t$8, SP\n");
emitline("\t"); emitline(mov); emitline("\t(SP), X1\n");
emitline("\tADDQ\t$8, SP\n");
emitline("\t"); emitline(sub); emitline("\tX1, X0\n");
return;
};
cgexpr(c, n.lhs);
if (n.op == tkind.TK_MINUS) { emitline("\tNEGQ\tAX\n"); return; };
if (n.op == tkind.TK_TILDE) { emitline("\tNOTQ\tAX\n"); return; };
if (n.op == tkind.TK_STAR) { emitline("\tMOVQ\t(AX), AX\n"); return; };
if (n.op == tkind.TK_AMP) {
let opnd: *node = n.lhs;
if (opnd != nil) {
if (opnd.kind == nkind.N_IDENT) {
let nm: str = opnd.str;
let off: i32 = localfind(c, nm);
if (off != 0) {
emitline("\tLEAQ\t");
emitoff(off: i64);
emitline("(BP), AX\n");
return;
};
// Top-level mutable let — RIP-relative LEAQ.
if (isletvar(c, nm)) {
emitline("\tLEAQ\t");
emitsymname(c, nm);
emitline("(SB), AX\n");
return;
};
};
};
return;
};
if (n.op == tkind.TK_NOT) {
let t: str = mklabel(c, "tt");
let e: str = mklabel(c, "te");
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJE\t"); emitline(t); emitline("\n");
emitline("\tMOVQ\t$0, AX\n");
emitline("\tJMP\t"); emitline(e); emitline("\n");
emitlabel(t);
emitline("\tMOVQ\t$1, AX\n");
emitlabel(e);
return;
};
return;
};
fn cgbin(c: *cgen, n: *node) void = {
let unsignd: bool = nodeisunsigned(c, n.lhs);
if (!unsignd) { unsignd = nodeisunsigned(c, n.rhs); };
// Float arithmetic: both operands flow through X0. Spill rhs
// across the stack (SUBQ/MOVSD/MOVSD/ADDQ) since there's no
// general FP register saver. ADDSD/SUBSD/MULSD/DIVSD pick SS
// variants for f32. Comparison uses UCOMISD + JCC and falls
// out to the existing CMPQ-based path below.
let lfk: i32 = exprfloatkind(c, n.lhs);
let rfk: i32 = exprfloatkind(c, n.rhs);
let fk: i32 = lfk;
if (fk == 0) { fk = rfk; };
if (fk != 0) {
let mov: str = "MOVSD";
if (fk == 1) { mov = "MOVSS"; };
if (n.op == tkind.TK_PLUS ||
n.op == tkind.TK_MINUS ||
n.op == tkind.TK_STAR ||
n.op == tkind.TK_SLASH) {
cgexpr(c, n.rhs);
emitline("\tSUBQ\t$8, SP\n");
emitline("\t"); emitline(mov); emitline("\tX0, (SP)\n");
cgexpr(c, n.lhs);
emitline("\t"); emitline(mov); emitline("\t(SP), X1\n");
emitline("\tADDQ\t$8, SP\n");
let op: str = "ADDSD";
if (n.op == tkind.TK_MINUS) { op = "SUBSD"; };
if (n.op == tkind.TK_STAR) { op = "MULSD"; };
if (n.op == tkind.TK_SLASH) { op = "DIVSD"; };
if (fk == 1) {
if (n.op == tkind.TK_PLUS) { op = "ADDSS"; };
if (n.op == tkind.TK_MINUS) { op = "SUBSS"; };
if (n.op == tkind.TK_STAR) { op = "MULSS"; };
if (n.op == tkind.TK_SLASH) { op = "DIVSS"; };
};
emitline("\t"); emitline(op); emitline("\tX1, X0\n");
return;
};
let isfcmp: bool = false;
let jcc: str = "";
// UCOMISD/SS sets ZF/PF/CF; unordered (NaN) propagates as
// "not equal / not less". JA/JAE/JB/JBE keys off CF which
// matches the ordered comparisons we need.
if (n.op == tkind.TK_EQ) { isfcmp = true; jcc = "JE"; };
if (n.op == tkind.TK_NEQ) { isfcmp = true; jcc = "JNE"; };
if (n.op == tkind.TK_LT) { isfcmp = true; jcc = "JB"; };
if (n.op == tkind.TK_LE) { isfcmp = true; jcc = "JBE"; };
if (n.op == tkind.TK_GT) { isfcmp = true; jcc = "JA"; };
if (n.op == tkind.TK_GE) { isfcmp = true; jcc = "JAE"; };
if (isfcmp) {
cgexpr(c, n.rhs);
emitline("\tSUBQ\t$8, SP\n");
emitline("\t"); emitline(mov); emitline("\tX0, (SP)\n");
cgexpr(c, n.lhs);
emitline("\t"); emitline(mov); emitline("\t(SP), X1\n");
emitline("\tADDQ\t$8, SP\n");
let ucomi: str = "UCOMISD";
if (fk == 1) { ucomi = "UCOMISS"; };
emitline("\t"); emitline(ucomi); emitline("\tX1, X0\n");
let t: str = mklabel(c, "ct");
let e: str = mklabel(c, "ce");
emitline("\t"); emitline(jcc); emitline("\t"); emitline(t); emitline("\n");
emitline("\tMOVQ\t$0, AX\n");
emitline("\tJMP\t"); emitline(e); emitline("\n");
emitlabel(t);
emitline("\tMOVQ\t$1, AX\n");
emitlabel(e);
return;
};
return;
};
cgexpr(c, n.rhs);
emitline("\tPUSHQ\tAX\n");
cgexpr(c, n.lhs);
emitline("\tPOPQ\tBX\n");
if (n.op == tkind.TK_PLUS) { emitline("\tADDQ\tBX, AX\n"); return; };
if (n.op == tkind.TK_MINUS) { emitline("\tSUBQ\tBX, AX\n"); return; };
if (n.op == tkind.TK_STAR) { emitline("\tIMULQ\tBX, AX\n"); return; };
if (n.op == tkind.TK_SLASH) {
emitline("\tMOVQ\t$0, DX\n");
if (unsignd) { emitline("\tDIVQ\tBX\n"); }
else { emitline("\tIDIVQ\tBX\n"); };
return;
};
if (n.op == tkind.TK_PERCENT) {
emitline("\tMOVQ\t$0, DX\n");
if (unsignd) { emitline("\tDIVQ\tBX\n"); }
else { emitline("\tIDIVQ\tBX\n"); };
emitline("\tMOVQ\tDX, AX\n");
return;
};
if (n.op == tkind.TK_AMP) { emitline("\tANDQ\tBX, AX\n"); return; };
if (n.op == tkind.TK_PIPE) { emitline("\tORQ\tBX, AX\n"); return; };
if (n.op == tkind.TK_CARET) { emitline("\tXORQ\tBX, AX\n"); return; };
if (n.op == tkind.TK_LSHIFT) {
emitline("\tMOVQ\tBX, CX\n");
emitline("\tSHLQ\tCX, AX\n");
return;
};
if (n.op == tkind.TK_RSHIFT) {
emitline("\tMOVQ\tBX, CX\n");
emitline("\tSHRQ\tCX, AX\n");
return;
};
if (n.op == tkind.TK_AND) { emitline("\tANDQ\tBX, AX\n"); return; };
if (n.op == tkind.TK_OR) { emitline("\tORQ\tBX, AX\n"); return; };
// Comparison: emit CMPQ, jump on signed/unsigned variant,
// materialise 0/1 in AX. Same shape as the C cgen.
let iscmp: bool = false;
let jcc: str = "";
if (n.op == tkind.TK_EQ) { iscmp = true; jcc = "JE"; };
if (n.op == tkind.TK_NEQ) { iscmp = true; jcc = "JNE"; };
if (n.op == tkind.TK_LT) { iscmp = true; if (unsignd) { jcc = "JB"; } else { jcc = "JL"; }; };
if (n.op == tkind.TK_LE) { iscmp = true; if (unsignd) { jcc = "JBE"; } else { jcc = "JLE"; }; };
if (n.op == tkind.TK_GT) { iscmp = true; if (unsignd) { jcc = "JA"; } else { jcc = "JG"; }; };
if (n.op == tkind.TK_GE) { iscmp = true; if (unsignd) { jcc = "JAE"; } else { jcc = "JGE"; }; };
if (iscmp) {
let t: str = mklabel(c, "ct");
let e: str = mklabel(c, "ce");
emitline("\tCMPQ\tBX, AX\n");
emitline("\t"); emitline(jcc); emitline("\t"); emitline(t); emitline("\n");
emitline("\tMOVQ\t$0, AX\n");
emitline("\tJMP\t"); emitline(e); emitline("\n");
emitlabel(t);
emitline("\tMOVQ\t$1, AX\n");
emitlabel(e);
return;
};
return;
};
// cgalloc — `alloc(value)` builtin lowering. Allocate sizeof(value)
// bytes via rt_alloc, then write the value's bytes into the new
// region. For an N_STRUCTLIT arg, allocate the struct's totsize and
// emit per-field stores at each field's offset. For a scalar/ptr,
// allocate 8 bytes and store one word. Mirrors cmd/w6c/cgen.c's
// alloc-special branch in N_CALL. Returns the heap ptr in AX.
fn cgalloc(c: *cgen, n: *node) void = {
let v: *node = n.list;
let sz: i32 = 8;
let si: *structinfo = nil;
if (v.kind == nkind.N_STRUCTLIT) {
let trefn: *node = v.lhs;
let sname: str;
sname.ptr = nil; sname.len = 0;
if (trefn != nil) {
if (trefn.kind == nkind.N_IDENT) { sname = trefn.str; }
else { if (trefn.kind == nkind.N_TNAME) { sname = trefn.str; }; };
};
si = structlookup(c, sname);
if (si != nil) { sz = si.totsize; };
};
emitline("\tMOVQ\t$");
emitint(sz: i64);
emitline(", DI\n");
emitline("\tCALL\trt_alloc(SB)\n");
emitline("\tPUSHQ\tAX\n");
if (v.kind == nkind.N_STRUCTLIT) {
if (si != nil) {
let f: *node = v.list;
for (f != nil) {
if (f.kind == nkind.N_FIELD) {
let fname: str = f.str;
let fi: *fieldinfo = si.fields;
for (fi != nil) {
let fn_: str = fi.fname;
if (streq(fn_, fname)) {
cgexpr(c, f.lhs);
// alloc(T{ fval = v }) for f64/f32 field: cgexpr left
// the value in X0, not AX — route the store via MOVSD/MOVSS.
if (isfloattype(c, fi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, fi.tnode)) { mov = "MOVSS"; };
emitline("\tMOVQ\t(SP), BX\n");
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitint(fi.foff: i64);
emitline("(BX)\n");
fi = nil;
} else {
emitline("\tMOVQ\t(SP), BX\n");
let sop: str = fieldstoreop(fi);
emitline("\t");
emitline(sop);
emitline("\tAX, ");
emitint(fi.foff: i64);
emitline("(BX)\n");
fi = nil;
};
} else {
fi = fi.finext;
};
};
};
f = f.next;
};
};
} else {
cgexpr(c, v);
emitline("\tMOVQ\t(SP), BX\n");
let sop: str = "MOVQ";
if (sz == 1) { sop = "MOVB"; }
else { if (sz == 4) { sop = "MOVL"; }; };
emitline("\t");
emitline(sop);
emitline("\tAX, (BX)\n");
};
emitline("\tPOPQ\tAX\n");
};
// cgappend — Hare-style `append(s, v)` / `append(s, items...)` lowering.
// Mirrors cmd/w6c/cgen.c's N_CALL append branch (rt::ensure model).
// Each value gets:
// ; cgexpr → AX
// ; PUSHQ AX
// ; ADDQ $1, s.len(BP)
// ; LEAQ s(BP), DI ; arg1 = &s
// ; MOVQ esz, SI ; arg2 = membsz
// ; CALL rt_ensure(SB)
// ; MOVQ s.len(BP), CX ; CX = new len
// ; SUBQ $1, CX ; CX = slot index
// ; [IMULQ esz, CX] ; byte offset (esz>1)
// ; MOVQ s.ptr(BP), BX
// ; ADDQ CX, BX
// ; POPQ AX
// ; MOV* AX, (BX) ; store (MOVB / MOVQ)
// nkind.N_SPREAD wraps the same body in a counted loop over items.len.
fn cgappend(c: *cgen, n: *node) void = {
let sn: *node = n.list;
if (sn == nil) { return; };
if (sn.kind != nkind.N_IDENT) { return; };
let snlocal: *local = localfindnode(c, sn.str);
if (snlocal == nil) { return; };
let sn_off: i32 = snlocal.off;
let esz: i32 = elemsizeof(snlocal.tnode);
let store_op: str = "MOVQ";
if (esz == 1) { store_op = "MOVB"; };
let vn: *node = sn.next;
for (vn != nil) {
if (vn.kind == nkind.N_SPREAD) {
let it: *node = vn.lhs;
if (it == nil) { vn = vn.next; continue; };
if (it.kind != nkind.N_IDENT) { vn = vn.next; continue; };
let itlocal: *local = localfindnode(c, it.str);
if (itlocal == nil) { vn = vn.next; continue; };
let it_off: i32 = itlocal.off;
let load_op: str = "MOVQ";
if (esz == 1) { load_op = "MOVZBQ"; };
emitline("\tSUBQ\t$8, SP\n");
emitline("\tMOVQ\t$0, (SP)\n");
let ll: str = mklabel(c, "spr_l");
let le: str = mklabel(c, "spr_e");
emitlabel(ll);
emitline("\tMOVQ\t(SP), CX\n");
emitline("\tMOVQ\t");
emitoff((it_off + 8): i64);
emitline("(BP), DX\n");
emitline("\tCMPQ\tDX, CX\n");
emitline("\tJGE\t"); emitline(le); emitline("\n");
emitline("\tMOVQ\t");
emitoff(it_off: i64);
emitline("(BP), BX\n");
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", AX\n");
emitline("\tIMULQ\tAX, CX\n");
};
emitline("\tADDQ\tCX, BX\n");
emitline("\t"); emitline(load_op); emitline("\t(BX), AX\n");
emitline("\tPUSHQ\tAX\n");
emitline("\tADDQ\t$1, ");
emitoff((sn_off + 8): i64);
emitline("(BP)\n");
emitline("\tLEAQ\t");
emitoff(sn_off: i64);
emitline("(BP), DI\n");
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", SI\n");
emitline("\tCALL\trt_ensure(SB)\n");
emitline("\tMOVQ\t");
emitoff((sn_off + 8): i64);
emitline("(BP), CX\n");
emitline("\tSUBQ\t$1, CX\n");
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", AX\n");
emitline("\tIMULQ\tAX, CX\n");
};
emitline("\tMOVQ\t");
emitoff(sn_off: i64);
emitline("(BP), BX\n");
emitline("\tADDQ\tCX, BX\n");
emitline("\tPOPQ\tAX\n");
emitline("\t"); emitline(store_op); emitline("\tAX, (BX)\n");
emitline("\tADDQ\t$1, (SP)\n");
emitline("\tJMP\t"); emitline(ll); emitline("\n");
emitlabel(le);
emitline("\tADDQ\t$8, SP\n");
vn = vn.next;
continue;
};
cgexpr(c, vn);
emitline("\tPUSHQ\tAX\n");
emitline("\tADDQ\t$1, ");
emitoff((sn_off + 8): i64);
emitline("(BP)\n");
emitline("\tLEAQ\t");
emitoff(sn_off: i64);
emitline("(BP), DI\n");
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", SI\n");
emitline("\tCALL\trt_ensure(SB)\n");
emitline("\tMOVQ\t");
emitoff((sn_off + 8): i64);
emitline("(BP), CX\n");
emitline("\tSUBQ\t$1, CX\n");
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", AX\n");
emitline("\tIMULQ\tAX, CX\n");
};
emitline("\tMOVQ\t");
emitoff(sn_off: i64);
emitline("(BP), BX\n");
emitline("\tADDQ\tCX, BX\n");
emitline("\tPOPQ\tAX\n");
emitline("\t"); emitline(store_op); emitline("\tAX, (BX)\n");
vn = vn.next;
};
return;
};
fn cgcall(c: *cgen, n: *node) void = {
// Hare-style `append(s, v)` / `append(s, items...)` builtin —
// special-cased before pushargsrev so the spread variant can run
// a counted loop over the items slice instead of a normal call.
let callee: *node = n.lhs;
if (callee != nil) {
if (callee.kind == nkind.N_IDENT) {
if (streq(callee.str, "append")) {
if (n.list != nil) {
if (n.list.next != nil) {
cgappend(c, n);
return;
};
};
};
// `alloc(value)` builtin: heap-init a fresh *T with the
// value's bytes. For struct literals, lower to rt_alloc
// + per-field stores. Mirrors cmd/w6c/cgen.c's N_CALL
// alloc path.
if (streq(callee.str, "alloc")) {
if (n.list != nil) {
cgalloc(c, n);
return;
};
};
};
};
// Look up the callee's declared params for tagged-union widening.
// fn-pointer calls (callee is a local) don't get widening — the
// user must build the tagged value explicitly. Matches the most
// common case (direct named calls).
let calleeparams: *node = nil;
if (callee != nil) {
if (callee.kind == nkind.N_IDENT) {
calleeparams = fnparamslookup(c, callee.str);
};
};
let nargs: i32 = pushargsrev(c, n.list, calleeparams);
// Pop forward. Float args were pushed as 8 bytes from X0 via
// SUBQ+MOVSD; pop into the XMM stream (X0..X7). Everything else
// pops into the int stream (DI..R9) per the SysV ABI. Walk the
// args list alongside the pop counter so we know each arg's
// register class. SysV has only 6 int arg regs (DI/SI/DX/CX/R8/R9);
// the remaining slots stay on the stack and the callee reads them
// via 16+8*k(BP). Caller-cleanup is emitted after the CALL.
let intidx: i32 = 0;
let fpidx: i32 = 0;
let a: *node = n.list;
let popped: i32 = 0;
let stackslots: i32 = 0;
for (a != nil) {
let fk: i32 = exprfloatkind(c, a);
if (fk != 0) {
let mov: str = "MOVSD";
if (fk == 1) { mov = "MOVSS"; };
if (fpidx < 8) {
emitline("\t");
emitline(mov);
emitline("\t(SP), ");
emitline(fargregname(fpidx));
emitline("\n");
emitline("\tADDQ\t$8, SP\n");
fpidx += 1;
} else {
stackslots += 1;
};
popped += 1;
} else {
let extra: i32 = 0;
if (nodeisstr(c, a)) { extra = 1; };
if (nodeisslice(c, a)) { extra = 2; };
let words: i32 = 1 + extra;
let w: i32 = 0;
for (w < words) {
if (intidx < 6) {
emitline("\tPOPQ\t");
emitline(argregname(intidx));
emitline("\n");
intidx += 1;
} else {
stackslots += 1;
};
popped += 1;
w += 1;
};
};
a = a.next;
};
// Drain any remaining slots that the arg-walker didn't account
// for (tagged-union arg sizes > 8B, struct-by-value, etc.). The
// existing C cgen pops these into the int stream, so the worst
// case here is identical pre-port behaviour.
let i: i32 = popped;
for (i < nargs) {
if (intidx < 6) {
emitline("\tPOPQ\t");
emitline(argregname(intidx));
emitline("\n");
intidx += 1;
} else {
stackslots += 1;
};
i += 1;
};
let callee: *node = n.lhs;
let calleename: str;
calleename.ptr = nil; calleename.len = 0;
// Detect fn-pointer field call: `w.emit(args)` where `w` is
// a struct local and `emit` is an nkind.N_TFN field. Load the
// field value into AX and CALL through it. Also detect a
// bare `fp(args)` where `fp` is a local holding a function
// pointer — mirror C cgen's localfind dispatch (commit
// 635818e). Without this the call emits `CALL fp(SB)` and
// the linker rightly fails.
let isfnptrcall: bool = false;
if (callee != nil) {
if (callee.kind == nkind.N_IDENT) {
let cn: str = callee.str;
if (localfindnode(c, cn) != nil) {
isfnptrcall = true;
};
};
if (callee.kind == nkind.N_DOT) {
let base: *node = callee.lhs;
let fld: str = callee.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;
if (tn != nil) {
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 inner: *node = tn.lhs;
if (inner != nil) {
if (inner.kind == nkind.N_TNAME) { sname = inner.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)) {
let ft: *node = fi.tnode;
if (ft != nil) {
if (ft.kind == nkind.N_TFN) {
isfnptrcall = true;
};
};
fi = nil;
} else {
fi = fi.finext;
};
};
};
};
};
};
};
};
};
};
if (isfnptrcall) {
// Load fn-ptr field value into AX; CALL AX. We emit the
// load AFTER the args have been popped (so AX/BX/etc
// don't get clobbered by the field load before the pops).
// `popped args` left DI/SI/etc set; AX is free.
cgexpr(c, callee);
emitline("\tCALL\tAX\n");
} else {
emitline("\tCALL\t");
if (callee != nil) {
if (callee.kind == nkind.N_IDENT) {
calleename = callee.str;
emitsymname(c, calleename);
} else { if (callee.kind == nkind.N_DOT) {
calleename = callee.str;
emitsymname(c, calleename);
};};
};
emitline("(SB)\n");
};
// Caller cleanup for stack-passed args (args 7+, or any
// overflow past the int/float reg windows). Mirrors C cgen:
// pushed 8 bytes each, ADDQ them off after the CALL.
if (stackslots > 0) {
emitline("\tADDQ\t$");
emitint((stackslots * 8): i64);
emitline(", SP\n");
};
// SysV returns 16-byte aggregates in (AX, DX). Our str
// convention is (AX, BX), so shuffle for str-returning calls.
if (calleename.len > 0) {
let rt: *node = fnretlookup(c, calleename);
if (isstrtype(c, rt)) {
emitline("\tMOVQ\tDX, BX\n");
};
};
return;
};
fn cgassign(c: *cgen, n: *node) void = {
let lhs: *node = n.lhs;
// Discard lvalue `_ = expr;` — evaluate rhs for side effects,
// write nothing. Detected by lhs being an nkind.N_IDENT with empty str
// (planted by parseprimary on the tkind.TK_UNDER token).
if (lhs != nil) {
if (lhs.kind == nkind.N_IDENT) {
if (lhs.str.len == 0) {
if (n.op == tkind.TK_ASSIGN) {
cgexpr(c, n.rhs);
return;
};
};
};
};
// `*p = v` — deref-assign. Element width comes from the
// pointer's declared type. Mirrors C cgen: eval rhs (AX,
// and BX if str), push, eval pointer, pop value, store.
// We default to MOVQ (8B) since most fixtures use it; for
// `*bool` / `*u8` / `*i32` we narrow via the local's tnode.
if (lhs != nil) {
if (lhs.kind == nkind.N_UN) {
if (lhs.op == tkind.TK_STAR) {
if (n.op == tkind.TK_ASSIGN) {
let inner: *node = lhs.lhs;
let elemstr: bool = false;
let elemfloat: bool = false;
let elemf32: bool = false;
let storeop: str = "MOVQ";
if (inner != nil) {
if (inner.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, inner.str);
if (lc != nil) {
let tn: *node = lc.tnode;
if (tn != nil) {
if (tn.kind == nkind.N_TPTR) {
let pe: *node = tn.lhs;
if (pe != nil) {
if (pe.kind == nkind.N_TNAME) {
if (streq(pe.str, "str")) { elemstr = true; }
else { if (streq(pe.str, "f64")) { elemfloat = true; }
else { if (streq(pe.str, "f32")) { elemfloat = true; elemf32 = true; }
else {
let ps: i32 = primsize(pe.str);
if (ps == 1) { storeop = "MOVB"; }
else { if (ps == 4) { storeop = "MOVL"; }; };
}; }; };
};
};
};
};
};
};
};
cgexpr(c, n.rhs);
// `*p = v` for *f64 / *f32: value sits in X0. Spill
// to the stack, evaluate the pointer (clobbers AX),
// then reload X0 and MOVSD/MOVSS through the pointer.
if (elemfloat) {
let mov: str = "MOVSD";
if (elemf32) { mov = "MOVSS"; };
emitline("\tSUBQ\t$8, SP\n");
emitline("\t");
emitline(mov);
emitline("\tX0, (SP)\n");
cgexpr(c, inner);
emitline("\tMOVQ\tAX, BX\n");
emitline("\t");
emitline(mov);
emitline("\t(SP), X0\n");
emitline("\tADDQ\t$8, SP\n");
emitline("\t");
emitline(mov);
emitline("\tX0, (BX)\n");
return;
};
// Push order matches C cgen
// (cmd/w6c/cgen.c:1033-1041): PUSHQ AX
// (ptr) first, then PUSHQ BX (len) if
// str, so the pop sequence is POP CX
// (len) → POP AX (ptr) → MOVQ AX,
// (BX) → MOVQ CX, 8(BX).
emitline("\tPUSHQ\tAX\n");
if (elemstr) { emitline("\tPUSHQ\tBX\n"); };
cgexpr(c, inner);
emitline("\tMOVQ\tAX, BX\n");
if (elemstr) {
emitline("\tPOPQ\tCX\n");
emitline("\tPOPQ\tAX\n");
emitline("\tMOVQ\tAX, (BX)\n");
emitline("\tMOVQ\tCX, 8(BX)\n");
return;
};
emitline("\tPOPQ\tAX\n");
emitline("\t");
emitline(storeop);
emitline("\tAX, (BX)\n");
return;
};
};
};
};
// Array/slice/ptr index store: `arr[i] = v;`. Element size
// from base.tnode picks MOVB vs MOVQ.
if (lhs != nil) {
if (lhs.kind == nkind.N_INDEX) {
if (n.op == tkind.TK_ASSIGN) {
let base: *node = lhs.lhs;
let idx: *node = lhs.rhs;
let esz: i32 = 8;
let baselocal: *local = nil;
let isglobalarr: bool = false;
let isglobalptr: bool = false;
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) {
esz = elemsizeof(baselocal.tnode);
} else {
let tn: *node = letvartnode(c, bn);
if (tn != nil) {
if (tn.kind == nkind.N_TARRAY) {
isglobalarr = true;
globalname = bn;
esz = elemsizeof(tn);
};
if (tn.kind == nkind.N_TPTR) {
isglobalptr = true;
globalname = bn;
esz = elemsizeof(tn);
};
};
};
} else { if (base.kind == nkind.N_DOT) {
esz = indexbaseesz(c, base);
};};
};
cgexpr(c, n.rhs); // value → AX
if (esz == 16) { emitline("\tPUSHQ\tBX\n"); };
emitline("\tPUSHQ\tAX\n");
cgexpr(c, idx); // idx → AX
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", CX\n");
emitline("\tIMULQ\tCX, AX\n");
};
emitline("\tPUSHQ\tAX\n"); // scaled idx
if (isglobalarr) {
emitline("\tLEAQ\t");
emitsymname(c, globalname);
emitline("(SB), BX\n");
} else { if (isglobalptr) {
emitline("\tMOVQ\t");
emitsymname(c, globalname);
emitline("(SB), BX\n");
} else { if (baselocal != nil) {
let tn: *node = baselocal.tnode;
let isarray: bool = false;
if (tn != nil) { if (tn.kind == nkind.N_TARRAY) { isarray = true; }; };
if (isarray) {
emitline("\tLEAQ\t");
emitoff(baselocal.off: i64);
emitline("(BP), BX\n");
} else {
emitline("\tMOVQ\t");
emitoff(baselocal.off: i64);
emitline("(BP), BX\n");
};
} else {
cgexpr(c, base);
emitline("\tMOVQ\tAX, BX\n");
};};};
emitline("\tPOPQ\tAX\n"); // scaled idx
emitline("\tADDQ\tAX, BX\n");
emitline("\tPOPQ\tAX\n"); // value
if (esz == 16) {
emitline("\tMOVQ\tAX, (BX)\n");
emitline("\tPOPQ\tCX\n");
emitline("\tMOVQ\tCX, 8(BX)\n");
return;
};
if (esz == 1) { emitline("\tMOVB\tAX, (BX)\n"); }
else { if (esz == 4) { emitline("\tMOVL\tAX, (BX)\n"); }
else { emitline("\tMOVQ\tAX, (BX)\n"); };};
return;
};
};
};
// Struct/ptr-to-struct field assignment: `s.f = expr;` or
// `p.f = expr;`. Only plain `=` is wired (compound on field
// is rare and not yet needed by our fixtures).
if (lhs != nil) {
if (lhs.kind == nkind.N_DOT) {
let base: *node = lhs.lhs;
let fld: str = lhs.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; };
// Pointer-to-struct: deref then store.
if (lkind == nkind.N_TPTR) {
let inner: *node = tn.lhs;
let sname: str;
sname.ptr = nil; sname.len = 0;
if (inner != nil) {
if (inner.kind == nkind.N_TNAME) { sname = inner.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)) {
if (n.op != tkind.TK_ASSIGN) {
// compound: load current value
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
let lop: str = fieldloadop(fi);
emitline("\t");
emitline(lop);
emitline("\t");
emitdispreg(fi.foff: i64, "BX");
emitline(", BX\n");
emitline("\tPUSHQ\tBX\n");
};
cgexpr(c, n.rhs);
if (n.op != tkind.TK_ASSIGN) {
emitline("\tPOPQ\tBX\n");
// PLUSEQ is commutative; MINUSEQ
// needs lhs - rhs (BX is old lhs,
// AX is rhs).
if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tBX, AX\n"); };
if (n.op == tkind.TK_MINUSEQ) {
emitline("\tSUBQ\tAX, BX\n");
emitline("\tMOVQ\tBX, AX\n");
};
};
// str field via *struct: rhs left
// (AX=ptr, BX=len). Use CX as the
// address scratch so we don't clobber
// the len half before storing it.
if (n.op == tkind.TK_ASSIGN) {
if (isstrtype(c, fi.tnode)) {
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), CX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(fi.foff: i64, "CX");
emitline("\n");
emitline("\tMOVQ\tBX, ");
emitdispreg((fi.foff + 8): i64, "CX");
emitline("\n");
return;
};
// f64/f32 plain `=` via *struct: cgexpr left the
// value in X0. Reload struct ptr and MOVSD/MOVSS.
if (isfloattype(c, fi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, fi.tnode)) { mov = "MOVSS"; };
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitdispreg(fi.foff: i64, "BX");
emitline("\n");
return;
};
};
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
let sop: str = fieldstoreop(fi);
emitline("\t");
emitline(sop);
emitline("\tAX, ");
emitdispreg(fi.foff: i64, "BX");
emitline("\n");
return;
};
fi = fi.finext;
};
};
};
};
// Direct struct local: store at off+foff.
if (lkind == nkind.N_TNAME) {
let sname: str = tn.str;
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)) {
cgexpr(c, n.rhs);
// str field: cgexpr left (AX=ptr, BX=len);
// store both halves at +0/+8. Without this,
// `L.src = s` would only write the ptr and
// `L.src.len` would carry whatever was on the
// stack.
if (isstrtype(c, fi.tnode)) {
emitline("\tMOVQ\tAX, ");
emitoff((lc.off + fi.foff): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tBX, ");
emitoff((lc.off + fi.foff + 8): i64);
emitline("(BP)\n");
return;
};
// f64/f32 direct struct local store: route via X0.
if (isfloattype(c, fi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, fi.tnode)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff((lc.off + fi.foff): i64);
emitline("(BP)\n");
return;
};
let sop: str = fieldstoreop(fi);
emitline("\t");
emitline(sop);
emitline("\tAX, ");
emitoff((lc.off + fi.foff): i64);
emitline("(BP)\n");
return;
};
fi = fi.finext;
};
};
};
// str/slice pseudo-field assignment.
let delta: i32 = -1;
if (streq(fld, "ptr")) { delta = 0; };
if (streq(fld, "len")) { delta = 8; };
if (streq(fld, "cap")) { delta = 16; };
if (delta >= 0) {
if (lkind == nkind.N_TPTR) {
let inner: *node = tn.lhs;
let innerkind: nkind = nkind.N_NONE;
if (inner != nil) { innerkind = inner.kind; };
let innerstr: bool = false;
if (innerkind == nkind.N_TNAME) {
if (streq(inner.str, "str")) { innerstr = true; };
};
if (innerkind == nkind.N_TSLICE) { innerstr = true; };
if (innerstr) {
if (n.op != tkind.TK_ASSIGN) {
// Compound on `(*str|*slice).field`: load
// current → push → eval rhs → combine → store.
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
emitline("\tMOVQ\t");
emitdispreg(delta: i64, "BX");
emitline(", BX\n");
emitline("\tPUSHQ\tBX\n");
cgexpr(c, n.rhs);
emitline("\tPOPQ\tBX\n");
// PLUSEQ is commutative; MINUSEQ
// needs lhs - rhs.
if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tBX, AX\n"); };
if (n.op == tkind.TK_MINUSEQ) {
emitline("\tSUBQ\tAX, BX\n");
emitline("\tMOVQ\tBX, AX\n");
};
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(delta: i64, "BX");
emitline("\n");
return;
};
cgexpr(c, n.rhs);
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(delta: i64, "BX");
emitline("\n");
return;
};
};
cgexpr(c, n.rhs);
emitline("\tMOVQ\tAX, ");
emitoff((lc.off + delta): i64);
emitline("(BP)\n");
return;
};
};
};
};
};
};
// Top-level struct global field assignment: `g.f = expr;` and
// `g.f += expr;` for a scalar/str field. Reached when the local
// lookup miss but the IDENT base is a registered struct `let`.
// LEAQ name(SB) into BX/CX takes the place of the frame slot
// addressing the local branches use. Compound (PLUSEQ/MINUSEQ)
// follows the same load → push → eval → combine → store shape
// as the via-ptr local path.
if (lhs != nil) {
if (lhs.kind == nkind.N_DOT) {
let base: *node = lhs.lhs;
let fld: str = lhs.str;
if (base != nil) {
if (base.kind == nkind.N_IDENT) {
let bn: str = base.str;
if (localfindnode(c, bn) == nil) {
let si: *structinfo = letvarstructinfo(c, bn);
if (si != nil) {
let fi: *fieldinfo = si.fields;
for (fi != nil) {
if (streq(fi.fname, fld)) {
if (n.op == tkind.TK_ASSIGN) {
cgexpr(c, n.rhs);
if (isstrtype(c, fi.tnode)) {
emitline("\tLEAQ\t");
emitsymname(c, bn);
emitline("(SB), CX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(fi.foff: i64, "CX");
emitline("\n");
emitline("\tMOVQ\tBX, ");
emitdispreg((fi.foff + 8): i64, "CX");
emitline("\n");
return;
};
// f64/f32 plain `=` on global struct field: value is
// in X0; LEAQ the base into BX and MOVSD/MOVSS.
if (isfloattype(c, fi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, fi.tnode)) { mov = "MOVSS"; };
emitline("\tLEAQ\t");
emitsymname(c, bn);
emitline("(SB), BX\n");
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitdispreg(fi.foff: i64, "BX");
emitline("\n");
return;
};
let sop: str = fieldstoreop(fi);
emitline("\tLEAQ\t");
emitsymname(c, bn);
emitline("(SB), BX\n");
emitline("\t");
emitline(sop);
emitline("\tAX, ");
emitdispreg(fi.foff: i64, "BX");
emitline("\n");
return;
};
// Compound on scalar field: load
// → push → eval rhs → combine →
// store. cgexpr clobbers BX, so
// re-LEAQ for the store.
let lop: str = fieldloadop(fi);
emitline("\tLEAQ\t");
emitsymname(c, bn);
emitline("(SB), BX\n");
emitline("\t");
emitline(lop);
emitline("\t");
emitdispreg(fi.foff: i64, "BX");
emitline(", BX\n");
emitline("\tPUSHQ\tBX\n");
cgexpr(c, n.rhs);
emitline("\tPOPQ\tBX\n");
if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tBX, AX\n"); };
if (n.op == tkind.TK_MINUSEQ) {
emitline("\tSUBQ\tAX, BX\n");
emitline("\tMOVQ\tBX, AX\n");
};
let sop: str = fieldstoreop(fi);
emitline("\tLEAQ\t");
emitsymname(c, bn);
emitline("(SB), BX\n");
emitline("\t");
emitline(sop);
emitline("\tAX, ");
emitdispreg(fi.foff: i64, "BX");
emitline("\n");
return;
};
fi = fi.finext;
};
};
};
};
};
};
};
// Chained `<expr>.field = v` where `<expr>` itself is a chain
// of dots resolving to a *struct. Mirrors the C cgen branch
// added to close trap 1 (cmd/w6c/cgen.c). Without this, only
// `local.field = v` and `local.fieldptr.field = v` get wired
// (the latter through the IDENT-base branch above) — chains
// like `s.last.snext = sy` (lib/ww/sym.ww) silently emit no
// store. Only plain `=` is wired here; chained compound on a
// pointer-field hasn't surfaced.
if (lhs != nil) {
if (lhs.kind == nkind.N_DOT) {
let base: *node = lhs.lhs;
let fld: str = lhs.str;
if (base != nil) {
if (base.kind == nkind.N_DOT) {
let innert: *node = dotinnerstructptr(c, base);
if (innert != nil) {
let sname: str = innert.str;
let si: *structinfo = structlookup(c, sname);
if (si != nil) {
let fi: *fieldinfo = si.fields;
for (fi != nil) {
if (streq(fi.fname, fld)) {
if (n.op == tkind.TK_ASSIGN) {
if (isstrtype(c, fi.tnode)) {
// str rhs: AX=ptr, BX=len.
// Stash both, then load
// the struct ptr into CX
// and write both halves.
cgexpr(c, n.rhs);
emitline("\tPUSHQ\tBX\n");
emitline("\tPUSHQ\tAX\n");
cgexpr(c, base);
emitline("\tMOVQ\tAX, CX\n");
emitline("\tPOPQ\tAX\n");
emitline("\tPOPQ\tBX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(fi.foff: i64, "CX");
emitline("\n");
emitline("\tMOVQ\tBX, ");
emitdispreg((fi.foff + 8): i64, "CX");
emitline("\n");
return;
};
// f64/f32 chained plain `=`: cgexpr rhs left value in
// X0. Spill to stack so cgexpr(base) can use AX, then
// reload and MOVSD/MOVSS into the slot.
if (isfloattype(c, fi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, fi.tnode)) { mov = "MOVSS"; };
cgexpr(c, n.rhs);
emitline("\tSUBQ\t$8, SP\n");
emitline("\t");
emitline(mov);
emitline("\tX0, (SP)\n");
cgexpr(c, base);
emitline("\tMOVQ\tAX, BX\n");
emitline("\t");
emitline(mov);
emitline("\t(SP), X0\n");
emitline("\tADDQ\t$8, SP\n");
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitdispreg(fi.foff: i64, "BX");
emitline("\n");
return;
};
cgexpr(c, n.rhs);
emitline("\tPUSHQ\tAX\n");
cgexpr(c, base);
emitline("\tMOVQ\tAX, BX\n");
emitline("\tPOPQ\tAX\n");
let sop: str = fieldstoreop(fi);
emitline("\t");
emitline(sop);
emitline("\tAX, ");
emitdispreg(fi.foff: i64, "BX");
emitline("\n");
return;
};
};
fi = fi.finext;
};
};
};
};
};
};
};
// Chained `(ident).f1.f2 = v` where f1 is a struct-by-value
// field. The earlier chained-DOT branch handles f1: *T (deref
// then store). This handles f1: T (in-place sub-struct), which
// would otherwise silently emit no store — lispcore's lexer had
// to flatten `cur.kind`/`cur.ival`/... into top-level fields to
// work around it. Only plain `=` is wired; compound on a by-
// value sub-field hasn't surfaced.
if (lhs != nil) {
if (lhs.kind == nkind.N_DOT) {
let base: *node = lhs.lhs;
let fld: str = lhs.str;
if (base != nil) { if (base.kind == nkind.N_DOT) {
let inner: *node = base.lhs;
let innerfld: str = base.str;
if (inner != nil) { if (inner.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, inner.str);
if (lc != nil) { if (lc.tnode != nil) {
let tn: *node = lc.tnode;
let lkind: nkind = tn.kind;
let outname: str;
outname.ptr = nil; outname.len = 0;
let isptr: bool = false;
if (lkind == nkind.N_TNAME) { outname = tn.str; };
if (lkind == nkind.N_TPTR) {
let pe: *node = tn.lhs;
if (pe != nil) { if (pe.kind == nkind.N_TNAME) {
outname = pe.str;
isptr = true;
};};
};
if (outname.len > 0) {
let osi: *structinfo = structlookup(c, outname);
if (osi != nil) {
let ofi: *fieldinfo = osi.fields;
for (ofi != nil) {
if (streq(ofi.fname, innerfld)) {
let oft: *node = ofi.tnode;
if (oft != nil) { if (oft.kind == nkind.N_TNAME) {
if (primsize(oft.str) == 0) {
let isi: *structinfo = structlookup(c, oft.str);
if (isi != nil) {
let ffi: *fieldinfo = isi.fields;
for (ffi != nil) {
if (streq(ffi.fname, fld)) {
if (n.op == tkind.TK_ASSIGN) {
let totoff: i32 = ofi.foff + ffi.foff;
cgexpr(c, n.rhs);
if (isstrtype(c, ffi.tnode)) {
if (isptr) {
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), CX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(totoff: i64, "CX");
emitline("\n");
emitline("\tMOVQ\tBX, ");
emitdispreg((totoff + 8): i64, "CX");
emitline("\n");
} else {
emitline("\tMOVQ\tAX, ");
emitoff((lc.off + totoff): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tBX, ");
emitoff((lc.off + totoff + 8): i64);
emitline("(BP)\n");
};
return;
};
if (isfloattype(c, ffi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, ffi.tnode)) { mov = "MOVSS"; };
if (isptr) {
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitdispreg(totoff: i64, "BX");
emitline("\n");
} else {
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff((lc.off + totoff): i64);
emitline("(BP)\n");
};
return;
};
let sop: str = fieldstoreop(ffi);
if (isptr) {
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
emitline("\t");
emitline(sop);
emitline("\tAX, ");
emitdispreg(totoff: i64, "BX");
emitline("\n");
} else {
emitline("\t");
emitline(sop);
emitline("\tAX, ");
emitoff((lc.off + totoff): i64);
emitline("(BP)\n");
};
return;
};
};
ffi = ffi.finext;
};
};
};
};};
};
ofi = ofi.finext;
};
};
};
};};
};};
};};
};
};
// Local-ident target — plain `=` and the simple compound
// forms (+= -= *= /=); other compounds fall back to
// "evaluate rhs, replace". Mirrors C cgen's IDENT-assign path.
if (lhs != nil) {
if (lhs.kind == nkind.N_IDENT) {
let nm: str = lhs.str;
let off: i32 = localfind(c, nm);
if (off == 0) {
// Top-level let target: RIP-relative store
// for `=`, or load→combine→store for the
// compound forms. For a str/slice global,
// take its address into CX and store both
// halves (plus cap for slice — stashed via
// DI since LEAQ overwrites CX); the asm has
// no `name+8(SB)` operand form.
if (!isletvar(c, nm)) { return; };
// Float global: rhs lands in X0; store via
// LEAQ+indirect since MOVSS/MOVSD have no
// D_EXTERN operand form.
let lvf: *letvar = c.lets;
let isfg: bool = false;
let isf32g: bool = false;
for (lvf != nil) {
if (streq(lvf.name, nm)) {
isfg = isfloattype(c, lvf.tnode);
isf32g = isf32type(c, lvf.tnode);
lvf = nil;
} else {
lvf = lvf.lvnext;
};
};
if (isfg && n.op == tkind.TK_ASSIGN) {
cgexpr(c, n.rhs);
let mov: str = "MOVSD";
if (isf32g) { mov = "MOVSS"; };
emitline("\tLEAQ\t");
emitsymname(c, nm);
emitline("(SB), CX\n");
emitline("\t");
emitline(mov);
emitline("\tX0, (CX)\n");
return;
};
cgexpr(c, n.rhs);
if (n.op == tkind.TK_ASSIGN) {
if (letvarisstr(c, nm)) {
emitline("\tLEAQ\t");
emitsymname(c, nm);
emitline("(SB), CX\n");
emitline("\tMOVQ\tAX, (CX)\n");
emitline("\tMOVQ\tBX, 8(CX)\n");
return;
};
if (letvarisslice(c, nm)) {
emitline("\tMOVQ\tCX, DI\n");
emitline("\tLEAQ\t");
emitsymname(c, nm);
emitline("(SB), CX\n");
emitline("\tMOVQ\tAX, (CX)\n");
emitline("\tMOVQ\tBX, 8(CX)\n");
emitline("\tMOVQ\tDI, 16(CX)\n");
return;
};
emitline("\tMOVQ\tAX, ");
emitsymname(c, nm);
emitline("(SB)\n");
return;
};
emitline("\tMOVQ\t");
emitsymname(c, nm);
emitline("(SB), BX\n");
let didcompound: bool = true;
if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tAX, BX\n"); }
else { if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tAX, BX\n"); }
else { if (n.op == tkind.TK_STAREQ) { emitline("\tIMULQ\tAX, BX\n"); }
else { if (n.op == tkind.TK_AMPEQ) { emitline("\tANDQ\tAX, BX\n"); }
else { if (n.op == tkind.TK_PIPEEQ) { emitline("\tORQ\tAX, BX\n"); }
else { if (n.op == tkind.TK_CARETEQ) { emitline("\tXORQ\tAX, BX\n"); }
else { if (n.op == tkind.TK_LSHIFTEQ) {
emitline("\tMOVQ\tAX, CX\n");
emitline("\tSHLQ\tCX, BX\n");
}
else { if (n.op == tkind.TK_RSHIFTEQ) {
emitline("\tMOVQ\tAX, CX\n");
emitline("\tSHRQ\tCX, BX\n");
}
else {
// Unsupported compound: store rhs
// directly. Mirrors the local path's
// fallback for TK_SLASHEQ etc.
didcompound = false;
emitline("\tMOVQ\tAX, ");
emitsymname(c, nm);
emitline("(SB)\n");
};};};};};};};};
if (didcompound) {
emitline("\tMOVQ\tBX, ");
emitsymname(c, nm);
emitline("(SB)\n");
};
return;
};
// Detect str/slice-typed local — assignment must store
// both halves (AX=ptr at +0, BX=len at +8) for str,
// plus the cap (CX at +16) for slice.
let lcstr: bool = false;
let lcsl: bool = false;
let lcn: *local = localfindnode(c, nm);
if (lcn != nil) {
lcstr = isstrtype(c, lcn.tnode);
lcsl = isslicetype(c, lcn.tnode);
};
let lcf: bool = false;
let lcf32: bool = false;
if (lcn != nil) {
lcf = isfloattype(c, lcn.tnode);
lcf32 = isf32type(c, lcn.tnode);
};
// Float-typed local: rhs lands in X0; store via MOVSD/
// MOVSS, no AX shuffle. Only plain `=` is wired; compound
// float-assign isn't.
if (lcf && n.op == tkind.TK_ASSIGN) {
cgexpr(c, n.rhs);
let mov: str = "MOVSD";
if (lcf32) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff(off: i64);
emitline("(BP)\n");
return;
};
cgexpr(c, n.rhs);
if (n.op == tkind.TK_ASSIGN) {
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
if (lcstr || lcsl) {
emitline("\tMOVQ\tBX, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
};
if (lcsl) {
emitline("\tMOVQ\tCX, ");
emitoff((off + 16): i64);
emitline("(BP)\n");
};
return;
};
if (n.op == tkind.TK_PLUSEQ) {
emitline("\tADDQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
return;
};
if (n.op == tkind.TK_MINUSEQ) {
emitline("\tSUBQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
return;
};
// Generic compound: load → combine in BX → store.
emitline("\tMOVQ\t");
emitoff(off: i64);
emitline("(BP), BX\n");
if (n.op == tkind.TK_STAREQ) { emitline("\tIMULQ\tAX, BX\n"); };
if (n.op == tkind.TK_AMPEQ) { emitline("\tANDQ\tAX, BX\n"); };
if (n.op == tkind.TK_PIPEEQ) { emitline("\tORQ\tAX, BX\n"); };
if (n.op == tkind.TK_CARETEQ) { emitline("\tXORQ\tAX, BX\n"); };
if (n.op == tkind.TK_LSHIFTEQ) {
emitline("\tMOVQ\tAX, CX\n");
emitline("\tSHLQ\tCX, BX\n");
};
if (n.op == tkind.TK_RSHIFTEQ) {
emitline("\tMOVQ\tAX, CX\n");
emitline("\tSHRQ\tCX, BX\n");
};
emitline("\tMOVQ\tBX, ");
emitoff(off: i64);
emitline("(BP)\n");
return;
};
};
return;
};