Files
ww/selfhost/cmd/wcc/cgenexpr.ww
Hojun-Cho 9732061a7e w6c+w6c_ww: free() compiles to a no-op (ww has no free) (fix #27)
The free(x) builtin lowered to CALL ffi_resolve("free") in cstage and
fell through to a generic CALL free in wwstage (which had no free arm
at all) -- an undefined reference at w6l unless an @symbol decl
happened to be in scope. ww has no free by design (rt/alloc.s:30 --
the bump allocator cannot reclaim a mid-chunk pointer; process exit
does), so both stages now evaluate the operand for side effects
(Hare's free(expr) evaluates expr) and emit nothing else, letting
Hare code that calls free() port verbatim (regex fold-2b calls it at
4+ sites). The 2-arg os.free(p, n) public API is untouched: the
builtin gate requires exactly one bare-ident-callee arg.

930_free_noop_run pins per row: w6c/w6c_ww byte-id, no free symbol
in the .s, deref-after-free validity, and the operand side effect
running once per free() via a global counter.
2026-06-04 06:07:18 +09:00

8668 lines
293 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.
package wcc;
import os;
import ast;
import tok;
import typ;
import sym;
import strconv;
// cgfloatbits — materialise a float constant in X0: MOVQ the IEEE bits
// into AX, PUSH, MOVSD off the stack into X0. Shared by N_FLOATLIT (bits
// already in n.uval from the lexer's bitcast) and the f64/f32-typed
// N_INTLIT arm (#103 FACE X).
fn cgfloatbits(c: *cgen, bits: u64) void = {
emitline("\tMOVQ\t$");
emitint(bits: i64);
emitline(", AX\n");
emitline("\tPUSHQ\tAX\n");
emitline("\tMOVSD\t(SP), X0\n");
emitline("\tADDQ\t$8, SP\n");
};
fn cgexpr(c: *cgen, n: *node) void = {
if (n == nil) { return; };
let k: nkind = n.kind;
switch (k) {
case nkind.N_INTLIT:
// A no-decimal `0f64`/`8f64` is an N_INTLIT carrying float
// TYPE; it must reach X0 like a true float literal, not the
// integer-immediate path (which strands it in AX and an SSE
// compare/mul reads a stale X0 — #103 FACE X). The bits are
// the IEEE pattern of the integer value, mirroring cstage's
// `(double)(long long)n->uval`; the (&fv):*u64 bitcast is the
// lex.ww idiom (lib/ww/lex/lex.ww).
if (isfloattype(c, n)) {
let fv: f64 = (n.uval: i64): f64;
let pu: *u64 = (&fv): *u64;
cgfloatbits(c, *pu);
// #104: cgfloatbits materialises a DOUBLE in X0; an
// f32-typed literal must narrow with hardware single-
// rounding so the downstream MOVSS reads a true single.
if (isf32type(c, n)) {
emitline("\tCVTSD2SS\tX0, X0\n");
};
return;
};
// 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;
case nkind.N_FLOATLIT:
// The bits come from n.uval — the parser populates it from
// the lexer's bitcast of t.fval.
cgfloatbits(c, n.uval);
// #104: narrow the double in X0 to single for an f32 literal.
if (isf32type(c, n)) {
emitline("\tCVTSD2SS\tX0, X0\n");
};
return;
case nkind.N_RUNELIT:
emitline("\tMOVQ\t$");
emitint(n.uval: i64);
emitline(", AX\n");
return;
case nkind.N_STRLIT: cgstrlit(c, n); return;
case nkind.N_TRUE:
emitline("\tMOVQ\t$1, AX\n");
return;
case nkind.N_FALSE:
emitline("\tMOVQ\t$0, AX\n");
return;
case nkind.N_NIL:
emitline("\tMOVQ\t$0, AX\n");
return;
case 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;
case nkind.N_IDENT: cgident(c, n); return;
case nkind.N_INDEX: cgindex(c, n); return;
case nkind.N_SLICE: cgslice(c, n); return;
case nkind.N_MATCH: cgmatch(c, n); return;
case nkind.N_CAST: cgcast(c, n); return;
case nkind.N_DOT: cgdot(c, n); return;
case nkind.N_UN: cgun(c, n); return;
case nkind.N_BIN: cgbin(c, n); return;
case nkind.N_CALL: cgcall(c, n); return;
case nkind.N_ASSIGN: cgassign(c, n); return;
case nkind.N_TRYPROP: cgtryprop(c, n); return;
case nkind.N_TRYUNW: cgtryunw(c, n); return;
case nkind.N_TYPETEST: cgtypetest(c, n); return;
case nkind.N_TYPEASSERT: cgtypeassert(c, n); return;
case nkind.N_TUPLE:
// #241: a literal tuple rvalue `(a, b)` is a value — pack its
// elements into the register cursor (mirror cgreturn's N_TUPLE
// arm) so a let-bind / destructure consumer reads every element,
// not just AX = 0 from the default arm below.
cgtuplelittocursor(c, n);
return;
case:
// 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(c: *cgen, tagged: *node, vt: *node) i32 = {
if (tagged == nil) { return -1; };
if (vt == nil) { return -1; };
if (tagged.kind != nkind.N_TTAGGED) { return -1; };
// `is []T` / `as []T` — slice-shape lookup routes through the
// element-aware helper, which carries the loose first-slice-shape
// fallback (cstage type_assignable stand-in) that flatvariantidx's
// strict typeeq below doesn't. Task #19; #66 refresh.
if (vt.kind == nkind.N_TSLICE) {
return flatslicevariantidx(c, tagged, vt.lhs);
};
// #66 Phase-N step 3: match by typeeq on vt's stamped tinfo
// (flatvariantidx), not vt's surface name.
return flatvariantidx(c, tagged, vt);
};
// cgtrytupleshift — #241: if the `?`/`!` operand's success variant (tag 0)
// is a tuple, the unwrapped payload is an rvalue tuple that must fill the
// register cursor (shift past the tag), and the scalar/str MOVQ DX,AX tail
// is skipped. Returns true when it emitted the shift. Reads the operand's
// stamped tagged result tinfo (n.lhs.type_) — the success variant is the
// first param, matching the `CMPQ $0` success-tag convention.
fn cgtrytupleshift(c: *cgen, n: *node) bool = {
if (n.lhs == nil) { return false; };
let ou: *tinfo = n.lhs.type_: *tinfo;
for (ou != nil && ou.kind == tykind.TY_NAMED) { ou = ou.under; };
if (ou == nil) { return false; };
if (ou.kind != tykind.TY_TAGGED) { return false; };
if (ou.params == nil) { return false; };
let sv: *tinfo = ou.params.type_;
for (sv != nil && sv.kind == tykind.TY_NAMED) { sv = sv.under; };
if (sv == nil) { return false; };
if (sv.kind != tykind.TY_TUPLE) { return false; };
cgtaggedtuplepayloadshift(c, sv);
return true;
};
// cgtryprop — `e?` propagates the error variant up the stack.
// Success tag = 0 (#216 tracks the legacy/flag-aware success-tag
// divergence — out of scope here, success check stays `CMPQ $0`).
fn cgtryprop(c: *cgen, n: *node) void = {
// #38b residuals (rule 7): the cursor read below cannot see an
// sret-classified call result (AX = dest pointer), and the
// propagate-RET cannot speak an sret-classified enclosing return
// (the caller reads memory, not the cursor). #40-family follow-ups.
// Mirrors cstage cgen.c N_TRYPROP gates.
if (n.lhs != nil) {
if (n.lhs.kind == nkind.N_CALL) {
if (callsretsize(c, n.lhs) > 0) {
let m38p: str = "#38b: `?` on an sret-class call result unwired (mem-based unwrap is a #40-family follow-up)\n";
os.write(2, m38p.ptr, m38p.len: u64);
os.exit(1);
};
};
};
if (sretretsize(c, c.fnret) > 0) {
let m38q: str = "#38b: `?` propagation into a >32B tagged return unwired (sret error-propagate is a #40-family follow-up)\n";
os.write(2, m38q.ptr, m38q.len: u64);
os.exit(1);
};
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");
// #173: remap the operand union's error-variant tag to the
// enclosing fn return union's variant order before propagating.
// When the `?` operand and the enclosing fn return differ in
// variant order, the raw operand tag names the WRONG variant in
// the return union. Mirrors cstage cmd/w6c/cgen.c:6161-6184.
// Payload words DX/CX/R8 ride the RET untouched; only AX (the
// tag) is rewritten. Same-order unions map every error variant to
// itself → zero instructions, byte-id with the pre-#173 emit.
let u: *tinfo = nil;
if (n.lhs != nil) { u = n.lhs.type_: *tinfo; };
for (u != nil && u.kind == tykind.TY_NAMED) { u = u.under; };
let r: *tinfo = nil;
if (c.fnret != nil) { r = c.fnret.type_: *tinfo; };
for (r != nil && r.kind == tykind.TY_NAMED) { r = r.under; };
if (u != nil && r != nil && r.kind == tykind.TY_TAGGED && u.params != nil) {
let propret: str;
propret.ptr = nil; propret.len = 0;
let haveret: bool = false;
let p: *tparam = u.params;
let i: i32 = 0;
for (p != nil) {
if (p.iserror) {
let j: i32 = flatvariantidxt(r, p.type_);
if (j < 0) { j = 0; };
if (j != i) {
let skip: str = mklabel(c, "tryprop_skip");
emitline("\tCMPQ\t$");
emitint(i: i64);
emitline(", AX\n");
emitline("\tJNE\t");
emitline(skip);
emitline("\n");
emitline("\tMOVQ\t$");
emitint(j: i64);
emitline(", AX\n");
if (!haveret) {
propret = mklabel(c, "tryprop_ret");
haveret = true;
};
emitline("\tJMP\t");
emitline(propret);
emitline("\n");
emitlabel(skip);
};
};
p = p.tnext;
i += 1;
};
if (haveret) { emitlabel(propret); };
};
emitline("\tMOVQ\tBP, SP\n\tPOPQ\tBP\n\tRET\n");
emitlabel(cl);
// #241: a tuple success payload is an rvalue tuple — fill the cursor
// (shift past the tag) so the destructure / let consumer reads every
// element, not just word0. Success variant = tag 0 (first param).
if (cgtrytupleshift(c, n)) { return; };
// 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;
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.kind == nkind.N_IDENT) {
cname = callee.str;
cmod = c.curmod;
};
if (callee.kind == nkind.N_DOT) {
cname = callee.str;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
};
if (cname.len > 0) {
let rtyp: *node = fnretlookupmod(c, cname, cmod);
if (rtyp != nil) {
if (rtyp.kind == nkind.N_TTAGGED) {
let first: *node = rtyp.list;
if (first != nil) {
if (isstrtype(c, first)) {
succisstr = true;
};
};
};
};
};
};
};
};
if (succisstr) {
// str IS []u8: success arrives DX=ptr, CX=len, R8=cap
// (slot 32B). Move len out before cap overwrites CX
// (#1/Phase 3).
emitline("\tMOVQ\tCX, BX\n");
emitline("\tMOVQ\tR8, CX\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 = {
// #38b residual (rule 7): see the cgtryprop twin.
if (n.lhs != nil) {
if (n.lhs.kind == nkind.N_CALL) {
if (callsretsize(c, n.lhs) > 0) {
let m38u: str = "#38b: `!` on an sret-class call result unwired (mem-based unwrap is a #40-family follow-up)\n";
os.write(2, m38u.ptr, m38u.len: u64);
os.exit(1);
};
};
};
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);
// #241: tuple success payload fills the cursor (shift past the tag) —
// same rvalue-tuple-into-cursor story as cgtryprop.
if (cgtrytupleshift(c, n)) { return; };
// 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;
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.kind == nkind.N_IDENT) {
cname = callee.str;
cmod = c.curmod;
};
if (callee.kind == nkind.N_DOT) {
cname = callee.str;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
};
if (cname.len > 0) {
let rtyp: *node = fnretlookupmod(c, cname, cmod);
if (rtyp != nil) {
if (rtyp.kind == nkind.N_TTAGGED) {
let first: *node = rtyp.list;
if (first != nil) {
if (isstrtype(c, first)) {
succisstr = true;
};
};
};
};
};
};
};
};
if (succisstr) {
// str IS []u8: success arrives DX=ptr, CX=len, R8=cap
// (slot 32B). Move len out before cap overwrites CX
// (#1/Phase 3).
emitline("\tMOVQ\tCX, BX\n");
emitline("\tMOVQ\tR8, CX\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;
// #38b residual (rule 7): an sret-class call result leaves AX =
// dest pointer, not the tag — mem-based test is a #40-family
// follow-up. Mirrors cstage cgen.c N_TYPETEST gate.
if (lhs != nil) {
if (lhs.kind == nkind.N_CALL) {
if (callsretsize(c, lhs) > 0) {
let m38t: str = "#38b: `is` on an sret-class call result unwired (#40-family follow-up)\n";
os.write(2, m38t.ptr, m38t.len: u64);
os.exit(1);
};
};
};
let scrutoff: i32 = 0;
let scrutt: *node = nil;
let nonident: bool = false;
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);
};
} else {
// #45: non-ident scrutinee (xs[i], p.field, call).
// cstage N_TYPETEST never spills — cgexpr leaves the
// scrutinee's tag word in AX (tagged element/field/
// call reads load the tag first), so compare AX
// directly. The `as` twin's @asrt_spill (#200) is
// NOT mirrored here: `as` re-reads payload words
// after the check; `is` consumes only the tag, and
// a spill would diverge from cstage's asm (rule 10).
// Pre-#45 this fell through to scrutoff=0 and the
// tag read landed on (BP) — the saved-BP word.
nonident = true;
cgexpr(c, lhs);
};
};
let want: i32 = 0;
let nullcarrier: *node = scrutt;
if (nonident) {
// Variant index from the STAMPED scrutinee type (cstage:
// u = n->lhs->type) — matchscrutt's node-shape walk can't
// carry N_DOT (returns the scrut node, which the
// cgtagvariantidx N_TTAGGED gate rejects). flatvariantidx /
// flatslicevariantidx read .type_ off any stamped carrier.
nullcarrier = lhs;
if (n.rhs != nil) {
if (n.rhs.kind == nkind.N_TSLICE) {
want = flatslicevariantidx(c, lhs, n.rhs.lhs);
} else {
want = flatvariantidx(c, lhs, n.rhs);
};
};
} else {
want = cgtagvariantidx(c, scrutt, n.rhs);
};
if (!nonident) {
emitline("\tMOVQ\t");
emitoff(scrutoff: i64);
emitline("(BP), AX\n");
};
let nel: str = mklabel(c, "is_ne");
let dnl: str = mklabel(c, "is_done");
if (isnullabletype(nullcarrier)) {
// Nullable `(*T | void)` fold: the word in AX IS the
// pointer — discriminate pointer-vs-null, not tag-vs-index
// (cstage cgen.c N_TYPETEST nullable arm). `want` stays RAW
// here: cstage tests tag == ptr_tag unclamped, so a
// no-match (-1) takes the void polarity.
emitline("\tCMPQ\t$0, AX\n");
if (want == nullableptrtag(nullcarrier)) {
emitline("\tJE\t");
} else {
emitline("\tJNE\t");
};
} else {
if (want < 0) { want = 0; };
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;
// #38b residual (rule 7): the spill below reads the cursor, which
// an sret-class call result never fills. Mirrors cstage cgen.c
// N_TYPEASSERT gate.
if (lhs != nil) {
if (lhs.kind == nkind.N_CALL) {
if (callsretsize(c, lhs) > 0) {
let m38a: str = "#38b: `as` on an sret-class call result unwired (#40-family follow-up)\n";
os.write(2, m38a.ptr, m38a.len: u64);
os.exit(1);
};
};
};
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);
};
} else {
// Non-ident scrutinee (call result, arr[i], p.field, ?,
// etc.). Mirror cgmatch's spill (cgenexpr.ww:1422-1460)
// and cstage cmd/w6c/cgen.c:6300-6316: alloc an
// `@asrt_spill` slot sized via matchspillsz, evaluate
// the LHS, then copy the AX/DX/CX[/R8] return-ABI words
// into the slot so the tag-check + payload load indexes
// off memory like the IDENT path. Without this, scrutoff
// stayed 0 and the tag read fell on (BP) — the saved-BP
// word — and the payload read on +8(BP) — the return
// address. Bug #200.
scrutt = matchscrutt(c, lhs);
let spillsz: i32 = matchspillsz(c, scrutt);
scrutoff = localalloc(c, "@asrt_spill", spillsz, nil);
cgexpr(c, lhs);
emitline("\tMOVQ\tAX, ");
emitoff(scrutoff: i64);
emitline("(BP)\n");
if (!isnullabletype(scrutt)) {
emitline("\tMOVQ\tDX, ");
emitoff((scrutoff + 8): i64);
emitline("(BP)\n");
// Mirror cstage cmd/w6c/cgen.c:6313-6315: only CX
// → +16 when slot_size > 16. The 4-word case
// (R8 → +24, slot_size > 24) is the cgmatch shape
// (cgenexpr.ww:1454-1458, cmd/w6c/cgen.c:5988-5990)
// but cstage cgtypeassert omits it; preserve the
// asymmetry rather than diverge from rule 10
// byte-id. Filed inline as a cstage twin task.
if (spillsz > 16) {
emitline("\tMOVQ\tCX, ");
emitoff((scrutoff + 16): i64);
emitline("(BP)\n");
};
};
};
};
let want: i32 = cgtagvariantidx(c, 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 = 0;
if (n.lhs != nil) {
let st: *tinfo = n.lhs.type_: *tinfo;
if (typeisf32(st)) { srcfk = 1; }
else { if (typeisfloat(st)) { srcfk = 2; }; };
};
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);
// str → []T: cgexpr left (AX=ptr, BX=len). Slice register
// convention is (AX=ptr, BX=len, CX=cap); synthesise cap = len
// so downstream arg-push / let-init paths see the canonical
// triple. Detect via dst-is-slice + src-ident's local-tnode
// being str (the common shape; non-ident sources rare).
if (isslicetype(c, n.rhs)) {
let srcstr: bool = false;
if (n.lhs != nil) {
if (n.lhs.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, n.lhs.str);
if (lc != nil) {
if (isstrtype(c, lc.tnode)) { srcstr = true; };
};
};
};
if (srcstr) { emitline("\tMOVQ\tBX, CX\n"); };
};
// 0=int, 1=f32, 2=f64. CVT picks one direction per combo;
// int↔int casts narrow via an explicit clamp before the early
// return so `(big_u64): u32` doesn't leak the upper 32 bits.
// Hare semantics: `expr: T` truncates to T's bit width (mod 2^n).
// Mirrors cmd/w6c/cgen.c's N_CAST clamp. Unsigned narrow clears
// the upper bits via MOVL/ANDQ; signed narrow sign-extends via
// MOVSBQ/MOVSWQ/MOVSXD reg-reg so the sign bit propagates.
//
// Identity-width identity-sign cast is a no-op at the machine-
// int level: src and dst share both width and signedness, so the
// natural slot/load already carries the right canonical 64-bit
// shape. Skip the clamp in that case. Symmetric with cstage's
// principled gate (#33). Replaces the previous N_TENUM lacuna in
// this walker (the alias-step missed `N_TENUM`, so any cast to
// an enum dst landed on tn==nil and skipped the clamp by
// accident — task #25 mirrored that into cstage as a single-site
// gate, and #33 retires both). The walker now follows N_TENUM
// too so a narrow-to-enum cast (u32→enum-u8, i64→enum-i32)
// resolves to the underlying primitive and the clamp fires —
// fixing a silent miscompile in the process.
if (srcfk == 0 && dstfk == 0) {
let sz: i32 = 0;
let is_unsigned: bool = false;
typenodeprimresolved(c, n.rhs, &sz, &is_unsigned);
let src_sz: i32 = 0;
let src_unsigned: bool = false;
exprprimresolved(c, n.lhs, &src_sz, &src_unsigned);
let identity: bool = false;
if (sz > 0) { if (src_sz == sz) {
if (src_unsigned == is_unsigned) { identity = true; };
}; };
// Detect bool dst by walking n.rhs to the leaf TNAME. bool
// keeps its dedicated ANDQ $255 contract regardless of
// upstream shape; it stays off the identity path.
let leaf_tn: *node = n.rhs;
for (leaf_tn != nil) {
let lk: nkind = leaf_tn.kind;
if (lk == nkind.N_TBANG) { leaf_tn = leaf_tn.lhs; }
else { if (lk == nkind.N_TENUM) { leaf_tn = leaf_tn.lhs; }
else { if (lk == nkind.N_TNAME) {
let lnm: str = leaf_tn.str;
if (primsize(lnm) > 0) { break; };
let lal: *node = aliaslookup(c, lnm);
if (lal == nil) { leaf_tn = nil; }
else { leaf_tn = lal; };
}
else { leaf_tn = nil; }; }; };
};
let is_bool: bool = false;
if (leaf_tn != nil) {
if (leaf_tn.kind == nkind.N_TNAME) {
is_bool = streq(leaf_tn.str, "bool");
};
};
// Symmetric narrow on signed vs unsigned (task #5):
// unsigned (incl. rune) clears upper bits; signed
// sign-extends. bool is size 1 but neither — falls
// through to its dedicated ANDQ $255 below.
if (sz > 0) { if (sz < 8) { if (!is_bool) { if (!identity) {
if (is_unsigned) {
if (sz == 4) {
emitline("\tMOVL\tAX, AX\n");
} else {
let mask: i64 = 0xFFi64;
if (sz == 2) { mask = 0xFFFFi64; };
emitline("\tANDQ\t$");
emitint(mask);
emitline(", AX\n");
};
} else {
if (sz == 1) {
emitline("\tMOVSBQ\tAX, AX\n");
} else { if (sz == 2) {
emitline("\tMOVSWQ\tAX, AX\n");
} else { if (sz == 4) {
emitline("\tMOVSXD\tAX, AX\n");
}; }; };
};
}; }; }; };
if (is_bool) { emitline("\tANDQ\t$255, AX\n"); };
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 = {
// str IS []u8: the (ptr, len, cap) triple — ptr in AX, len in BX,
// cap in CX. A static literal has no spare storage, so cap = len
// (#1/Phase 3). Call sites that expect a str arg pick these up.
let nstr: str = n.str;
let lab: str = internstrlit(c, nstr);
emitline("\tLEAQ\t");
emitbytes( lab.ptr, lab.len: u64);
emitline("(SB), AX\n");
emitline("\tMOVQ\t$");
emitint(nstr.len: i64);
emitline(", BX\n");
emitline("\tMOVQ\t$");
emitint(nstr.len: i64);
emitline(", CX\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;
// #241: a tuple ident is a value — leave the whole tuple in the
// register cursor (`yield t` / `return t` / `let q = t`), not
// just word0 in AX. Mirror of cstage cgexpr N_IDENT tuple arm.
let itu: *tinfo = nil;
if (lc.tnode != nil) { itu = lc.tnode.type_: *tinfo; };
for (itu != nil && itu.kind == tykind.TY_NAMED) { itu = itu.under; };
if (itu != nil) { if (itu.kind == tykind.TY_TUPLE) {
cgtupleslottocursor(c, off, itu);
return;
}; };
// 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;
};
// str / slice locals load (ptr[, len[, cap]]) through MOVQ
// since the header is always 8B-clean. Scalar locals route
// through localloadop so signed-narrow slots sign-extend
// after a narrow deref-store.
let isstr: bool = isstrtype(c, lc.tnode);
let issl: bool = isslicetype(c, lc.tnode);
let lop: str = "MOVQ";
if (!isstr) { if (!issl) { lop = localloadop(c, lc.tnode); }; };
emitline("\t");
emitline(lop);
emitline("\t");
emitoff(off: i64);
emitline("(BP), AX\n");
if (isstr) {
// str IS []u8: load (ptr,len,cap) into AX/BX/CX,
// identical to the slice arm below (#1/Phase 3).
emitline("\tMOVQ\t");
emitoff((off + 8): i64);
emitline("(BP), BX\n");
emitline("\tMOVQ\t");
emitoff((off + 16): i64);
emitline("(BP), CX\n");
};
if (issl) {
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.
// Str defs (rhs N_STRLIT) aren't laid out at a SB symbol; the
// MOVQ symname(SB) fallback below would emit a bogus reference
// (e.g. `alpha.MSG(SB)`, never DATAW-defined). Strlit-inline
// the (LEAQ ptr, MOVQ $len) pair instead, mirroring cstage
// Sdef walk #1 N_IDENT bare-load (cmd/w6c/cgen.c). Filed #12.
if (deflookup(c, nm)) {
let drhs: *node = deflookuprhs(c, nm);
if (drhs != nil) {
if (drhs.kind == nkind.N_STRLIT) {
let bytes: str = drhs.str;
let lab: str = internstrlit(c, bytes);
emitline("\tLEAQ\t");
emitbytes( lab.ptr, lab.len: u64);
emitline("(SB), AX\n");
emitline("\tMOVQ\t$");
emitint(bytes.len: i64);
emitline(", BX\n");
// str IS []u8: cap = len for a static def literal
// (#1/Phase 3).
emitline("\tMOVQ\t$");
emitint(bytes.len: i64);
emitline(", CX\n");
return;
};
};
// Float def: load via LEAQ + MOVSS/MOVSD into X0, same shape
// as the let-float arm below — MOVSS/MOVSD have no D_EXTERN
// operand form. Pre-#129 fell through to the MOVQ-AX
// integer-convention fallback, leaving X0 untouched (#129
// LOAD-side twin of the emitfloatlitdata DATA-side SSoT).
if (isfloattype(c, n)) {
let mov: str = "MOVSD";
if (isf32type(c, n)) { mov = "MOVSS"; };
emitline("\tLEAQ\t");
emitsymnamehint(c, nm, c.curmod);
emitline("(SB), CX\n");
emitline("\t");
emitline(mov);
emitline("\t(CX), X0\n");
return;
};
emitline("\tMOVQ\t");
emitsymnamehint(c, nm, c.curmod);
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
// emitfnname 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.
// Bare ident → same-module by ww's resolver, hint with c.curmod.
let rtyp: *node = fnretlookup(c, nm);
if (rtyp != nil) {
emitline("\tLEAQ\t");
emitfnname(c, nm, c.curmod);
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) {
// str IS []u8: both str and slice carry a third 8B
// (cap); load it unconditionally. The address holder CX
// is overwritten by the cap as the last step, after
// ptr/len are already loaded (#1/Phase 3).
emitline("\tLEAQ\t");
emitsymnamehint(c, nm, c.curmod);
emitline("(SB), CX\n");
emitline("\tMOVQ\t(CX), AX\n");
emitline("\tMOVQ\t8(CX), BX\n");
emitline("\tMOVQ\t16(CX), CX\n");
return;
};
// Float global: same LEAQ-indirect shape, since MOVSS/
// MOVSD have no D_EXTERN operand form in w6a. Signed-narrow
// scalar globals route through the same LEAQ scratch since
// MOVSXD/MOVSWQ/MOVSBQ also have no D_EXTERN form.
let lvtnode: *node = nil;
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");
emitsymnamehint(c, nm, c.curmod);
emitline("(SB), CX\n");
emitline("\t");
emitline(mov);
emitline("\t(CX), X0\n");
return;
};
lvtnode = lv.tnode;
lv = nil;
} else {
lv = lv.lvnext;
};
};
let glop: str = localloadop(c, lvtnode);
if (streq(glop, "MOVQ")) {
emitline("\tMOVQ\t");
emitsymnamehint(c, nm, c.curmod);
emitline("(SB), AX\n");
} else {
emitline("\tLEAQ\t");
emitsymnamehint(c, nm, c.curmod);
emitline("(SB), CX\n");
emitline("\t");
emitline(glop);
emitline("\t(CX), AX\n");
};
return;
};
return;
};
// cgslicehdr — load the 24B slice/str header at base+0 into the
// (AX=ptr, BX=len, CX=cap) triple. base holds the element address;
// the load that targets base destroys it, so that word is emitted
// LAST. Order otherwise mirrors the slice-field arm (len, cap, ptr).
// Shared by the cgindex str-element arms (caller does the kind-gate)
// and, later, the typeassert str-variant leaf (#9). c retained unused
// for callsite symmetry with cstage cgslicehdr.
fn cgslicehdr(c: *cgen, base: str) void = {
if (!streq(base, "BX")) { emitmovqload(8i64, base, "BX"); };
if (!streq(base, "CX")) { emitmovqload(16i64, base, "CX"); };
if (!streq(base, "AX")) { emitmovqload(0i64, base, "AX"); };
if (streq(base, "BX")) { emitmovqload(8i64, base, "BX"); };
if (streq(base, "CX")) { emitmovqload(16i64, base, "CX"); };
if (streq(base, "AX")) { emitmovqload(0i64, base, "AX"); };
};
// dotchainaddr — emit the ADDRESS of a dot/ident lvalue chain into
// `dstreg`, dereferencing pointer links mid-chain. Returns true on
// success, false if a link isn't a struct / ptr-to-struct it can
// resolve. Recursion mirrors the cstage read spine: for `x.f`, recurse
// to &x, deref if x is a *struct (so dstreg holds the pointee base),
// then add f's offset. Touches ONLY dstreg (no AX, no stack) — same
// spill contract as dotbaseaddr. The chained-base arm of dotbaseaddr
// (#253) is its sole caller. Cstage twin: cmd/w6c/cgen.c
// `cg_dotchain_addr`.
fn dotchainaddr(c: *cgen, n: *node, dstreg: str) bool = {
if (n == nil) { return false; };
if (n.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, n.str);
if (lc != nil) {
emitline("\tLEAQ\t");
emitoff(lc.off: i64);
emitline("(BP), ");
emitline(dstreg);
emitline("\n");
return true;
};
// #256: carry cstage cg_dotchain_addr's `let_islet ||
// def_isstructdef` guard (never-silent ethos). Unreachable on
// valid input — a struct-typed chain root is always local, a
// let-global, or a struct def — so this adds zero divergent
// asm; it just refuses to LEAQ name(SB) for a name that names
// neither. deflookup is the broader def-registry twin (ww has no
// struct-specific def predicate; harmless given unreachability).
if (isletvar(c, n.str) || deflookup(c, n.str)) {
emitline("\tLEAQ\t");
emitsymname(c, n.str);
emitline("(SB), ");
emitline(dstreg);
emitline("\n");
return true;
};
return false;
};
if (n.kind != nkind.N_DOT) { return false; };
let x: *node = n.lhs;
if (x == nil) { return false; };
let xu: *tinfo = x.type_: *tinfo;
for (xu != nil && xu.kind == tykind.TY_NAMED) { xu = xu.under; };
if (xu == nil) { return false; };
let xviaptr: bool = false;
let st: *tinfo = nil;
if (xu.kind == tykind.TY_PTR) {
let p: *tinfo = xu.sub;
for (p != nil && p.kind == tykind.TY_NAMED) { p = p.under; };
if (p != nil) { if (p.kind == tykind.TY_STRUCT) {
st = p;
xviaptr = true;
}; };
} else { if (xu.kind == tykind.TY_STRUCT) {
st = xu;
}; };
if (st == nil) { return false; };
let f: *tfield = st.fields;
let foff: i64 = -1;
for (f != nil) {
if (streq(f.name, n.str)) {
foff = f.offset: i64;
break;
};
f = f.tnext;
};
if (foff < 0) { return false; };
if (!dotchainaddr(c, x, dstreg)) { return false; };
if (xviaptr) {
emitline("\tMOVQ\t(");
emitline(dstreg);
emitline("), ");
emitline(dstreg);
emitline("\n");
};
if (foff != 0) {
emitline("\tADDQ\t$");
emitint(foff);
emitline(", ");
emitline(dstreg);
emitline("\n");
};
return true;
};
// dotbaseaddr — emit `&(inner.field)` into `dstreg` when `base` is an
// N_DOT with N_IDENT inner OR a chained N_DOT inner (#253: `o.p.m` /
// `o.i.m` / `o.a.b.m`). Returns true if emitted; callers fall back
// to `cgexpr(c, base); MOVQ AX, dstreg` on false. Cstage twin:
// cmd/w6c/cgen.c `cg_dotbase_addr`.
//
// #135: cgexpr on an N_DOT whose .field is a `[N]T`-typed field auto-
// derefs + loads the field's 8-byte VALUE as if it were a pointer. For
// an LHS or index-base shape (`d.fld[i] = v` / `d.fld[i]` read / `d.fld
// [i] OP= v`), the caller wants the field's ADDRESS — this helper
// supplies it inline. Reusable primitive of the inverse template
// `arr[i].field = v` (cstage cgen.c arr[i].field address-eval).
//
// #253: a chained inner (`inner` is itself an N_DOT) routes through
// dotchainaddr to recover the container base — the pointer VALUE of
// inner when inner is a *struct (viaptr), else the ADDRESS of inner —
// then adds the field offset. Closes the array-field-base-address
// family across every op (index r/w, addr-of, slice, compound).
fn dotbaseaddr(c: *cgen, base: *node, dstreg: str) bool = {
if (base == nil) { return false; };
if (base.kind != nkind.N_DOT) { return false; };
let inner: *node = base.lhs;
if (inner == nil) { return false; };
let chained: bool = (inner.kind == nkind.N_DOT);
if (inner.kind != nkind.N_IDENT && !chained) { return false; };
// #128b: module-qualified `mod.arr` where arr is an imported
// top-level `let X: [N]T`. The checker leaves SK_USE module-
// idents without a localfindnode entry; detect via letvartnode
// resolving to N_TARRAY and emit LEAQ X(SB). Without this, the
// cgindex fallback's cgexpr(base) auto-MOVQs the symbol's first
// 8 bytes as if it were a pointer-var — wrong shape (cstage
// sister fix in cg_dotbase_addr). N_IDENT-inner only — a chained
// inner has a valid stamped type_ and routes through dotchainaddr.
let lc: *local = nil;
let isglobal: bool = false;
if (!chained) {
lc = localfindnode(c, inner.str);
if (lc == nil) {
let gt: *node = letvartnode(c, base.str);
if (gt != nil && gt.kind == nkind.N_TARRAY) {
emitline("\tLEAQ\t");
emitsymname(c, base.str);
emitline("(SB), ");
emitline(dstreg);
emitline("\n");
return true;
};
// #249 (sibling of #135): inner is a module-GLOBAL struct value
// (let/def), not a local — lc is nil but inner.type_ is a valid
// struct. Resolve the field below and emit a global base (LEAQ
// name(SB)). A non-struct inner (e.g. an SK_USE module qualifier,
// type ty_err) falls through the struct gate to `return false`.
isglobal = true;
};
};
let bu: *tinfo = inner.type_: *tinfo;
for (bu != nil && bu.kind == tykind.TY_NAMED) { bu = bu.under; };
if (bu == nil) { return false; };
let viaptr: bool = false;
let structt: *tinfo = nil;
if (bu.kind == tykind.TY_PTR) {
let st: *tinfo = bu.sub;
for (st != nil && st.kind == tykind.TY_NAMED) { st = st.under; };
if (st != nil) { if (st.kind == tykind.TY_STRUCT) {
structt = st;
viaptr = true;
}; };
} else { if (bu.kind == tykind.TY_STRUCT) {
structt = bu;
}; };
if (structt == nil) { return false; };
let f: *tfield = structt.fields;
let foff: i64 = -1;
let ft: *tinfo = nil;
for (f != nil) {
if (streq(f.name, base.str)) {
foff = f.offset: i64;
ft = f.type_;
break;
};
f = f.tnext;
};
if (foff < 0) { return false; };
// Only fire on `[N]T` fields — for `*T` / `[]T` / `str` fields
// the existing cgexpr(base) path correctly loads the pointer/
// header value; over-firing here would skip the deref. Cstage
// twin gate at cg_dotbase_addr.
for (ft != nil && ft.kind == tykind.TY_NAMED) { ft = ft.under; };
if (ft == nil) { return false; };
if (ft.kind != tykind.TY_ARRAY) { return false; };
// #253: chained inner — compute the container base via the dot-chain
// spine (pointer VALUE of inner when viaptr, else its ADDRESS), then
// add the field offset. dotchainaddr keeps the spill contract.
if (chained) {
if (!dotchainaddr(c, inner, dstreg)) { return false; };
if (viaptr) {
emitline("\tMOVQ\t(");
emitline(dstreg);
emitline("), ");
emitline(dstreg);
emitline("\n");
};
if (foff != 0) {
emitline("\tADDQ\t$");
emitint(foff);
emitline(", ");
emitline(dstreg);
emitline("\n");
};
return true;
};
let innoff: i64 = 0;
if (lc != nil) { innoff = lc.off: i64; };
if (viaptr) {
emitline("\tMOVQ\t");
emitoff(innoff);
emitline("(BP), ");
emitline(dstreg);
emitline("\n");
if (foff != 0) {
emitline("\tADDQ\t$");
emitint(foff);
emitline(", ");
emitline(dstreg);
emitline("\n");
};
} else if (isglobal) {
// #249: LEAQ name(SB) + field offset. Mirror cstage
// cg_dotbase_addr's global value-struct arm.
emitline("\tLEAQ\t");
emitsymname(c, inner.str);
emitline("(SB), ");
emitline(dstreg);
emitline("\n");
if (foff != 0) {
emitline("\tADDQ\t$");
emitint(foff);
emitline(", ");
emitline(dstreg);
emitline("\n");
};
} else {
emitline("\tLEAQ\t");
emitoff(innoff + foff);
emitline("(BP), ");
emitline(dstreg);
emitline("\n");
};
return true;
};
// aggargsrcaddr — land the ADDRESS of an addressable aggregate (struct/
// array) call-arg source in dstreg (#271, mirror of cstage
// aggarg_srcaddr). Reuses the closed #265/#268 let-init-copy dispatch:
// local ident slot (LEAQ off(BP)), module-let global (LEAQ name(SB)),
// deref operand (cgexpr of the pointer), N_DOT field (dotchainaddr,
// #253), N_INDEX element of an N_IDENT array base (the &base[i] spine,
// #252/#270). Returns false for an uncovered source kind (caller loud-
// stops, rule 7). The CALL source is handled at the push site.
fn aggargsrcaddr(c: *cgen, src: *node, dst: str) bool = {
if (src.kind == nkind.N_UN) {
if (src.op == tkind.TK_STAR) {
cgexpr(c, src.lhs);
if (!streq(dst, "AX")) {
emitline("\tMOVQ\tAX, ");
emitline(dst);
emitline("\n");
};
return true;
};
};
if (src.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, src.str);
if (lc != nil) {
emitline("\tLEAQ\t");
emitoff(lc.off: i64);
emitline("(BP), ");
emitline(dst);
emitline("\n");
return true;
};
// global value source. Gated to a module-`let` (letvartnode,
// the cstage let_islet twin); a const array/struct `def`
// aggregate ARG is untested + out of scope (#274; both stages
// loud-stop, rule-10 aligned).
if (letvartnode(c, src.str) != nil) {
emitline("\tLEAQ\t");
emitsymname(c, src.str);
emitline("(SB), ");
emitline(dst);
emitline("\n");
return true;
};
return false;
};
if (src.kind == nkind.N_DOT) {
return dotchainaddr(c, src, dst);
};
if (src.kind == nkind.N_INDEX) {
let base: *node = src.lhs;
let idx: *node = src.rhs;
if (base == nil) { return false; };
if (base.kind != nkind.N_IDENT) { return false; };
let bu: *tinfo = base.type_: *tinfo;
for (bu != nil && bu.kind == tykind.TY_NAMED) { bu = bu.under; };
if (bu == nil) { return false; };
if (bu.kind != tykind.TY_ARRAY) { return false; };
let esz: i32 = 1;
if (bu.sub != nil) { esz = bu.sub.size: i32; };
cgexpr(c, idx);
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", CX\n");
emitline("\tIMULQ\tCX, AX\n");
};
let boff: *local = localfindnode(c, base.str);
if (boff != nil) {
emitline("\tLEAQ\t");
emitoff(boff.off: i64);
emitline("(BP), BX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, base.str);
emitline("(SB), BX\n");
};
emitline("\tADDQ\tBX, AX\n");
if (!streq(dst, "AX")) {
emitline("\tMOVQ\tAX, ");
emitline(dst);
emitline("\n");
};
return true;
};
return false;
};
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;
// #119: float element loads route to MOVSS/MOVSD into X0, not the
// integer loadopsz into AX. float_elem/f32_elem are set per-branch
// from the SAME tinfo esz reads — never a fresh node-stamp (#121).
let float_elem: bool = false;
let f32_elem: bool = false;
// #156 (PREREQ-1 read-half): element is itself an array ([N][M]T →
// element [M]T) → leave the sub-array's ADDRESS in the result reg
// instead of dereferencing; the outer index adds its offset and the
// final scalar element dereferences. Sister of #135. Mirrors cstage
// esubu->kind == TY_ARRAY. Node-based (elemisarrayc) for ident bases,
// n.type_ tinfo-based for N_DOT/N_INDEX bases — same source split as
// esz above.
let elem_isarray: bool = false;
// #1/Phase 3: str and slice are both 24B (and a >16B struct is
// 24B+ too), so the header branches below MUST gate on KIND
// (elemisstr/elemisslice, mirroring cstage's elem_is_str||
// elem_is_slice), not a bare `esz == primtypesize("str")` size
// check — a size gate would route a plain >16B struct into the
// 3-word {ptr,len,cap} load and diverge from cstage (#60 collision
// class; sentinel 754).
let elemisstr: bool = false;
let elemisslice: 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 = elemsizeofc(c, baselocal.tnode);
signed_elem = elemissignedc(c, baselocal.tnode);
float_elem = elemisfloatc(c, baselocal.tnode);
f32_elem = elemisf32c(c, baselocal.tnode);
elem_isarray = elemisarrayc(c, baselocal.tnode);
} else {
let tn: *node = letvartnode(c, bn);
// #129 A.3: array-typed defs now have DATA storage;
// resolve their base via the same N_TARRAY path as
// lets. defvartnode is the def-side sister of
// letvartnode (parallel to defvarstructinfo at the
// A.2 cgdot widening site).
if (tn == nil) { tn = defvartnode(c, bn); };
if (tn != nil) {
// #10: dispatch esz + base-materialization off the
// global's RESOLVED type via elemsizeofc, NOT an
// N_TARRAY/N_TPTR kind whitelist. A global str (tnode
// N_TNAME "str") / slice (N_TSLICE) matched NEITHER old
// arm, so esz stayed at the default 8 and the base fell
// to the wide-header fallback below (8B stride + full-
// word MOVQ) instead of loading the .ptr + an element-
// width load. cstage dispatches uniformly off
// idx_eff(lhs->type)->sub->size (cmd/w6c/cgen.c
// N_INDEX); the sister fn cgslice (this file) already
// resolves esz via elemsizeofc and the base via
// N_TARRAY?LEAQ:MOVQ name(SB) with no kind gate. Align
// cgindex UP to that template: any indexable global
// resolves esz off the type table, N_TARRAY -> LEAQ (the
// symbol IS the storage), every other -> MOVQ name(SB)
// (the symbol's first word IS the .ptr). The existing
// isglobalptr emission (the loadopsz path below) then
// yields the cstage-identical MOVZBQ for a str byte
// (esz=1).
globalname = bn;
esz = elemsizeofc(c, tn);
signed_elem = elemissignedc(c, tn);
float_elem = elemisfloatc(c, tn);
f32_elem = elemisf32c(c, tn);
elem_isarray = elemisarrayc(c, tn);
if (tn.kind == nkind.N_TARRAY) {
isglobalarr = true;
} else {
isglobalptr = true;
};
};
};
} else { if (base.kind == nkind.N_DOT) {
// `s.ptr[i]` / struct-field index: stride is the
// checker-stamped element tinfo's natural size, the
// same idiom as the N_INDEX-base arm below (#60/#72).
// cstage idx_eff(base->type)->sub->size (cmd/w6c/
// cgen.c:3517-18). Signedness from the same element tinfo
// so a signed-narrow field element sign-extends on load
// (loadopsz keys on (signed,sz)); cstage's fldloadop reads
// it from the element type — align ww up (#255).
let dt: *tinfo = n.type_: *tinfo;
if (dt != nil) { esz = dt.size: i32; signed_elem = typeissigned(dt); elemisstr = typeisstr(dt); elemisslice = typeisslice(dt); float_elem = typeisfloat(dt); f32_elem = typeisf32(dt); };
elem_isarray = tinfoisarray(dt);
} else { if (base.kind == nkind.N_INDEX) {
// #60: chained `names[i][k]` — n.type_ is the checker-
// stamped outer element tinfo (indexresult over the inner
// index's value type). cstage reads base->type->sub->size
// for esz (cmd/w6c/cgen.c:2070-2071). Drops the
// indexvaluetnode walk.
let et: *tinfo = n.type_: *tinfo;
if (et != nil) {
esz = et.size: i32;
signed_elem = typeissigned(et);
float_elem = typeisfloat(et);
f32_elem = typeisf32(et);
elem_isarray = tinfoisarray(et);
};
};};};
};
// Tagged-union element: load slot words into (AX=tag, DX=val0,
// CX=val1) matching the tagged-return ABI so call-arg / let /
// match consumers see the same shape as a tagged-returning fn.
// Slot size = esz (8/16/24); nullable folded element is one
// word, which the fallthrough below handles via MOVQ AX.
let elem_tagged: bool = false;
let elem_slot_sz: i32 = esz;
if (base != nil) {
if (base.kind == nkind.N_IDENT) {
let bl: *local = baselocal;
let etn: *node = nil;
if (bl != nil) {
let btn: *node = bl.tnode;
if (btn != nil) {
let bk: nkind = btn.kind;
if (bk == nkind.N_TARRAY) { etn = btn.lhs; };
if (bk == nkind.N_TSLICE) { etn = btn.lhs; };
if (bk == nkind.N_TPTR) { etn = btn.lhs; };
};
} else {
let tn: *node = letvartnode(c, base.str);
if (tn != nil) {
let bk: nkind = tn.kind;
if (bk == nkind.N_TARRAY) { etn = tn.lhs; };
if (bk == nkind.N_TSLICE) { etn = tn.lhs; };
if (bk == nkind.N_TPTR) { etn = tn.lhs; };
};
};
if (istaggedtype(c, etn)) {
if (!isnullabletype(etn)) {
elem_tagged = true;
elem_slot_sz = slotsize(c, etn);
esz = elem_slot_sz;
};
};
elemisstr = isstrtype(c, etn);
elemisslice = isslicetype(c, etn);
};
// N_DOT / N_INDEX base (`x.o[i]`, chained `m[i][j]`): the
// element tinfo is n.type_ (the checker-stamped indexresult),
// same source the esz/str/slice/float flags read above. Mirror
// cstage cgen.c:8101 `esubu->kind == TY_TAGGED` — classified for
// ANY base, NOT gated on N_IDENT, and NOT excluding the nullable
// fold (slot_sz=8 degrades the copy arm to one MOVQ, matching
// cstage's fallback `MOVQ AX,BX; MOVQ (BX),AX`). #261: without
// this an N_DOT-base tagged element fell to the scalar
// loadopsz path and silently dropped the tag/payload-high word.
if (base.kind == nkind.N_DOT || base.kind == nkind.N_INDEX) {
let dt: *tinfo = n.type_: *tinfo;
if (dt != nil) {
if (typeistagged(dt)) {
elem_tagged = true;
elem_slot_sz = dt.size: i32;
esz = elem_slot_sz;
};
};
};
};
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");
// #156: array element ([N][M]T) → leave the sub-array ADDRESS
// in AX (BX holds base+idx*esz); nested index dereferences.
if (elem_isarray) {
emitline("\tMOVQ\tBX, AX\n");
return;
};
if (elem_tagged) {
if (elem_slot_sz > 24) {
emitline("\tMOVQ\t24(BX), R8\n");
};
if (elem_slot_sz > 16) {
emitline("\tMOVQ\t16(BX), CX\n");
};
if (elem_slot_sz > 8) {
emitline("\tMOVQ\t8(BX), DX\n");
};
emitline("\tMOVQ\t(BX), AX\n");
return;
};
// str/slice element: load the full (ptr, len, cap) header into
// (AX, BX, CX) — both are 24B since #1, so cap must survive.
// Kind-gate on isstrtype||isslicetype, never size==24 (a >16B
// struct is 24B+ too but takes the struct-copy path). Base is BX.
if (elemisstr || elemisslice) {
cgslicehdr(c, "BX");
return;
};
// #119: float element → MOVSS/MOVSD into X0 (the consumer's
// ADDSD/MOVSD spill machinery already expects X0); the integer
// loadopsz below would leave it in AX and the SSE side reads
// stale. Twin of cgen.c:2014's scalar-float global load.
if (float_elem) {
let fop1: str = "MOVSD";
if (f32_elem) { fop1 = "MOVSS"; };
emitline("\t");
emitline(fop1);
emitline("\t(BX), X0\n");
return;
};
let lop1: str = loadopsz(signed_elem, esz);
emitline("\t");
emitline(lop1);
emitline("\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");
// #156: array element ([N][M]T) → leave the sub-array ADDRESS
// in AX (BX holds base+idx*esz); nested index dereferences.
if (elem_isarray) {
emitline("\tMOVQ\tBX, AX\n");
return;
};
if (elem_tagged) {
if (elem_slot_sz > 24) {
emitline("\tMOVQ\t24(BX), R8\n");
};
if (elem_slot_sz > 16) {
emitline("\tMOVQ\t16(BX), CX\n");
};
if (elem_slot_sz > 8) {
emitline("\tMOVQ\t8(BX), DX\n");
};
emitline("\tMOVQ\t(BX), AX\n");
return;
};
// str/slice element: full (ptr, len, cap) header into (AX, BX, CX);
// cap must survive (#1). Kind-gate, never size==24. Base BX.
if (elemisstr || elemisslice) {
cgslicehdr(c, "BX");
return;
};
// #119: float element → X0 (see the global arm above).
if (float_elem) {
let fop2: str = "MOVSD";
if (f32_elem) { fop2 = "MOVSS"; };
emitline("\t");
emitline(fop2);
emitline("\t(BX), X0\n");
return;
};
let lop2: str = loadopsz(signed_elem, esz);
emitline("\t");
emitline(lop2);
emitline("\t(BX), AX\n");
return;
};
// Generic fallback when base isn't a plain ident.
// #135: N_DOT base on `[N]T` field needs the field's ADDRESS,
// not its value. cgexpr would auto-deref + load the 8-byte value
// as if it were a pointer. dotbaseaddr emits the address inline.
emitline("\tPUSHQ\tAX\n");
if (!dotbaseaddr(c, base, "AX")) {
cgexpr(c, base);
};
emitline("\tPOPQ\tBX\n");
emitline("\tADDQ\tBX, AX\n");
// #156: array element ([N][M]T) → AX already holds &elem
// (base+idx*esz); a nested index dereferences. See ident arms.
if (elem_isarray) {
return;
};
if (elem_tagged) {
// AX holds the element address. Copy to BX (loading slot+0
// into AX clobbers it), then read slot words. #48: the >24
// R8 word was missing ONLY in this fallback arm (both ident
// arms have it) — a >24B-slot element via a non-ident base
// under-read the cursor and the match spill stored stale R8.
// Mirrors cstage cgen.c:9106-9117.
emitline("\tMOVQ\tAX, BX\n");
if (elem_slot_sz > 24) {
emitline("\tMOVQ\t24(BX), R8\n");
};
if (elem_slot_sz > 16) {
emitline("\tMOVQ\t16(BX), CX\n");
};
if (elem_slot_sz > 8) {
emitline("\tMOVQ\t8(BX), DX\n");
};
emitline("\tMOVQ\t(BX), AX\n");
return;
};
// str/slice element via fallback base: full (ptr, len, cap) header
// into (AX, BX, CX); cap must survive (#1). Kind-gate, never
// size==24. Base AX.
if (elemisstr || elemisslice) {
cgslicehdr(c, "AX");
return;
};
// #119: float element → X0 (see the global arm above). The base
// address is in AX; MOVSS/MOVSD reads the element into X0.
if (float_elem) {
let fop3: str = "MOVSD";
if (f32_elem) { fop3 = "MOVSS"; };
emitline("\t");
emitline(fop3);
emitline("\t(AX), X0\n");
return;
};
let lop3: str = loadopsz(signed_elem, esz);
emitline("\t");
emitline(lop3);
emitline("\t(AX), AX\n");
return;
};
// cgbasecap — load the capacity of a sub-slice's UNDERLYING storage
// into `dst` for the #20 cap = base_cap - lo formula (drew: harec
// eval.c:1017 slice cap-=start / eval.c:1024 array cap=length-start;
// ensure.ha:4-8 distinct capacity field). array [N]T -> N (literal);
// slice/str -> the .capacity word in the header at +16 (mirrors the
// hi-default +8 length dispatch, emitted unconditionally). Returns
// false when base_cap isn't cleanly available so the caller keeps the
// prior cap=len: a non-ident base (its header cap was discarded;
// recomputing would re-evaluate a possibly side-effecting base -- #74,
// which also owns the pre-existing defaulted-hi len gap there), or
// a GLOBAL str base (no +16 load here, #73 -- matching the cstage
// carve-out keeps both stages byte-identical). cstage twin:
// cmd/w6c/cgen.c cg_base_cap.
fn cgbasecap(c: *cgen, base: *node, dst: str) bool = {
if (base == nil) { return false; };
if (base.kind != nkind.N_IDENT) { return false; };
let baselocal: *local = localfindnode(c, base.str);
if (baselocal != nil) {
let tn: *node = baselocal.tnode;
if (tn == nil) { return false; };
if (tn.kind == nkind.N_TARRAY) {
let lenn: *node = tn.rhs;
if (lenn == nil) { return false; };
if (lenn.kind != nkind.N_INTLIT) { return false; };
emitline("\tMOVQ\t$");
emituint(lenn.uval);
emitline(", ");
emitline(dst);
emitline("\n");
return true;
};
if (tn.kind == nkind.N_TSLICE) {
emitline("\tMOVQ\t");
emitoff((baselocal.off + 16): i64);
emitline("(BP), ");
emitline(dst);
emitline("\n");
return true;
};
if (tn.kind == nkind.N_TNAME) {
if (streq(tn.str, "str")) {
emitline("\tMOVQ\t");
emitoff((baselocal.off + 16): i64);
emitline("(BP), ");
emitline(dst);
emitline("\n");
return true;
};
};
return false;
};
let gt: *node = letvartnode(c, base.str);
if (gt == nil) { return false; };
if (gt.kind == nkind.N_TARRAY) {
let lenn: *node = gt.rhs;
if (lenn == nil) { return false; };
if (lenn.kind != nkind.N_INTLIT) { return false; };
emitline("\tMOVQ\t$");
emituint(lenn.uval);
emitline(", ");
emitline(dst);
emitline("\n");
return true;
};
if (gt.kind == nkind.N_TSLICE) {
emitline("\tLEAQ\t");
emitsymname(c, base.str);
emitline("(SB), ");
emitline(dst);
emitline("\n");
emitline("\tMOVQ\t16(");
emitline(dst);
emitline("), ");
emitline(dst);
emitline("\n");
return true;
};
return false;
};
// cgslice — `base[lo:hi]` as a slice value. Leaves (AX=base+lo*esz,
// BX=hi-lo, CX=base_cap-lo) so callers can route to a slice slot,
// return, or arg with the same triple ABI. cap is the storage
// remaining to the base's end (#20, Go/Hare-identical) via cgbasecap.
// ptr advances by BYTES (lo*esz, #76; ref/hare/rt/ensure.ha:30
// membsz-unit); esz from the type table, mirroring the cgindex idiom.
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;
let globaltn: *node = nil;
let globalname: str;
globalname.ptr = nil; globalname.len = 0;
if (base != nil) {
if (base.kind == nkind.N_IDENT) {
baselocal = localfindnode(c, base.str);
if (baselocal == nil) {
let gt: *node = letvartnode(c, base.str);
if (gt != nil) {
globaltn = gt;
globalname = base.str;
};
};
};
};
// #252: an N_DOT `[N]T`-field base (`s.obuf[lo:hi]`) carries no
// tnode — resolve esz / default-hi / base-address from the checker-
// stamped element tinfo on base.type_ instead. Cstage twin reads
// base->type (cgen.c N_SLICE esz + bu->kind==TY_ARRAY default-hi).
let dotbu: *tinfo = nil;
if (base != nil) { if (base.kind == nkind.N_DOT) {
dotbu = base.type_: *tinfo;
for (dotbu != nil && dotbu.kind == tykind.TY_NAMED) {
dotbu = dotbu.under;
};
};};
// #31: an N_ARRLIT base (the desugared one-step `let xs:[]T=[..]`
// borrow — the ONLY context that reaches here; call-arg/return/assign
// loud-reject at the checker, #33) has no storage. Its [count]T type
// NODE is stashed on base.lhs by checkletassign's #25 re-stamp; size /
// count come NODE-wise (elemsizeofc / .rhs intlit), because wwstage
// narrow-primitive tinfos are unsized (#8). Cstage twin reads base->type
// (its Type IS sized).
let arrlittn: *node = nil;
if (base != nil) { if (base.kind == nkind.N_ARRLIT) {
arrlittn = base.lhs;
};};
// esz from the type table for an N_IDENT base (#76; mirrors the
// cgindex idiom) or an N_DOT array/slice-field base (#252: scale by
// the field's element width, not esz=1 — silently wrong for non-u8).
// Other non-ident bases stay esz=1 -> ptr unscaled.
let esz: i32 = 1;
if (baselocal != nil) {
esz = elemsizeofc(c, baselocal.tnode);
} else { if (globaltn != nil) {
esz = elemsizeofc(c, globaltn);
} else { if (dotbu != nil && dotbu.sub != nil) {
esz = dotbu.sub.size: i32;
} else { if (arrlittn != nil) {
esz = elemsizeofc(c, arrlittn);
};};};};
// 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 (globaltn != nil) {
// Top-level let: [N]T → LEAQ name(SB); pointer/slice/str
// → MOVQ name(SB) (the symbol holds the {ptr,len,cap} or
// {ptr,len} or pointer value).
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 { if (base != nil && base.kind == nkind.N_ARRLIT
&& arrlittn != nil) {
// #31: materialise the array literal into a FRESH per-borrow
// @slicescr stack slot (distinct slot per borrow — a borrow's
// backing must outlive the lowering, so it can't share a cached
// slot; localalloc is always-fresh, mirror of cstage local_alloc),
// fill it via the shared element-fill, then LEAQ the slot as base.
// Size/count NODE-wise off the stashed [count]T tnode (#8: tinfo
// primitive sizes are 0). Escape (WHY, rob): a `let xs:[]T=[..];
// return xs;` returns a slice into this frame slot, freed on
// return = dangling — IDENTICAL to the named-array borrow and
// Hare-consistent (no escape analysis / GC / heap promotion; a
// local borrowed past its frame is a footgun, not promoted).
let cnt: i32 = 0;
if (arrlittn.rhs != nil) {
if (arrlittn.rhs.kind == nkind.N_INTLIT) {
cnt = arrlittn.rhs.uval: i32;
};
};
let bsz: i32 = elemsizeofc(c, arrlittn) * cnt;
if (bsz < 1) { bsz = 1; };
let scr: i32 = localalloc(c, "@slicescr", bsz, nil);
cgarrlitfillbp(c, arrlittn, base, scr);
emitline("\tLEAQ\t");
emitoff(scr: i64);
emitline("(BP), AX\n");
} else { if (base != nil) {
// #252: N_DOT `[N]T`-field base → field ADDRESS via
// dotbaseaddr (LEAQ), not the auto-deref VALUE load cgexpr
// emits. Sibling of the #135 read-side.
if (!dotbaseaddr(c, base, "AX")) {
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 { if (globaltn != nil) {
let handled: bool = false;
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");
handled = true;
};
};
} else { if (globaltn.kind == nkind.N_TSLICE) {
emitline("\tLEAQ\t");
emitsymname(c, globalname);
emitline("(SB), CX\n");
emitline("\tMOVQ\t8(CX), AX\n");
handled = true;
};};
if (!handled) { emitline("\tMOVQ\t$0, AX\n"); };
} else { if (dotbu != nil && dotbu.kind == tykind.TY_ARRAY) {
// #252: default-hi `s.obuf[lo:]` on a struct array-field →
// element count from the field's array tinfo. Cstage twin
// cgexpr_int(bu->alen) (cgen.c N_SLICE default-hi).
emitline("\tMOVQ\t$");
emitint(dotbu.alen: i64);
emitline(", AX\n");
} else { if (arrlittn != nil) {
// #31: default-hi for the arrlit base = its element count (the
// stashed [count]T tnode's .rhs intlit).
let hc: i64 = 0i64;
if (arrlittn.rhs != nil) {
if (arrlittn.rhs.kind == nkind.N_INTLIT) {
hc = arrlittn.rhs.uval: i64;
};
};
emitline("\tMOVQ\t$");
emitint(hc);
emitline(", AX\n");
} else {
emitline("\tMOVQ\t$0, AX\n");
};};};};};
emitline("\tMOVQ\tAX, BX\n");
emitline("\tPOPQ\tCX\n");
emitline("\tPOPQ\tAX\n");
// ptr = base + lo*esz (#76; ensure.ha:30 membsz-unit).
// DX=lo*esz; CX=lo PRESERVED for len + cap (#20).
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", DX\n");
emitline("\tIMULQ\tCX, DX\n");
emitline("\tADDQ\tDX, AX\n");
} else {
emitline("\tADDQ\tCX, AX\n");
};
emitline("\tSUBQ\tCX, BX\n");
// cap = base_cap - lo (#20); CX=lo, BX=len here.
if (cgbasecap(c, base, "DX")) {
emitline("\tSUBQ\tCX, DX\n");
emitline("\tMOVQ\tDX, CX\n");
} else {
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, arr[i], p.field,
// ?, etc.). Spill into an `@match_spill` scratch slot
// and dispatch off it. Tagged returns (N_CALL) follow
// the AX:DX:CX[:R8] convention; tagged-element loads
// (N_INDEX) and tagged-field loads (N_DOT, fixed by
// #28) produce the same triple. Nullable returns are
// single-word (AX = ptr); only +0 is read.
// Scrutinee type + spill size resolved through matchscrutt
// / matchspillsz at first use (#15) — see cgenutil.ww
// (task #9 align-down to cstage).
scrutt = matchscrutt(c, scrut);
let spillsz: i32 = matchspillsz(c, scrutt);
scrutoff = localalloc(c, "@match_spill", spillsz, nil);
// #38b: sret-classified tagged call scrutinee — pass
// the scrut slot itself as the sret dest and skip the
// cursor spill; downstream tag dispatch / case-let
// binds already read the slot from memory. Mirrors
// cstage cgen.c N_MATCH.
let msret: i32 = 0;
if (scrut.kind == nkind.N_CALL) {
msret = callsretsize(c, scrut);
};
if (msret > 0) {
c.sretdestoff = scrutoff;
cgexpr(c, scrut);
c.sretdestoff = 0;
} else {
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");
// CX/R8 writes gated on spill size so 1-word-
// payload variants (slot 16B) don't bump the
// frame past the tag+word0 the receiver reads.
// Mirrors cmd/w6c/cgen.c cgmatch's
// `if (slot_size > 16)` / `> 24` guards.
if (spillsz > 16) {
emitline("\tMOVQ\tCX, ");
emitoff((scrutoff + 16): i64);
emitline("(BP)\n");
};
if (spillsz > 24) {
emitline("\tMOVQ\tR8, ");
emitoff((scrutoff + 24): 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) {
// #67: gate on the stamped tinfo, not the node kind
// — matchscrutt now returns the scrutinee node itself
// for an N_DOT field (its .type_ is the tagged tinfo)
// rather than the resolved N_TTAGGED node.
if (istaggedtype(c, scrutt)) {
let r: i32 = -1;
let pattype: *tinfo = pat.type_: *tinfo;
if (pattype != nil) {
// #179: kind-agnostic dispatch on the resolved
// tinfo. Cstage cg_tag_for_variant works on
// Type, so N_TPTR / N_TFN / N_TPTR(N_TFN) case-
// patterns all reach typeeq; the prior pat.kind
// gate dropped them to r=-1 → tag 0 collapse.
// Slice arm still goes through flatslicevariantidx
// (task #19 untyped-elem fallback when typeeq
// can't match a (scalar | []T) shape).
if (typeisslice(pattype)) {
r = flatslicevariantidx(c, scrutt, pat.lhs);
} else {
r = flatvariantidxt(scrutt.type_: *tinfo, pattype);
};
};
if (r >= 0) { want = r; };
};
};
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 {
// Size the bind from the variant's declared
// layout. slotsize covers str (16), []T (24),
// N_TNAME named struct (si.totsize), aliases,
// tuples, primitives (8). Hardcoding str/slice
// + fall-through-8 dropped the high words of a
// TY_STRUCT variant (e.g. only v.x reached the
// bind for `case let v: pair`, project #31);
// mirrors cstage's `bu->size` fallback in
// cgen.c cgmatch.
let bsz: i32 = slotsize(c, pat);
if (bsz <= 0) { bsz = 8; };
// 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);
// Word-by-word copy. Round bsz up to 8 in case
// a non-multiple-of-8 struct size leaked through
// (registerstruct already pads totsize, but be
// defensive — same shape as cstage's nwords =
// (bsz + 7) / 8).
let nwords: i32 = (bsz + 7) / 8;
let bw: i32 = 0;
for (bw < nwords) {
emitline("\tMOVQ\t");
emitoff((scrutoff + 8 + 8 * bw): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((voff + 8 * bw): i64);
emitline("(BP)\n");
bw += 1;
};
};
};
};
// 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;
// `(*p).f` read retarget: parser produces n.lhs = N_UN(STAR,
// IDENT(p)). Substitute the inner IDENT as dotlhs so the
// pointer-auto-deref branch (lhs.kind == N_IDENT && N_TPTR
// tnode) fires the same as `p.f`. Mirror of the N_ASSIGN N_DOT
// lhs retarget in cgassign. v1 scope: N_IDENT inner only;
// (*expr).f follow-up task pending. Enum-leaf lookup above and
// chained-N_DOT branches below keep checking raw lhs since
// (*p) is neither shape.
let dotlhs: *node = lhs;
if (dotlhs != nil) {
if (dotlhs.kind == nkind.N_UN) {
if (dotlhs.op == tkind.TK_STAR) {
if (dotlhs.lhs != nil) {
if (dotlhs.lhs.kind == nkind.N_IDENT) {
dotlhs = dotlhs.lhs;
};
};
};
};
};
// Enum member access: `EnumName.MEMBER` or `pkg.EnumName.MEMBER`
// → inline the pre-computed constant. `pkg.Enum.MEMBER` keeps
// `pkg` so enumlookupmod can prefer the explicit module on a
// leaf collision; bare `Enum.MEMBER` falls back to c.curmod via
// enumlookup's same-module-first walk.
if (lhs != nil) {
let etname: str;
let etmod: str;
etname.ptr = nil; etname.len = 0;
etmod.ptr = nil; etmod.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;
etmod = lhs.lhs.str;
};
};
};
if (etname.len > 0) {
let en: *enumtype = enumlookupmod(c, etname, etmod);
if (en != nil) {
let v: u64;
if (enummemberval(en, fld, &v)) {
emitline("\tMOVQ\t$");
emitint(v: i64);
emitline(", AX\n");
return;
};
};
};
};
if (dotlhs != nil) {
if (dotlhs.kind == nkind.N_IDENT) {
let nm: str = dotlhs.str;
let lc: *local = localfindnode(c, nm);
if (lc != nil) {
let tn: *node = lc.tnode;
// Receiver is a NAMED alias chain — peel via aliaslookup
// until tn exposes a non-N_TNAME kind (or a struct alias).
// Without this, `type vs = *vt` leaves lkind == N_TNAME and
// structlookupchain misses (vs is not a struct), so we fall
// through to the SB-global fallback and emit a wrong
// `MOVQ <fld>(SB), AX`. Mirror cstage type_chase_named
// (cmd/w6c/cgen.c:144-155); LOOP, not single-peel — Phase-N
// builds NAMED chains (project_tinfo_lossy_nominal). Stops
// at struct aliases so the existing direct-struct arm below
// stays byte-id with pre-fix #22 callers. #191.
//
// #223: the break is MODULE-AWARE. cstage type_chase_named
// follows the resolved NAMED.under pointer (module-correct);
// wwstage re-resolves by name (lossy), so a same-module
// alias whose leaf collides with a FOREIGN struct of the
// same name (io.stream = *vtable vs memio.stream struct)
// would wrongly halt the peel at the foreign struct via
// structlookup's any-module fallback → field load drops to a
// bogus `MOVQ <fld>(SB)`. Sibling of #208 (lossy name-keyed
// resolution leaking to a global leaf). Break ONLY on a
// same-module struct (a genuine struct-value receiver); a
// same-module alias keeps peeling; a foreign leaf (in
// neither registry for c.curmod) falls back to the prior
// any-module heuristic. The broader cross-module same-leaf
// STRUCT name-keying at the direct-struct arm below is
// filed separately as #224 (not FLIP-triggered).
for (tn != nil && tn.kind == nkind.N_TNAME) {
if (structsamemod(c, tn.str) != nil) { break; };
let nx: *node = aliassamemod(c, tn.str);
if (nx == nil) {
if (structlookup(c, tn.str) != nil) { break; };
nx = aliaslookup(c, tn.str);
if (nx == nil) { break; };
};
tn = nx;
};
if (tn == nil) { return; };
let lkind: nkind = 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) {
// structlookupchain walks the alias chain on
// a miss so `*tokenizer` where tokenizer is
// a transitively-aliased struct still
// resolves to the underlying fieldinfo (#22).
let si: *structinfo = structlookupchain(c, inner);
if (si != nil) {
let fi: *fieldinfo = si.fields;
for (fi != nil) {
let fn_: str = fi.fname;
if (streq(fn_, fld)) {
// tagged-union field via *struct: stage
// the *struct in BX, then load the four
// payload regs via cgloadtaggedfield.
// BX isn't a target (AX/DX/CX/R8), so
// load order doesn't matter. Mirrors
// the direct-local branch above so the
// match / let-init / call-arg consumer
// shape is identical regardless of
// pointer rooting.
if (istaggedtype(c, fi.tnode)) {
let tsz: i32 = slotsize(c, fi.tnode);
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
cgloadtaggedfield(c, "BX",
fi.foff, tsz);
return;
};
// str IS []u8 — same 3-word {ptr,len,cap}
// as a slice field via *struct: load
// (ptr, len, cap) into (AX, BX, CX). BX
// holds the *struct pointer, so load .len
// LAST so the earlier reads still index
// off the base. str folds onto the slice
// arm (#1/Phase 3 collapse; cite cstage
// cgen.c N_DOT *struct S2).
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
if (isstrtype(c, fi.tnode) || isslicetype(c, fi.tnode)) {
emitline("\tMOVQ\t");
emitdispreg(fi.foff: i64, "BX");
emitline(", AX\n");
emitline("\tMOVQ\t");
emitdispreg((fi.foff + 16): i64, "BX");
emitline(", CX\n");
emitline("\tMOVQ\t");
emitdispreg((fi.foff + 8): i64, "BX");
emitline(", 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(c, 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) {
// structlookupchain walks the alias chain on
// miss so a transitively-aliased struct (`type
// b = a; a = struct`) still resolves to the
// underlying fieldinfo (#22).
let si: *structinfo = structlookupchain(c, tn);
if (si != nil) {
let fi: *fieldinfo = si.fields;
for (fi != nil) {
let fn_: str = fi.fname;
if (streq(fn_, fld)) {
// tagged-union field: emit the AX=tag,
// DX=word0, CX=word1[, R8=word2] load
// sequence so the match / let-init /
// call-arg consumers see the same shape
// as a tagged-returning fn. Pre-#28 fell
// through to the scalar fieldloadop and
// only AX (tag) was loaded — payload
// words came from whatever the caller
// left in DX/CX/R8.
if (istaggedtype(c, fi.tnode)) {
let tsz: i32 = slotsize(c, fi.tnode);
cgloadtaggedfield(c, "BP",
lc.off + fi.foff, tsz);
return;
};
// str IS []u8 — same 3-word {ptr,len,cap}
// as a slice field: load (ptr, len, cap)
// into (AX, BX, CX). Base is BP so no
// aliasing — order doesn't matter. str
// folds onto the slice arm (#1/Phase 3
// collapse; cite cstage cgen.c N_DOT S1).
if (isstrtype(c, fi.tnode) || isslicetype(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");
emitline("\tMOVQ\t");
emitoff((lc.off + fi.foff + 16): i64);
emitline("(BP), CX\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(c, 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: scalar in an
// 8B slot, str in 24B — str IS []u8, #1/Phase 3). For a
// str element, load (ptr, len, cap) into (AX, BX, CX),
// the canonical slice-header ABI. No slice-element
// sibling here, so the triple is hand-authored; base is
// BP (frame, not a target reg) so ptr/len/cap order has
// no clobber risk.
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.lhs);
tp = tp.next;
i += 1;
};
};
if (tp != nil) {
let tpt: *node = tp.lhs;
if (isstrtype(c, tpt)) {
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");
emitline("\tMOVQ\t");
emitoff((lc.off + foff + 16): i64);
emitline("(BP), CX\n");
return;
};
// f64/f32 tuple field must ride X0 via
// MOVSD/MOVSS; the integer load op left it
// in AX (#103 FACE Z). Mirrors the float
// local load above and cstage cgen.c:1462,
// 1838 (the #96 pattern).
if (isfloattype(c, tpt)) {
let mov: str = "MOVSD";
if (isf32type(c, tpt)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\t");
emitoff((lc.off + foff): i64);
emitline("(BP), X0\n");
return;
};
let sz: i32 = slotsize(c, tpt);
let op: str = tnodeloadop(c, tpt, sz);
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");
emitbytes( 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; };
// str IS []u8: .cap is valid on a str global too,
// not slice-only — mirrors cstage (#1/Phase 3, #11).
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 [N]T global pseudo-fields (#7): `.len` is the static
// element count (immediate from the array type node's length child);
// `.ptr` is the array's base address (LEAQ name(SB)). Without this a
// module-level array's `x.len` falls to the module-qualified SB
// fallback below and mis-emits `MOVQ len(SB), AX` (linker: undefined
// reference to len). Mirror of the local-array arm above and cstage
// cg_base_cap's `aimm(bu->alen)` immediate (cgen.c:1692).
if (lhs != nil) {
if (lhs.kind == nkind.N_IDENT) {
let gtn: *node = letvartnode(c, lhs.str);
if (gtn != nil) {
if (gtn.kind == nkind.N_TARRAY) {
if (streq(fld, "ptr")) {
emitline("\tLEAQ\t");
emitsymname(c, lhs.str);
emitline("(SB), AX\n");
return;
};
if (streq(fld, "len")) {
let lenn: *node = gtn.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;
};
};
};
};
};
// 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.
// #129 A.2: also handles struct-typed `def`s via defvarstructinfo;
// emitstructdata gives them DATA storage at name(SB), and this
// LEAQ-and-offset shape mirrors the let path. Pre-A.2 the def
// fell through to the integer-let MOVQ catch-all (reading garbage
// from the wrong offset).
if (lhs != nil) {
if (lhs.kind == nkind.N_IDENT) {
let si: *structinfo = letvarstructinfo(c, lhs.str);
if (si == nil) { si = defvarstructinfo(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");
// tagged-union field: load via the tagged-
// return ABI off CX. cgloadtaggedfield orders
// the loads so CX (word1 target) is written
// LAST — otherwise the base address would be
// trashed before the +24/R8 (slice variant)
// read could index off it. Pre-#28 fell
// through to fieldloadop and dropped payload.
if (istaggedtype(c, fi.tnode)) {
let tsz: i32 = slotsize(c, fi.tnode);
cgloadtaggedfield(c, "CX", fi.foff, tsz);
return;
};
// str IS []u8 — 3-word {ptr,len,cap}, the local
// slice-field arm (BP) retargeted to the CX global
// base. cap→CX LAST: CX is the base, so .ptr/.len
// must read first. cstage folds local+global in one
// base_reg arm; ww splits them, so this global arm
// carries its own lift (filed divergence task).
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");
emitline("\tMOVQ\t");
emitdispreg((fi.foff + 16): i64, "CX");
emitline(", CX\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(c, fi);
emitline("\t");
emitline(op);
emitline("\t");
emitdispreg(fi.foff: i64, "CX");
emitline(", AX\n");
}; };
return;
};
fi = fi.finext;
};
};
};
};
// `arr[i].field` — element-then-field through a `[N]*S` / `[N]S`
// (and slice/`*[N]S`) base. Without this the cgen falls through
// to the module-qualified SB fallback below and emits
// `MOVQ <fld>(SB), AX` (linker: `undefined reference to <fld>`).
// One branch covers both shapes: compute `&arr[i]` into BX, then
// either deref (`*Struct` element) or move-to-AX (value `Struct`
// element), so the leaf load is `(field.offset)(AX)` either way.
// Bypasses cgindex deliberately — cgindex's final MOVQ would
// truncate a value-struct element to 8 bytes.
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);
// #21 (READ twin of #11): a module-GLOBAL base makes
// localfindnode return nil, so this field-offset-aware
// branch was skipped and `g[i].field` fell through to
// the module-qualified fallback below (garbage — no
// main.g load at all). Resolve the global's tnode via
// letvartnode/defvartnode (the same source cgindex's
// global arm uses) and dispatch the base load by shape:
// array -> LEAQ name(SB) (the symbol IS the storage),
// slice/ptr -> MOVQ name(SB) (the symbol's first word
// IS the .ptr). Mirrors cstage cgen.c's #21 arm.
let tn: *node = nil;
let isglobal: bool = false;
let gname: str;
gname.ptr = nil; gname.len = 0;
if (lc != nil) {
tn = lc.tnode;
} else {
tn = letvartnode(c, idxbase.str);
if (tn == nil) { tn = defvartnode(c, idxbase.str); };
if (tn != nil) { isglobal = true; gname = idxbase.str; };
};
if (tn != nil) {
let elemt: *node = nil;
let baseisarray: bool = false;
let tk: nkind = tn.kind;
if (tk == nkind.N_TSLICE) { elemt = tn.lhs; };
if (tk == nkind.N_TARRAY) { elemt = tn.lhs; baseisarray = true; };
if (tk == nkind.N_TPTR) { elemt = tn.lhs; };
let sname: str;
sname.ptr = nil; sname.len = 0;
let viaptr: bool = false;
if (elemt != nil) {
if (elemt.kind == nkind.N_TPTR) {
let inner: *node = elemt.lhs;
if (inner != nil) { if (inner.kind == nkind.N_TNAME) {
sname = inner.str;
viaptr = true;
};};
} else { if (elemt.kind == nkind.N_TNAME) {
sname = elemt.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 esz: i32 = elemsizeofc(c, tn);
cgexpr(c, lhs.rhs); // idx → AX
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", CX\n");
emitline("\tIMULQ\tCX, AX\n");
};
if (isglobal) {
if (baseisarray) {
emitline("\tLEAQ\t");
emitsymname(c, gname);
emitline("(SB), BX\n");
} else {
emitline("\tMOVQ\t");
emitsymname(c, gname);
emitline("(SB), BX\n");
};
} else {
if (baseisarray) {
emitline("\tLEAQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
} else {
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
};
};
emitline("\tADDQ\tAX, BX\n");
if (viaptr) {
emitline("\tMOVQ\t(BX), AX\n");
} else {
emitline("\tMOVQ\tBX, AX\n");
};
// #270-1a: an `[N]T`-typed field of an
// array element (`a[i].m[j]`) — leave the
// field's ADDRESS, a base for the outer
// index, NEVER deref. AX holds &a[i]; the
// field address is &a[i]+foff. The #135
// read-side for `d.m[i]`, applied to an
// array-element base. Without this an array
// field fell to fieldloadop below and loaded
// its first 8 bytes as a value → garbage
// base → SEGFAULT in the outer index.
if (tinfoisarray(fi.tnode.type_: *tinfo)) {
if (fi.foff != 0) {
emitline("\tADDQ\t$");
emitint(fi.foff: i64);
emitline(", AX\n");
};
return;
};
if (isstrtype(c, fi.tnode) || isslicetype(c, fi.tnode)) {
// str/slice: the 3-word {ptr,len,cap}
// slice header (#1). AX holds the
// element base, so load .ptr (which
// targets AX) LAST. Matches the
// caseB *struct slice arm and
// cgslicehdr(D_AX).
emitline("\tMOVQ\t");
emitdispreg((fi.foff + 8): i64, "AX");
emitline(", BX\n");
emitline("\tMOVQ\t");
emitdispreg((fi.foff + 16): i64, "AX");
emitline(", CX\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(c, 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` for the 8B case;
// signed-narrow leaves route through LEAQ + localloadop so a
// prior narrow deref-store doesn't leave stale upper bytes. Same
// fallback the C cgen takes when bt is NULL/tyerr.
if (lhs != nil) {
if (lhs.kind == nkind.N_IDENT) {
// `let p = mod.fn` — fn rvalue via N_DOT. Mirror of
// cstage cgdot's TY_FN branch (mafn with module hint).
// Without this the MOVQ leaf(SB) fallback below would
// load 8 bytes of fn-prologue code into AX instead of
// the fn address.
// lhs.str is the explicit module hint so a same-leaf
// def in another module (head of c.fnrets) can't shadow
// the explicit qualifier (#17 N_DOT-arm omission audit).
let frt: *node = fnretlookupmod(c, fld, lhs.str);
if (frt != nil) {
emitline("\tLEAQ\t");
emitfnname(c, fld, lhs.str);
emitline("(SB), AX\n");
return;
};
// `mod.MSG` where MSG is `def MSG: str = "..."` —
// strlit-inline matches cstage Sdef walk #2 in
// cmd/w6c/cgen.c N_DOT mod-qualified. Without this
// the MOVQ leaf(SB) fallback emits a bogus ref
// (`alpha.MSG(SB)`, never DATAW-defined). lhs.str is
// the explicit module hint — a 3rd-module qualifier
// `alpha.MSG` from gamma needs alpha (not c.curmod)
// to beat a head-of-c.defs beta.MSG collision (#11).
let drhs: *node = deflookuprhsmod(c, fld, lhs.str);
if (drhs != nil) {
if (drhs.kind == nkind.N_STRLIT) {
let bytes: str = drhs.str;
let lab: str = internstrlit(c, bytes);
emitline("\tLEAQ\t");
emitbytes( lab.ptr, lab.len: u64);
emitline("(SB), AX\n");
emitline("\tMOVQ\t$");
emitint(bytes.len: i64);
emitline(", BX\n");
return;
};
};
let mqop: str = localloadop(c, letvartnode(c, fld));
// #229: thread the dotted module (lhs.str), not c.curmod
// — the non-preferring emitsymname mis-mangled `aa.v` onto
// a same-leaf global. The TY_FN branch above already
// threads lhs.str via emitfnname.
if (streq(mqop, "MOVQ")) {
emitline("\tMOVQ\t");
emitsymnamehint(c, fld, lhs.str);
emitline("(SB), AX\n");
} else {
emitline("\tLEAQ\t");
emitsymnamehint(c, fld, lhs.str);
emitline("(SB), CX\n");
emitline("\t");
emitline(mqop);
emitline("\t(CX), AX\n");
};
return;
};
};
// Chained N_DOT spine through value-struct fields (any depth).
// Walks the spine to a root ident, summing field offsets, then
// emits ONE load at base + total_off. Also handles a slice/str
// pseudo-field leaf (`b.buf.len`): the walk lands on the slice/
// str header and slicedelta picks ptr/len/cap. Mirror of cstage
// cgen.c's chained-DOT read branch. Without this, depth ≥ 3
// shapes (`v.a.a.a`) and `b.buf.len` fall through to the non-
// ident-base pseudo branch below — which would cgexpr the inner
// (loading only .ptr into AX) and shuffle stale BX into AX.
// Placed BEFORE the .ptr/.len fast paths so the chain wins.
if (lhs != nil) {
if (lhs.kind == nkind.N_DOT) {
let rootname: str = "";
let rootoff: i32 = 0;
let totaloff: i32 = 0;
let leaftype: *tinfo = nil;
let slicedelta: i32 = -1;
let isglobal: bool = false;
let ptrroot: bool = false;
let pok: bool = dotchainresolve(c, n,
&rootname, &rootoff, &totaloff,
&leaftype, &slicedelta, &isglobal, &ptrroot);
if (pok) {
// `*T` root: load the pointer slot once into CX,
// then index every leaf at total_off off CX. Same
// emit shape as the global path (LEAQ → CX) — only
// the loader instruction differs.
let viacx: bool = isglobal || ptrroot;
if (slicedelta >= 0) {
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), CX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), CX\n");
};
emitline("\tMOVQ\t");
emitdispreg((totaloff + slicedelta): i64, "CX");
emitline(", AX\n");
} else {
emitline("\tMOVQ\t");
emitoff((rootoff + totaloff + slicedelta): i64);
emitline("(BP), AX\n");
};
return;
};
if (typeisstr(leaftype)) {
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), CX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), CX\n");
};
emitline("\tMOVQ\t");
emitdispreg(totaloff: i64, "CX");
emitline(", AX\n");
emitline("\tMOVQ\t");
emitdispreg((totaloff + 8): i64, "CX");
emitline(", BX\n");
} else {
emitline("\tMOVQ\t");
emitoff((rootoff + totaloff): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\t");
emitoff((rootoff + totaloff + 8): i64);
emitline("(BP), BX\n");
};
return;
};
if (typeisslice(leaftype)) {
// Slice leaf: load all three header words into
// (AX=ptr, BX=len, CX=cap). For the viacx path
// (global or `*T` root) CX is the base; load
// .cap LAST so the base survives the earlier
// reads. For BP-rooted locals the registers
// don't alias so order is free.
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), CX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), CX\n");
};
emitline("\tMOVQ\t");
emitdispreg(totaloff: i64, "CX");
emitline(", AX\n");
emitline("\tMOVQ\t");
emitdispreg((totaloff + 8): i64, "CX");
emitline(", BX\n");
emitline("\tMOVQ\t");
emitdispreg((totaloff + 16): i64, "CX");
emitline(", CX\n");
} else {
emitline("\tMOVQ\t");
emitoff((rootoff + totaloff): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\t");
emitoff((rootoff + totaloff + 8): i64);
emitline("(BP), BX\n");
emitline("\tMOVQ\t");
emitoff((rootoff + totaloff + 16): i64);
emitline("(BP), CX\n");
};
return;
};
if (typeisfloat(leaftype)) {
let mov: str = "MOVSD";
if (typeisf32(leaftype)) { mov = "MOVSS"; };
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), CX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), CX\n");
};
emitline("\t");
emitline(mov);
emitline("\t");
emitdispreg(totaloff: i64, "CX");
emitline(", X0\n");
} else {
emitline("\t");
emitline(mov);
emitline("\t");
emitoff((rootoff + totaloff): i64);
emitline("(BP), X0\n");
};
return;
};
let lop: str = loadopsz(typeissigned(leaftype),
leaftype.slotsize: i32);
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), CX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), CX\n");
};
emitline("\t");
emitline(lop);
emitline("\t");
emitdispreg(totaloff: i64, "CX");
emitline(", AX\n");
} else {
emitline("\t");
emitline(lop);
emitline("\t");
emitoff((rootoff + totaloff): i64);
emitline("(BP), AX\n");
};
return;
};
};
};
// Non-ident base pseudo-field: e.g. `"abc".ptr` / `"abc".len`.
// A string literal is TY_UNTYPED_STR, so it misses the typed
// slice/str gate above and lands here. Evaluate the str-producing
// expression — that leaves (AX=ptr, BX=len). Then `.ptr` returns
// AX as is; `.len` shuffles BX→AX. cstage cgen.c was aligned UP
// to this shuffle in #14 (it had returned the ptr for `.len`).
if (streq(fld, "ptr")) { cgexpr(c, lhs); return; };
if (streq(fld, "len")) {
cgexpr(c, lhs);
emitline("\tMOVQ\tBX, AX\n");
return;
};
// .cap on a non-ident base (indexed element `t[i].cap`, call,
// dot-slice): cgexpr leaves the full {ptr,len,cap} header via
// cgslicehdr — shuffle CX→AX. The shuffle fires ONLY for a TYPED
// slice/str base (kind TY_SLICE/TY_STR after NAMED-chase) that is NOT
// a bare string literal: N_STRLIT's cgexpr loads only AX=ptr/BX=len
// (cgen.ww), never a CX cap, so `"abc".cap` must return AX unshuffled.
// cstage reaches that outcome by typing N_STRLIT as untyped_str
// (cgen.c:8456 catch-all, no cap shuffle); the wwstage checker types
// N_STRLIT as `str` instead (check.ww:2322 vs cstage check.c:1079 —
// divergence filed separately), so the TY_STR kind-gate alone would
// wrongly fire. The N_STRLIT exclusion keeps this byte-identical with
// cstage. #13 read-fix, sibling of the #20 store.
if (streq(fld, "cap")) {
cgexpr(c, lhs);
if (lhs != nil && lhs.kind != nkind.N_STRLIT) {
let lu: *tinfo = lhs.type_: *tinfo;
for (lu != nil && lu.kind == tykind.TY_NAMED) { lu = lu.under; };
if (lu != nil && (lu.kind == tykind.TY_SLICE
|| lu.kind == tykind.TY_STR)) {
emitline("\tMOVQ\tCX, 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) {
// #70 (#12): inner-struct layout via the stamped lhs.type_
// (peel *→struct) + tinfo.fields, replacing dotinnerstructptr's
// structinfo walk. Gate is strict-equal to the deleted helper:
// fire only when the chain root is a LOCAL ident AND every dot
// in the chain resolves through a *struct (dotinnerstructptr
// recursed per level on a *struct field and bailed on a by-
// value-struct intermediate). Reproducing that exactly avoids
// an untested widening past cstage; a deliberate widen, if ever
// wanted, is a future task with its own probe. Global-root
// chains stay in their pre-existing shared base-eval breakage
// (filed #27), untouched here.
let croot: *node = lhs;
let allptr: bool = true;
for (croot != nil && croot.kind == nkind.N_DOT) {
let ct: *tinfo = croot.type_: *tinfo;
for (ct != nil && ct.kind == tykind.TY_NAMED) { ct = ct.under; };
let okp: bool = false;
if (ct != nil) { if (ct.kind == tykind.TY_PTR) {
let cs: *tinfo = ct.sub;
for (cs != nil && cs.kind == tykind.TY_NAMED) { cs = cs.under; };
if (cs != nil) { if (cs.kind == tykind.TY_STRUCT) { okp = true; }; };
}; };
if (!okp) { allptr = false; };
croot = croot.lhs;
};
let it: *tinfo = nil;
if (allptr && croot != nil && croot.kind == nkind.N_IDENT &&
localfindnode(c, croot.str) != nil) {
it = lhs.type_: *tinfo;
};
for (it != nil && it.kind == tykind.TY_NAMED) { it = it.under; };
if (it != nil) { if (it.kind == tykind.TY_PTR) {
let st: *tinfo = it.sub;
for (st != nil && st.kind == tykind.TY_NAMED) { st = st.under; };
if (st != nil) { if (st.kind == tykind.TY_STRUCT) {
let tf: *tfield = st.fields;
for (tf != nil) {
if (streq(tf.name, fld)) {
let ft: *tinfo = tf.type_;
cgexpr(c, lhs); // AX = ptr to inner struct
// str IS []u8 — same 3-word {ptr,len,cap}
// as a slice field: load (ptr, len, cap)
// into (AX, BX, CX). AX is the *struct
// base, so load .ptr (which targets
// AX) LAST. str folds onto the slice
// arm (#1/Phase 3 collapse; cite cstage
// cgen.c N_DOT chained *struct caseB).
if (typeisstr(ft) || typeisslice(ft)) {
emitline("\tMOVQ\t");
emitdispreg((tf.offset + 8u64): i64, "AX");
emitline(", BX\n");
emitline("\tMOVQ\t");
emitdispreg((tf.offset + 16u64): i64, "AX");
emitline(", CX\n");
emitline("\tMOVQ\t");
emitdispreg(tf.offset: i64, "AX");
emitline(", AX\n");
return;
};
// f64/f32 chained field: route through X0.
if (typeisfloat(ft)) {
let mov: str = "MOVSD";
if (typeisf32(ft)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\t");
emitdispreg(tf.offset: i64, "AX");
emitline(", X0\n");
return;
};
let lop: str = loadopsz(typeissigned(ft), ft.slotsize: i32);
emitline("\t");
emitline(lop);
emitline("\t");
emitdispreg(tf.offset: i64, "AX");
emitline(", AX\n");
return;
};
tf = tf.tnext;
};
}; };
}; };
};
};
// 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`.
// Kept as a fallback below the generalized walker above (placed
// earlier in cgdot) to preserve byte-identical output on shapes
// it already handles.
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(c, 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;
};
};
};
};};
};};
};
};
// Nested module-qualified field where the chain didn't fold to a
// known shape (raw w6c on a single file with `use mod;` but no
// driver concatenation — the inner enum / struct hasn't been
// seen). Emit `MOVQ <leaf>(SB), AX` so the linker surfaces a
// clean undefined-symbol error on the leaf. Mirror of
// cmd/w6c/cgen.c N_DOT nested fallback.
if (lhs != nil) {
if (lhs.kind == nkind.N_DOT) {
emitline("\tMOVQ\t");
emitsymname(c, fld);
emitline("(SB), AX\n");
return;
};
};
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 = 0;
if (n.lhs != nil) {
let lt: *tinfo = n.lhs.type_: *tinfo;
if (typeisf32(lt)) { fk = 1; }
else { if (typeisfloat(lt)) { fk = 2; }; };
};
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;
};
// Address-of has its own evaluation strategy — we want the address
// of the operand, not its value. Special-case here so `&arr[i]`
// doesn't compile the value load and then discard it.
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;
};
// #180: address-of a top-level fn name. Twin of
// the N_IDENT value-of-fn read-arm in cgident
// (LEAQ + emitfnname(c, nm, c.curmod)). Previously
// fell through silently — the AX-store at the
// assign site picked up whatever AX held.
if (fnretlookup(c, nm) != nil) {
emitline("\tLEAQ\t");
emitfnname(c, nm, c.curmod);
emitline("(SB), AX\n");
return;
};
if (isletvar(c, nm)) {
emitline("\tLEAQ\t");
emitsymname(c, nm);
emitline("(SB), AX\n");
return;
};
// #149/#147: address-of a top-level def with DATA
// storage. emitdefs / emitstructdata / emitarraydata
// all emit to emitsymname(name), so the address is
// the same LEAQ name(SB) as a let. Address-of twin of
// A.2/A.3's LOAD-side widening.
if (defisaddressable(c, opnd)) {
emitline("\tLEAQ\t");
emitsymname(c, nm);
emitline("(SB), AX\n");
return;
};
// rule-7: the name IS a def but has no DATA symbol
// (str def inlined, or computed-rhs float like
// `def NAN = 0.0/0.0`). Loud, not a wild deref.
if (deflookup(c, nm)) {
let m1: str = "ww: cannot take address of non-addressable def '";
os.write(2, m1.ptr, m1.len: u64);
os.write(2, nm.ptr, nm.len: u64);
let m2: str = "': no DATA symbol (str/computed-rhs def; #149/#147)\n";
os.write(2, m2.ptr, m2.len: u64);
os.exit(1);
};
return;
};
// Address-of through a DOT chain. Mirror of cstage
// cgen.c TK_AMP N_DOT branch. Three shapes converge
// here, all returning an 8B address (no fldloadop —
// just LEAQ / MOVQ+LEAQ).
//
// 1. Value-struct fields, any depth (`&o.f`,
// `&o.i.a`, `&o.a.b.c`) and slice/str pseudo-field
// tail (`&s.len`, `&b.buf.len`): the chained
// (depth ≥ 2) case reuses dotchainresolve; the
// single-DOT case is handled below by inspecting
// the IDENT base's tnode. Byte-identical to the
// cstage spine walker for both depths.
// 2. Pointer-field (`&p.f` where p:*T): single-DOT
// only; spine walker aborts on the *T base. Load
// p into AX, then LEAQ field_off(AX), AX. Mirror
// of the read at cgdot 1144.
if (opnd.kind == nkind.N_DOT) {
// Shape 1 chained: depth-≥2 via dotchainresolve.
// `opnd.lhs.kind == N_DOT` gates the helper at
// nsteps ≥ 2 (matches the read path's gate).
if (opnd.lhs != nil) {
if (opnd.lhs.kind == nkind.N_DOT) {
let rootname: str = "";
let rootoff: i32 = 0;
let totaloff: i32 = 0;
let leaftype: *tinfo = nil;
let slicedelta: i32 = -1;
let isglobal: bool = false;
let ptrroot: bool = false;
let pok: bool = dotchainresolve(c, opnd,
&rootname, &rootoff, &totaloff,
&leaftype, &slicedelta, &isglobal,
&ptrroot);
// `&` through a `*T`-rooted chain is a
// separate shape (would need MOVQ + LEAQ
// disp(CX), AX). Not exercised by current
// callers — skip and fall through.
if (ptrroot) { pok = false; };
if (pok) {
let extra: i32 = 0;
if (slicedelta >= 0) { extra = slicedelta; };
if (isglobal) {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), CX\n");
emitline("\tLEAQ\t");
emitdispreg((totaloff + extra): i64, "CX");
emitline(", AX\n");
} else {
emitline("\tLEAQ\t");
emitoff((rootoff + totaloff + extra): i64);
emitline("(BP), AX\n");
};
return;
};
};
};
// Shape 1/2 single-DOT on an IDENT base. Inspect
// the base's tnode to pick value-struct vs slice/
// str pseudo vs pointer-field.
if (opnd.lhs != nil) {
if (opnd.lhs.kind == nkind.N_IDENT) {
let basenm: str = opnd.lhs.str;
let fld: str = opnd.str;
let lc: *local = localfindnode(c, basenm);
if (lc != nil) {
let tn: *node = lc.tnode;
let lkind: nkind = nkind.N_NONE;
if (tn != nil) { lkind = tn.kind; };
// Pointer-field: &p.f where p:*T.
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) {
if (streq(fi.fname, fld)) {
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), AX\n");
emitline("\tLEAQ\t");
emitdispreg(fi.foff: i64, "AX");
emitline(", AX\n");
return;
};
fi = fi.finext;
};
};
};
};
// Value-struct local: &o.f.
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) {
if (streq(fi.fname, fld)) {
emitline("\tLEAQ\t");
emitoff((lc.off + fi.foff): i64);
emitline("(BP), AX\n");
return;
};
fi = fi.finext;
};
};
};
// Slice/str pseudo-field on a local:
// &s.ptr / &s.len / &s.cap. Delta is
// 0/8/16 — matches the spine walker.
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) {
let isslor: bool = false;
if (lkind == nkind.N_TSLICE) { isslor = true; };
if (lkind == nkind.N_TNAME) {
if (streq(tn.str, "str")) { isslor = true; };
};
if (isslor) {
emitline("\tLEAQ\t");
emitoff((lc.off + delta): i64);
emitline("(BP), AX\n");
return;
};
};
};
// Global root: top-level let, either a
// struct or a slice/str.
if (isletvar(c, basenm)) {
let gsi: *structinfo = letvarstructinfo(c, basenm);
if (gsi != nil) {
let fi: *fieldinfo = gsi.fields;
for (fi != nil) {
if (streq(fi.fname, fld)) {
emitline("\tLEAQ\t");
emitsymname(c, basenm);
emitline("(SB), CX\n");
emitline("\tLEAQ\t");
emitdispreg(fi.foff: i64, "CX");
emitline(", AX\n");
return;
};
fi = fi.finext;
};
};
let isstr: bool = letvarisstr(c, basenm);
let issl: bool = letvarisslice(c, basenm);
if (isstr || issl) {
let gdelta: i32 = -1;
if (streq(fld, "ptr")) { gdelta = 0; };
if (streq(fld, "len")) { gdelta = 8; };
// str IS []u8: &str.cap is valid too, not slice-only
// — mirrors cstage (#1/Phase 3, #11).
if (streq(fld, "cap")) { gdelta = 16; };
if (gdelta >= 0) {
emitline("\tLEAQ\t");
emitsymname(c, basenm);
emitline("(SB), CX\n");
emitline("\tLEAQ\t");
emitdispreg(gdelta: i64, "CX");
emitline(", AX\n");
return;
};
};
};
};
};
// #149 Shape 2: `&mod.G` module-qualified address-of
// of an exported global (let or def). The base is an
// N_IDENT that's neither a local nor a global let, so
// it's an SK_USE module qualifier; LEAQ the leaf
// symbol. Kind-agnostic (covers cross-module &let /
// &def / &scalar) — the address-of twin of the value-
// read mod-qual path (cgenexpr.ww). A fn leaf resolves
// via emitfnname (fn address), mirroring that read
// path's TY_FN branch.
if (opnd.lhs != nil) {
if (opnd.lhs.kind == nkind.N_IDENT) {
let basenm: str = opnd.lhs.str;
if (localfindnode(c, basenm) == nil) {
// A def base (`&Pdef.field`) is NOT a module
// qualifier: cstage's Shape-2 gate (base
// type_ == NULL/ty_err) excludes it because
// the checker types a def-struct/def-array
// base, but the ww gate (not-local && not-let)
// does not. Without this guard a def base would
// mis-LEAQ the field leaf (e.g. `y(SB)`) while
// cstage silent-drops, breaking cs==ww (rule
// 10). Excluding defs restores byte-id; the
// `&def.field` silent-drop itself is a separate
// pre-#149 gap (file as #150-family).
if (!isletvar(c, basenm) && !deflookup(c, basenm)) {
let fld: str = opnd.str;
let frt: *node = fnretlookupmod(c, fld, basenm);
if (frt != nil) {
emitline("\tLEAQ\t");
emitfnname(c, fld, basenm);
emitline("(SB), AX\n");
return;
};
// #229: dotted-module value mangle
// (basenm), twin of the read — so
// &aa.v takes aa's global, not a
// same-leaf collision.
emitline("\tLEAQ\t");
emitsymnamehint(c, fld, basenm);
emitline("(SB), AX\n");
return;
};
};
};
};
// Fall through silently (mirrors cstage silent-
// drop fallback at the end of the TK_AMP block).
return;
};
if (opnd.kind == nkind.N_INDEX) {
// &base[i] = base + i*esz, no dereference.
let base: *node = opnd.lhs;
let idx: *node = opnd.rhs;
let esz: i32 = 8;
let isglobalarr: bool = false;
let isglobalptr: bool = false;
let globalname: str;
globalname.ptr = nil; globalname.len = 0;
let baselocal: *local = nil;
let isarr: bool = false;
if (base != nil) {
if (base.kind == nkind.N_IDENT) {
baselocal = localfindnode(c, base.str);
if (baselocal != nil) {
esz = elemsizeofc(c, baselocal.tnode);
let tn: *node = baselocal.tnode;
if (tn != nil) {
if (tn.kind == nkind.N_TARRAY) { isarr = true; };
};
} else {
// #11: addr-of twin of the #10 cgindex read
// fix. Dispatch esz + base load off the
// global's RESOLVED type, NOT an N_TARRAY/
// N_TPTR kind whitelist — a global str (tnode
// N_TNAME) / slice (N_TSLICE) matched NEITHER
// old arm, so esz stayed at the default 8 and
// the base fell to the complex-base fallback,
// yielding a wide-stride &s[i]. cstage's
// TK_AMP N_INDEX (cmd/w6c/cgen.c) is uniform:
// esz=bu->sub->size, base is_arr?LEAQ:MOVQ
// name(SB) (a str/slice's .ptr IS the symbol's
// first word). Align UP, mirroring cgindex.
let tn: *node = letvartnode(c, base.str);
if (tn != nil) {
globalname = base.str;
esz = elemsizeofc(c, tn);
if (tn.kind == nkind.N_TARRAY) {
isglobalarr = true;
} else {
isglobalptr = true;
};
};
};
} else { if (base.kind == nkind.N_DOT) {
// `&p.ptr[i]`: stride is the checker-stamped
// element tinfo's natural size, mirroring
// cgindex's N_DOT arm so &p.ptr[i] and
// p.ptr[i] agree. cstage idx_eff(base->type)
// ->sub->size (cmd/w6c/cgen.c:3517-18). #72.
let dt: *tinfo = opnd.type_: *tinfo;
if (dt != nil) { esz = dt.size: i32; };
};};
};
cgexpr(c, idx);
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", CX\n");
emitline("\tIMULQ\tCX, AX\n");
};
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) {
if (isarr) {
emitline("\tLEAQ\t");
emitoff(baselocal.off: i64);
emitline("(BP), BX\n");
} else {
emitline("\tMOVQ\t");
emitoff(baselocal.off: i64);
emitline("(BP), BX\n");
};
} else {
// Complex base: spill scaled idx, eval
// base to AX, restore idx into BX.
// Mirrors cstage's lean three-line shape
// (cmd/w6c/cgen.c TK_AMP N_INDEX complex
// base 2104-2107); the prior MOVQ AX, BX
// + POPQ AX scratch shuffle was rule-10
// verbose-defensive on the wwstage side
// with no semantic asymmetry (task #21).
// #252: an N_DOT `[N]T`-field base needs the
// field ADDRESS (dotbaseaddr LEAQ), not the
// auto-deref VALUE load cgexpr emits. Sibling
// of the #135 read-side wiring.
emitline("\tPUSHQ\tAX\n");
if (!dotbaseaddr(c, base, "AX")) {
cgexpr(c, base);
};
emitline("\tPOPQ\tBX\n");
};};};
emitline("\tADDQ\tBX, AX\n");
return;
};
};
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");
// NOTQ inverts the whole 64-bit register; clamp narrow
// unsigned results to type width so subsequent 64-bit
// compares against typed literals agree. u32 uses MOVL r,r
// (zero-extends upper 32) because ANDQ $0xFFFFFFFF would
// sign-extend imm32 to all-ones and act as a no-op.
if (nodeisunsigned(c, n.lhs)) {
let w: i32 = nodeprimwidth(c, n.lhs);
if (w == 1) { emitline("\tANDQ\t$255, AX\n"); };
if (w == 2) { emitline("\tANDQ\t$65535, AX\n"); };
if (w == 4) { emitline("\tMOVL\tAX, AX\n"); };
};
return;
};
if (n.op == tkind.TK_STAR) {
// #185: deref of *fn — the pointer value IS the fn address.
// cgexpr(n.lhs) left AX = fn-addr; a generic MOVQ (AX),AX
// would load the first instruction word and a subsequent
// CALL would segfault. Mirror ref/harec/src/check.c
// expr_call's STORAGE_POINTER→STORAGE_FUNCTION skip.
let rti: *tinfo = n.type_: *tinfo;
for (rti != nil && rti.kind == tykind.TY_NAMED) { rti = rti.under; };
if (rti != nil && rti.kind == tykind.TY_FN) { return; };
// f64/f32 result rides X0 (SSE), not AX — an integer MOVQ
// strands the value off the float ABI and the caller's
// MOVSD X0 reads stale bits (#96). Mirrors the float
// field/ident load idiom.
if (isfloattype(c, n)) {
let mov: str = "MOVSD";
if (isf32type(c, n)) { mov = "MOVSS"; };
emitline("\t"); emitline(mov); emitline("\t(AX), X0\n");
} else {
// Load-twin of the landed signed-narrow-scalar-reads
// sweep (selfhost/CLAUDE.md "Signed-narrow scalar
// reads sign-extend honestly"); TK_STAR was the
// omitted site, refiled as #116. A raw MOVQ pulls 8B
// through a narrow `*iN` and overlaps the next element
// — the `*p` value reads honest only when the caller's
// sink truncates (i32 store, i32 return). Width-
// preserving sinks (CMPQ, 64-bit arith) saw garbage in
// the high bytes. localloadop keys MOVSXD/MOVSWQ/
// MOVSBQ + MOVL/MOVZWQ/MOVZBQ off n.type_; n is the
// deref expression, n.type_ is the pointee tinfo
// (check.ww unoptype TK_STAR L1871-1886 with
// TY_NAMED/TY_ENUM peel pre-folded by
// tinfofornode/typeissigned), the same shape the
// float arm above feeds isfloattype.
let lop: str = localloadop(c, n);
emitline("\t"); emitline(lop);
emitline("\t(AX), AX\n");
};
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 = {
// Short-circuit `&&` / `||`. Operands are bool (0/1); the type
// checker enforces it. Eval LHS into AX, branch over RHS on the
// short-circuit polarity, otherwise eval RHS into AX. The
// surviving AX is the result. Must precede any eager-eval path
// below — `if (p != nil && p.x > 0)` would segfault on a nil
// deref otherwise. Byte-identical to cmd/w6c/cgen.c N_BIN.
if (n.op == tkind.TK_AND || n.op == tkind.TK_OR) {
let prefix: str = "andend";
let jshrt: str = "JE";
if (n.op == tkind.TK_OR) { prefix = "orend"; jshrt = "JNE"; };
let end: str = mklabel(c, prefix);
cgexpr(c, n.lhs);
emitline("\tCMPQ\t$0, AX\n");
emitline("\t"); emitline(jshrt); emitline("\t");
emitline(end); emitline("\n");
cgexpr(c, n.rhs);
emitlabel(end);
return;
};
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.
// Value-class read off the checker stamp (n.type_) — the SSoT
// shared with cstage cgen.c node_isfloat / type_isf32. The armed
// asserttyped bail (check.ww) guarantees every checked value-node
// is stamped, so the read can't see a nil-typed float operand;
// the sibling-evidence loud-aborts that used to pin that contract
// are therefore dead and removed.
let lfk: i32 = 0;
if (n.lhs != nil) {
let llt: *tinfo = n.lhs.type_: *tinfo;
if (typeisf32(llt)) { lfk = 1; }
else { if (typeisfloat(llt)) { lfk = 2; }; };
};
let rfk: i32 = 0;
if (n.rhs != nil) {
let rrt: *tinfo = n.rhs.type_: *tinfo;
if (typeisf32(rrt)) { rfk = 1; }
else { if (typeisfloat(rrt)) { rfk = 2; }; };
};
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;
if (n.op == tkind.TK_EQ) { isfcmp = true; };
if (n.op == tkind.TK_NEQ) { isfcmp = true; };
if (n.op == tkind.TK_LT) { isfcmp = true; };
if (n.op == tkind.TK_LE) { isfcmp = true; };
if (n.op == tkind.TK_GT) { isfcmp = true; };
if (n.op == tkind.TK_GE) { isfcmp = true; };
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");
// IEEE-754: UCOMISD/SS sets PF=ZF=CF=1 on unordered (a
// NaN operand). Any relop with a NaN operand is
// unordered -> `!=` true, the other five false. PF must
// steer `!=`/`==`/`<`/`<=` (#97): JNE keys on ZF=0 so
// `nan != nan` came out false; JE/JB/JBE fire on the
// unordered ZF/CF. `>`/`>=` (JA/JAE) need CF=0, which
// unordered never gives, so they are ALREADY NaN-correct
// and stay byte-identical to the pre-#97 single-template
// arm — no redundant PF guard.
if (n.op == tkind.TK_NEQ) {
// not-equal OR unordered -> true
let t: str = mklabel(c, "ct");
let e: str = mklabel(c, "ce");
emitline("\tJNE\t"); emitline(t); emitline("\n");
emitline("\tJP\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;
};
if (n.op == tkind.TK_EQ || n.op == tkind.TK_LT ||
n.op == tkind.TK_LE) {
// unordered -> false; otherwise the ordered Jcc decides.
let jcc: str = "JE";
if (n.op == tkind.TK_LT) { jcc = "JB"; };
if (n.op == tkind.TK_LE) { jcc = "JBE"; };
let fl: str = mklabel(c, "cf");
let t: str = mklabel(c, "ct");
let e: str = mklabel(c, "ce");
emitline("\tJP\t"); emitline(fl); emitline("\n");
emitline("\t"); emitline(jcc); emitline("\t"); emitline(t); emitline("\n");
emitlabel(fl);
emitline("\tMOVQ\t$0, AX\n");
emitline("\tJMP\t"); emitline(e); emitline("\n");
emitlabel(t);
emitline("\tMOVQ\t$1, AX\n");
emitlabel(e);
return;
};
// `>`/`>=`: JA/JAE already reject unordered (CF=1), so
// keep the pre-#97 single-template shape verbatim.
let jcc: str = "JA";
if (n.op == tkind.TK_GE) { jcc = "JAE"; };
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) {
// Signed IDIV reads dividend from RDX:RAX; CQO sign-extends
// RAX. Zero-filling DX would treat a negative RAX as a huge
// positive 128-bit value. Unsigned DIV needs RDX zero.
if (unsignd) {
emitline("\tMOVQ\t$0, DX\n");
emitline("\tDIVQ\tBX\n");
} else {
emitline("\tCQO\n");
emitline("\tIDIVQ\tBX\n");
};
return;
};
if (n.op == tkind.TK_PERCENT) {
if (unsignd) {
emitline("\tMOVQ\t$0, DX\n");
emitline("\tDIVQ\tBX\n");
} else {
emitline("\tCQO\n");
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) {
// #136: signed RSHIFT → SAR (arithmetic, sign-extends MSB);
// unsigned → SHR (logical, zero-fill). `unsignd` derived above
// at cgbin head from nodeisunsigned(lhs) || nodeisunsigned(rhs).
emitline("\tMOVQ\tBX, CX\n");
if (unsignd) { emitline("\tSHRQ\tCX, AX\n"); }
else { emitline("\tSARQ\tCX, AX\n"); };
return;
};
// TK_AND / TK_OR handled with short-circuit codegen at the top of
// cgbin — they never reach this eager-eval tail.
// 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_malloc, 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.
//
// Task #30: result is the graduated `(*T | nomem)` tagged-pointer
// pair (AX=tag, DX=ptr). rt_malloc now returns 0 on OOM
// (rt/alloc.s); branch on AX to emit the nomem variant (tag=1,
// DX=0) or the success variant (tag=0, DX=ptr) after the
// value-init stores complete. Callers wrap with `!` / `?` to
// consume the union.
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; };
};
let okl: str = mklabel(c, "alloc_ok");
let donel: str = mklabel(c, "alloc_done");
emitline("\tMOVQ\t$");
emitint(sz: i64);
emitline(", DI\n");
emitline("\tCALL\t");
emitline(ffiresolve(c, "malloc"));
emitline("(SB)\n");
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJNE\t"); emitline(okl); emitline("\n");
emitline("\tMOVQ\t$1, AX\n");
emitline("\tMOVQ\t$0, DX\n");
emitline("\tJMP\t"); emitline(donel); emitline("\n");
emitlabel(okl);
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, ");
emitdispreg(fi.foff: i64, "BX");
emitline("\n");
fi = nil;
} else { if (isstrtype(c, fi.tnode)) {
// str IS []u8: cgexpr leaves (AX=ptr,
// BX=len, CX=cap). Route the heap base
// through DX so all three survive — CX
// holds cap, BX holds len (#1/Phase 3).
emitline("\tMOVQ\t(SP), DX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(fi.foff: i64, "DX");
emitline("\n");
emitline("\tMOVQ\tBX, ");
emitdispreg((fi.foff + 8): i64, "DX");
emitline("\n");
emitline("\tMOVQ\tCX, ");
emitdispreg((fi.foff + 16): i64, "DX");
emitline("\n");
fi = nil;
} else {
emitline("\tMOVQ\t(SP), BX\n");
let sop: str = fieldstoreop(c, fi);
emitline("\t");
emitline(sop);
emitline("\tAX, ");
emitdispreg(fi.foff: i64, "BX");
emitline("\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\tDX\n");
emitline("\tMOVQ\t$0, AX\n");
emitlabel(donel);
};
// 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;
// #34: esz off the DECLARED slice local's stamped tnode via
// elemsizeofc — bare elemsizeof returns the 8 sentinel for a
// named tagged/struct element (the #8 family; cgappend was never
// upgraded), under-feeding rt_ensure's membsz AND mis-striding
// the slot index vs cstage's su->sub->size.
let esz: i32 = elemsizeofc(c, snlocal.tnode);
let etnode: *node = nil;
if (snlocal.tnode != nil) {
let stk: nkind = snlocal.tnode.kind;
if (stk == nkind.N_TSLICE) { etnode = snlocal.tnode.lhs; };
if (stk == nkind.N_TARRAY) { etnode = snlocal.tnode.lhs; };
if (stk == nkind.N_TPTR) { etnode = snlocal.tnode.lhs; };
};
let store_op: str = tnodestoreop(c, etnode, esz);
// #34 element-kind store dispatch: the scalar 1-word store below
// silently gutted every wide element (str/slice 24B header,
// tagged box, struct body). Kind off the stamped slice tinfo —
// the value node's literal tinfo is the #25/#31 esz=0 trap.
// Mirrors cstage cgen.c's append arm + the #270/#12/#20
// array-literal element dispatch (cgarrlitfillbp).
let sti: *tinfo = nil;
if (snlocal.tnode != nil) { sti = snlocal.tnode.type_: *tinfo; };
for (sti != nil && sti.kind == tykind.TY_NAMED) { sti = sti.under; };
let esubti: *tinfo = nil;
if (sti != nil) { esubti = sti.sub; };
for (esubti != nil && esubti.kind == tykind.TY_NAMED) {
esubti = esubti.under;
};
let elstr: bool = esubti != nil && esubti.kind == tykind.TY_STR;
let elslice: bool = esubti != nil && esubti.kind == tykind.TY_SLICE;
let eltagged: bool = esubti != nil && esubti.kind == tykind.TY_TAGGED;
let elstruct: bool = esubti != nil && esubti.kind == tykind.TY_STRUCT;
let elwide: bool = elstr || elslice || eltagged || elstruct;
if (!elwide && esz > 8) {
let m34k: str = "#34: append() element kind unsupported (rule-7)\n";
os.write(2, m34k.ptr, m34k.len: u64);
os.exit(1);
};
let vn: *node = sn.next;
for (vn != nil) {
if (vn.kind == nkind.N_SPREAD) {
// #34 review: a non-ident/non-local spread source used
// to be silently SKIPPED here while cstage fell past
// its spread arm into the single-value stores with the
// N_SPREAD node (garbage store) — divergent. Deferred
// source shape, task #37.
let it: *node = vn.lhs;
if (it == nil || it.kind != nkind.N_IDENT) {
let m34p: str = "#34: append() spread source shape unsupported (rule-7)\n";
os.write(2, m34p.ptr, m34p.len: u64);
os.exit(1);
};
let itlocal: *local = localfindnode(c, it.str);
if (itlocal == nil) {
let m34q: str = "#34: append() spread source ident is not a local (rule-7)\n";
os.write(2, m34q.ptr, m34q.len: u64);
os.exit(1);
};
let it_off: i32 = itlocal.off;
let load_op: str = tnodeloadop(c, etnode, esz);
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");
if (elwide) {
// #34: a spread element is already a fully-formed
// T in the source slice (tag included), so a
// whole-width word-copy is the store — no boxing.
// Grow FIRST: rt_ensure may realloc, so both
// addresses are recomputed from the slice headers
// after the call (i reloads from the counter
// slot; CX was clobbered).
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(SP), CX\n");
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", AX\n");
emitline("\tIMULQ\tAX, CX\n");
};
emitline("\tMOVQ\t");
emitoff(it_off: i64);
emitline("(BP), BX\n");
emitline("\tADDQ\tCX, BX\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), DX\n");
emitline("\tADDQ\tCX, DX\n");
let wk: i32 = 0;
for (wk + 8 <= esz) {
emitline("\tMOVQ\t");
emitdispreg(wk: i64, "BX");
emitline(", AX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(wk: i64, "DX");
emitline("\n");
wk += 8;
};
if (wk + 4 <= esz) {
emitline("\tMOVL\t");
emitdispreg(wk: i64, "BX");
emitline(", AX\n");
emitline("\tMOVL\tAX, ");
emitdispreg(wk: i64, "DX");
emitline("\n");
wk += 4;
};
if (wk + 2 <= esz) {
emitline("\tMOVW\t");
emitdispreg(wk: i64, "BX");
emitline(", AX\n");
emitline("\tMOVW\tAX, ");
emitdispreg(wk: i64, "DX");
emitline("\n");
wk += 2;
};
if (wk + 1 <= esz) {
emitline("\tMOVB\t");
emitdispreg(wk: i64, "BX");
emitline(", AX\n");
emitline("\tMOVB\tAX, ");
emitdispreg(wk: i64, "DX");
emitline("\n");
wk += 1;
};
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;
};
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;
};
if (elstr || elslice) {
// #34: 24B {ptr,len,cap} header. cgexpr leaves
// AX/BX/CX; all three must survive rt_ensure. dst
// lands in DX, NOT BX — the pops put the element
// .len back in BX (the #24 register discipline).
cgexpr(c, vn);
emitline("\tPUSHQ\tAX\n");
emitline("\tPUSHQ\tBX\n");
emitline("\tPUSHQ\tCX\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");
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", AX\n");
emitline("\tIMULQ\tAX, CX\n");
emitline("\tMOVQ\t");
emitoff(sn_off: i64);
emitline("(BP), DX\n");
emitline("\tADDQ\tCX, DX\n");
emitline("\tPOPQ\tCX\n");
emitline("\tPOPQ\tBX\n");
emitline("\tPOPQ\tAX\n");
emitline("\tMOVQ\tAX, (DX)\n");
emitline("\tMOVQ\tBX, 8(DX)\n");
emitline("\tMOVQ\tCX, 16(DX)\n");
vn = vn.next;
continue;
};
if (eltagged || elstruct) {
// #34: no register form survives rt_ensure for these —
// grow FIRST, then fill through the dst pointer
// (tagged: the #12 widen choke-point cgexprs the value
// internally; struct: literal fill / ident word-copy).
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");
if (eltagged) {
cgwidentaggedstore(c, esubti, vn, "BX", 0, esz);
vn = vn.next;
continue;
};
if (vn.kind == nkind.N_STRUCTLIT) {
let scroff: i32 = localadd(c, "@appendscr", 8, nil);
emitline("\tMOVQ\tBX, ");
emitoff(scroff: i64);
emitline("(BP)\n");
let esi: *structinfo = structlookupchain(c, etnode);
if (esi == nil) {
let m34s: str = "#34: append() struct element has no structinfo (rule-7)\n";
os.write(2, m34s.ptr, m34s.len: u64);
os.exit(1);
};
cgstructlitfill(c, esi, vn, 1, scroff, "", 0);
vn = vn.next;
continue;
};
if (vn.kind == nkind.N_IDENT) {
let sl: *local = localfindnode(c, vn.str);
if (sl == nil) {
let m34i: str = "#34: append() struct element source ident is not a local (rule-7)\n";
os.write(2, m34i.ptr, m34i.len: u64);
os.exit(1);
};
let soff: i32 = sl.off;
let ck: i32 = 0;
for (ck + 8 <= esz) {
emitline("\tMOVQ\t");
emitoff((soff + ck): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(ck: i64, "BX");
emitline("\n");
ck += 8;
};
if (ck + 4 <= esz) {
emitline("\tMOVL\t");
emitoff((soff + ck): i64);
emitline("(BP), AX\n");
emitline("\tMOVL\tAX, ");
emitdispreg(ck: i64, "BX");
emitline("\n");
ck += 4;
};
if (ck + 2 <= esz) {
emitline("\tMOVW\t");
emitoff((soff + ck): i64);
emitline("(BP), AX\n");
emitline("\tMOVW\tAX, ");
emitdispreg(ck: i64, "BX");
emitline("\n");
ck += 2;
};
if (ck + 1 <= esz) {
emitline("\tMOVB\t");
emitoff((soff + ck): i64);
emitline("(BP), AX\n");
emitline("\tMOVB\tAX, ");
emitdispreg(ck: i64, "BX");
emitline("\n");
ck += 1;
};
vn = vn.next;
continue;
};
let m34e: str = "#34: append() struct element source shape unsupported (rule-7)\n";
os.write(2, m34e.ptr, m34e.len: u64);
os.exit(1);
};
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_malloc
// + per-field stores. Mirrors cmd/w6c/cgen.c's N_CALL
// alloc path.
//
// Same-module-scope guard: skip the builtin when a fn
// `alloc` is declared in the current module (lib/os and
// rt/ensure both shadow it). Mirrors cstage check.c's
// scope_lookup_prefer gating on the `abort` precedent;
// without it, the bare same-module call lands in the
// typed-builtin path and shadows the user decl. Task #23.
if (streq(callee.str, "alloc")) {
if (n.list != nil) {
if (!samemodfn(c, "alloc")) {
cgalloc(c, n);
return;
};
};
};
// `len(x)` Hare builtin — mirror cmd/w6c/cgen.c:4283-4297.
// Required for byte-id when compiler-imported lib code uses
// len(fixedarray) (e.g. lib/strconv/decimal.ha's `len(d.digits)`
// over the [800]u8 field). Without this intercept wwstage falls
// through to a regular CALL len(SB) while cstage folds to
// `MOVQ $alen, AX` — rule-10 byte-id break (#131).
//
// Argument-type-driven branches:
// TY_SLICE / TY_STR (+ N_IDENT operand) → load .len at BP+off+8.
// TY_ARRAY → fold `MOVQ $alen, AX`.
// else → evaluate operand (cstage's pseudo-.len fallback —
// unlikely to fire on Hare-shaped sources).
if (streq(callee.str, "len")) {
if (n.list != nil) {
let a: *node = n.list;
let at: *tinfo = a.type_: *tinfo;
let u: *tinfo = at;
for (u != nil && u.kind == tykind.TY_NAMED) {
u = u.under;
};
if (u != nil) {
if ((u.kind == tykind.TY_SLICE
|| u.kind == tykind.TY_STR)
&& a.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, a.str);
if (lc != nil) {
emitline("\tMOVQ\t");
emitoff((lc.off + 8): i64);
emitline("(BP), AX\n");
return;
};
// #231: str/slice GLOBAL — the
// local-only path above lacked it,
// so cgexpr fell through and left
// AX=.ptr (not .len). The .len word
// lives at the global's address+8;
// route the LEAQ through the post-#1
// value mangle (c.curmod) so a
// private same-leaf global isn't
// mis-resolved.
if (isletvar(c, a.str)) {
emitline("\tLEAQ\t");
emitsymnamehint(c, a.str, c.curmod);
emitline("(SB), CX\n");
emitline("\tMOVQ\t8(CX), AX\n");
return;
};
};
// #235: len() of a tuple-element slice/str
// (`len(t.N)`). The tuple-element read leaves
// only AX=.ptr — no slice-header sibling (that
// gap is #238) — so the cgexpr fallback below
// returned .ptr AS the length. Load the element's
// .len word directly at BP + element_off + 8,
// mirroring the N_IDENT slice arm above and the
// tuple-field-offset walk (cgenexpr.ww N_TTUPLE).
if ((u.kind == tykind.TY_SLICE
|| u.kind == tykind.TY_STR)
&& a.kind == nkind.N_DOT
&& a.lhs != nil
&& a.lhs.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, a.lhs.str);
if (lc != nil) {
let tn: *node = lc.tnode;
for (tn != nil && tn.kind == nkind.N_TNAME) {
tn = aliaslookup(c, tn.str);
};
if (tn != nil) {
if (tn.kind == nkind.N_TTUPLE) {
let idx: i32 = fldnumidx(a.str);
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.lhs);
tp = tp.next;
i += 1;
};
};
if (tp != nil) {
emitline("\tMOVQ\t");
emitoff((lc.off + foff + 8): i64);
emitline("(BP), AX\n");
return;
};
};
};
};
};
};
// #19: len() of an INDEXED str/slice element
// (`len(xs[i])`). The N_INDEX str/slice load
// leaves AX=.ptr, BX=.len, CX=.cap — the bare
// cgexpr fallback below returned AX (the ptr)
// AS the length. Shuffle BX (the len word)
// into AX, the same MOVQ BX,AX shape as the
// #14 .len pseudo-field fix. Same family as
// #18 (shared cstage==wwstage gap, not rule-10).
if ((u.kind == tykind.TY_SLICE
|| u.kind == tykind.TY_STR)
&& a.kind == nkind.N_INDEX) {
cgexpr(c, a);
emitline("\tMOVQ\tBX, AX\n");
return;
};
if (u.kind == tykind.TY_ARRAY) {
emitline("\tMOVQ\t$");
emitint(u.alen: i64);
emitline(", AX\n");
return;
};
};
// Fallback: evaluate the argument and let AX carry
// whatever the value-load shape yields. Mirrors
// cstage's `cgexpr(c, a, locals)` fallthrough.
cgexpr(c, a);
return;
};
};
// free(x) — documented NO-OP, mirror of cstage's cgexpr
// N_CALL free arm (#27): ww has no free by design
// (rt/alloc.s:30 — the bump allocator cannot reclaim a
// mid-chunk pointer; process exit does). The operand is
// still evaluated — Hare's free(expr) evaluates expr —
// so Hare code ports verbatim with its side effects
// intact. Pre-#27 wwstage fell through to a generic
// CALL free → undefined reference at link.
if (streq(callee.str, "free")) {
if (n.list != nil) {
if (n.list.next == nil) {
cgexpr(c, n.list);
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.
//
// N_DOT (`mod.fn(...)`) covers cross-module calls; pre-#28 wwstage
// only handled N_IDENT, leaving N_DOT calls without widening
// detection — pushargsrev then fell through to the N_IDENT-slice
// fast path and dropped the variant tag word on widened slice args.
// Cstage finds params via the checker-set `n->lhs->type`, sidestepping
// the name-driven registry entirely (cmd/w6c/cgen.c:4161-4165).
let calleeparams: *node = nil;
if (callee != nil) {
if (callee.kind == nkind.N_IDENT) {
calleeparams = fnparamslookup(c, callee.str);
} else { if (callee.kind == nkind.N_DOT) {
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
calleeparams = fnparamslookupmod(c, callee.str, cmod);
}; };
};
// Hare-style variadic last param: gather N tail args into a
// frame-resident [N]T (vararg_d slot) plus a 24B slice
// descriptor (vararg_sl slot), then splice a synthesised
// N_IDENT pointing at the descriptor into n.list so the rest
// of the call machinery sees one slice slot for the variadic.
// Forwarding shape (`xs...`) skips the gather: the spread's
// inner slice expression replaces the wrapper in place. Empty
// (no trailing args) writes a {nil, 0, 0} descriptor. Slot
// names come from mklabel (mirrors cstage cgen.c:5427/5431) so
// the shared labelseq advances in lockstep — vararg_d only when
// nvar>0, vararg_sl always — keeping later match labels aligned.
{
let nfixed_v: i32 = 0;
let varp: *node = callee_variadic_param(c, callee, &nfixed_v);
if (varp != nil) {
let nargs0: i32 = 0;
let aw: *node = n.list;
for (aw != nil) { nargs0 += 1; aw = aw.next; };
let nvar: i32 = nargs0 - nfixed_v;
if (nvar < 0) { nvar = 0; };
let forwarding: bool = false;
if (nvar == 1) {
let aaf: *node = n.list;
let kk: i32 = 0;
for (kk < nfixed_v) {
aaf = aaf.next;
kk += 1;
};
if (aaf != nil) {
if (aaf.kind == nkind.N_SPREAD) {
forwarding = true;
};
};
};
if (forwarding) {
let prev: *node = nil;
let cur2: *node = n.list;
let kk2: i32 = 0;
for (kk2 < nfixed_v) {
prev = cur2;
cur2 = cur2.next;
kk2 += 1;
};
let inner: *node = cur2.lhs;
if (inner != nil) { inner.next = nil; };
if (prev == nil) { n.list = inner; }
else { prev.next = inner; };
} else {
// Use raw element size, not stack-padded
// slotsize. cstage cmd/w6c/cgen.c cgcall
// gathers a `T...` slice at velem->size stride
// (MOVL for u32, MOVB for u8); the callee
// `arg[i]` reads at the same raw stride. wwstage
// previously sized through slotsize which pads
// scalars to 8, mismatching the stride at the
// callee read site — runtime miscompile in
// `(rune...)` callees per #36.
// check.ww installparams promotes varp.lhs to
// []T (mirrors cstage check.c:455 tp->type
// wrap). Element predicates / esz read varp.lhs
// .lhs; Ken's gate: only deref when the wrap
// shape is confirmed N_TSLICE (mirrors cstage
// cgen.c:4352 `vsu->kind == TY_SLICE` guard).
let velem: *node = varp.lhs;
if (varp.lhs != nil
&& varp.lhs.kind == nkind.N_TSLICE) {
velem = varp.lhs.lhs;
};
let esz: i32 = 8;
if (velem != nil) {
if (velem.kind == nkind.N_TNAME) {
let ps: i32 = primsize(velem.str);
if (ps > 0) { esz = ps; }
else { esz = slotsize(c, velem); };
} else {
esz = slotsize(c, velem);
};
};
if (esz < 1) { esz = 1; };
let velemtagged: bool = istaggedtype(c, velem);
let velemstr: bool = isstrtype(c, velem);
let velemslice: bool = isslicetype(c, velem);
let doff: i32 = 0;
if (nvar > 0) {
// mklabel, not a separate vararg counter, so
// labelseq advances with cstage cgen.c:5427 — the
// slot name never reaches asm, but the shared
// counter numbers later match labels.
let dname: str = mklabel(c, "vararg_d");
doff = localadd(c, dname, nvar * esz, nil);
};
// #60: vararg gather builds a {ptr,len,cap} slice
// descriptor — route through tyslicesize so #34's
// slice-header bump propagates here. varp.lhs is
// already the []T wrap from installparams, so we
// consume it directly (re-slicewrap → [][]T).
// vararg_sl always allocated (cstage cgen.c:5431),
// bumping labelseq whether or not nvar>0.
let sname: str = mklabel(c, "vararg_sl");
let soff: i32 = localadd(c, sname, tyslicesize(): i32,
varp.lhs);
let aa2: *node = n.list;
let kk3: i32 = 0;
for (kk3 < nfixed_v) {
aa2 = aa2.next;
kk3 += 1;
};
let j: i32 = 0;
let prevarg: *node = n.list;
if (nfixed_v == 0) { prevarg = nil; }
else {
let kk4: i32 = 0;
for (kk4 < nfixed_v - 1) {
prevarg = prevarg.next;
kk4 += 1;
};
};
for (aa2 != nil) {
let slot: i32 = doff + j * esz;
if (velemtagged) {
// dst is the per-element tagged type;
// pass velem (cstage cgen.c:4382 passes
// velem, not the slice wrap vsu).
cgwidentaggedstore(c, velem.type_: *tinfo,
aa2, "BP", slot, esz);
} else { if (velemstr) {
cgexpr(c, aa2);
emitline("\tMOVQ\tAX, ");
emitoff(slot: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tBX, ");
emitoff((slot + 8): i64);
emitline("(BP)\n");
} else { if (velemslice) {
cgexpr(c, aa2);
emitline("\tMOVQ\tAX, ");
emitoff(slot: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tBX, ");
emitoff((slot + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((slot + 16): i64);
emitline("(BP)\n");
} else {
cgexpr(c, aa2);
let op: str = tnodestoreop(c, varp.lhs, esz);
emitline("\t");
emitline(op);
emitline("\tAX, ");
emitoff(slot: i64);
emitline("(BP)\n");
}; }; };
j += 1;
aa2 = aa2.next;
};
if (nvar > 0) {
emitline("\tLEAQ\t");
emitoff(doff: i64);
emitline("(BP), AX\n");
} else {
emitline("\tXORQ\tAX, AX\n");
};
emitline("\tMOVQ\tAX, ");
emitoff(soff: i64);
emitline("(BP)\n");
emitline("\tMOVQ\t$");
emitint(nvar: i64);
emitline(", AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((soff + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tAX, ");
emitoff((soff + 16): i64);
emitline("(BP)\n");
let sn: *node = newnode(nkind.N_IDENT,
"", 0, 0);
sn.str = sname;
// Synthesised after the checker has run, so the
// asserttyped bail (check.ww) never stamps it.
// Stamp the variadic param's []T slice tinfo
// (resolvefnbody resolve-walks varp.lhs) so the
// downstream value-class reads see a non-nil
// stamp — the one cgen node the bail can't cover.
sn.type_ = varp.lhs.type_;
if (prevarg == nil) { n.list = sn; }
else { prevarg.next = sn; };
};
};
};
let nargs: i32 = pushargsrev(c, n.list, calleeparams);
// sret call (#23): callee returns plain TY_STRUCT > 24B. The
// dest pointer lands in RDI; start intidx at 1 to skip RDI in
// the user-arg pop loop and emit `LEAQ off(BP), DI` AFTER all
// pops have finished (so they don't clobber RDI). The dest off
// is either the receive site's slot (c.sretdestoff, propagated
// from cglet / cgassign ident) or the per-fn @sretscr discard
// slot, sized at first use per #15/#26c.
let sretcs: i32 = callsretsize(c, n);
let sretcalloff: i32 = 0;
// #220: GLOBAL dest — RDI gets `LEAQ name(SB)` below; no @sretscr
// slot (the callee writes the struct straight into g's storage).
let sretdestn: *node = nil;
if (sretcs > 0) {
if (c.sretdestnode != nil) {
sretdestn = c.sretdestnode;
c.sretdestnode = nil;
} else { if (c.sretdestoff != 0) {
sretcalloff = c.sretdestoff;
c.sretdestoff = 0;
} else {
sretcalloff = localadd(c, "@sretscr",
sretcs, nil);
};};
};
// 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;
if (sretcs > 0) { intidx = 1; };
let fpidx: i32 = 0;
let a: *node = n.list;
let popped: i32 = 0;
let stackslots: i32 = 0;
for (a != nil) {
let fk: i32 = 0;
if (a != nil) {
let at: *tinfo = a.type_: *tinfo;
if (typeisf32(at)) { fk = 1; }
else { if (typeisfloat(at)) { fk = 2; }; };
};
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 tuparg: *node = rettupleof(c, a);
if (tuparg != nil) {
// #163: drain the tuple's staged words (slot+0 pushed
// first) into the SysV arg cursor by SysV class — a
// float MOVSD/MOVSS off (SP) into the next XMM, else
// POPQ into the next INTEGER arg reg; a slice/str its
// 3 words. Reg overflow loud-stops (rule 7); the
// partial-spill stitch is out of scope (twin of #164).
let p: *node = tuparg.list;
for (p != nil) {
let et: *node = p.lhs;
if (isfloattype(c, et)) {
if (fpidx >= 8) {
let msg: str = "tuple arg float element overflows SSE arg regs (X0..X7); stitch out of scope, see #163\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
let mov: str = "MOVSD";
if (isf32type(c, et)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\t(SP), ");
emitline(fargregname(fpidx));
emitline("\n");
emitline("\tADDQ\t$8, SP\n");
fpidx += 1;
popped += 1;
} else {
let wide: bool = isstrtype(c, et) || isslicetype(c, et);
let eb: i32 = tupebytes(wide);
if (intidx + eb > 6) {
let msg: str = "tuple arg element overflows integer arg regs (DI/SI/DX/CX/R8/R9); stitch out of scope, see #163\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
let k: i32 = 0;
for (k < eb) {
emitline("\tPOPQ\t");
emitline(argregname(intidx));
emitline("\n");
intidx += 1;
popped += 1;
k += 1;
};
};
p = p.next;
};
} else {
let stfc: i32 = 0;
if (a.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, a.str);
if (lc != nil) { stfc = structfloatclass(c, lc.tnode); };
};
if (stfc != 0) {
// #165: float-bearing struct arg — drain by SysV
// eightbyte class: a lone-f64 eightbyte MOVSD off
// (SP) into the next XMM (X0..X7), a pure-INT
// eightbyte POPQ into the next INTEGER arg reg
// (DI/SI/..). The struct-ident push staged raw slot
// words (class-independent); only the drain differs.
// Gated to qualifying floats; all-int + f32-packed
// keep the generic pop below. Reg overflow loud-
// stops (rule 7), the partial-spill stitch out of
// scope (#163 twin).
let nb: i32 = stfc & 15;
let e: i32 = 0;
for (e < nb) {
let issse: bool = (stfc & (16 << e)) != 0;
if (issse) {
if (fpidx >= 8) {
let msg: str = "float struct arg eightbyte overflows SSE arg regs (X0..X7); stitch out of scope, see #165\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
emitline("\tMOVSD\t(SP), ");
emitline(fargregname(fpidx));
emitline("\n");
emitline("\tADDQ\t$8, SP\n");
fpidx += 1;
} else {
if (intidx >= 6) {
let msg: str = "float struct arg eightbyte overflows integer arg regs (DI/SI/DX/CX/R8/R9); stitch out of scope, see #165\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
emitline("\tPOPQ\t");
emitline(argregname(intidx));
emitline("\n");
intidx += 1;
};
popped += 1;
e += 1;
};
} else {
let extra: i32 = 0;
// str IS []u8: 3-word arg, same as slice (#1/Phase 3).
if (nodeisstr(c, a)) { extra = 2; };
if (nodeisslice(c, a)) { extra = 2; };
// #21: tagged-CALL arg was pushed AX/DX/CX/R8 high→low
// by pushargsrev; size the per-arg pop to match so the
// next arg's POPQ doesn't land on residual tag/payload
// words and shift intidx out of sync.
let tcs: i32 = taggedcallslot(c, a);
if (tcs > 0) { extra = tcs / 8 - 1; };
// #271: array / >16B-struct / non-ident 16B-struct
// aggregate arg — pushargsrev staged ceil(sz/8) words;
// drain exactly that many so intidx tracks per-arg
// (the ≤16B struct IDENT case is the stfc branch
// above). Mirror of cstage node_isaggarg drain arm.
let aggsz: i32 = aggargsizetn(a.type_: *tinfo);
if (aggsz > 0) { extra = (aggsz + 7) / 8 - 1; };
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;
};
// `callee` is already in scope from line 2827; reuse it. Pre-#32
// silent-redecl masked the second `let callee` here as a no-op
// (same value, same fn-body scope post-#27).
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;
};
};
// #181-cgen: a non-named callee (`(*f)(...)` → N_UN TK_STAR,
// or any other expression-valued fn) is an indirect call.
// cgexpr the callee into AX; CALL AX. Mirrors cstage's
// default-fallthrough at cmd/w6c/cgen.c:5918-5921 which
// catches every callee shape that isn't a bare-IDENT module
// fn or N_DOT module-qualified call. Pre-fix wwstage emitted
// `CALL (SB)` (empty symbol) for the N_UN-callee shape — the
// IDENT/DOT name-emit branches missed and isfnptrcall stayed
// false.
if (callee.kind != nkind.N_IDENT
&& callee.kind != nkind.N_DOT) {
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;
};
};
};
};
};
};
};
};
};
};
// sret hidden first-arg (#23): load &dest into RDI AFTER all
// user-arg pops have finished — intidx started at 1 so RDI was
// never written. The CALL emit follows immediately.
//
// Forwarding (task #9 follow-up): when outer's `return f();`
// forwards through an sret callee, source RDI from outer's
// saved @sretarg — inner writes directly into outer's caller-
// prealloc dest. No temporary in outer's frame. The @sretscr
// slot stays reserved for byte-id with cstage; it goes unused
// on the forwarding branch.
if (sretcs > 0) {
if (c.sretforward != 0) {
let sretargoff: i32 = localfind(c, "@sretarg");
emitline("\tMOVQ\t");
emitoff(sretargoff: i64);
emitline("(BP), DI\n");
c.sretforward = 0;
} else { if (sretdestn != nil) {
// #220: sret into a GLOBAL — RDI = &g(SB).
emitline("\tLEAQ\t");
emitsymname(c, sretdestn.str);
emitline("(SB), DI\n");
} else {
emitline("\tLEAQ\t");
emitoff(sretcalloff: i64);
emitline("(BP), DI\n");
};};
};
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) {
// Bare `f()` — same-module by ww's resolver,
// so c.curmod is the disambiguation hint.
calleename = callee.str;
emitfnname(c, calleename, c.curmod);
} else { if (callee.kind == nkind.N_DOT) {
// `m.f()` — pass the explicit module bareword
// so cross-module same-leaf exports resolve.
calleename = callee.str;
let hint: str;
hint.ptr = nil;
hint.len = 0;
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
hint = callee.lhs.str;
};
};
emitfnname(c, calleename, hint);
};};
};
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");
};
// str IS []u8: a str-returning callee leaves AX=ptr, BX=len,
// CX=cap — same as a slice, so there is no receive-side shuffle
// (#1/Phase 3).
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;
};
};
};
};
// Tagged-union local reassignment: `r = expr;` where r has a
// tagged-union type. Delegate to cgwidentaggedstore (same path
// as cglet's tagged-init). Covers nullable fold, tagged source,
// struct payload, str payload, scalar payload, with tag remap.
if (lhs != nil) {
if (lhs.kind == nkind.N_IDENT) {
if (n.op == tkind.TK_ASSIGN) {
let lc: *local = localfindnode(c, lhs.str);
if (lc != nil) {
if (istaggedtype(c, lc.tnode)) {
// #38b: an sret-classified tagged CALL
// result is in memory, not the cursor —
// an exact-type reassign sret's into the
// local's own slot; a widening receive
// needs mem-to-mem tag-remap (#40).
// Mirrors cstage cgen.c N_ASSIGN tagged
// arm + the generic sret receive.
let asret: i32 = 0;
if (n.rhs != nil) {
if (n.rhs.kind == nkind.N_CALL) {
asret = callsretsize(c, n.rhs);
};
};
if (asret > 0) {
let aru: *tinfo = n.rhs.type_: *tinfo;
for (aru != nil && aru.kind == tykind.TY_NAMED) {
aru = aru.under;
};
let alu: *tinfo = lc.tnode.type_: *tinfo;
for (alu != nil && alu.kind == tykind.TY_NAMED) {
alu = alu.under;
};
let aexact: bool = false;
if (aru != nil && aru == alu) { aexact = true; }
else {
if (typeeq(n.rhs.type_: *tinfo,
lc.tnode.type_: *tinfo)) {
aexact = true;
};
};
if (!aexact) {
let m40d: str = "#40: sret-class call result cannot be widened into a tagged slot (mem-to-mem widen unwired)\n";
os.write(2, m40d.ptr, m40d.len: u64);
os.exit(1);
};
c.sretdestoff = lc.off;
cgexpr(c, n.rhs);
c.sretdestoff = 0;
return;
};
let lsz: i32 = slotsize(c, lc.tnode);
cgwidentaggedstore(c, lc.tnode.type_: *tinfo,
n.rhs, "BP", lc.off, lsz);
return;
};
};
// #38b: sret receive into a tagged GLOBAL
// lvalue unwired (rule 7; cstage twin fatals).
if (lc == nil && n.rhs != nil) {
if (n.rhs.kind == nkind.N_CALL) {
let gru: *tinfo = lhs.type_: *tinfo;
for (gru != nil && gru.kind == tykind.TY_NAMED) {
gru = gru.under;
};
if (gru != nil && gru.kind == tykind.TY_TAGGED
&& callsretsize(c, n.rhs) > 0) {
let m38g: str = "#38b: sret receive into a tagged GLOBAL lvalue unwired\n";
os.write(2, m38g.ptr, m38g.len: u64);
os.exit(1);
};
};
};
};
};
};
// `*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);
// #11: primsize is name-keyed and
// misses a `!`/enum/name alias
// (`type errno = !i32`); peel it to
// the underlying primitive width so
// the store narrows, as cstage's
// type-resolved pointee does
// (cgen.c:4647-4652). Residual:
// non-ident pointers + str/float-alias
// deref-store widths stay name-blind
// (#10 wwstage->tinfo SSoT).
if (ps == 0) {
let uns: bool = false;
typenodeprimresolved(c, pe, &ps, &uns);
};
if (ps == 1) { storeop = "MOVB"; }
else { if (ps == 4) { storeop = "MOVL"; }; };
}; }; };
};
// A slice IS the same 3-word {ptr,len,cap}
// header as str (ref/hare/rt/ensure.ha:4-8),
// so `*p = sliceval` takes str's stash+store
// path (#79; precedent cgenstmt.ww:1631,
// cgenexpr.ww:1684). LIKE str this is
// alias-BLIND: a slice-alias `*Foo` / non-ident
// deref-store stays 1-word, the SAME divergence
// str carries; resolved-vs-syntactic detection
// is unified UP in #80, not patched here.
if (pe.kind == nkind.N_TSLICE) { elemstr = true; };
};
};
};
};
};
};
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;
};
// str IS []u8: PUSHQ AX (ptr) first, then
// PUSHQ BX (len) + PUSHQ CX (cap) across the
// pointer eval which clobbers BX/CX. Pop drains
// cap (top) → 16(BX), then len, then ptr → 0(BX)
// with len → 8(BX) (#1/Phase 3).
emitline("\tPUSHQ\tAX\n");
if (elemstr) {
emitline("\tPUSHQ\tBX\n");
emitline("\tPUSHQ\tCX\n");
};
cgexpr(c, inner);
emitline("\tMOVQ\tAX, BX\n");
if (elemstr) {
emitline("\tPOPQ\tCX\n");
emitline("\tMOVQ\tCX, 16(BX)\n");
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;
};
};
};
};
// `*p OP= v` — compound assign through a pointer deref. The
// plain-assign branch above only fires for TK_ASSIGN; without
// this, compound ops fall through and emit nothing (silent
// no-op — exactly the trap that broke fmt.println). Mirror of
// cmd/w6c/cgen.c's N_UN/TK_STAR compound branch.
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 loadop: str = "MOVQ";
let storeop: str = "MOVQ";
// Pointee node for the lhs-sign side of the /=
// and %= dispatch. Mirror of cstage's `vt` at
// cmd/w6c/cgen.c's TK_STAR-compound branch.
let pe: *node = nil;
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) {
pe = tn.lhs;
if (pe != nil) {
let ps: i32 = fieldsize(c, pe);
if (ps == 1 || ps == 2 || ps == 4) {
loadop = tnodeloadop(c, pe, ps);
storeop = tnodestoreop(c, pe, ps);
};
};
};
};
};
};
};
cgexpr(c, n.rhs);
emitline("\tPUSHQ\tAX\n");
cgexpr(c, inner);
emitline("\tMOVQ\tAX, BX\n");
emitline("\t");
emitline(loadop);
emitline("\t(BX), AX\n");
emitline("\tPOPQ\tCX\n");
// Post-63332fe: /= and %= via CQO/IDIVQ on the
// signed arm and MOVQ-zero/DIVQ on the unsigned
// arm. Pre-fix the default branch silently stored
// rhs into *p (combineop = MOVQ shape).
// #136: lift unsignd above the SLASHEQ block so
// RSHIFTEQ can route SHRQ vs SARQ on the same key.
let unsignd: bool = false;
if (pe != nil) {
unsignd = typeisunsigned(pe.type_: *tinfo);
};
if (!unsignd) {
unsignd = nodeisunsigned(c, n.rhs);
};
if (n.op == tkind.TK_SLASHEQ || n.op == tkind.TK_PERCENTEQ) {
if (unsignd) {
emitline("\tMOVQ\t$0, DX\n");
emitline("\tDIVQ\tCX\n");
} else {
emitline("\tCQO\n");
emitline("\tIDIVQ\tCX\n");
};
if (n.op == tkind.TK_PERCENTEQ) {
emitline("\tMOVQ\tDX, AX\n");
};
emitline("\t");
emitline(storeop);
emitline("\tAX, (BX)\n");
return;
};
let combineop: str = "MOVQ";
if (n.op == tkind.TK_PLUSEQ) { combineop = "ADDQ"; }
else { if (n.op == tkind.TK_MINUSEQ) { combineop = "SUBQ"; }
else { if (n.op == tkind.TK_STAREQ) { combineop = "IMULQ"; }
else { if (n.op == tkind.TK_AMPEQ) { combineop = "ANDQ"; }
else { if (n.op == tkind.TK_PIPEEQ) { combineop = "ORQ"; }
else { if (n.op == tkind.TK_CARETEQ) { combineop = "XORQ"; }
else { if (n.op == tkind.TK_LSHIFTEQ) { combineop = "SHLQ"; }
else { if (n.op == tkind.TK_RSHIFTEQ) {
// #136: signed RSHIFTEQ → SARQ.
if (unsignd) { combineop = "SHRQ"; }
else { combineop = "SARQ"; };
};
}; }; }; }; }; }; };
emitline("\t");
emitline(combineop);
emitline("\tCX, AX\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;
let elemtn: *node = nil;
if (base != nil) {
if (base.kind == nkind.N_IDENT) {
let bn: str = base.str;
baselocal = localfindnode(c, bn);
if (baselocal != nil) {
esz = elemsizeofc(c, baselocal.tnode);
let btn: *node = baselocal.tnode;
if (btn != nil) {
let bk: nkind = btn.kind;
if (bk == nkind.N_TARRAY) { elemtn = btn.lhs; };
if (bk == nkind.N_TSLICE) { elemtn = btn.lhs; };
if (bk == nkind.N_TPTR) { elemtn = btn.lhs; };
};
} else {
// #11: store/compound twin of the #10 cgindex
// read fix. A global str/slice element store hit
// the same kind whitelist — N_TNAME (str) /
// N_TSLICE matched NEITHER arm, so esz stayed 8
// and the store emitted a full-word MOVQ — an
// 8-byte OUT-OF-BOUNDS write past a 1-byte
// element — instead of MOVB. cstage
// (cmd/w6c/cgen.c N_INDEX store) dispatches esz
// off idx_eff->sub->size + the elem-kind flags
// off eff->sub uniformly, base is_arr?LEAQ:MOVQ
// name(SB). Align UP and resolve elemtn exactly
// like the local branch above (element node for
// ARRAY/SLICE/PTR; nil for str so tnodestoreop
// picks MOVB on the store arm, and the compound
// arm's str/slice hard-error still fires on a
// []str element).
let tn: *node = letvartnode(c, bn);
if (tn != nil) {
globalname = bn;
esz = elemsizeofc(c, tn);
let bk: nkind = tn.kind;
if (bk == nkind.N_TARRAY) {
isglobalarr = true;
elemtn = tn.lhs;
} else {
isglobalptr = true;
if (bk == nkind.N_TSLICE) { elemtn = tn.lhs; };
if (bk == nkind.N_TPTR) { elemtn = tn.lhs; };
};
};
};
} else { if (base.kind == nkind.N_DOT) {
// lhs.type_ is the checker-stamped element tinfo
// of the N_INDEX: esz is its natural size and the
// tagged-element gate (below) reads the same
// .type_ — same idiom as cgindex's n.type_ read
// (#60/#72). cstage idx_eff(base->type)->sub->size
// (cmd/w6c/cgen.c:3517-18).
let dt: *tinfo = lhs.type_: *tinfo;
if (dt != nil) { esz = dt.size: i32; elemtn = lhs; };
} else { if (base.kind == nkind.N_INDEX) {
// Chained-write write-side parallel of the
// cgindex N_INDEX-base arm (#24): `names[i][k]
// = v` (names: **u8) — outer element is u8 so
// the store is MOVB, not MOVQ. lhs.type_ is the
// checker-stamped outer element tinfo; esz is
// its natural size and the gate reads it via
// .type_. Drops the indexvaluetnode walk
// (#69/#61d, mirror #60). cstage: esz =
// idx_eff(base->type)->sub->size
// (cmd/w6c/cgen.c:3517-3518).
let et: *tinfo = lhs.type_: *tinfo;
if (et != nil) {
esz = et.size: i32;
elemtn = lhs;
};
};};};
};
// Tagged-union element: materialize source in a shared
// scratch slot via cgwidentaggedstore (handles struct /
// str / scalar / subset / nullable variants uniformly),
// then compute &arr[i] and byte-copy. The scratch
// (@tagscr<sz>) is reused across all same-size tagged-arr
// stores in the function; first-use sizes the slot
// (#15/#26c, size-keyed by #44).
if (elemtn != nil) {
if (istaggedtype(c, elemtn)) {
let slot_sz: i32 = slotsize(c, elemtn);
let scroff: i32 = tagscradd(c, slot_sz);
// Pre-zero scratch (matches push helper).
emitline("\tXORQ\tAX, AX\n");
let zz: i32 = 0;
for (zz < slot_sz) {
emitline("\tMOVQ\tAX, ");
emitoff((scroff + zz): i64);
emitline("(BP)\n");
zz += 8;
};
cgwidentaggedstore(c, elemtn.type_: *tinfo, n.rhs,
"BP", scroff, slot_sz);
cgexpr(c, idx);
if (slot_sz > 1) {
emitline("\tMOVQ\t$");
emitint(slot_sz: i64);
emitline(", CX\n");
emitline("\tIMULQ\tCX, AX\n");
};
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 isarr: bool = false;
if (tn != nil) {
if (tn.kind == nkind.N_TARRAY) {
isarr = true;
};
};
if (isarr) {
emitline("\tLEAQ\t");
emitoff(baselocal.off: i64);
emitline("(BP), BX\n");
} else {
emitline("\tMOVQ\t");
emitoff(baselocal.off: i64);
emitline("(BP), BX\n");
};
} else { if (dotbaseaddr(c, base, "BX")) {
// #259: N_DOT base resolved inline to
// the field address; cgexpr fallback
// would auto-deref + load the array
// field as a VALUE (broken shape). dst
// BX keeps the scaled index live in AX.
} else {
emitline("\tPUSHQ\tAX\n");
cgexpr(c, base);
emitline("\tMOVQ\tAX, BX\n");
emitline("\tPOPQ\tAX\n");
};};};};
emitline("\tADDQ\tAX, BX\n");
let cc: i32 = 0;
for (cc < slot_sz) {
emitline("\tMOVQ\t");
emitoff((scroff + cc): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff(cc: i64);
emitline("(BX)\n");
cc += 8;
};
return;
};
};
// #234: over-cap sret STORE into an indexed lvalue —
// `arr[i] = wide();` STORE-twin of the Fold-B sret RECEIVE
// (a937d67). c.sretdestoff is a STATIC BP-relative offset, so
// only a CONSTANT index into a LOCAL value array yields a
// static dest slot (off + idx*esz) the callee can sret
// straight into. Every other indexed form — runtime index,
// slice/ptr base, N_DOT-base (`s.arr[i]`), chained (`a[i][k]`),
// global base — needs the runtime RDI-pointer dest variant
// deferred to #234-tail and HARD-STOPS loud (rule 7). Mirror of
// cstage cgen.c (#234) N_INDEX arm.
//
// Gate ENTRY on callsretsize(n.rhs) — the callee-return-type
// SSoT (cgenutil.ww) the receive sites use — NOT on
// sretretsize(elemtn): elemtn is only a type node for an
// N_IDENT base, but a value node for N_DOT (4695) / chained
// (4710), which fell to sretretsize=0 and let those forms
// drop SILENTLY through to the truncating store. The callee
// return type equals the dest-element type (checker-guaranteed),
// so the verdict is byte-identical to cstage's cg_sret_retsize.
// The base-shape split below then loud-stops every non-local-
// array form, base-kind-independent.
if (n.rhs != nil && n.rhs.kind == nkind.N_CALL
&& callsretsize(c, n.rhs) > 0) {
let islocalarr: bool = false;
if (baselocal != nil) {
let btn: *node = baselocal.tnode;
if (btn != nil) {
if (btn.kind == nkind.N_TARRAY) { islocalarr = true; };
};
};
let constidx: bool = false;
let cidx: i32 = 0;
if (idx != nil) {
if (idx.kind == nkind.N_INTLIT) {
constidx = true;
cidx = idx.uval: i32;
};
};
if (!islocalarr || !constidx) {
let m234: str = "#234-tail: over-cap tuple sret store to non-local/dynamic-index dest unsupported\n";
os.write(2, m234.ptr, m234.len: u64);
os.exit(1);
};
c.sretdestoff = baselocal.off + cidx * esz;
cgexpr(c, n.rhs);
c.sretdestoff = 0;
return;
};
// #270-1b: aggregate (struct/array/tuple >8B) element
// STORE `a[i] = val`. The scalar store path below copies
// only the first 8 bytes (tnodestoreop MOVQ) — a silent
// truncation. Compute &a[i] (dest) and the rhs SOURCE
// address, then word-copy esz bytes: the WRITE-twin of
// the #268 let-init copy loop. Source shapes mirror that
// loop (ident, N_DOT field via dotchainaddr, `*p`
// deref); a by-value call result is the deferred #271, so
// N_CALL/literal sources fall through unchanged. esz>8
// non-str/non-slice IS a struct/array/tuple here (the
// tagged element already returned above; floats are ≤8).
let aggsrc: bool = (n.rhs.kind == nkind.N_IDENT)
|| (n.rhs.kind == nkind.N_DOT)
|| (n.rhs.kind == nkind.N_UN
&& n.rhs.op == tkind.TK_STAR);
if (esz > 8 && !isstrtype(c, elemtn)
&& !isslicetype(c, elemtn) && aggsrc) {
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 tn2: *node = baselocal.tnode;
let isarr2: bool = false;
if (tn2 != nil) { if (tn2.kind == nkind.N_TARRAY) { isarr2 = true; }; };
if (isarr2) {
emitline("\tLEAQ\t");
emitoff(baselocal.off: i64);
emitline("(BP), BX\n");
} else {
emitline("\tMOVQ\t");
emitoff(baselocal.off: i64);
emitline("(BP), BX\n");
};
} else { if (dotbaseaddr(c, base, "BX")) {
// N_DOT array-field base resolved inline.
} else {
cgexpr(c, base);
emitline("\tMOVQ\tAX, BX\n");
};};};};
emitline("\tPOPQ\tAX\n"); // scaled idx
emitline("\tADDQ\tAX, BX\n");
emitline("\tPUSHQ\tBX\n"); // spill dest
// rhs source address → SI
if (n.rhs.kind == nkind.N_UN
&& n.rhs.op == tkind.TK_STAR) {
cgexpr(c, n.rhs.lhs);
emitline("\tMOVQ\tAX, SI\n");
} else { if (n.rhs.kind == nkind.N_IDENT) {
let sl: *local = localfindnode(c, n.rhs.str);
if (sl != nil) {
emitline("\tLEAQ\t");
emitoff(sl.off: i64);
emitline("(BP), SI\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, n.rhs.str);
emitline("(SB), SI\n");
};
} else {
dotchainaddr(c, n.rhs, "SI");
};};
emitline("\tPOPQ\tBX\n"); // dest
let kc: i32 = 0;
for (kc + 8 <= esz) {
emitline("\tMOVQ\t");
emitoff(kc: i64);
emitline("(SI), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff(kc: i64);
emitline("(BX)\n");
kc += 8;
};
if (kc + 4 <= esz) {
emitline("\tMOVL\t");
emitoff(kc: i64);
emitline("(SI), AX\n");
emitline("\tMOVL\tAX, ");
emitoff(kc: i64);
emitline("(BX)\n");
kc += 4;
};
if (kc + 2 <= esz) {
emitline("\tMOVW\t");
emitoff(kc: i64);
emitline("(SI), AX\n");
emitline("\tMOVW\tAX, ");
emitoff(kc: i64);
emitline("(BX)\n");
kc += 2;
};
if (kc + 1 <= esz) {
emitline("\tMOVB\t");
emitoff(kc: i64);
emitline("(SI), AX\n");
emitline("\tMOVB\tAX, ");
emitoff(kc: i64);
emitline("(BX)\n");
kc += 1;
};
return;
};
cgexpr(c, n.rhs); // value → AX
// str/slice: spill cap (CX) + len (BX) before
// computing the index so the post-index store can
// pop all three. str=24B (#1/Phase 3) collides with
// slice=24B, so this MUST gate on kind (cstage's
// elem_is_str||elem_is_slice, cmd/w6c/cgen.c:3581),
// never a bare esz==24: a >16B struct is also >=24B
// but takes the struct-copy path, not this 3-word
// {ptr,len,cap} store. Write-side mirror of the
// cgindex read-path gate (#7/754).
if (isstrtype(c, elemtn) || isslicetype(c, elemtn)) {
emitline("\tPUSHQ\tCX\n");
emitline("\tPUSHQ\tBX\n");
};
// Float element: spill X0 (not AX — AX is junk for
// floats) across the idx/base eval. A call-index
// (a[geti()]=v) clobbers X0 and would otherwise lose
// the value. Mirrors the *p=v float deref store
// twin in cgassign (#125).
let spisfloat: bool = isfloattype(c, elemtn);
let spmov: str = "MOVSD";
if (isf32type(c, elemtn)) { spmov = "MOVSS"; };
if (spisfloat) {
emitline("\tSUBQ\t$8, SP\n");
emitline("\t");
emitline(spmov);
emitline("\tX0, (SP)\n");
} else {
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 { if (dotbaseaddr(c, base, "BX")) {
// #135: N_DOT base address-of-field inline.
} else {
cgexpr(c, base);
emitline("\tMOVQ\tAX, BX\n");
};};};};
emitline("\tPOPQ\tAX\n"); // scaled idx
emitline("\tADDQ\tAX, BX\n");
// Reload value: float reloads X0 from the spill slot;
// non-float pops AX. Twin of the value-spill site
// above (#125).
if (spisfloat) {
emitline("\t");
emitline(spmov);
emitline("\t(SP), X0\n");
emitline("\tADDQ\t$8, SP\n");
} else {
emitline("\tPOPQ\tAX\n"); // value
};
// str/slice: pop the saved len + cap and store
// all three words. Kind-gate, not size — see the
// spill site above (#1/Phase 3, #7/754).
if (isstrtype(c, elemtn) || isslicetype(c, elemtn)) {
emitline("\tMOVQ\tAX, (BX)\n");
emitline("\tPOPQ\tCX\n");
emitline("\tMOVQ\tCX, 8(BX)\n");
emitline("\tPOPQ\tCX\n");
emitline("\tMOVQ\tCX, 16(BX)\n");
return;
};
// float element → store FROM X0 (MOVSS/MOVSD): cgexpr
// left the value in X0, and the value-spill pair
// above keeps X0 live across the idx/base eval so
// a call-index (a[geti()]=v) doesn't lose it (#125).
// For f32 the #104 CVTSD2SS narrowing only touches X0,
// so the AX store below would write raw double low-
// bits, garbage for f32 (#122, mirrors cstage cgen.c
// arr[i]= float store).
if (isfloattype(c, elemtn)) {
let fmov: str = "MOVSD";
if (isf32type(c, elemtn)) { fmov = "MOVSS"; };
emitline("\t");
emitline(fmov);
emitline("\tX0, (BX)\n");
return;
};
let isop: str = tnodestoreop(c, elemtn, esz);
emitline("\t");
emitline(isop);
emitline("\tAX, (BX)\n");
return;
};
// Compound assign on an indexed scalar element
// (`arr[i] OP= v`). Pre-#133 the outer `if (n.op ==
// TK_ASSIGN)` had no else and non-ASSIGN ops fell off
// the cgassign function emitting NOTHING — silent
// no-op. Mirror the chained-pointer-field compound
// template at cmd/w6c/cgen.c:3281-3317: same address
// computation as the ASSIGN arm above, then
// tnodeloadop(BX)→AX, POP rhs→CX, combine, tnodestoreop.
// Float / str / slice / tagged element compound stays
// unwired — cstage's compound template never carried
// those payload kinds. Same shape gate as the cstage
// branch (cgen.c #133).
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;
let elemtn: *node = nil;
if (base != nil) {
if (base.kind == nkind.N_IDENT) {
let bn: str = base.str;
baselocal = localfindnode(c, bn);
if (baselocal != nil) {
esz = elemsizeofc(c, baselocal.tnode);
let btn: *node = baselocal.tnode;
if (btn != nil) {
let bk: nkind = btn.kind;
if (bk == nkind.N_TARRAY) { elemtn = btn.lhs; };
if (bk == nkind.N_TSLICE) { elemtn = btn.lhs; };
if (bk == nkind.N_TPTR) { elemtn = btn.lhs; };
};
} else {
// #11: store/compound twin of the #10 cgindex
// read fix. A global str/slice element store hit
// the same kind whitelist — N_TNAME (str) /
// N_TSLICE matched NEITHER arm, so esz stayed 8
// and the store emitted a full-word MOVQ — an
// 8-byte OUT-OF-BOUNDS write past a 1-byte
// element — instead of MOVB. cstage
// (cmd/w6c/cgen.c N_INDEX store) dispatches esz
// off idx_eff->sub->size + the elem-kind flags
// off eff->sub uniformly, base is_arr?LEAQ:MOVQ
// name(SB). Align UP and resolve elemtn exactly
// like the local branch above (element node for
// ARRAY/SLICE/PTR; nil for str so tnodestoreop
// picks MOVB on the store arm, and the compound
// arm's str/slice hard-error still fires on a
// []str element).
let tn: *node = letvartnode(c, bn);
if (tn != nil) {
globalname = bn;
esz = elemsizeofc(c, tn);
let bk: nkind = tn.kind;
if (bk == nkind.N_TARRAY) {
isglobalarr = true;
elemtn = tn.lhs;
} else {
isglobalptr = true;
if (bk == nkind.N_TSLICE) { elemtn = tn.lhs; };
if (bk == nkind.N_TPTR) { elemtn = tn.lhs; };
};
};
};
} else { if (base.kind == nkind.N_DOT) {
let dt: *tinfo = lhs.type_: *tinfo;
if (dt != nil) { esz = dt.size: i32; elemtn = lhs; };
} else { if (base.kind == nkind.N_INDEX) {
let et: *tinfo = lhs.type_: *tinfo;
if (et != nil) {
esz = et.size: i32;
elemtn = lhs;
};
};};};
};
// #133-expanded: hard-error unwired payload kinds
// LOUD (rule-7) — replaces prior silent skip.
if (elemtn != nil) {
if (istaggedtype(c, elemtn)) {
let msg: str = "indexed-lvalue compound on tagged element not wired (#133/rule-7)\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
if (isstrtype(c, elemtn)) {
let msg: str = "indexed-lvalue compound on str element not wired (#133/rule-7)\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
if (isslicetype(c, elemtn)) {
let msg: str = "indexed-lvalue compound on slice element not wired (#133/rule-7)\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
if (isfloattype(c, elemtn)) {
let msg: str = "indexed-lvalue compound on float element not wired (#133/rule-7)\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
};
cgexpr(c, n.rhs);
emitline("\tPUSHQ\tAX\n");
cgexpr(c, idx);
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", CX\n");
emitline("\tIMULQ\tCX, AX\n");
};
emitline("\tPUSHQ\tAX\n");
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 { if (dotbaseaddr(c, base, "BX")) {
// #135: N_DOT base address-of-field inline.
} else {
cgexpr(c, base);
emitline("\tMOVQ\tAX, BX\n");
};};};};
emitline("\tPOPQ\tAX\n");
emitline("\tADDQ\tAX, BX\n");
let lop: str = tnodeloadop(c, elemtn, esz);
emitline("\t");
emitline(lop);
emitline("\t(BX), AX\n");
emitline("\tPOPQ\tCX\n");
// #133-expanded: all 10 integer compound ops wired.
// SLASHEQ/PERCENTEQ: CQO+IDIVQ (signed) or zero-DX+
// DIVQ (unsigned). LSHIFTEQ via SHLQ; RSHIFTEQ via
// SARQ (signed) or SHRQ (unsigned) per #136.
// Signedness from elemtn.type_.
let unsignd_c: bool = false;
if (elemtn != nil) {
if (elemtn.type_ != nil) {
unsignd_c = typeisunsigned(elemtn.type_: *tinfo);
};
};
let wired: bool = false;
if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tCX, AX\n"); wired = true; };
if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tCX, AX\n"); wired = true; };
if (n.op == tkind.TK_STAREQ) { emitline("\tIMULQ\tCX, AX\n"); wired = true; };
if (n.op == tkind.TK_AMPEQ) { emitline("\tANDQ\tCX, AX\n"); wired = true; };
if (n.op == tkind.TK_PIPEEQ) { emitline("\tORQ\tCX, AX\n"); wired = true; };
if (n.op == tkind.TK_CARETEQ) { emitline("\tXORQ\tCX, AX\n"); wired = true; };
if (n.op == tkind.TK_SLASHEQ) {
if (unsignd_c) { emitline("\tMOVQ\t$0, DX\n"); emitline("\tDIVQ\tCX\n"); }
else { emitline("\tCQO\n"); emitline("\tIDIVQ\tCX\n"); };
wired = true;
};
if (n.op == tkind.TK_PERCENTEQ) {
if (unsignd_c) { emitline("\tMOVQ\t$0, DX\n"); emitline("\tDIVQ\tCX\n"); }
else { emitline("\tCQO\n"); emitline("\tIDIVQ\tCX\n"); };
emitline("\tMOVQ\tDX, AX\n");
wired = true;
};
if (n.op == tkind.TK_LSHIFTEQ) { emitline("\tSHLQ\tCX, AX\n"); wired = true; };
if (n.op == tkind.TK_RSHIFTEQ) {
if (unsignd_c) { emitline("\tSHRQ\tCX, AX\n"); }
else { emitline("\tSARQ\tCX, AX\n"); };
wired = true;
};
if (!wired) {
let msg: str = "indexed-lvalue compound: unknown compound op (#133/rule-7)\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
let sop: str = tnodestoreop(c, elemtn, esz);
emitline("\t");
emitline(sop);
emitline("\tAX, (BX)\n");
return;
};
};
};
// `arr[i].field = v`: N_DOT lhs whose lhs is N_INDEX. Symmetric
// write-side of the cgdot N_INDEX-lhs branch added for task #8.
// Compute &arr[i] inline (LEAQ for `[N]Struct`, MOVQ for
// `[N]*Struct` / `[]Struct` / `*Struct`), deref once when the
// element is `*Struct`, then store rhs at field.offset(addr).
// Without this both shapes silently drop the store — there is no
// existing wwstage branch for N_DOT(N_INDEX,...) lhs at all (the
// N_INDEX-lhs branch above handles bare `arr[i] = v`, not the
// field write).
if (lhs != nil) {
if (lhs.kind == nkind.N_DOT && lhs.lhs != nil
&& lhs.lhs.kind == nkind.N_INDEX) {
let idxbase: *node = lhs.lhs.lhs;
let idx: *node = lhs.lhs.rhs;
let fld2: str = lhs.str;
if (idxbase != nil) { if (idxbase.kind == nkind.N_IDENT) {
if (idx != nil) {
let lc: *local = localfindnode(c, idxbase.str);
if (lc != nil) { if (lc.tnode != nil) {
let tn: *node = lc.tnode;
let elemt: *node = nil;
let baseisarray: bool = false;
let tk: nkind = tn.kind;
if (tk == nkind.N_TSLICE) { elemt = tn.lhs; };
if (tk == nkind.N_TARRAY) { elemt = tn.lhs; baseisarray = true; };
if (tk == nkind.N_TPTR) { elemt = tn.lhs; };
let sname: str;
sname.ptr = nil; sname.len = 0;
let viaptr: bool = false;
if (elemt != nil) {
if (elemt.kind == nkind.N_TPTR) {
let inner: *node = elemt.lhs;
if (inner != nil) { if (inner.kind == nkind.N_TNAME) {
sname = inner.str;
viaptr = true;
};};
} else { if (elemt.kind == nkind.N_TNAME) {
sname = elemt.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, fld2)) {
let esz: i32 = elemsizeofc(c, tn);
// f64/f32: rhs in X0. Spill to stack,
// compute &arr[i] in BX (deref if *T),
// then reload X0 and MOVSD/MOVSS.
if (n.op == tkind.TK_ASSIGN) {
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, idx);
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", CX\n");
emitline("\tIMULQ\tCX, AX\n");
};
if (baseisarray) {
emitline("\tLEAQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
} else {
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
};
emitline("\tADDQ\tAX, BX\n");
if (viaptr) { emitline("\tMOVQ\t(BX), 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;
};
// str/slice: rhs leaves AX=ptr,
// BX=len, CX=cap (#1/Phase 3). Spill
// all three across the index/address
// computation (IMULQ's CX scratch
// clobbers cap), stage &arr[i] in DX
// off the str AX/BX/CX convention
// (mirrors s.f=v), then store the full
// triple at foff+0/+8/+16.
if (isstrtype(c, fi.tnode) || isslicetype(c, fi.tnode)) {
cgexpr(c, n.rhs);
emitline("\tPUSHQ\tCX\n");
emitline("\tPUSHQ\tBX\n");
emitline("\tPUSHQ\tAX\n");
cgexpr(c, idx);
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", CX\n");
emitline("\tIMULQ\tCX, AX\n");
};
if (baseisarray) {
emitline("\tLEAQ\t");
emitoff(lc.off: i64);
emitline("(BP), DX\n");
} else {
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), DX\n");
};
emitline("\tADDQ\tAX, DX\n");
if (viaptr) { emitline("\tMOVQ\t(DX), DX\n"); };
emitline("\tPOPQ\tAX\n");
emitline("\tPOPQ\tBX\n");
emitline("\tPOPQ\tCX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(fi.foff: i64, "DX");
emitline("\n");
emitline("\tMOVQ\tBX, ");
emitdispreg((fi.foff + 8): i64, "DX");
emitline("\n");
emitline("\tMOVQ\tCX, ");
emitdispreg((fi.foff + 16): i64, "DX");
emitline("\n");
return;
};
// scalar plain `=`
cgexpr(c, n.rhs);
emitline("\tPUSHQ\tAX\n");
cgexpr(c, idx);
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", CX\n");
emitline("\tIMULQ\tCX, AX\n");
};
if (baseisarray) {
emitline("\tLEAQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
} else {
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
};
emitline("\tADDQ\tAX, BX\n");
if (viaptr) { emitline("\tMOVQ\t(BX), BX\n"); };
emitline("\tPOPQ\tAX\n");
let sop: str = fieldstoreop(c, fi);
emitline("\t");
emitline(sop);
emitline("\tAX, ");
emitdispreg(fi.foff: i64, "BX");
emitline("\n");
return;
};
// compound: rhs→push; compute struct
// addr→BX (deref if *T); push addr;
// load old field→AX; pop addr→BX,
// rhs→CX; combine; store. Float/str
// compound not wired.
cgexpr(c, n.rhs);
emitline("\tPUSHQ\tAX\n");
cgexpr(c, idx);
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", CX\n");
emitline("\tIMULQ\tCX, AX\n");
};
if (baseisarray) {
emitline("\tLEAQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
} else {
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
};
emitline("\tADDQ\tAX, BX\n");
if (viaptr) { emitline("\tMOVQ\t(BX), BX\n"); };
emitline("\tPUSHQ\tBX\n");
let lop: str = fieldloadop(c, fi);
emitline("\t");
emitline(lop);
emitline("\t");
emitdispreg(fi.foff: i64, "BX");
emitline(", AX\n");
emitline("\tPOPQ\tBX\n");
emitline("\tPOPQ\tCX\n");
if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tCX, AX\n"); };
if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tCX, AX\n"); };
if (n.op == tkind.TK_STAREQ) { emitline("\tIMULQ\tCX, AX\n"); };
if (n.op == tkind.TK_AMPEQ) { emitline("\tANDQ\tCX, AX\n"); };
if (n.op == tkind.TK_PIPEEQ) { emitline("\tORQ\tCX, AX\n"); };
if (n.op == tkind.TK_CARETEQ) { emitline("\tXORQ\tCX, AX\n"); };
let sop2: str = fieldstoreop(c, fi);
emitline("\t");
emitline(sop2);
emitline("\tAX, ");
emitdispreg(fi.foff: i64, "BX");
emitline("\n");
return;
};
fi = fi.finext;
};
};
};
};};
};
};};
};
};
// 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). Base accepts the
// explicit-deref form `(*p).f = ...` (parser N_UN(STAR, IDENT))
// by retargeting to the inner IDENT so the via_ptr branch fires
// the same as auto-deref `p.f = v`. v1 scope: bare-IDENT inner.
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_UN) {
if (base.op == tkind.TK_STAR) {
if (base.lhs != nil) {
if (base.lhs.kind == nkind.N_IDENT) {
base = base.lhs;
};
};
};
};
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) {
// structlookupchain (#22) handles the
// alias-chain miss; same shape as the
// cgdot pointer-to-struct read site.
let si: *structinfo = structlookupchain(c, inner);
if (si != nil) {
let fi: *fieldinfo = si.fields;
for (fi != nil) {
let fn_: str = fi.fname;
if (streq(fn_, fld)) {
// Tagged-union field via *struct base — full slot
// rewrite via cgwidentaggedstore basereg="BX". Pre-#26
// fell through to the scalar store and dropped tag
// + payload.
if (n.op == tkind.TK_ASSIGN
&& istaggedtype(c, fi.tnode)) {
let fsz: i32 = slotsize(c, fi.tnode);
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
cgwidentaggedstore(c, fi.tnode.type_: *tinfo,
n.rhs, "BX", fi.foff, fsz);
return;
};
// #234-tail: via-ptr (`p.f`) sret field STORE. The dest must
// be a runtime RDI pointer (the BP-relative c.sretdestoff
// can't name `p.f`); deferred. HARD-STOP loud, never fall
// through to the truncating generic store (rule 7).
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_CALL
&& sretretsize(c, fi.tnode) > 0) {
let m234: str = "#234-tail: over-cap tuple sret store to via-ptr field dest unsupported\n";
os.write(2, m234.ptr, m234.len: u64);
os.exit(1);
};
// struct-typed field via *struct base — three
// rhs shapes (call/structlit added with #5;
// closes #27 marker here):
// N_IDENT: word-copy from rhs slot.
// N_CALL: cgexpr → AX/DX/CX per #4's cgreturn
// ABI; load *struct ptr into BX after the
// call, sized stores per the ABI size.
// N_STRUCTLIT: field-walk; reload BX before
// each store so cgexpr can clobber AX/BX.
// register RECV reads AX/DX/CX at 8-byte
// granularity — size via structabisize (cstage
// SSoT lu->size, check.c:760; cgen.c:7720
// sz=lu->size at the receive twin).
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_CALL
&& fi.tnode != nil
&& fi.tnode.kind == nkind.N_TNAME
&& primsize(fi.tnode.str) == 0) {
let ssi: *structinfo = structlookup(c, fi.tnode.str);
if (ssi != nil) {
let ssz: i32 = structabisize(ssi);
if (ssz <= 24) {
let tlm: i32 = ssz - (ssz / 8) * 8;
if (tlm == 0 || tlm == 1
|| tlm == 2 || tlm == 4) {
cgexpr(c, n.rhs);
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
let full: i32 = ssz / 8;
let i: i32 = 0;
for (i < full) {
let reg: str = "AX";
if (i == 1) { reg = "DX"; };
if (i == 2) { reg = "CX"; };
emitline("\tMOVQ\t");
emitline(reg);
emitline(", ");
emitdispreg((fi.foff + i * 8): i64, "BX");
emitline("\n");
i += 1;
};
if (tlm > 0) {
let top: str = "MOVB";
if (tlm == 4) { top = "MOVL"; };
if (tlm == 2) { top = "MOVW"; };
let treg: str = "AX";
if (full == 1) { treg = "DX"; };
if (full == 2) { treg = "CX"; };
emitline("\t");
emitline(top);
emitline("\t");
emitline(treg);
emitline(", ");
emitdispreg((fi.foff + full * 8): i64, "BX");
emitline("\n");
};
return;
};
};
};
};
// #18: delegate to cgstructlitfill so a nested struct-
// typed structlit value recurses instead of dropping
// its trailing bytes. mode=1 (DST_PTR_LOCAL) reloads BX
// from lc.off(BP) before zero-fill and before every
// field store.
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_STRUCTLIT
&& fi.tnode != nil
&& fi.tnode.kind == nkind.N_TNAME
&& primsize(fi.tnode.str) == 0) {
let ssi: *structinfo = structlookup(c, fi.tnode.str);
if (ssi != nil) {
cgstructlitfill(c, ssi, n.rhs, 1, lc.off, "",
fi.foff);
return;
};
};
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_IDENT
&& fi.tnode != nil
&& fi.tnode.kind == nkind.N_TNAME
&& primsize(fi.tnode.str) == 0) {
let ssi: *structinfo = structlookup(c, fi.tnode.str);
let srhs: *local = localfindnode(c, n.rhs.str);
if (ssi != nil) { if (srhs != nil) {
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
let ssz: i32 = ssi.totsize;
let k: i32 = 0;
for (k + 8 <= ssz) {
emitline("\tMOVQ\t");
emitoff((srhs.off + k): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg((fi.foff + k): i64, "BX");
emitline("\n");
k += 8;
};
if (k < ssz) {
let tail: i32 = ssz - k;
let lop: str = "MOVQ";
if (tail == 4) { lop = "MOVL"; }
else { if (tail == 1) { lop = "MOVB"; }; };
emitline("\t");
emitline(lop);
emitline("\t");
emitoff((srhs.off + k): i64);
emitline("(BP), AX\n");
emitline("\t");
emitline(lop);
emitline("\tAX, ");
emitdispreg((fi.foff + k): i64, "BX");
emitline("\n");
};
return;
};};
};
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(c, 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");
};
};
if (n.op == tkind.TK_ASSIGN) {
// str/slice field via *struct: str IS []u8, so both
// store the full 3-word {ptr,len,cap} from (AX,BX,CX).
// CX holds cap, so stage the struct addr in DX and
// store at foff/+8/+16 (#1/Phase 3).
if (isstrtype(c, fi.tnode) || isslicetype(c, fi.tnode)) {
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), DX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(fi.foff: i64, "DX");
emitline("\n");
emitline("\tMOVQ\tBX, ");
emitdispreg((fi.foff + 8): i64, "DX");
emitline("\n");
emitline("\tMOVQ\tCX, ");
emitdispreg((fi.foff + 16): i64, "DX");
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(c, 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) {
// structlookupchain (#22) — same shape
// as the cgdot direct-local read site.
let si: *structinfo = structlookupchain(c, tn);
if (si != nil) {
let fi: *fieldinfo = si.fields;
for (fi != nil) {
let fn_: str = fi.fname;
if (streq(fn_, fld)) {
// Tagged-union field in a direct struct local —
// full slot rewrite at (lc.off + fi.foff)(BP)
// via cgwidentaggedstore basereg="BP". Pre-#26
// fell through and dropped tag + payload.
if (n.op == tkind.TK_ASSIGN
&& istaggedtype(c, fi.tnode)) {
let fsz: i32 = slotsize(c, fi.tnode);
cgwidentaggedstore(c, fi.tnode.type_: *tinfo,
n.rhs, "BP", lc.off + fi.foff, fsz);
return;
};
// #234: over-cap sret STORE into a LOCAL struct field —
// `s.f = wide();` where f's type returns via sret
// (sretretsize > 0: a >24B struct OR an over-cap tuple).
// STORE-twin of the Fold-B sret RECEIVE (a937d67): point
// the callee's hidden RDI dest at the field slot
// (c.sretdestoff = lc.off + fi.foff) so it writes the
// WHOLE value there, never the truncating generic store
// below. Mirror of cstage cgen.c (#234) field local arm.
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_CALL
&& sretretsize(c, fi.tnode) > 0) {
c.sretdestoff = lc.off + fi.foff;
cgexpr(c, n.rhs);
c.sretdestoff = 0;
return;
};
// struct-typed field on a direct struct
// local — three rhs shapes (call/structlit
// added with #5; closes #27 marker here):
// N_IDENT: word-copy from rhs slot.
// N_CALL: cgexpr → AX/DX/CX; sized stores
// directly at (lc.off+fi.foff)(BP).
// N_STRUCTLIT: field-walk; each inner
// field stored at +fi.foff+inner_foff(BP).
// BP-rel direct, no addr scratch needed.
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_CALL
&& fi.tnode != nil
&& fi.tnode.kind == nkind.N_TNAME
&& primsize(fi.tnode.str) == 0) {
let ssi: *structinfo = structlookup(c, fi.tnode.str);
if (ssi != nil) {
let ssz: i32 = structabisize(ssi);
if (ssz <= 24) {
let tlm: i32 = ssz - (ssz / 8) * 8;
if (tlm == 0 || tlm == 1
|| tlm == 2 || tlm == 4) {
cgexpr(c, n.rhs);
let full: i32 = ssz / 8;
let i: i32 = 0;
for (i < full) {
let reg: str = "AX";
if (i == 1) { reg = "DX"; };
if (i == 2) { reg = "CX"; };
emitline("\tMOVQ\t");
emitline(reg);
emitline(", ");
emitoff((lc.off + fi.foff + i * 8): i64);
emitline("(BP)\n");
i += 1;
};
if (tlm > 0) {
let top: str = "MOVB";
if (tlm == 4) { top = "MOVL"; };
if (tlm == 2) { top = "MOVW"; };
let treg: str = "AX";
if (full == 1) { treg = "DX"; };
if (full == 2) { treg = "CX"; };
emitline("\t");
emitline(top);
emitline("\t");
emitline(treg);
emitline(", ");
emitoff((lc.off + fi.foff + full * 8): i64);
emitline("(BP)\n");
};
return;
};
};
};
};
// #18: delegate to cgstructlitfill so a nested struct-
// typed structlit value recurses instead of dropping
// its trailing bytes. mode=0 (DST_BP) — direct BP-rel,
// no BX reload.
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_STRUCTLIT
&& fi.tnode != nil
&& fi.tnode.kind == nkind.N_TNAME
&& primsize(fi.tnode.str) == 0) {
let ssi: *structinfo = structlookup(c, fi.tnode.str);
if (ssi != nil) {
cgstructlitfill(c, ssi, n.rhs, 0, 0, "",
lc.off + fi.foff);
return;
};
};
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_IDENT
&& fi.tnode != nil
&& fi.tnode.kind == nkind.N_TNAME
&& primsize(fi.tnode.str) == 0) {
let ssi: *structinfo = structlookup(c, fi.tnode.str);
let srhs: *local = localfindnode(c, n.rhs.str);
if (ssi != nil) { if (srhs != nil) {
let ssz: i32 = ssi.totsize;
let k: i32 = 0;
for (k + 8 <= ssz) {
emitline("\tMOVQ\t");
emitoff((srhs.off + k): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((lc.off + fi.foff + k): i64);
emitline("(BP)\n");
k += 8;
};
if (k < ssz) {
let tail: i32 = ssz - k;
let lop: str = "MOVQ";
if (tail == 4) { lop = "MOVL"; }
else { if (tail == 1) { lop = "MOVB"; }; };
emitline("\t");
emitline(lop);
emitline("\t");
emitoff((srhs.off + k): i64);
emitline("(BP), AX\n");
emitline("\t");
emitline(lop);
emitline("\tAX, ");
emitoff((lc.off + fi.foff + k): i64);
emitline("(BP)\n");
};
return;
};};
};
if (n.op != tkind.TK_ASSIGN) {
// Compound on direct struct-local
// scalar field: load current → push
// → eval rhs → combine → store
// (mirror cgen.c:3477 local arm).
let lop: str = fieldloadop(c, fi);
emitline("\t");
emitline(lop);
emitline("\t");
emitoff((lc.off + fi.foff): i64);
emitline("(BP), BX\n");
emitline("\tPUSHQ\tBX\n");
};
cgexpr(c, n.rhs);
if (n.op != tkind.TK_ASSIGN) {
emitline("\tPOPQ\tBX\n");
// PLUSEQ commutes; MINUSEQ needs
// lhs-rhs (BX old lhs, AX 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/slice field direct: str IS []u8, so both store the
// full 3-word {ptr,len,cap} from (AX,BX,CX) at +0/+8/+16.
// BP base, no scratch reload needed; the generic fldstoreop
// below would write only AX, dropping .len/.cap (#1/Phase 3).
if (isstrtype(c, fi.tnode) || isslicetype(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");
emitline("\tMOVQ\tCX, ");
emitoff((lc.off + fi.foff + 16): 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(c, 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;
};
};
if (n.op != tkind.TK_ASSIGN) {
// Compound on local `str|slice` pseudo-field:
// load current → push → eval rhs → combine →
// store (mirror cgen.c:3235 local arm).
emitline("\tMOVQ\t");
emitoff((lc.off + delta): i64);
emitline("(BP), 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");
};
emitline("\tMOVQ\tAX, ");
emitoff((lc.off + delta): i64);
emitline("(BP)\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)) {
// #234-tail: GLOBAL (`g.f`) sret field STORE. c.sretdestoff is
// BP-relative only and can't name a global slot; the runtime
// RDI-pointer dest variant is deferred. HARD-STOP loud, never
// the truncating generic store (rule 7).
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_CALL
&& sretretsize(c, fi.tnode) > 0) {
let m234: str = "#234-tail: over-cap tuple sret store to global field dest unsupported\n";
os.write(2, m234.ptr, m234.len: u64);
os.exit(1);
};
// struct-typed field on a global struct base —
// three rhs shapes (call/structlit added with
// #5; closes #27 marker here):
// N_IDENT: word-copy from rhs slot.
// N_CALL: cgexpr → AX/DX/CX; LEAQ base into BX
// after call, sized stores per natural size.
// N_STRUCTLIT: field-walk; reload BX per store.
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_CALL
&& fi.tnode != nil
&& fi.tnode.kind == nkind.N_TNAME
&& primsize(fi.tnode.str) == 0) {
let ssi: *structinfo = structlookup(c, fi.tnode.str);
if (ssi != nil) {
let ssz: i32 = structabisize(ssi);
if (ssz <= 24) {
let tlm: i32 = ssz - (ssz / 8) * 8;
if (tlm == 0 || tlm == 1
|| tlm == 2 || tlm == 4) {
cgexpr(c, n.rhs);
emitline("\tLEAQ\t");
emitsymname(c, bn);
emitline("(SB), BX\n");
let full: i32 = ssz / 8;
let i: i32 = 0;
for (i < full) {
let reg: str = "AX";
if (i == 1) { reg = "DX"; };
if (i == 2) { reg = "CX"; };
emitline("\tMOVQ\t");
emitline(reg);
emitline(", ");
emitdispreg((fi.foff + i * 8): i64, "BX");
emitline("\n");
i += 1;
};
if (tlm > 0) {
let top: str = "MOVB";
if (tlm == 4) { top = "MOVL"; };
if (tlm == 2) { top = "MOVW"; };
let treg: str = "AX";
if (full == 1) { treg = "DX"; };
if (full == 2) { treg = "CX"; };
emitline("\t");
emitline(top);
emitline("\t");
emitline(treg);
emitline(", ");
emitdispreg((fi.foff + full * 8): i64, "BX");
emitline("\n");
};
return;
};
};
};
};
// #18: delegate to cgstructlitfill so a nested struct-
// typed structlit value recurses instead of dropping
// its trailing bytes. mode=2 (DST_GLOBAL) reloads BX
// via LEAQ bn(SB) before zero-fill and before every
// field store.
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_STRUCTLIT
&& fi.tnode != nil
&& fi.tnode.kind == nkind.N_TNAME
&& primsize(fi.tnode.str) == 0) {
let ssi: *structinfo = structlookup(c, fi.tnode.str);
if (ssi != nil) {
cgstructlitfill(c, ssi, n.rhs, 2, 0, bn,
fi.foff);
return;
};
};
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_IDENT
&& fi.tnode != nil
&& fi.tnode.kind == nkind.N_TNAME
&& primsize(fi.tnode.str) == 0) {
let ssi: *structinfo = structlookup(c, fi.tnode.str);
let srhs: *local = localfindnode(c, n.rhs.str);
if (ssi != nil) { if (srhs != nil) {
emitline("\tLEAQ\t");
emitsymname(c, bn);
emitline("(SB), BX\n");
let ssz: i32 = ssi.totsize;
let k: i32 = 0;
for (k + 8 <= ssz) {
emitline("\tMOVQ\t");
emitoff((srhs.off + k): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg((fi.foff + k): i64, "BX");
emitline("\n");
k += 8;
};
if (k < ssz) {
let tail: i32 = ssz - k;
let lop: str = "MOVQ";
if (tail == 4) { lop = "MOVL"; }
else { if (tail == 1) { lop = "MOVB"; }; };
emitline("\t");
emitline(lop);
emitline("\t");
emitoff((srhs.off + k): i64);
emitline("(BP), AX\n");
emitline("\t");
emitline(lop);
emitline("\tAX, ");
emitdispreg((fi.foff + k): i64, "BX");
emitline("\n");
};
return;
};};
};
if (n.op == tkind.TK_ASSIGN) {
cgexpr(c, n.rhs);
if (isstrtype(c, fi.tnode)) {
// str IS []u8: cgexpr left
// (AX=ptr, BX=len, CX=cap). CX
// holds cap, so stage the base
// addr in DX and store all three
// words (#1/Phase 3).
emitline("\tLEAQ\t");
emitsymname(c, bn);
emitline("(SB), DX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(fi.foff: i64, "DX");
emitline("\n");
emitline("\tMOVQ\tBX, ");
emitdispreg((fi.foff + 8): i64, "DX");
emitline("\n");
emitline("\tMOVQ\tCX, ");
emitdispreg((fi.foff + 16): i64, "DX");
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(c, 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(c, 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(c, 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) {
// #70 (#12): inner-struct layout via the stamped
// base.type_ (peel *→struct) + tinfo.fields,
// replacing dotinnerstructptr's structinfo walk.
// Gate is strict-equal to the deleted helper: fire
// only when the chain root is a LOCAL ident AND every
// dot resolves through a *struct (dotinnerstructptr
// recursed per level on a *struct field, bailing on a
// by-value-struct intermediate). Reproducing that
// exactly avoids an untested widening past cstage.
// Global-root chains stay in their pre-existing shared
// base-eval breakage (filed #27).
let croot: *node = base;
let allptr: bool = true;
for (croot != nil && croot.kind == nkind.N_DOT) {
let ct: *tinfo = croot.type_: *tinfo;
for (ct != nil && ct.kind == tykind.TY_NAMED) { ct = ct.under; };
let okp: bool = false;
if (ct != nil) { if (ct.kind == tykind.TY_PTR) {
let cs: *tinfo = ct.sub;
for (cs != nil && cs.kind == tykind.TY_NAMED) { cs = cs.under; };
if (cs != nil) { if (cs.kind == tykind.TY_STRUCT) { okp = true; }; };
}; };
if (!okp) { allptr = false; };
croot = croot.lhs;
};
let it: *tinfo = nil;
if (allptr && croot != nil && croot.kind == nkind.N_IDENT &&
localfindnode(c, croot.str) != nil) {
it = base.type_: *tinfo;
};
for (it != nil && it.kind == tykind.TY_NAMED) { it = it.under; };
if (it != nil) { if (it.kind == tykind.TY_PTR) {
let st: *tinfo = it.sub;
for (st != nil && st.kind == tykind.TY_NAMED) { st = st.under; };
if (st != nil) { if (st.kind == tykind.TY_STRUCT) {
let tf: *tfield = st.fields;
for (tf != nil) {
if (streq(tf.name, fld)) {
let ft: *tinfo = tf.type_;
if (n.op == tkind.TK_ASSIGN) {
if (typeisstr(ft) || typeisslice(ft)) {
// str/slice: rhs leaves AX=ptr,
// BX=len, CX=cap (#1/Phase 3). Spill
// all three across the base-expr eval
// (it may clobber any reg), stage the
// *struct ptr in DX off the str
// AX/BX/CX convention (mirrors s.f=v),
// then store the full triple at
// foff+0/+8/+16.
cgexpr(c, n.rhs);
emitline("\tPUSHQ\tCX\n");
emitline("\tPUSHQ\tBX\n");
emitline("\tPUSHQ\tAX\n");
cgexpr(c, base);
emitline("\tMOVQ\tAX, DX\n");
emitline("\tPOPQ\tAX\n");
emitline("\tPOPQ\tBX\n");
emitline("\tPOPQ\tCX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(tf.offset: i64, "DX");
emitline("\n");
emitline("\tMOVQ\tBX, ");
emitdispreg((tf.offset + 8u64): i64, "DX");
emitline("\n");
emitline("\tMOVQ\tCX, ");
emitdispreg((tf.offset + 16u64): i64, "DX");
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 (typeisfloat(ft)) {
let mov: str = "MOVSD";
if (typeisf32(ft)) { 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(tf.offset: 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 = tnodestoreop(c, n.rhs, ft.slotsize: i32);
emitline("\t");
emitline(sop);
emitline("\tAX, ");
emitdispreg(tf.offset: i64, "BX");
emitline("\n");
return;
};
// #133-expanded site 3: chained-pointer-
// field compound. Pre-#133-expanded the
// wwstage chained-DOT-spine branch only
// handled TK_ASSIGN; compound ops on a
// chained-*struct.field shape (e.g.
// `d.i.v += 7`) silently emitted nothing.
// cstage cgen.c:3281-3317 handles this
// (now-expanded for the same 10 ops +
// hard-errors); this is its rule-10 twin.
// All 10 integer compound ops wired;
// float/str/slice/tagged field-type
// hard-errors LOUD. Signed RSHIFTEQ uses
// SARQ (signed) or SHRQ (unsigned) per #136.
if (n.op != tkind.TK_ASSIGN) {
if (typeisstr(ft)) {
let m: str = "chained-ptr-field compound on str element not wired (#133/rule-7)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
if (typeisslice(ft)) {
let m: str = "chained-ptr-field compound on slice element not wired (#133/rule-7)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
if (typeisfloat(ft)) {
let m: str = "chained-ptr-field compound on float element not wired (#133/rule-7)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
if (typeistagged(ft)) {
let m: str = "chained-ptr-field compound on tagged element not wired (#133/rule-7)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
cgexpr(c, n.rhs);
emitline("\tPUSHQ\tAX\n");
cgexpr(c, base);
emitline("\tPUSHQ\tAX\n");
let fsz: i32 = ft.slotsize: i32;
let unsignd_f: bool = typeisunsigned(ft);
let lopf: str = loadopsz(!unsignd_f, fsz);
emitline("\t");
emitline(lopf);
emitline("\t");
emitdispreg(tf.offset: i64, "AX");
emitline(", AX\n");
emitline("\tPOPQ\tBX\n");
emitline("\tPOPQ\tCX\n");
let wired_f: bool = false;
if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tCX, AX\n"); wired_f = true; };
if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tCX, AX\n"); wired_f = true; };
if (n.op == tkind.TK_STAREQ) { emitline("\tIMULQ\tCX, AX\n"); wired_f = true; };
if (n.op == tkind.TK_AMPEQ) { emitline("\tANDQ\tCX, AX\n"); wired_f = true; };
if (n.op == tkind.TK_PIPEEQ) { emitline("\tORQ\tCX, AX\n"); wired_f = true; };
if (n.op == tkind.TK_CARETEQ) { emitline("\tXORQ\tCX, AX\n"); wired_f = true; };
if (n.op == tkind.TK_SLASHEQ) {
if (unsignd_f) { emitline("\tMOVQ\t$0, DX\n"); emitline("\tDIVQ\tCX\n"); }
else { emitline("\tCQO\n"); emitline("\tIDIVQ\tCX\n"); };
wired_f = true;
};
if (n.op == tkind.TK_PERCENTEQ) {
if (unsignd_f) { emitline("\tMOVQ\t$0, DX\n"); emitline("\tDIVQ\tCX\n"); }
else { emitline("\tCQO\n"); emitline("\tIDIVQ\tCX\n"); };
emitline("\tMOVQ\tDX, AX\n");
wired_f = true;
};
if (n.op == tkind.TK_LSHIFTEQ) { emitline("\tSHLQ\tCX, AX\n"); wired_f = true; };
if (n.op == tkind.TK_RSHIFTEQ) {
if (unsignd_f) { emitline("\tSHRQ\tCX, AX\n"); }
else { emitline("\tSARQ\tCX, AX\n"); };
wired_f = true;
};
if (!wired_f) {
let m: str = "chained-ptr-field compound: unknown op (#133/rule-7)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
let sopf: str = tnodestoreop(c, n.rhs, fsz);
emitline("\t");
emitline(sopf);
emitline("\tAX, ");
emitdispreg(tf.offset: i64, "BX");
emitline("\n");
return;
};
};
tf = tf.tnext;
};
}; };
}; };
};
};
};
};
// Chained N_DOT spine write through value-struct fields (any
// depth) — `o.i.a = 10`, `v.a.b.c = …`. Also handles a slice/str
// pseudo-field leaf (`b.buf.len = 5`). Mirror of cstage cgen.c's
// chained-DOT write branch. Without this, depth ≥ 3 writes and
// the slice/str pseudo-field write through a value-struct chain
// silently emit no store. Only plain `=` is wired.
if (lhs != nil) {
if (lhs.kind == nkind.N_DOT && lhs.lhs != nil
&& lhs.lhs.kind == nkind.N_DOT
&& n.op == tkind.TK_ASSIGN) {
let rootname: str = "";
let rootoff: i32 = 0;
let totaloff: i32 = 0;
let leaftype: *tinfo = nil;
let slicedelta: i32 = -1;
let isglobal: bool = false;
let ptrroot: bool = false;
let yok: bool = dotchainresolve(c, lhs,
&rootname, &rootoff, &totaloff,
&leaftype, &slicedelta, &isglobal, &ptrroot);
if (yok) {
// `*T` root and global share the CX-based emit:
// loader runs AFTER cgexpr(rhs) so AX/BX/X0 stay
// intact, then stores at total_off off CX.
let viacx: bool = isglobal || ptrroot;
if (slicedelta >= 0) {
cgexpr(c, n.rhs);
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), CX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), CX\n");
};
emitline("\tMOVQ\tAX, ");
emitdispreg((totaloff + slicedelta): i64, "CX");
emitline("\n");
} else {
emitline("\tMOVQ\tAX, ");
emitoff((rootoff + totaloff + slicedelta): i64);
emitline("(BP)\n");
};
return;
};
if (typeisstr(leaftype) || typeisslice(leaftype)) {
// str/slice: store ptr/len/cap. cgexpr leaves
// CX=cap, so the viacx base goes in DX (not CX) to
// avoid clobbering it — same as the single-dot str
// field store (#1/Phase 3).
cgexpr(c, n.rhs);
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), DX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), DX\n");
};
emitline("\tMOVQ\tAX, ");
emitdispreg(totaloff: i64, "DX");
emitline("\n");
emitline("\tMOVQ\tBX, ");
emitdispreg((totaloff + 8): i64, "DX");
emitline("\n");
emitline("\tMOVQ\tCX, ");
emitdispreg((totaloff + 16): i64, "DX");
emitline("\n");
} else {
emitline("\tMOVQ\tAX, ");
emitoff((rootoff + totaloff): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tBX, ");
emitoff((rootoff + totaloff + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((rootoff + totaloff + 16): i64);
emitline("(BP)\n");
};
return;
};
// TY_STRUCT terminal: three rhs shapes:
// - N_IDENT: word-copy from the rhs local slot
// (cgexpr is skipped — no whole-struct register
// convention for an arbitrary local).
// - N_CALL (added with #5): cgexpr leaves the
// value in AX/DX/CX per #4's cgreturn ABI; sized
// stores write only the declared field size.
// cgreturn touches only AX/DX/CX so for
// ptrroot/global we load the dst addr into BX
// (not CX) after the call to keep CX as the
// third value word.
// - N_STRUCTLIT (added with #5): field-by-field
// store; for ptrroot/global the dst addr is
// reloaded into BX before each store so cgexpr
// can clobber AX/BX between fields.
// #71: the struct-terminal cases below still drive the
// structinfo machinery (structnaturalsize /
// cgstructlitfill), so recover the struct NAME from the
// leaf tinfo's TY_NAMED wrapper. Peeled-TY_STRUCT +
// structlookup!=nil is byte-equal to the old `N_TNAME &&
// primsize==0 && structlookup` guard: a named non-struct
// (tagged/alias) peels to a non-STRUCT kind, and
// structlookup decides struct-ness off the same declared
// name either way.
let leafstruct: bool = false;
let leafname: str = "";
if (leaftype != nil) {
let lp: *tinfo = leaftype;
for (lp != nil && lp.kind == tykind.TY_NAMED) {
lp = lp.under;
};
if (lp != nil) {
if (lp.kind == tykind.TY_STRUCT) { leafstruct = true; };
};
if (leaftype.kind == tykind.TY_NAMED) {
leafname = leaftype.name;
};
};
if (n.rhs != nil
&& n.rhs.kind == nkind.N_CALL
&& leafstruct) {
let lsi: *structinfo = structlookup(c, leafname);
if (lsi != nil) {
// register RECV reads AX/DX/CX at 8-byte
// granularity — size via structabisize
// (cstage SSoT lu->size, check.c:760).
let lsz: i32 = structabisize(lsi);
if (lsz <= 24) {
let tlm: i32 = lsz - (lsz / 8) * 8;
if (tlm == 0 || tlm == 1
|| tlm == 2 || tlm == 4) {
cgexpr(c, n.rhs);
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), BX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), BX\n");
};
};
let full: i32 = lsz / 8;
let i: i32 = 0;
for (i < full) {
let reg: str = "AX";
if (i == 1) { reg = "DX"; };
if (i == 2) { reg = "CX"; };
if (viacx) {
emitline("\tMOVQ\t");
emitline(reg);
emitline(", ");
emitdispreg((totaloff + i * 8): i64, "BX");
emitline("\n");
} else {
emitline("\tMOVQ\t");
emitline(reg);
emitline(", ");
emitoff((rootoff + totaloff + i * 8): i64);
emitline("(BP)\n");
};
i += 1;
};
if (tlm > 0) {
let top: str = "MOVB";
if (tlm == 4) { top = "MOVL"; };
if (tlm == 2) { top = "MOVW"; };
let treg: str = "AX";
if (full == 1) { treg = "DX"; };
if (full == 2) { treg = "CX"; };
if (viacx) {
emitline("\t");
emitline(top);
emitline("\t");
emitline(treg);
emitline(", ");
emitdispreg((totaloff + full * 8): i64, "BX");
emitline("\n");
} else {
emitline("\t");
emitline(top);
emitline("\t");
emitline(treg);
emitline(", ");
emitoff((rootoff + totaloff + full * 8): i64);
emitline("(BP)\n");
};
};
return;
};
};
};
};
// #18: delegate to cgstructlitfill so a nested struct-
// typed structlit value recurses instead of dropping
// its trailing bytes. mode picks the dst flavor:
// ptrroot → mode=1 (DST_PTR_LOCAL), reload BX from
// rootoff(BP).
// isglobal → mode=2 (DST_GLOBAL), reload BX via
// LEAQ rootname(SB).
// else → mode=0 (DST_BP), direct BP-rel, no reload.
if (n.rhs != nil
&& n.rhs.kind == nkind.N_STRUCTLIT
&& leafstruct) {
let lsi: *structinfo = structlookup(c, leafname);
if (lsi != nil) {
let dmode: i32 = 0;
let ddisp: i32 = rootoff + totaloff;
if (ptrroot) {
dmode = 1;
ddisp = totaloff;
};
if (isglobal) {
dmode = 2;
ddisp = totaloff;
};
cgstructlitfill(c, lsi, n.rhs,
dmode, rootoff, rootname,
ddisp);
return;
};
};
if (n.rhs != nil
&& n.rhs.kind == nkind.N_IDENT
&& leafstruct) {
let ssi: *structinfo = structlookup(c, leafname);
let srhs: *local = localfindnode(c, n.rhs.str);
if (ssi != nil) { if (srhs != nil) {
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), CX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), CX\n");
};
};
let ssz: i32 = ssi.totsize;
let k: i32 = 0;
for (k + 8 <= ssz) {
emitline("\tMOVQ\t");
emitoff((srhs.off + k): i64);
emitline("(BP), AX\n");
if (viacx) {
emitline("\tMOVQ\tAX, ");
emitdispreg((totaloff + k): i64, "CX");
emitline("\n");
} else {
emitline("\tMOVQ\tAX, ");
emitoff((rootoff + totaloff + k): i64);
emitline("(BP)\n");
};
k += 8;
};
if (k < ssz) {
let tail: i32 = ssz - k;
let lop: str = "MOVQ";
if (tail == 4) { lop = "MOVL"; }
else { if (tail == 1) { lop = "MOVB"; }; };
emitline("\t");
emitline(lop);
emitline("\t");
emitoff((srhs.off + k): i64);
emitline("(BP), AX\n");
if (viacx) {
emitline("\t");
emitline(lop);
emitline("\tAX, ");
emitdispreg((totaloff + k): i64, "CX");
emitline("\n");
} else {
emitline("\t");
emitline(lop);
emitline("\tAX, ");
emitoff((rootoff + totaloff + k): i64);
emitline("(BP)\n");
};
};
return;
};};
};
if (typeisfloat(leaftype)) {
let mov: str = "MOVSD";
if (typeisf32(leaftype)) { mov = "MOVSS"; };
cgexpr(c, n.rhs);
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), CX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), CX\n");
};
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitdispreg(totaloff: i64, "CX");
emitline("\n");
} else {
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff((rootoff + totaloff): i64);
emitline("(BP)\n");
};
return;
};
// Scalar leaf store-op by size — the same size→op
// dispatch fieldstoreop used on the leaf fieldinfo, now
// keyed on the leaf tinfo's slot width (#71).
let sop: str = "MOVQ";
if (leaftype != nil) {
let ssz: i32 = leaftype.slotsize: i32;
if (ssz == 1) { sop = "MOVB"; }
else { if (ssz == 2) { sop = "MOVW"; }
else { if (ssz == 4) { sop = "MOVL"; }; }; };
};
cgexpr(c, n.rhs);
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), CX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), CX\n");
};
emitline("\t");
emitline(sop);
emitline("\tAX, ");
emitdispreg(totaloff: i64, "CX");
emitline("\n");
} else {
emitline("\t");
emitline(sop);
emitline("\tAX, ");
emitoff((rootoff + totaloff): i64);
emitline("(BP)\n");
};
return;
};
};
};
// 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.
// Kept as fallback below the generalized walker for any shape
// the walker doesn't recognize.
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(c, 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;
let lvftn: *node = nil;
for (lvf != nil) {
if (streq(lvf.name, nm)) {
isfg = isfloattype(c, lvf.tnode);
isf32g = isf32type(c, lvf.tnode);
lvftn = lvf.tnode;
lvf = nil;
} else {
lvf = lvf.lvnext;
};
};
if (isfg) {
cgexpr(c, n.rhs);
let mov: str = "MOVSD";
let addf: str = "ADDSD";
let subf: str = "SUBSD";
let mulf: str = "MULSD";
let divf: str = "DIVSD";
if (isf32g) {
mov = "MOVSS";
addf = "ADDSS";
subf = "SUBSS";
mulf = "MULSS";
divf = "DIVSS";
};
emitline("\tLEAQ\t");
emitsymname(c, nm);
emitline("(SB), CX\n");
if (n.op == tkind.TK_ASSIGN) {
emitline("\t");
emitline(mov);
emitline("\tX0, (CX)\n");
return;
};
// Compound: X1 = load; X1 OP= X0; store X1.
// ADDSD/SUBSD/MULSD/DIVSD are register-register
// only, so we can't combine direct to memory.
let fop: str;
fop.ptr = nil; fop.len = 0;
if (n.op == tkind.TK_PLUSEQ) { fop = addf; };
if (n.op == tkind.TK_MINUSEQ) { fop = subf; };
if (n.op == tkind.TK_STAREQ) { fop = mulf; };
if (n.op == tkind.TK_SLASHEQ) { fop = divf; };
if (fop.len == 0) {
// Unsupported (e.g., %= on float):
// fall back to plain store of rhs.
emitline("\t");
emitline(mov);
emitline("\tX0, (CX)\n");
return;
};
emitline("\t");
emitline(mov);
emitline("\t(CX), X1\n");
emitline("\t");
emitline(fop);
emitline("\tX0, X1\n");
emitline("\t");
emitline(mov);
emitline("\tX1, (CX)\n");
return;
};
// #220: `g = f();` where g is a GLOBAL aggregate >24B
// (struct or #272 array). No BP slot for the sret dest,
// so route RDI to g's symbol; the callee writes straight
// into g's storage. The scalar store below would emit a
// truncated `MOVQ AX, g(SB)` and drop the body. Mirror
// of the C cgen N_ASSIGN global arm (cmd/w6c/cgen.c).
if (n.op == tkind.TK_ASSIGN && n.rhs != nil
&& n.rhs.kind == nkind.N_CALL && lvftn != nil) {
let gsz: i32 = 0;
if (lvftn.kind == nkind.N_TNAME) {
let gsi: *structinfo = structlookup(c, lvftn.str);
if (gsi != nil) { gsz = structabisize(gsi); };
};
if (lvftn.kind == nkind.N_TARRAY) {
let gat: *tinfo = lvftn.type_: *tinfo;
for (gat != nil && gat.kind == tykind.TY_NAMED) { gat = gat.under; };
if (gat != nil) { gsz = gat.size: i32; };
};
if (gsz > 24) {
let rscs: i32 = callsretsize(c, n.rhs);
if (rscs > 0) {
c.sretdestnode = lhs;
cgexpr(c, n.rhs);
c.sretdestnode = nil;
return;
};
};
};
// #272: `g = f();` where g is a GLOBAL ARRAY ≤24B.
// The callee leaves AX/DX/CX (#272 reg-return; an array
// is never float-class, so AX/DX/CX is always the
// transport); the scalar store below would truncate to
// MOVQ AX, g(SB). LEAQ the symbol into DI, store the
// full+tail words. Mirror of cstage cgen.c ≤24B global
// arm. #276: a ≤24B STRUCT global receive can be
// float-class (X0/X1) so it stays at its pre-existing
// behaviour — no consumer (rule-10 aligned with cstage;
// note non-float struct globals also truncate symmetrically
// here, byte-id-clean — #276 covers both).
if (n.op == tkind.TK_ASSIGN && n.rhs != nil
&& n.rhs.kind == nkind.N_CALL && lvftn != nil
&& lvftn.kind == nkind.N_TARRAY) {
let aggsz: i32 = 0;
let aat: *tinfo = lvftn.type_: *tinfo;
for (aat != nil && aat.kind == tykind.TY_NAMED) { aat = aat.under; };
if (aat != nil) { aggsz = aat.size: i32; };
if (aggsz > 0 && aggsz <= 24) {
cgexpr(c, n.rhs);
emitline("\tLEAQ\t");
emitsymname(c, nm);
emitline("(SB), DI\n");
let full: i32 = aggsz / 8;
let tail: i32 = aggsz - full * 8;
let i: i32 = 0;
for (i < full) {
emitline("\tMOVQ\t");
emitline(tupreg(i));
emitline(", ");
emitoff((i * 8): i64);
emitline("(DI)\n");
i += 1;
};
if (tail > 0) {
let top: str = "MOVB";
if (tail == 4) { top = "MOVL"; };
if (tail == 2) { top = "MOVW"; };
emitline("\t");
emitline(top);
emitline("\t");
emitline(tupreg(full));
emitline(", ");
emitoff((full * 8): i64);
emitline("(DI)\n");
};
return;
};
};
cgexpr(c, n.rhs);
if (n.op == tkind.TK_ASSIGN) {
// str/slice top-level let: str IS []u8, so both store the
// full 3-word {ptr,len,cap}. Stash cap in DI before LEAQ
// overwrites CX, then store ptr/len/cap via &name(SB)
// (#1/Phase 3).
if (letvarisstr(c, nm) || 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;
};
// Compound RMW for a top-level let: load through
// LEAQ + localloadop when the slot is narrow so
// a prior `*(&letname): *iN` deref-store doesn't
// leave stale upper bytes feeding the combine.
let glop: str = localloadop(c, lvftn);
if (streq(glop, "MOVQ")) {
emitline("\tMOVQ\t");
emitsymname(c, nm);
emitline("(SB), BX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, nm);
emitline("(SB), CX\n");
emitline("\t");
emitline(glop);
emitline("\t(CX), 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) {
// #136: signed RSHIFTEQ → SARQ.
let unsignd_r: bool = false;
if (lvftn != nil) {
if (lvftn.type_ != nil) {
unsignd_r = typeisunsigned(lvftn.type_: *tinfo);
};
};
if (!unsignd_r) {
unsignd_r = nodeisunsigned(c, n.rhs);
};
emitline("\tMOVQ\tAX, CX\n");
if (unsignd_r) { emitline("\tSHRQ\tCX, BX\n"); }
else { emitline("\tSARQ\tCX, BX\n"); };
}
// Post-63332fe: /= and %= for a top-level
// let. Same shape as the IDENT-local path:
// park rhs in CX, slot value (BX) into AX,
// CQO (or zero DX), IDIVQ (or DIVQ) CX,
// ferry AX or DX back to BX for the shared
// store-BX tail below.
else { if (n.op == tkind.TK_SLASHEQ || n.op == tkind.TK_PERCENTEQ) {
let unsignd: bool = false;
if (lvftn != nil) {
unsignd = typeisunsigned(lvftn.type_: *tinfo);
};
if (!unsignd) {
unsignd = nodeisunsigned(c, n.rhs);
};
emitline("\tMOVQ\tAX, CX\n");
emitline("\tMOVQ\tBX, AX\n");
if (unsignd) {
emitline("\tMOVQ\t$0, DX\n");
emitline("\tDIVQ\tCX\n");
} else {
emitline("\tCQO\n");
emitline("\tIDIVQ\tCX\n");
};
if (n.op == tkind.TK_SLASHEQ) {
emitline("\tMOVQ\tAX, BX\n");
} else {
emitline("\tMOVQ\tDX, BX\n");
};
}
else {
// Unsupported compound: store rhs
// directly. Mirrors the local path's
// legacy fallback for unknown ops.
didcompound = false;
emitline("\tMOVQ\tAX, ");
emitsymname(c, nm);
emitline("(SB)\n");
};};};};};};};};};
if (didcompound) {
emitline("\tMOVQ\tBX, ");
emitsymname(c, nm);
emitline("(SB)\n");
};
return;
};
// #10 Fold B: over-cap tuple reassign `t = f();`. t's
// slot (off) IS the caller-prealloc dest; the callee
// writes the whole tuple through hidden RDI. Keys on
// callsretsize (the shared sret SSoT) for a tuple-
// returning call — rettupleof distinguishes it from a
// >24B struct, which keeps its own size-aware recv
// below. Mirrors the cstage N_ASSIGN-ident over-cap arm.
if (n.op == tkind.TK_ASSIGN && n.rhs != nil
&& n.rhs.kind == nkind.N_CALL) {
let rtup: *node = rettupleof(c, n.rhs);
if (rtup != nil) {
let rscs: i32 = callsretsize(c, n.rhs);
if (rscs > 0) {
c.sretdestoff = off;
cgexpr(c, n.rhs);
c.sretdestoff = 0;
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);
};
// Struct-typed local reassignment: `s = expr;` where s
// is a TY_STRUCT local of size <=24B. Two rhs shapes
// (mirrors cglet's N_STRUCTLIT and the call-result
// receive branch):
// - N_STRUCTLIT: walk fields, store at off+foff
// directly. ASYMMETRY-safe (no register copy from
// the caller; values come from cgexpr).
// - N_CALL: cgexpr → AX/DX/CX, sized stores per the
// declared struct size — MOVQ for full 8B chunks
// plus MOVL/MOVW/MOVB tail. See cglet receive
// site for the ASYMMETRY rationale.
// Struct-IDENT word-copy rhs (s = p) is left unwired;
// #5 is scoped to receive-side of #4 (calls + literals).
// fsz dispatch uses the explicit {1→MOVB, 4→MOVL, else
// MOVQ} pattern (not fieldstoreop) to match cstage
// cgen.c N_ASSIGN byte-identically — wwstage's
// fieldstoreop returns MOVW for fsz==2 which cstage
// doesn't emit (tracked separately as the cstage/
// wwstage MOVW divergence task).
if (lcn != nil) {
let lctn: *node = lcn.tnode;
let lcsname: str;
lcsname.ptr = nil; lcsname.len = 0;
if (lctn != nil) {
if (lctn.kind == nkind.N_TNAME) {
lcsname = lctn.str;
};
};
if (lcsname.len > 0) {
let lcsi: *structinfo = structlookup(c, lcsname);
if (lcsi != nil) {
// register RECV reads AX/DX/CX at 8-byte
// granularity — size via structabisize (cstage
// N_ASSIGN-IDENT branch sets sz = lu->size,
// cgen.c:4704; check.c:760 SSoT). The pre-
// #169 structnaturalsize shorts struct{i64,i32}
// (natural 12, ABI 16) to MOVQ+MOVL where
// cstage writes MOVQ+MOVQ.
let lcnsz: i32 = structabisize(lcsi);
if (n.op == tkind.TK_ASSIGN) {
if (n.rhs != nil
&& n.rhs.kind == nkind.N_STRUCTLIT) {
// Delegate to the shared BP-relative
// structlit fill helper. Handles
// TK_ELLIPSIS autofill + per-field
// walk; nested struct-typed values
// recurse via the helper (#17 fix).
// Helper uses the explicit {1→MOVB,
// 4→MOVL, else MOVQ} sized-store
// dispatch (NOT fieldstoreop) to stay
// byte-identical with cstage pending
// #13 (fsz==2 MOVW divergence). See
// cgstructlitfillbp docstring.
cgstructlitfillbp(c, lcsi, n.rhs, off);
return;
};
if (n.rhs != nil
&& n.rhs.kind == nkind.N_CALL) {
// sret receive (#23): plain
// TY_STRUCT > 24B from a CALL.
// `s` is the prealloc dest; the
// callee writes through hidden RDI
// directly into off(BP). Mirror of
// cglet's sret branch.
if (lcnsz > 24) {
let rscs: i32 = callsretsize(c, n.rhs);
if (rscs > 0) {
c.sretdestoff = off;
cgexpr(c, n.rhs);
c.sretdestoff = 0;
return;
};
};
let lcsz: i32 = lcnsz;
if (lcsz <= 24) {
let tlm: i32 = lcsz - (lcsz / 8) * 8;
if (tlm == 0 || tlm == 1
|| tlm == 2 || tlm == 4) {
cgexpr(c, n.rhs);
let full: i32 = lcsz / 8;
let i: i32 = 0;
for (i < full) {
let reg: str = "AX";
if (i == 1) { reg = "DX"; };
if (i == 2) { reg = "CX"; };
emitline("\tMOVQ\t");
emitline(reg);
emitline(", ");
emitoff((off + i * 8): i64);
emitline("(BP)\n");
i += 1;
};
if (tlm > 0) {
let top: str = "MOVB";
if (tlm == 4) { top = "MOVL"; };
if (tlm == 2) { top = "MOVW"; };
let treg: str = "AX";
if (full == 1) { treg = "DX"; };
if (full == 2) { treg = "CX"; };
emitline("\t");
emitline(top);
emitline("\t");
emitline(treg);
emitline(", ");
emitoff((off + full * 8): i64);
emitline("(BP)\n");
};
return;
};
};
};
};
};
};
};
// #267: array return-by-value RECV — `c = mk()` where c is
// an array local. Arrays ride the struct reg/sret recv path.
// >24B sret keys on callsretsize (the shared SSoT, c's slot
// IS the prealloc dest); ≤24B arrives in AX/DX/CX, sized
// stores. Array natural size (tinfo.size = sub.size*len)
// mirrors cstage lu->size. No structfloatclass (pure-int
// element arrays).
if (lcn != nil && n.op == tkind.TK_ASSIGN
&& n.rhs != nil && n.rhs.kind == nkind.N_CALL) {
let acati: *tinfo = nil;
if (lcn.tnode != nil) { acati = lcn.tnode.type_: *tinfo; };
for (acati != nil && acati.kind == tykind.TY_NAMED) {
acati = acati.under;
};
if (acati != nil && acati.kind == tykind.TY_ARRAY) {
let lcsz: i32 = acati.size: i32;
if (lcsz > 24) {
let rscs: i32 = callsretsize(c, n.rhs);
if (rscs > 0) {
c.sretdestoff = off;
cgexpr(c, n.rhs);
c.sretdestoff = 0;
return;
};
};
if (lcsz <= 24) {
let tlm: i32 = lcsz - (lcsz / 8) * 8;
if (tlm == 0 || tlm == 1
|| tlm == 2 || tlm == 4) {
cgexpr(c, n.rhs);
let full: i32 = lcsz / 8;
let i: i32 = 0;
for (i < full) {
let reg: str = "AX";
if (i == 1) { reg = "DX"; };
if (i == 2) { reg = "CX"; };
emitline("\tMOVQ\t");
emitline(reg);
emitline(", ");
emitoff((off + i * 8): i64);
emitline("(BP)\n");
i += 1;
};
if (tlm > 0) {
let top: str = "MOVB";
if (tlm == 4) { top = "MOVL"; };
if (tlm == 2) { top = "MOVW"; };
let treg: str = "AX";
if (full == 1) { treg = "DX"; };
if (full == 2) { treg = "CX"; };
emitline("\t");
emitline(top);
emitline("\t");
emitline(treg);
emitline(", ");
emitoff((off + full * 8): i64);
emitline("(BP)\n");
};
return;
};
};
};
};
// Float-typed local: rhs lands in X0; store via MOVSD/
// MOVSS, no AX shuffle. Compound (+= -= *= /=) loads
// slot into X1, combines into X1, stores X1 back —
// ADDSD/SUBSD/MULSD/DIVSD are register-register only.
if (lcf) {
cgexpr(c, n.rhs);
let mov: str = "MOVSD";
let addf: str = "ADDSD";
let subf: str = "SUBSD";
let mulf: str = "MULSD";
let divf: str = "DIVSD";
if (lcf32) {
mov = "MOVSS";
addf = "ADDSS";
subf = "SUBSS";
mulf = "MULSS";
divf = "DIVSS";
};
if (n.op == tkind.TK_ASSIGN) {
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff(off: i64);
emitline("(BP)\n");
return;
};
let fop: str;
fop.ptr = nil; fop.len = 0;
if (n.op == tkind.TK_PLUSEQ) { fop = addf; };
if (n.op == tkind.TK_MINUSEQ) { fop = subf; };
if (n.op == tkind.TK_STAREQ) { fop = mulf; };
if (n.op == tkind.TK_SLASHEQ) { fop = divf; };
if (fop.len == 0) {
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff(off: i64);
emitline("(BP)\n");
return;
};
emitline("\t");
emitline(mov);
emitline("\t");
emitoff(off: i64);
emitline("(BP), X1\n");
emitline("\t");
emitline(fop);
emitline("\tX0, X1\n");
emitline("\t");
emitline(mov);
emitline("\tX1, ");
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");
};
// str IS []u8: store the cap word too, identical to
// the slice store (#1/Phase 3).
if (lcstr || lcsl) {
emitline("\tMOVQ\tCX, ");
emitoff((off + 16): i64);
emitline("(BP)\n");
};
return;
};
// Pick the load width for compound RMW. Signed-narrow
// locals must sign-extend the slot before the combine
// — ADDQ/SUBQ on amem reads 8B raw, which is wrong
// after a 4B deref-store leaves the upper bytes stale.
let llop: str = "MOVQ";
if (lcn != nil) { llop = localloadop(c, lcn.tnode); };
if (streq(llop, "MOVQ")) {
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("\t");
emitline(llop);
emitline("\t");
emitoff(off: i64);
emitline("(BP), BX\n");
if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tAX, BX\n"); };
if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tAX, 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) {
// #136: signed RSHIFTEQ → SARQ.
let unsignd_r: bool = false;
if (lcn != nil) {
if (lcn.tnode != nil) {
if (lcn.tnode.type_ != nil) {
unsignd_r = typeisunsigned(lcn.tnode.type_: *tinfo);
};
};
};
if (!unsignd_r) {
unsignd_r = nodeisunsigned(c, n.rhs);
};
emitline("\tMOVQ\tAX, CX\n");
if (unsignd_r) { emitline("\tSHRQ\tCX, BX\n"); }
else { emitline("\tSARQ\tCX, BX\n"); };
};
// Post-63332fe: /= and %= for an IDENT local. Pre-fix
// fell through with no case, so BX (still holding the
// freshly loaded slot value) was stored back unchanged
// — a silent no-op rather than the natural rhs-only
// shape the global/deref siblings took. Park rhs in
// CX, slot value (BX) into AX, CQO/IDIVQ, ferry AX
// (quotient) or DX (remainder) back to BX.
if (n.op == tkind.TK_SLASHEQ || n.op == tkind.TK_PERCENTEQ) {
let unsignd: bool = false;
if (lcn != nil) {
if (lcn.tnode != nil) {
unsignd = typeisunsigned(lcn.tnode.type_: *tinfo);
};
};
if (!unsignd) {
unsignd = nodeisunsigned(c, n.rhs);
};
emitline("\tMOVQ\tAX, CX\n");
emitline("\tMOVQ\tBX, AX\n");
if (unsignd) {
emitline("\tMOVQ\t$0, DX\n");
emitline("\tDIVQ\tCX\n");
} else {
emitline("\tCQO\n");
emitline("\tIDIVQ\tCX\n");
};
if (n.op == tkind.TK_SLASHEQ) {
emitline("\tMOVQ\tAX, BX\n");
} else {
emitline("\tMOVQ\tDX, BX\n");
};
};
emitline("\tMOVQ\tBX, ");
emitoff(off: i64);
emitline("(BP)\n");
return;
};
};
return;
};