Files
ww/selfhost/cmd/wcc/cgenexpr.ww
Hojun-Cho 267e81b89e wcc/cgen: GAP-A.ptr global-array base — LEAQ name(SB) not (BP) (#11, both stages)
A global fixed array's .ptr (= &A[0]) must take the SB base, but cstage
emitted frame-relative LEAQ off(BP) for BOTH let- and def-global arrays
-> *A.ptr read frame garbage (0 instead of the element). cstage-SILENT;
wwstage def-global was a loud link-error. The .ptr read arm now gates
off==0 && (let_islet || def_isarraydef) -> LEAQ name(SB), reusing the
def-array index base predicate (cgen.c:4367, the #94/#231/#48 class).
Locals (off != 0) stay BP-relative -- the 14 toolchain backing-ptr sites
unaffected.

wwstage let-global was already correct; this adds the missing def-global
arm (cgenexpr.ww), converging cstage/wwstage byte-identical across all
three flavors (local / let-global / def-global) and closing a latent
cstage-only let-global cs!=ww divergence.

Byte-id 990-997 8/8 (corpus has no global .ptr); w6c/w6c_ww binaries move
(cgen changed). test/wcc/818 table-driven, build+run+byte-id per flavor.
2026-06-08 17:39:45 +09:00

11939 lines
403 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, nil);
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) {
// #22a: a STAMPED-CARRIER scrutinee (the #67 matchscrutt
// shape — is/as on a tuple element t.N, a struct field, an
// indexed element) is not an N_TTAGGED type-AST node; its
// tagged type rides .type_. Resolve via the tinfo twin
// (flatvariantidxt), the same core the widen-store uses —
// cstage cg_tag_for_variant is type-based for every
// scrutinee shape, so the AST-keyed -1 here was a silent
// tag-0 clamp on wwstage (cs CMPQ $1 vs ww CMPQ $0).
let sti: *tinfo = tagged.type_: *tinfo;
sti = tichase(sti);
if (sti != nil && sti.kind == tykind.TY_TAGGED && vt.type_ != nil) {
return flatvariantidxt(sti, vt.type_: *tinfo, false);
};
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;
ou = tichase(ou);
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_;
sv = tichase(sv);
if (sv == nil) { return false; };
if (sv.kind != tykind.TY_TUPLE) { return false; };
cgtaggedtuplepayloadshift(c, sv);
return true;
};
// cgtrytaggedshift — Family C (#35, unwrap source): if the `?`/`!`
// operand's success variant (tag 0) is itself a TAGGED union, the
// unwrapped value is a NESTED box (ww keeps nested unions
// un-flattened) riding the payload words intact — shift past the
// outer tag so consumers see the standard AX=tag cursor. The scalar
// MOVQ DX,AX tail carried only the inner tag and dropped the payload
// (ken unw16). Nullable folds to one word and stays on the scalar
// move. Twin of cgtrytupleshift; mirrors cstage N_TRYPROP/N_TRYUNW.
fn cgtrytaggedshift(c: *cgen, n: *node) bool = {
if (n.lhs == nil) { return false; };
let ou: *tinfo = n.lhs.type_: *tinfo;
ou = tichase(ou);
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_;
sv = tichase(sv);
if (sv == nil) { return false; };
if (sv.kind != tykind.TY_TAGGED) { return false; };
if (sv.nullable != 0) { return false; };
emitline("\tMOVQ\tDX, AX\n");
if (sv.size: i32 > 8) { emitline("\tMOVQ\tCX, DX\n"); };
if (sv.size: i32 > 16) { emitline("\tMOVQ\tR8, CX\n"); };
return true;
};
// cgtryunwcursor — Family C (#35/#46): land the `?`/`!` operand's
// tagged box in the AX/DX/CX/R8 cursor for the unwrap tail. Non-call
// sources don't fill the cursor on their own: an IDENT loads it from
// its frame slot, a mem-based read (deref at any size, >32B
// INDEX/DOT) from the box address cgexpr leaves in AX. Both were
// silent word0 unwraps pre-#35. >32B non-call stays loud (the cursor
// cannot carry it; #40 family). Mirrors cstage N_TRYPROP/N_TRYUNW.
fn cgtryunwcursor(c: *cgen, n: *node, opname: str) void = {
let u: *tinfo = nil;
if (n.lhs != nil) { u = n.lhs.type_: *tinfo; };
u = tichase(u);
let utag: bool = false;
if (u != nil) {
if (u.kind == tykind.TY_TAGGED && u.nullable == 0) {
utag = true;
};
};
if (utag && u.size: i32 > TUPLE_GPCAP * 8
&& n.lhs.kind != nkind.N_CALL) {
let p37: str = "#37: `";
os.write(2, p37.ptr, p37.len: u64);
os.write(2, opname.ptr, opname.len: u64);
let m37t: str = "` on a >32B mem-based tagged read unwired (#40-family follow-up)\n";
os.write(2, m37t.ptr, m37t.len: u64);
os.exit(1);
};
if (utag && n.lhs.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, n.lhs.str);
if (lc == nil) {
let p35: str = "#35: `";
os.write(2, p35.ptr, p35.len: u64);
os.write(2, opname.ptr, opname.len: u64);
let m35g: str = "` on a global tagged ident unwired (rule 7)\n";
os.write(2, m35g.ptr, m35g.len: u64);
os.exit(1);
};
let boff: i32 = lc.off;
let bsz: i32 = u.size: i32;
if (bsz > 24) {
emitline("\tMOVQ\t");
emitoff((boff + 24): i64);
emitline("(BP), R8\n");
};
if (bsz > 16) {
emitline("\tMOVQ\t");
emitoff((boff + 16): i64);
emitline("(BP), CX\n");
};
if (bsz > 8) {
emitline("\tMOVQ\t");
emitoff((boff + 8): i64);
emitline("(BP), DX\n");
};
emitline("\tMOVQ\t");
emitoff(boff: i64);
emitline("(BP), AX\n");
return;
};
if (taggedmemread(c, n.lhs)) {
let bsz2: i32 = 0;
if (u != nil) { bsz2 = u.size: i32; };
cgexpr(c, n.lhs);
if (bsz2 > 24) { emitline("\tMOVQ\t24(AX), R8\n"); };
if (bsz2 > 16) { emitline("\tMOVQ\t16(AX), CX\n"); };
if (bsz2 > 8) { emitline("\tMOVQ\t8(AX), DX\n"); };
emitline("\tMOVQ\t(AX), AX\n");
return;
};
cgexpr(c, n.lhs);
};
// 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);
};
// Family C (#35/#46): ident/deref sources land the box in the
// cursor here (was a silent word0 unwrap); call sources keep
// the plain cgexpr emission byte-for-byte.
cgtryunwcursor(c, n, "?");
// 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; };
u = tichase(u);
let r: *tinfo = nil;
if (c.fnret != nil) { r = c.fnret.type_: *tinfo; };
r = tichase(r);
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_, false);
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; };
if (cgtrytaggedshift(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);
};
};
};
// Family C (#35/#46): see the cgtryprop twin.
cgtryunwcursor(c, n, "!");
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; };
if (cgtrytaggedshift(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.
// Family C (#35): identity casts are transport no-ops — peel so
// the ident emission carries; a WIDENING tagged cast renumbers
// the tag the compare keys on and has no wired source arm —
// loud below, not a mis-keyed test. Mirrors cstage N_TYPETEST.
let lhs: *node = taggedidcastpeel(c, 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;
// Family C catch-all (rule 7): a widening tagged
// cast source — loud. Mirrors cstage.
{
let icu: *tinfo = lhs.type_: *tinfo;
icu = tichase(icu);
if (lhs.kind == nkind.N_CAST && icu != nil
&& icu.kind == tykind.TY_TAGGED
&& icu.nullable == 0) {
let m35i: str = "#35: tagged cast source shape unwired at `is` (rule 7)\n";
os.write(2, m35i.ptr, m35i.len: u64);
os.exit(1);
};
};
cgexpr(c, lhs);
// #37: a >32B box read leaves its ADDRESS in AX —
// load the tag word from memory before the compare.
// Mirrors cstage N_TYPETEST.
if (taggedmemread(c, lhs)) {
emitline("\tMOVQ\t(AX), AX\n");
};
};
};
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.
// Family C (#35): identity-cast peel — see the cgtypetest twin.
let lhs: *node = taggedidcastpeel(c, 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);
if (taggedmemread(c, lhs)) {
// #37: >32B box read — ADDRESS in AX; copy the
// whole box from memory. Mirrors cstage.
cgexpr(c, lhs);
let ak37: i32 = 0;
for (ak37 < spillsz) {
emitline("\tMOVQ\t");
emitdispreg(ak37: i64, "AX");
emitline(", DX\n");
emitline("\tMOVQ\tDX, ");
emitoff((scrutoff + ak37): i64);
emitline("(BP)\n");
ak37 += 8;
};
} else {
// #37 (rule 7): >32B from a non-mem-based kind would
// spill an unfilled cursor. Mirrors cstage.
if (!isnullabletype(scrutt) && spillsz > TUPLE_GPCAP * 8) {
let m37s: str = "#37: `as` on a >32B tagged value from a non-mem-based source unwired (rule 7)\n";
os.write(2, m37s.ptr, m37s.len: u64);
os.exit(1);
};
// Family C catch-all (rule 7): a widening tagged
// cast source — loud. Mirrors cstage.
{
let acu: *tinfo = lhs.type_: *tinfo;
acu = tichase(acu);
if (lhs.kind == nkind.N_CAST && acu != nil
&& acu.kind == tykind.TY_TAGGED
&& acu.nullable == 0) {
let m35s: str = "#35: tagged cast source shape unwired at `as` (rule 7)\n";
os.write(2, m35s.ptr, m35s.len: u64);
os.exit(1);
};
};
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;
// primsize-ok (#101/#109): this IS an alias chase loop
// — primsize is the leaf-primitive break test the loop
// wraps (aliaslookup advances the cursor on a miss).
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; };
itu = tichase(itu);
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)) {
// C-t3 (#48, rule 7): a GLOBAL tuple as a first-class VALUE
// (`let q = g;` / `return g;` / `f(g)`) has no slot-to-cursor
// path (cgtupleslottocursor is BP-relative) — pre-fix it fell
// to the scalar MOVQ below, loading word0 only, and the
// receive read a STALE cursor for words 1+. Element reads
// (g.N) are the supported surface. Mirrors the cstage cgexpr
// non-local ident guard.
let gtt: *node = letvartnode(c, nm);
for (gtt != nil && gtt.kind == nkind.N_TNAME) {
gtt = aliaslookup(c, gtt.str);
};
if (gtt != nil) {
if (gtt.kind == nkind.N_TTUPLE) {
let mgt: str = "#48: global tuple as a first-class value unwired (element reads only; rule 7)\n";
os.write(2, mgt.ptr, mgt.len: u64);
os.exit(1);
};
};
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;
xu = tichase(xu);
if (xu == nil) { return false; };
let xviaptr: bool = false;
let st: *tinfo = nil;
if (xu.kind == tykind.TY_PTR) {
let p: *tinfo = xu.sub;
p = tichase(p);
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;
bu = tichase(bu);
if (bu == nil) { return false; };
let viaptr: bool = false;
let structt: *tinfo = nil;
if (bu.kind == tykind.TY_PTR) {
let st: *tinfo = bu.sub;
st = tichase(st);
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.
ft = tichase(ft);
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;
bu = tichase(bu);
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;
};
// aggcopy — the ONE place-resolved mem-to-mem aggregate copy: sz
// bytes (SI) → (BX) via AX, a MOVQ run plus a 4/2/1 sized tail.
// Extracted verbatim from the C1.25 assign-resolver tail so every
// aggregate copy position (resolver field store, #49 ident reassign,
// #49 structlit fill-field) funnels through one loop — close-by-
// construction, no per-site width logic to skew. Mirror of cstage
// cg_aggcopy.
fn aggcopy(c: *cgen, sz: i32) void = {
let k: i32 = 0;
for (k + 8 <= sz) {
emitline("\tMOVQ\t");
emitoff(k: i64);
emitline("(SI), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff(k: i64);
emitline("(BX)\n");
k += 8;
};
if (k + 4 <= sz) {
emitline("\tMOVL\t");
emitoff(k: i64);
emitline("(SI), AX\n");
emitline("\tMOVL\tAX, ");
emitoff(k: i64);
emitline("(BX)\n");
k += 4;
};
if (k + 2 <= sz) {
emitline("\tMOVW\t");
emitoff(k: i64);
emitline("(SI), AX\n");
emitline("\tMOVW\tAX, ");
emitoff(k: i64);
emitline("(BX)\n");
k += 2;
};
if (k + 1 <= sz) {
emitline("\tMOVB\t");
emitoff(k: i64);
emitline("(SI), AX\n");
emitline("\tMOVB\tAX, ");
emitoff(k: i64);
emitline("(BX)\n");
k += 1;
};
};
// cgplaceaddr — compute the ADDRESS of an arbitrary place (lvalue)
// expression into dstreg; returns true when the shape is wired, false
// otherwise (the caller loud-stops — rule 7, never a silent drop).
// F6 resolver, commit C1 — mirror of cstage cmd/w6c/cgen.c
// cgplaceaddr: `(*p)[i].f` as N_UN(STAR) root, N_INDEX hop over a
// slice/array place, N_DOT struct-field hop with one deref for a
// *struct base. C2 (F4 read-walker) adds the N_IDENT root (local /
// let / DATA-backed def) so indexed-ident spines resolve too. All
// type keys come off the checker-STAMPED tinfo (.type_), never tnode
// names — the #209/#211 discipline. Enumerated arms still win at
// every dispatch site (they are checked first), so shapes that worked
// pre-C1 keep their asm. ADDRESS COMPUTATION ONLY — every call-site
// keeps its own load/store/copy emission. Clobbers AX/CX (cgexpr on
// index / pointer operands) and balances its own PUSHQ/POPQ; dstreg
// must not be AX or CX.
fn cgplaceaddr(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;
};
let gok: bool = isletvar(c, n.str);
if (!gok) {
if (defvarstructinfo(c, n.str) != nil) { gok = true; };
};
if (!gok) {
let dtn: *node = defvartnode(c, n.str);
if (dtn != nil) {
if (dtn.kind == nkind.N_TARRAY) { gok = true; };
};
};
if (gok) {
emitline("\tLEAQ\t");
emitsymname(c, n.str);
emitline("(SB), ");
emitline(dstreg);
emitline("\n");
return true;
};
return false;
};
if (n.kind == nkind.N_UN) {
if (n.op != tkind.TK_STAR) { return false; };
// &(*e) is e's value — no load.
cgexpr(c, n.lhs);
emitline("\tMOVQ\tAX, ");
emitline(dstreg);
emitline("\n");
return true;
};
if (n.kind == nkind.N_INDEX) {
let base: *node = n.lhs;
let idx: *node = n.rhs;
if (base == nil || idx == nil) { return false; };
// C2: any addressable base — recursion decides (deref /
// ident / dot / index spine). Ident-rooted shapes with
// enumerated arms never reach the resolver (those arms
// dispatch first), so their asm is untouched.
let bu: *tinfo = base.type_: *tinfo;
bu = tichase(bu);
if (bu == nil) { return false; };
if (bu.kind != tykind.TY_SLICE && bu.kind != tykind.TY_ARRAY) {
return false;
};
let et: *tinfo = n.type_: *tinfo;
et = tichase(et);
if (et == nil) { return false; };
let esz: i32 = et.size: i32;
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 (!cgplaceaddr(c, base, dstreg)) { return false; };
// A slice place holds the {ptr,len,cap} header — the
// element base is its .ptr word; an array place IS the
// element storage.
if (bu.kind == tykind.TY_SLICE) {
emitline("\tMOVQ\t(");
emitline(dstreg);
emitline("), ");
emitline(dstreg);
emitline("\n");
};
emitline("\tPOPQ\tAX\n");
emitline("\tADDQ\tAX, ");
emitline(dstreg);
emitline("\n");
return true;
};
if (n.kind == nkind.N_DOT) {
let base: *node = n.lhs;
if (base == nil) { return false; };
let bu: *tinfo = base.type_: *tinfo;
bu = tichase(bu);
if (bu == nil) { return false; };
let viaptr: bool = false;
let st: *tinfo = nil;
if (bu.kind == tykind.TY_PTR) {
let p: *tinfo = bu.sub;
p = tichase(p);
if (p != nil) { if (p.kind == tykind.TY_STRUCT) {
st = p;
viaptr = true;
}; };
} else { if (bu.kind == tykind.TY_STRUCT) {
st = bu;
}; };
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 (!cgplaceaddr(c, base, 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;
};
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;
// Direct non-ident index bases that match none of the typed arms
// below (e.g. a cast-expression base) keep this 8B default —
// cs!=ww for narrow elements. Team task #19 (#61-residual B).
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;
// #60 (alias arc #5): the base's declared type is an N_IDENT alias
// (`type arr = [4]int`). The tnode walks below see only the N_TNAME
// leaf — esz fell to the 1-sentinel and the base classified as a
// POINTER (MOVQ + no IMULQ → SEGV / prefix-luck reads). Set once
// here, consumed by the elem-fact override, the etn fallback and
// the LEAQ-vs-MOVQ base classify.
let basealias: 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;
};
};
};
// #60: element facts off the chased checker-stamped
// tinfos — cstage reads them via type_chase_named/
// idx_eff uniformly (cmd/w6c/cgen.c N_INDEX).
let bt60: *tinfo = base.type_: *tinfo;
if (bt60 != nil) {
if (bt60.kind == tykind.TY_NAMED) { basealias = true; };
};
if (basealias) {
let et60: *tinfo = tichase(n.type_: *tinfo);
if (et60 != nil) {
esz = et60.size: i32;
signed_elem = typeissigned(et60);
float_elem = typeisfloat(et60);
f32_elem = typeisf32(et60);
};
// global base: array-vs-pointer re-keyed off the
// chased BASE kind (cstage isglobal && u->kind ==
// TY_ARRAY → LEAQ). Runtime-unreachable until the
// alias-typed global DATA emit lands (#77/#78);
// kept so the read leg is already cs-aligned.
if (isglobalarr || isglobalptr) {
let bu60: *tinfo = tichase(bt60);
if (bu60 != nil) {
isglobalarr = bu60.kind == tykind.TY_ARRAY;
isglobalptr = !isglobalarr;
};
};
};
// Alias-typed ELEMENT under an ident base (`[2]row`,
// row = [3]int): elemisarrayc's node walk can't see
// through the element's N_TNAME — the chased stamped
// element tinfo is the authority (cstage gates on
// esubu = type_chase_named(esub) == TY_ARRAY).
if (!elem_isarray) {
elem_isarray = tinfoisarray(n.type_: *tinfo);
};
} else { if (base.kind == nkind.N_DOT
|| (base.kind == nkind.N_UN && base.op == tkind.TK_STAR)) {
// `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).
// N_UN deref base (`(*p)[i]`, #61 C): same stamped-tinfo
// source — cstage reads base->type uniformly; without
// this arm esz fell to the 8B default (wrong stride for
// narrow elements once the deref base materializes).
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;
// idxelemtn drills `*[N]T` to the pointee array's own
// element (#61) — an undrilled etn classified the whole
// array, missing tagged/str/slice elements behind a
// pointer-to-array base.
if (bl != nil) {
etn = idxelemtn(bl.tnode);
} else {
etn = idxelemtn(letvartnode(c, base.str));
};
// #60: an alias-NAMED base has no element tnode
// (idxelemtn sees the N_TNAME leaf, nil) — classify
// off the stamped index-result n.type_, the same
// source the N_DOT/N_INDEX arms read.
if (basealias) { etn = n; };
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.
// N_UN deref base joins for the same reason (#61 C).
if (base.kind == nkind.N_DOT || base.kind == nkind.N_INDEX
|| (base.kind == nkind.N_UN && base.op == tkind.TK_STAR)) {
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;
};
};
};
};
// #121 leg (b): whole TUPLE element `let e = tbl[i]`. Classify off
// the chased index-result tinfo (n.type_, the same source the N_DOT/
// N_INDEX arms read). gptotal = sum(tupeslot/8); the per-base arms
// below fill that many cursor words (tupreg) from the element
// address. Mirrors cstage cgen.c N_INDEX TY_TUPLE arms.
let elem_tuple: bool = false;
let tuple_nwords: i32 = 0;
{
let eti: *tinfo = tichase(n.type_: *tinfo);
if (eti != nil) { if (eti.kind == tykind.TY_TUPLE) {
elem_tuple = true;
// ww tuples store elements in .tupleelems, not .params.
let tpw: *ttupleelem = eti.tupleelems;
for (tpw != nil) {
tuple_nwords += tupeslot(tpw.type_) / 8;
tpw = tpw.tnext;
};
};};
};
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) {
// #37: >32B box — ADDRESS in AX (the taggedmemread
// convention); the 4-reg cursor walk below would
// truncate past payload word 2. Mirrors cstage.
if (elem_slot_sz > TUPLE_GPCAP * 8) {
emitline("\tMOVQ\tBX, AX\n");
return;
};
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;
};
// #121 leg (b): whole TUPLE element — fill gptotal cursor words
// from the element address (BX); descending so AX loads last,
// mirroring the tagged arm. Over-cap leaves the ADDRESS in AX.
if (elem_tuple) {
if (tuple_nwords > TUPLE_GPCAP) {
emitline("\tMOVQ\tBX, AX\n");
return;
};
let kw: i32 = tuple_nwords - 1;
for (kw >= 0) {
emitline("\tMOVQ\t");
emitdispreg((kw * 8): i64, "BX");
emitline(", ");
emitline(tupreg(kw));
emitline("\n");
kw -= 1;
};
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; }; };
// #60: alias-NAMED base — LEAQ-vs-MOVQ off the chased stamped
// kind (cstage u->kind == TY_ARRAY, cmd/w6c/cgen.c N_INDEX).
if (basealias) {
let bu60: *tinfo = tichase(base.type_: *tinfo);
if (bu60 != nil) { isarray = bu60.kind == tykind.TY_ARRAY; };
};
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) {
// #37: >32B box — ADDRESS in AX (the taggedmemread
// convention); the 4-reg cursor walk below would
// truncate past payload word 2. Mirrors cstage.
if (elem_slot_sz > TUPLE_GPCAP * 8) {
emitline("\tMOVQ\tBX, AX\n");
return;
};
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;
};
// #121 leg (b): whole TUPLE element — twin of the global arm
// above (base in BX). Over-cap leaves the ADDRESS in AX.
if (elem_tuple) {
if (tuple_nwords > TUPLE_GPCAP) {
emitline("\tMOVQ\tBX, AX\n");
return;
};
let kw: i32 = tuple_nwords - 1;
for (kw >= 0) {
emitline("\tMOVQ\t");
emitdispreg((kw * 8): i64, "BX");
emitline(", ");
emitline(tupreg(kw));
emitline("\n");
kw -= 1;
};
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.
// #37: >32B box — AX already holds the element address;
// leave it (taggedmemread). Mirrors cstage.
if (elem_slot_sz > TUPLE_GPCAP * 8) {
return;
};
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;
};
// #121 leg (b) via fallback base: whole TUPLE element. AX holds the
// element address — copy to BX (the cursor fill into AX clobbers it),
// then fill the gptotal cursor words. Over-cap leaves AX as the addr.
if (elem_tuple) {
if (tuple_nwords > TUPLE_GPCAP) {
return;
};
emitline("\tMOVQ\tAX, BX\n");
let kw: i32 = tuple_nwords - 1;
for (kw >= 0) {
emitline("\tMOVQ\t");
emitdispreg((kw * 8): i64, "BX");
emitline(", ");
emitline(tupreg(kw));
emitline("\n");
kw -= 1;
};
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;
};
};
// #60: alias-NAMED base — chased kind (cstage cg_base_cap
// receives bu pre-chased by the N_SLICE arm, cgen.c:1888).
let bt60: *tinfo = base.type_: *tinfo;
if (bt60 != nil) { if (bt60.kind == tykind.TY_NAMED) {
let bu60: *tinfo = tichase(bt60);
if (bu60 != nil) {
if (bu60.kind == tykind.TY_ARRAY) {
emitline("\tMOVQ\t$");
emitint(bu60.alen: i64);
emitline(", ");
emitline(dst);
emitline("\n");
return true;
};
if (bu60.kind == tykind.TY_SLICE
|| bu60.kind == tykind.TY_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;
};
// #60: alias-NAMED global base — chased kind; global STR keeps
// the #73 cap=len carve-out (cstage isglobal && TY_STR → 0).
// Runtime-unreachable until #77/#78 global DATA.
let gbt60: *tinfo = base.type_: *tinfo;
if (gbt60 != nil) { if (gbt60.kind == tykind.TY_NAMED) {
let gbu60: *tinfo = tichase(gbt60);
if (gbu60 != nil) {
if (gbu60.kind == tykind.TY_ARRAY) {
emitline("\tMOVQ\t$");
emitint(gbu60.alen: i64);
emitline(", ");
emitline(dst);
emitline("\n");
return true;
};
if (gbu60.kind == tykind.TY_SLICE) {
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.
// slicebaseesz — element width of a sliceable base, exactly mirroring the
// esz cascade cgslice computes inline for ptr-scaling (baselocal/globaltn →
// elemsizeofc; N_DOT field → dotbu.sub.size; N_ARRLIT → elemsizeofc; alias-
// NAMED override → chased sub.size). The #145 slice-copy-assign arm scales
// the COUNT by this same width, so it MUST match cgslice's ptr scaling
// byte-for-byte (cgslice supplies the dst ptr). Cstage twin: the N_SLICE-LHS
// arm in cgen.c N_ASSIGN reuses the read-path one-liner directly.
fn slicebaseesz(c: *cgen, base: *node) i32 = {
if (base == nil) { return 1; };
let esz: i32 = 1;
if (base.kind == nkind.N_IDENT) {
let bl: *local = localfindnode(c, base.str);
if (bl != nil) {
esz = elemsizeofc(c, bl.tnode);
} else {
let gt: *node = letvartnode(c, base.str);
if (gt != nil) { esz = elemsizeofc(c, gt); };
};
let bt: *tinfo = base.type_: *tinfo;
if (bt != nil) { if (bt.kind == tykind.TY_NAMED) {
let bu60: *tinfo = tichase(bt);
let es60: *tinfo = tichase(bu60.sub);
if (es60 != nil) { esz = es60.size: i32; };
};};
} else { if (base.kind == nkind.N_DOT) {
let db: *tinfo = tichase(base.type_: *tinfo);
if (db != nil) { if (db.sub != nil) { esz = db.sub.size: i32; }; };
} else { if (base.kind == nkind.N_ARRLIT) {
esz = elemsizeofc(c, base.lhs);
};};};
return esz;
};
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;
dotbu = tichase(dotbu);
};};
// #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;
};};
// #60 (alias arc #5): alias-NAMED N_IDENT base — the tnode reads
// below see only the N_TNAME leaf (esz 1-sentinel, MOVQ base,
// $0 default-hi, cap=len). All four reads re-key off the chased
// stamped tinfo, mirroring cstage N_SLICE's single
// bu = type_chase_named(base->type) source (cmd/w6c/cgen.c).
let basealias: bool = false;
let bu60: *tinfo = nil;
if (base != nil) { if (base.kind == nkind.N_IDENT) {
let bt60: *tinfo = base.type_: *tinfo;
if (bt60 != nil) { if (bt60.kind == tykind.TY_NAMED) {
basealias = true;
bu60 = tichase(bt60);
};};
};};
// esz from the type table for an N_IDENT base (#76; mirrors the
// cgindex idiom) or an N_DOT array/slice-field base (#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);
};};};};
if (bu60 != nil) {
let es60: *tinfo = tichase(bu60.sub);
if (es60 != nil) { esz = es60.size: i32; };
};
// 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; };
};
// #60: alias-NAMED base — chased kind (see cgindex twin).
if (bu60 != nil) { isarray = bu60.kind == tykind.TY_ARRAY; };
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).
let gisarr: bool = globaltn.kind == nkind.N_TARRAY;
// #60: alias-NAMED base — chased kind; runtime-unreachable
// until #77/#78 global DATA (see cgindex twin).
if (bu60 != nil) { gisarr = bu60.kind == tykind.TY_ARRAY; };
if (gisarr) {
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;
};
};};};
};
// #60: alias-NAMED base default-hi — chased kind (cstage
// N_SLICE hi-default reads bu uniformly: TY_ARRAY → $alen,
// TY_SLICE/TY_STR → len word at +8).
if (!handled && bu60 != nil) {
if (bu60.kind == tykind.TY_ARRAY) {
emitline("\tMOVQ\t$");
emitint(bu60.alen: i64);
emitline(", AX\n");
handled = true;
} else { if (bu60.kind == tykind.TY_SLICE
|| bu60.kind == tykind.TY_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;
};};
// #60: alias-NAMED global base default-hi — chased kind;
// runtime-unreachable until #77/#78 global DATA (cstage
// emits LEAQ+8 for global SLICE/STR alike).
if (!handled && bu60 != nil) {
if (bu60.kind == tykind.TY_ARRAY) {
emitline("\tMOVQ\t$");
emitint(bu60.alen: i64);
emitline(", AX\n");
handled = true;
} else { if (bu60.kind == tykind.TY_SLICE
|| bu60.kind == tykind.TY_STR) {
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).
// Family C (#35): identity-cast peel — see the cgtypetest twin.
let scrut: *node = taggedidcastpeel(c, 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 { if (isletvar(c, scrut.str)) {
// #87: a tagged-union GLOBAL scrutinee — the box lives
// in static DATA at name(SB), not the BP frame.
// localfindnode returns nil and the dispatch would read
// saved BP as the tag (garbage). The PLAIN-tagged twin
// of cstage #78 SB-resolution: LEAQ the address, copy
// the box into an @match_spill slot indexed off BP.
let gtt: *node = resolvetagged(c, letvartnode(c, scrut.str));
if (gtt != nil && !isnullabletype(gtt)) {
scrutt = gtt;
let gspill: i32 = matchspillsz(c, gtt);
scrutoff = localalloc(c, "@match_spill", gspill, nil);
emitline("\tLEAQ\t");
emitsymnamehint(c, scrut.str, c.curmod);
emitline("(SB), AX\n");
let gk: i32 = 0;
for (gk < gspill) {
emitline("\tMOVQ\t");
emitdispreg(gk: i64, "AX");
emitline(", DX\n");
emitline("\tMOVQ\tDX, ");
emitoff((scrutoff + gk): i64);
emitline("(BP)\n");
gk += 8;
};
};
}; };
} 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 { if (taggedmemread(c, scrut)) {
// #37: >32B box read (insts[pc], t.N) — cgexpr left
// its ADDRESS in AX; copy the whole box from memory.
// Mirrors cstage cgmatch.
cgexpr(c, scrut);
let mk37: i32 = 0;
for (mk37 < spillsz) {
emitline("\tMOVQ\t");
emitdispreg(mk37: i64, "AX");
emitline(", DX\n");
emitline("\tMOVQ\tDX, ");
emitoff((scrutoff + mk37): i64);
emitline("(BP)\n");
mk37 += 8;
};
} else {
// #37 (rule 7): a >32B box from a kind with no mem-read
// convention would spill the cursor it never filled —
// loud, not garbage. Mirrors cstage.
if (!isnullabletype(scrutt) && spillsz > TUPLE_GPCAP * 8) {
let m37m: str = "#37: >32B tagged match scrutinee from a non-mem-based source unwired (rule 7)\n";
os.write(2, m37m.ptr, m37m.len: u64);
os.exit(1);
};
// #37 (rule 7) stamped twin: matchscrutt returns nil
// for kinds it can't resolve (deref/cast/...), so
// spillsz defaults under cap and the guard above is
// blind there. cstage sizes the spill from the
// stamped s->type, so it louds — key on scrut.type_
// to match. Surfaced by reviewer-37's `match (*p)`
// probe on a 56B box.
let ms37: *tinfo = scrut.type_: *tinfo;
ms37 = tichase(ms37);
if (ms37 != nil && ms37.kind == tykind.TY_TAGGED
&& ms37.size: i32 > TUPLE_GPCAP * 8) {
let m37n: str = "#37: >32B tagged match scrutinee from a non-mem-based source unwired (rule 7)\n";
os.write(2, m37n.ptr, m37n.len: u64);
os.exit(1);
};
// Family C catch-all (rule 7): a widening tagged
// cast scrutinee has no cursor — loud. Mirrors
// cstage cgmatch.
if (scrut.kind == nkind.N_CAST && ms37 != nil
&& ms37.kind == tykind.TY_TAGGED
&& ms37.nullable == 0) {
let m35m: str = "#35: tagged cast source shape unwired at match (rule 7)\n";
os.write(2, m35m.ptr, m35m.len: u64);
os.exit(1);
};
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, false);
};
};
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 {
// C-t0/#22: slot stride
// (tupeslot accessor).
foff += tupeslotn(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;
};
// #22a: tagged element — load the box
// into the tagged value regs (AX=tag,
// DX/CX/R8=payload), the cursor the
// is/as spill + match read. Byte-id
// twin of cstage's N_DOT TY_TUPLE
// tagged arm.
if (istaggedtype(c, tpt)) {
let eslot: i32 = tupeslotn(tpt);
// #37: a >32B box overruns the
// 4-reg cursor — leave its
// ADDRESS in AX (taggedmemread,
// the sret-receive convention);
// consumers copy from memory.
// Replaces the #22b loud bound.
// Mirrors cstage.
if (eslot > TUPLE_GPCAP * 8) {
emitline("\tLEAQ\t");
emitoff((lc.off + foff): i64);
emitline("(BP), AX\n");
return;
};
let k: i32 = 0;
for (k < eslot / 8) {
emitline("\tMOVQ\t");
emitoff((lc.off + foff + k * 8): i64);
emitline("(BP), ");
emitline(tupreg(k));
emitline("\n");
k += 1;
};
return;
};
// C-t0: load at the element's NATURAL
// width (narrow MOVL/MOVSXD/... at the
// slot base), not the 8B slot width —
// byte-id twin of cstage's fldloadop
// in the N_DOT TY_TUPLE arm.
let nsz: i32 = 8;
let tpti: *tinfo = tpt.type_: *tinfo;
if (tpti != nil) { nsz = tpti.size: i32; };
let op: str = tnodeloadop(c, tpt, nsz);
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;
};
};
};
};
};
// GAP-A (#7 def-twin): `def NAME: [N]T = arrlit;` `.len` = static
// elem count. The let-global arm above resolves via letvartnode
// (c.lets only); a def lives in c.defs, misses it, and falls to the
// SB fallback → MOVQ len(SB) (w6l: undefined 'len'). cstage cgen.c
// emits MOVQ $alen here. defvartnode is the def-side mirror of
// letvartnode (returns dtnode = the N_TARRAY whose .rhs length child
// is #11-stamped). `.ptr` mirrors the let-global arm above: the
// array's backing pointer ≡ &A[0] = LEAQ name(SB) (drew #13,
// .ai/drew-gapa-ptr-ruling.md; GAP-A.ptr now fixed cstage-side too,
// so both stages byte-id). `.cap` stays unmirrored — arrays have no
// .cap (both checkers reject, task GAP-A.cap).
if (lhs != nil) {
if (lhs.kind == nkind.N_IDENT) {
if (streq(fld, "ptr")) {
let dtn: *node = defvartnode(c, lhs.str);
if (dtn != nil) {
if (dtn.kind == nkind.N_TARRAY) {
emitline("\tLEAQ\t");
emitsymname(c, lhs.str);
emitline("(SB), AX\n");
return;
};
};
};
if (streq(fld, "len")) {
let dtn: *node = defvartnode(c, lhs.str);
if (dtn != nil) {
if (dtn.kind == nkind.N_TARRAY) {
let lenn: *node = dtn.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 TUPLE global positional read (C-t3, #48): `g.N` —
// LEAQ name(SB) into CX, then load at the element's SLOT offset
// (C-t0 layout), the element's natural width. Mirrors the local
// N_TTUPLE arm above and cstage's N_DOT TY_TUPLE global base.
// Pre-C-t3 the tuple global wasn't in collectlets at all (no
// DATA) and the module-leaf fallback mis-emitted the field index
// as a symbol (`MOVQ 0(SB), AX`).
if (lhs != nil) {
if (lhs.kind == nkind.N_IDENT) {
let gtt: *node = letvartnode(c, lhs.str);
for (gtt != nil && gtt.kind == nkind.N_TNAME) {
gtt = aliaslookup(c, gtt.str);
};
if (gtt != nil) {
if (gtt.kind == nkind.N_TTUPLE) {
let gidx: i32 = fldnumidx(fld);
if (gidx >= 0) {
let gtp: *node = gtt.list;
let gfoff: i32 = 0;
let gi: i32 = 0;
for (gi < gidx) {
if (gtp == nil) { gi = gidx; }
else {
// C-t0/#22: slot stride
// (tupeslot accessor).
gfoff += tupeslotn(gtp.lhs);
gtp = gtp.next;
gi += 1;
};
};
if (gtp != nil) {
let gpt: *node = gtp.lhs;
emitline("\tLEAQ\t");
emitsymname(c, lhs.str);
emitline("(SB), CX\n");
if (isstrtype(c, gpt)) {
// CX (the base) is written LAST so
// it survives the +0/+8 reads.
emitline("\tMOVQ\t");
emitdispreg((gfoff + 0): i64, "CX");
emitline(", AX\n");
emitline("\tMOVQ\t");
emitdispreg((gfoff + 8): i64, "CX");
emitline(", BX\n");
emitline("\tMOVQ\t");
emitdispreg((gfoff + 16): i64, "CX");
emitline(", CX\n");
return;
};
if (isfloattype(c, gpt)) {
let mov: str = "MOVSD";
if (isf32type(c, gpt)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\t");
emitdispreg(gfoff: i64, "CX");
emitline(", X0\n");
return;
};
let gnsz: i32 = 8;
let gpti: *tinfo = gpt.type_: *tinfo;
if (gpti != nil) { gnsz = gpti.size: i32; };
let gop: str = tnodeloadop(c, gpt, gnsz);
emitline("\t");
emitline(gop);
emitline("\t");
emitdispreg(gfoff: i64, "CX");
emitline(", AX\n");
return;
};
};
};
};
};
};
// Top-level struct global field read — LEAQ name(SB), CX then
// load at fi.foff(CX). Mirrors the local "Direct struct local"
// branch above, swapping the BP frame slot for the global VA.
// Field-width-aware op handles MOVQ / MOVL / MOVZBQ / MOVSXD.
// #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.
// C3 (task #8): keyed on the checker-stamped tinfo (lhs.type_ /
// idxbase.type_), not tnode KINDs + structlookup-by-name — the
// name-key dropped any base typed via an N_TNAME alias (`type
// result = []capture`, F10) into the silent fallbacks below.
// Mirrors cstage cgen.c case N_DOT N_INDEX-lhs arm 1:1; #209/#211
// name-keyed→tinfo-SSoT cluster.
if (lhs != nil) {
if (lhs.kind == nkind.N_INDEX) {
let idxbase: *node = lhs.lhs;
if (idxbase != nil) { if (idxbase.kind == nkind.N_IDENT) {
let elemt: *tinfo = lhs.type_: *tinfo;
let elemu: *tinfo = elemt;
elemu = tichase(elemu);
let st: *tinfo = nil;
let viaptr: bool = false;
if (elemu != nil) {
if (elemu.kind == tykind.TY_PTR) {
let pin: *tinfo = elemu.sub;
pin = tichase(pin);
if (pin != nil) { if (pin.kind == tykind.TY_STRUCT) {
st = pin;
viaptr = true;
};};
} else { if (elemu.kind == tykind.TY_STRUCT) {
st = elemu;
};};
};
if (st != nil) {
let fnd: *tfield = nil;
let fwalk: *tfield = st.fields;
for (fwalk != nil) {
if (streq(fwalk.name, fld)) { fnd = fwalk; break; };
fwalk = fwalk.tnext;
};
let bu: *tinfo = idxbase.type_: *tinfo;
bu = tichase(bu);
let baseisarray: bool = false;
let baseok: bool = false;
if (bu != nil) {
if (bu.kind == tykind.TY_ARRAY) { baseisarray = true; baseok = true; };
if (bu.kind == tykind.TY_SLICE) { baseok = true; };
if (bu.kind == tykind.TY_PTR) { baseok = true; };
};
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). Classify via the let registry /
// array-typed def registry (cstage let_islet ||
// def_isarraydef) 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 isglobal: bool = false;
if (lc == nil) {
if (isletvar(c, idxbase.str)) {
isglobal = true;
} else {
let dtn: *node = defvartnode(c, idxbase.str);
if (dtn != nil) {
if (dtn.kind == nkind.N_TARRAY) { isglobal = true; };
};
};
};
if (fnd != nil && baseok && (lc != nil || isglobal)) {
let esz: i32 = elemt.size: i32;
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, idxbase.str);
emitline("(SB), BX\n");
} else {
emitline("\tMOVQ\t");
emitsymname(c, idxbase.str);
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");
};
let foff: i64 = fnd.offset: i64;
let ft: *tinfo = fnd.type_;
let fu: *tinfo = ft;
fu = tichase(fu);
// #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 the scalar load below and loaded
// its first 8 bytes as a value → garbage
// base → SEGFAULT in the outer index.
if (fu != nil && fu.kind == tykind.TY_ARRAY) {
if (foff != 0) {
emitline("\tADDQ\t$");
emitint(foff);
emitline(", AX\n");
};
return;
};
if (fu != nil && (fu.kind == tykind.TY_STR
|| fu.kind == tykind.TY_SLICE)) {
// 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(foff + 8, "AX");
emitline(", BX\n");
emitline("\tMOVQ\t");
emitdispreg(foff + 16, "AX");
emitline(", CX\n");
emitline("\tMOVQ\t");
emitdispreg(foff, "AX");
emitline(", AX\n");
return;
};
if (typeisfloat(ft)) {
let mov: str = "MOVSD";
if (typeisf32(ft)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\t");
emitdispreg(foff, "AX");
emitline(", X0\n");
return;
};
// #58: a TAGGED field of an indexed array
// element (`xs[i].f`). AX holds &xs[i]; load
// the box cursor (AX=tag, DX/CX/R8=payload)
// mirroring taggedmemread's <=32B convention,
// tag LAST (it clobbers the base AX). Without
// this arm the field fell to the scalar load
// below, reading only the tag word and leaving
// the payload cursor (DX) stale (`xs[i].f as
// T` read garbage; #38a INDEX-spine residual).
// >32B box: a wide-box union (largest variant
// >32B) IS constructible via a NARROW variant
// (not unbuildable as earlier triage assumed;
// #54/#23 fires only on STRUCT-LITERAL
// payloads), but the mem-based read (LEAQ
// foff(AX),AX) is not yet wired here — so this
// arm LOUD-STOPS rather than silently reading a
// truncated box (rule 7, the #41 untested-arm
// trap), byte-id-neutral. Reachable + pinned
// expect-loud (test/wcc/944 cfail rows). When
// #114 wires it, that commit replaces this with
// the LEAQ box-address emission + a >32B value
// pin row. Mirrors cstage cgen.c.
if (fu != nil && fu.kind == tykind.TY_TAGGED) {
let bsz: i32 = fu.size: i32;
if (bsz > TUPLE_GPCAP * 8) {
let m58r: str = "#58: >32B tagged-field indexed read unreachable until #114\n";
os.write(2, m58r.ptr, m58r.len: u64);
os.exit(1);
};
if (bsz > 24) {
emitline("\tMOVQ\t");
emitdispreg(foff + 24, "AX");
emitline(", R8\n");
};
if (bsz > 16) {
emitline("\tMOVQ\t");
emitdispreg(foff + 16, "AX");
emitline(", CX\n");
};
if (bsz > 8) {
emitline("\tMOVQ\t");
emitdispreg(foff + 8, "AX");
emitline(", DX\n");
};
emitline("\tMOVQ\t");
emitdispreg(foff, "AX");
emitline(", AX\n");
return;
};
let fsz: i32 = 8;
if (ft != nil) { fsz = ft.size: i32; };
let lop: str = loadopsz(typeissigned(ft), fsz);
emitline("\t");
emitline(lop);
emitline("\t");
emitdispreg(foff, "AX");
emitline(", AX\n");
return;
};
};
};};
};
};
// #121 leg (a): `tbl[i].N` — a positional FIELD of an indexed
// TUPLE element. The struct N_INDEX-lhs block above handles
// struct / ptr-to-struct elements; a tuple element fell through
// to the read-resolver and died LOUD. Resolve &tbl[i] via the
// place-spine (cgplaceaddr, the #116 mechanism) into BX→AX, then
// read the field at addr+foff reusing the per-element-kind arms
// the local N_TTUPLE block wires: str-triple / float-X0 / fn-or-
// scalar loadopsz. Narrow (rob Q3): tagged / nested-aggregate
// fields stay LOUD. Mirrors cstage cgen.c leg-(a) arm 1:1.
if (lhs != nil) {
if (lhs.kind == nkind.N_INDEX) {
let eu: *tinfo = tichase(lhs.type_: *tinfo);
if (eu != nil) { if (eu.kind == tykind.TY_TUPLE) {
let idx: i32 = fldnumidx(fld);
if (idx >= 0) {
// ww tuples store elements in .tupleelems
// (ttupleelem chain), NOT .params (that is
// fn-only). foff via tupeslot accumulation =
// cstage tuple_eslot, byte-id.
let tp: *ttupleelem = eu.tupleelems;
let foff: i32 = 0;
let i: i32 = 0;
for (i < idx) {
if (tp == nil) { i = idx; }
else {
foff += tupeslot(tp.type_);
tp = tp.tnext;
i += 1;
};
};
if (tp != nil) {
let ft: *tinfo = tp.type_;
let fu: *tinfo = tichase(ft);
if (fu != nil && (fu.kind == tykind.TY_TAGGED
|| fu.kind == tykind.TY_STRUCT
|| fu.kind == tykind.TY_TUPLE
|| fu.kind == tykind.TY_ARRAY)) {
let mt: str = "#121: aggregate/tagged tuple-element field read off an indexed base unwired\n";
os.write(2, mt.ptr, mt.len: u64);
os.exit(1);
};
if (!cgplaceaddr(c, lhs, "BX")) {
let mp: str = "#121: indexed tuple base not place-resolvable\n";
os.write(2, mp.ptr, mp.len: u64);
os.exit(1);
};
emitline("\tMOVQ\tBX, AX\n");
if (typeisfloat(ft)) {
let mov: str = "MOVSD";
if (typeisf32(ft)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\t");
emitdispreg(foff: i64, "AX");
emitline(", X0\n");
return;
};
if (fu != nil && (fu.kind == tykind.TY_STR
|| fu.kind == tykind.TY_SLICE)) {
emitline("\tMOVQ\t");
emitdispreg((foff + 8): i64, "AX");
emitline(", BX\n");
emitline("\tMOVQ\t");
emitdispreg((foff + 16): i64, "AX");
emitline(", CX\n");
emitline("\tMOVQ\t");
emitdispreg(foff: i64, "AX");
emitline(", AX\n");
return;
};
let fsz: i32 = 8;
if (ft != nil) { fsz = ft.size: i32; };
let lop: str = loadopsz(typeissigned(ft), fsz);
emitline("\t");
emitline(lop);
emitline("\t");
emitdispreg(foff: i64, "AX");
emitline(", AX\n");
return;
};
};
};};
};
};
// 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;
};
// tagged leaf (#38a): load the box into the tagged
// cursor via cgloadtaggedfield (AX=tag, DX=word0,
// R8=word2 before CX=word1 — CX may be the base;
// >32B box leaves its ADDRESS in AX, the #37
// convention) — the single-dot tagged-field arm
// verbatim. Pre-#38a the scalar loadopsz tail pulled
// ONE word (the tag): is-tests passed by tag-luck
// while as/match/let consumers read stale payload
// registers (ken x5c: o.r.min as size added DX).
if (typeistagged(leaftype)) {
let tlu: *tinfo = leaftype;
tlu = tichase(tlu);
let ttsz: i32 = tlu.size: 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");
};
cgloadtaggedfield(c, "CX", totaloff, ttsz);
} else {
cgloadtaggedfield(c, "BP",
rootoff + totaloff, ttsz);
};
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`).
// C2 (F4): gated to a slice/str/untyped-str STAMPED base (or an
// N_STRLIT, which ww types as `str` anyway) — pre-C2 this arm was
// shape-blind, so a STRUCT field that merely shares a pseudo-field
// NAME behind a non-ident spine took the offset-blind cgexpr path
// while cstage (type-gated) routes it to the read-resolver.
{
let pbu: *tinfo = nil;
if (lhs != nil) { pbu = lhs.type_: *tinfo; };
pbu = tichase(pbu);
let pbok: bool = false;
if (pbu != nil) {
if (pbu.kind == tykind.TY_SLICE
|| pbu.kind == tykind.TY_STR
|| pbu.kind == tykind.TY_UNTYPED_STR) { pbok = true; };
};
if (lhs != nil) {
if (lhs.kind == nkind.N_STRLIT) { pbok = true; };
};
if (pbok) {
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 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) {
if (pbu != nil && (pbu.kind == tykind.TY_SLICE
|| pbu.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;
ct = tichase(ct);
let okp: bool = false;
if (ct != nil) { if (ct.kind == tykind.TY_PTR) {
let cs: *tinfo = ct.sub;
cs = tichase(cs);
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;
};
it = tichase(it);
if (it != nil) { if (it.kind == tykind.TY_PTR) {
let st: *tinfo = it.sub;
st = tichase(st);
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
// tagged leaf (#38a): AX holds the
// *struct base and the tagged cursor
// targets AX (tag) — stage the base in
// BX, then cgloadtaggedfield (cstage
// chained-*struct twin; ken b8: the
// scalar tail read stale DX as payload).
if (typeistagged(ft)) {
let plu: *tinfo = ft;
plu = tichase(plu);
emitline("\tMOVQ\tAX, BX\n");
cgloadtaggedfield(c, "BX",
tf.offset: i32,
plu.size: i32);
return;
};
// 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 (aliasprimsize(c, 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. C2 (F4): gated to UNTYPED
// chains only — pre-C2 it swallowed every unmatched dot-over-dot
// chain, turning a TYPED depth-2 read behind an index/deref spine
// into a silent global read of a colliding leaf symbol.
if (lhs != nil) {
if (lhs.kind == nkind.N_DOT) {
let sbt: *tinfo = lhs.type_: *tinfo;
if (sbt == nil || sbt.kind == tykind.TY_ERR) {
emitline("\tMOVQ\t");
emitsymname(c, fld);
emitline("(SB), AX\n");
return;
};
};
};
// cstage reads ptr-chained fields (`a.p.f`, any root) in its
// chained-*struct arm; wwstage's #70 mirror above is gated
// root-local — the uncovered remainder (global / indexed roots)
// must not take the resolver (its sequence differs from cstage's
// arm → cs≠ww). Loud; the wwstage alignment is filed as task
// #37.
if (lhs != nil) {
if (lhs.kind == nkind.N_DOT) {
let pgu: *tinfo = lhs.type_: *tinfo;
pgu = tichase(pgu);
if (pgu != nil) {
if (pgu.kind == tykind.TY_PTR) {
let mp: str = "cgdot: ptr-chained field read unwired in wwstage (task #37)\n";
os.write(2, mp.ptr, mp.len: u64);
os.exit(1);
};
};
};
};
// C2 read-resolver (F4 + FA3): a TYPED N_DOT read no enumerated
// arm matched — depth-2+ chains and slice/str/scalar fields behind
// index/deref spines. Address via cgplaceaddr (the C1 resolver),
// leaf load emitted here by kind. Leaf kinds with no canonical
// register convention in expr position stay LOUD; any shape the
// resolver can't address dies LOUD (rule 7) — the pre-C2 tail
// silently emitted NOTHING. Mirror of cstage cgen.c case N_DOT
// read-resolver tail.
{
let rdt: *tinfo = n.type_: *tinfo;
let rdu: *tinfo = rdt;
rdu = tichase(rdu);
if (rdu != nil) {
if (rdu.kind == tykind.TY_TAGGED) {
let mt: str = "read-resolver: tagged field read not wired (rule-7)\n";
os.write(2, mt.ptr, mt.len: u64);
os.exit(1);
};
// LET-position aggregate leaves route through cglet's
// resolver copy (C4, task #7) before cgexpr ever sees
// them; this loud guards the remaining non-let expr
// positions (no register convention for a >8B leaf),
// symmetric with cstage's read-resolver tail.
if (rdu.kind == tykind.TY_STRUCT
|| rdu.kind == tykind.TY_TUPLE) {
let ma: str = "read-resolver: aggregate field read not wired (rule-7)\n";
os.write(2, ma.ptr, ma.len: u64);
os.exit(1);
};
};
if (!cgplaceaddr(c, n, "BX")) {
let mu: str = "unsupported field-read shape\n";
os.write(2, mu.ptr, mu.len: u64);
os.exit(1);
};
if (typeisfloat(rdt)) {
let mov: str = "MOVSD";
if (typeisf32(rdt)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\t(BX), X0\n");
return;
};
if (rdu != nil && rdu.kind == tykind.TY_ARRAY) {
// `[N]T` leaf: leave the field ADDRESS — a base for
// an outer index, never a value (#270-1a semantics).
emitline("\tMOVQ\tBX, AX\n");
return;
};
if (rdu != nil && (rdu.kind == tykind.TY_STR
|| rdu.kind == tykind.TY_SLICE)) {
// str IS []u8 — 3-word {ptr,len,cap} into (AX, BX,
// CX). BX is the place base, so load .len (which
// targets BX) LAST.
emitline("\tMOVQ\t(BX), AX\n");
emitline("\tMOVQ\t16(BX), CX\n");
emitline("\tMOVQ\t8(BX), BX\n");
return;
};
let lop: str = "MOVQ";
if (rdt != nil) {
lop = loadopsz(typeissigned(rdt), rdt.slotsize: i32);
};
emitline("\t");
emitline(lop);
emitline("\t(BX), AX\n");
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.
// #102 (ken B6-c3 re-attribution): an
// alias-NAMED pointee misses the bare
// name-keyed lookup, so &p.f fell to the
// generic cgplaceaddr route — runtime-
// correct but byte-divergent from the
// dedicated shape cs pins post-B6-c3.
// structlookupchain (#22) chases the alias
// chain; plain rows short-circuit at its
// structlookup head, byte-id by
// construction.
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 = structlookupchain(c, inner);
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.
// #102 review-found sibling: same
// alias-blind miss one leg below the
// &p.f gate — an alias-NAMED value
// struct fell to the generic route.
// Same chase, same construction.
if (lkind == nkind.N_TNAME) {
let si: *structinfo = structlookupchain(c, tn);
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;
};
};
};
};
// C2 (F4 family, reviewer-A route): address-of
// through an indexed/deref dot spine — the
// arms above root only at idents. Route the
// place address through cgplaceaddr (read-twin
// in cgdot). Any remaining shape dies LOUD:
// the pre-C2 silent drop left stale AX as the
// "address" — a gate-blind SEGFAULT at the
// deref. Mirror of cstage TK_AMP tail.
if (cgplaceaddr(c, opnd, "BX")) {
emitline("\tMOVQ\tBX, AX\n");
return;
};
let mam: str = "unsupported address-of shape\n";
os.write(2, mam.ptr, mam.len: u64);
os.exit(1);
};
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);
// #94: a def-array base resolves via
// defvartnode (the def-side sister), the same
// fallback cgindex's read-side already takes
// (cgenexpr.ww:1762). Without it &D[i] over a
// def array fell to the complex-base value-load
// below (MOVQ name(SB) = D[0] not the address),
// divergent from cstage's zero-base SEGV — both
// wild. The N_TARRAY tnode classifies isglobalarr
// → LEAQ name(SB), the cstage-identical base.
if (tn == nil) { tn = defvartnode(c, base.str); };
if (tn != nil) {
globalname = base.str;
esz = elemsizeofc(c, tn);
if (tn.kind == nkind.N_TARRAY) {
isglobalarr = true;
} else {
isglobalptr = true;
};
};
};
// #82: the tnode-kind classify above misses an
// alias-typed base (N_TNAME) — base materialized
// as MOVQ (element-0 VALUE) instead of LEAQ, a
// wild pointer. Re-key arrayness off the chased
// checker-stamped base type, the cgindex #60
// idiom (cgenexpr.ww:1800-1820); cstage already
// classifies off type_chase_named uniformly
// (cmd/w6c/cgen.c:4172-4188). TY_NAMED gate
// keeps non-alias rows byte-id by construction.
let bt82: *tinfo = base.type_: *tinfo;
let basealias82: bool = false;
if (bt82 != nil) {
if (bt82.kind == tykind.TY_NAMED) { basealias82 = true; };
};
if (basealias82) {
let bu82: *tinfo = tichase(bt82);
if (bu82 != nil) {
if (baselocal != nil) {
isarr = bu82.kind == tykind.TY_ARRAY;
};
if (isglobalarr || isglobalptr) {
isglobalarr = bu82.kind == tykind.TY_ARRAY;
isglobalptr = !isglobalarr;
};
};
};
} else { if (base.kind == nkind.N_DOT
|| (base.kind == nkind.N_UN
&& base.op == tkind.TK_STAR)) {
// `&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.
// N_UN deref base (`&(*p)[i]`, #61 C): same
// stamped source; cstage reads base->type
// uniformly.
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;
};
// C2: deref-rooted (`&(*p)`) and other non-ident
// operands — resolver-or-loud, the same tail as the
// N_DOT arm above (cstage has ONE shared tail for
// both).
if (cgplaceaddr(c, opnd, "BX")) {
emitline("\tMOVQ\tBX, AX\n");
return;
};
let mam2: str = "unsupported address-of shape\n";
os.write(2, mam2.ptr, mam2.len: u64);
os.exit(1);
};
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.
// #61 C: same skip for an ARRAY pointee — an array value IS
// its address everywhere in this cgen (#270-1a), so `*p` on
// `*[N]T` leaves AX = p's value. The scalar load below
// pulled a[0]'s VALUE and `(*p)[i]` then dereferenced it as
// the index base — a wild pointer, SIGSEGV on both stages.
let rti: *tinfo = n.type_: *tinfo;
rti = tichase(rti);
if (rti != nil && rti.kind == tykind.TY_FN) { return; };
if (rti != nil && rti.kind == tykind.TY_ARRAY) { return; };
// Family C (#35/#46): a tagged box behind *p joins the
// mem-based class at ANY size (taggedmemread) — AX = p's
// value IS the box address. The scalar load below pulled
// word0 (the tag) and every cursor consumer transported
// garbage payload words — silent-wrong both stages (ken
// f35/D3a/D3b). The nullable one-word fold stays a scalar
// deref. Mirrors cstage N_UN TK_STAR.
if (rti != nil && rti.kind == tykind.TY_TAGGED) {
if (rti.nullable == 0 && rti.size: i32 > 8) { 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);
};
// cgappendgrow — FA1 (#15): append() header-place grow, cgplaceaddr's
// append consumer. direct = ident-local header in the frame (BP-disp —
// the legacy emission, kept byte-identical); indirect = header address
// pre-spilled to @apphdrscr by the resolver. len+=1, &hdr→DI, esz→SI,
// CALL rt_ensure. In indirect mode the len bump goes through DI so the
// loaded address doubles as the call argument. Mirrors cstage
// cg_append_grow.
fn cgappendgrow(c: *cgen, direct: bool, off: i32, scr: i32, esz: i32) void = {
if (direct) {
emitline("\tADDQ\t$1, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
emitline("\tLEAQ\t");
emitoff(off: i64);
emitline("(BP), DI\n");
} else {
emitline("\tMOVQ\t");
emitoff(scr: i64);
emitline("(BP), DI\n");
emitline("\tADDQ\t$1, 8(DI)\n");
};
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", SI\n");
emitline("\tCALL\trt_ensure(SB)\n");
};
// cgappendslot — post-rt_ensure slot address: CX = (len-1)*esz,
// dst = .ptr + CX. Clobbers AX (the IMUL immediate) and CX, like the
// emission it replaces; dst must not be AX or CX. Mirrors cstage
// cg_append_slot.
fn cgappendslot(c: *cgen, direct: bool, off: i32, scr: i32, esz: i32, dst: str) void = {
if (direct) {
emitline("\tMOVQ\t");
emitoff((off + 8): i64);
emitline("(BP), CX\n");
} else {
emitline("\tMOVQ\t");
emitoff(scr: i64);
emitline("(BP), ");
emitline(dst);
emitline("\n");
emitline("\tMOVQ\t8(");
emitline(dst);
emitline("), 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");
};
if (direct) {
emitline("\tMOVQ\t");
emitoff(off: i64);
emitline("(BP), ");
emitline(dst);
emitline("\n");
} else {
emitline("\tMOVQ\t(");
emitline(dst);
emitline("), ");
emitline(dst);
emitline("\n");
};
emitline("\tADDQ\tCX, ");
emitline(dst);
emitline("\n");
};
// cgappend — Hare-style `append(s, v)` / `append(s, items...)` lowering.
// Mirrors cmd/w6c/cgen.c's N_CALL append branch (rt::ensure model).
// Each value gets:
// ; cgexpr → AX
// ; PUSHQ AX
// ; ADDQ $1, s.len(BP)
// ; LEAQ s(BP), DI ; arg1 = &s
// ; MOVQ esz, SI ; arg2 = membsz
// ; CALL rt_ensure(SB)
// ; MOVQ s.len(BP), CX ; CX = new len
// ; SUBQ $1, CX ; CX = slot index
// ; [IMULQ esz, CX] ; byte offset (esz>1)
// ; MOVQ s.ptr(BP), BX
// ; ADDQ CX, BX
// ; POPQ AX
// ; MOV* AX, (BX) ; store (MOVB / MOVQ)
// nkind.N_SPREAD wraps the same body in a counted loop over items.len.
fn cgappend(c: *cgen, n: *node) void = {
let sn: *node = n.list;
if (sn == nil) { return; };
// FA1 (#15): the old `sn.kind != N_IDENT → return` and
// `snlocal == nil → return` gates were SILENT zero-emission
// (gate-blind cs≠ww: cstage 0-defaulted the header base and
// corrupted the caller frame instead). A non-ident-local target
// now resolves its header address through cgplaceaddr; a shape
// the resolver can't address is loud.
let sndirect: bool = false;
let sn_off: i32 = 0;
let snlocal: *local = nil;
if (sn.kind == nkind.N_IDENT) {
snlocal = localfindnode(c, sn.str);
if (snlocal != nil) {
sndirect = true;
sn_off = snlocal.off;
};
};
let esz: i32 = 0;
let etnode: *node = nil;
let sti: *tinfo = nil;
if (sndirect) {
// #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.
esz = elemsizeofc(c, snlocal.tnode);
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; };
sti = snlocal.tnode.type_: *tinfo;
};
} else {
// FA1: `*p` has no declared tnode — key esz/element kind off
// the checker-STAMPED target tinfo (#209/#211 discipline), the
// same source cstage reads (sn->type → su->sub->size).
sti = sn.type_: *tinfo;
let fsti: *tinfo = sti;
fsti = tichase(fsti);
if (fsti != nil && fsti.sub != nil) { esz = fsti.sub.size: i32; };
if (esz <= 0) {
let m15z: str = "#15: append() target element size unresolved (rule-7)\n";
os.write(2, m15z.ptr, m15z.len: u64);
os.exit(1);
};
};
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).
sti = tichase(sti);
let esubti: *tinfo = nil;
if (sti != nil) { esubti = sti.sub; };
// FA1: pre-peel handle — the indirect struct-lit fill keys its
// structinfo off the NAMED element tinfo's name (the same leaf
// structlookupchain resolves from the declared tnode).
let esubnamed: *tinfo = esubti;
esubti = tichase(esubti);
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 snscr: i32 = 0;
if (!sndirect) {
if (!cgplaceaddr(c, sn, "BX")) {
let m15p: str = "#15: append() target place unsupported (rule-7)\n";
os.write(2, m15p.ptr, m15p.len: u64);
os.exit(1);
};
// Spill across rt_ensure: realloc moves .ptr, never the
// header, so the slot stays valid for every later reload.
// Fresh slot per SITE via localalloc (NOT localadd: its `@`
// dedup would share one slot per fn, and a nested
// append-through-pointer inside a value expression —
// match-yield arm — would clobber the outer's spilled header
// address: silent cross-slice corruption. Mirrors cstage's
// never-deduping local_alloc at the same point.)
snscr = localalloc(c, "@apphdrscr", 8, nil);
emitline("\tMOVQ\tBX, ");
emitoff(snscr: i64);
emitline("(BP)\n");
};
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.
let it: *node = vn.lhs;
// #35: only a {ptr,len,cap}-headered source reads as a
// header below; a [N]T array place IS its storage — the
// ident path used to read its first 16 data bytes as
// ptr/len, silently. Loud until wired (task #27); str
// shares the slice header layout.
let itu: *tinfo = nil;
if (it != nil) { itu = it.type_: *tinfo; };
itu = tichase(itu);
if (itu == nil || (itu.kind != tykind.TY_SLICE
&& itu.kind != tykind.TY_STR)) {
let m35p: str = "#35: append() spread source shape unsupported (rule-7)\n";
os.write(2, m35p.ptr, m35p.len: u64);
os.exit(1);
};
let it_off: i32 = 0;
let itscr: i32 = 0;
if (it.kind == nkind.N_IDENT) {
let itlocal: *local = localfindnode(c, it.str);
if (itlocal != nil) { it_off = itlocal.off; };
};
if (it_off == 0) {
// #35: place-chain source (deref spine, indexed
// chain, global ident — the regex.ha:569/820 dup
// shapes) resolves its header ADDRESS through
// cgplaceaddr ONCE, pre-grow: the chain's rvalues
// run exactly once (the #49 split's pre-grow
// half) and every iteration re-reads .ptr/.len
// THROUGH the spilled header post-grow (the live
// re-derivation half). A header reached through a
// buffer the grow reallocs keeps Hare's
// stale-base hole — see the #49 comment below.
// Rvalue sources (CALL, slicing exprs) have no
// place — loud, task #27. Fresh spill slot per
// SITE for the same nesting reason as @apphdrscr
// above.
if (!cgplaceaddr(c, it, "BX")) {
let m35q: str = "#35: append() spread source shape unsupported (rule-7)\n";
os.write(2, m35q.ptr, m35q.len: u64);
os.exit(1);
};
itscr = localalloc(c, "@appsprscr", 8, nil);
emitline("\tMOVQ\tBX, ");
emitoff(itscr: i64);
emitline("(BP)\n");
};
let load_op: str = tnodeloadop(c, etnode, esz);
if (!sndirect) {
// FA1: no etnode behind `*p` — signedness off the
// stamped element tinfo, the predicate cstage's
// fldloadop applies to su->sub.
load_op = loadopsz(typeissigned(esubti), 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");
if (itscr != 0) {
emitline("\tMOVQ\t");
emitoff(itscr: i64);
emitline("(BP), DX\n");
emitline("\tMOVQ\t8(DX), DX\n");
} else {
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).
cgappendgrow(c, sndirect, sn_off, snscr, esz);
emitline("\tMOVQ\t(SP), CX\n");
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", AX\n");
emitline("\tIMULQ\tAX, CX\n");
};
if (itscr != 0) {
emitline("\tMOVQ\t");
emitoff(itscr: i64);
emitline("(BP), BX\n");
emitline("\tMOVQ\t(BX), BX\n");
} else {
emitline("\tMOVQ\t");
emitoff(it_off: i64);
emitline("(BP), BX\n");
};
emitline("\tADDQ\tCX, BX\n");
cgappendslot(c, sndirect, sn_off, snscr, esz, "DX");
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;
};
if (itscr != 0) {
emitline("\tMOVQ\t");
emitoff(itscr: i64);
emitline("(BP), BX\n");
emitline("\tMOVQ\t(BX), BX\n");
} else {
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");
cgappendgrow(c, sndirect, sn_off, snscr, esz);
cgappendslot(c, sndirect, sn_off, snscr, esz, "BX");
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");
cgappendgrow(c, sndirect, sn_off, snscr, esz);
cgappendslot(c, sndirect, sn_off, snscr, esz, "DX");
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.
// struct: grow FIRST, then fill through the dst pointer
// (literal fill / ident word-copy). tagged: #50 — the
// #12 widen choke-point cgexprs the value internally,
// so boxing must run PRE-grow (Hare's argument order:
// a `xs.len` read in v sees the pre-append len, like
// the scalar arm); box into a frame scratch, grow,
// raw-copy the finished box in.
// #49 (#35's single-element sibling): a place-chain
// source (indexed field `threads[i].root_capture`
// regex.ha:819, deref spine, computed index) SPLITS
// around the grow per the #49 ruling: the chain's
// rvalues (deref-root pointer expr, index expr)
// evaluate exactly once PRE-grow — an index reading
// the slice header sees the pre-append len, Hare's
// argument order — and only the BASE re-derives
// POST-grow from the live storage, so a self-append
// source re-roots in the post-realloc buffer. harec
// resolves an aggregate source address wholly PRE-grow
// (gen.c: gen_load returns the address for
// STORAGE_STRUCT, gen_store copies after rt.ensure) —
// a use-after-free under a reclaiming allocator; per
// #263 we align to the runtime-correct side, not the
// reference. A pointer ALIASING the grown buffer keeps
// Hare's own stale-base hole (sound today only because
// rt/malloc.ww never reclaims). Spec not vendored
// (ref/hare/docs = man pages only), spec-silence
// assumed — re-verify if the spec is ever vendored.
// Bounded shapes: root (local/global ident | deref) +
// at most one index + trailing direct fields; all else
// stays on the #34 loud exit (incl. CALL rvalues, the
// #42-style bound).
let aplace: bool = false;
let afld: i32 = 0;
let aidxesz: i32 = 0;
let abaseslice: bool = false;
let arootoff: i32 = 0;
let asroot: i32 = 0;
let asoff: i32 = 0;
let aroot: *node = nil;
let aidx: *node = nil;
if (elstruct && vn.kind != nkind.N_STRUCTLIT && vn.kind != nkind.N_IDENT) {
let ch: *node = vn;
let aok: bool = true;
for (aok && ch.kind == nkind.N_DOT) {
let ab: *node = ch.lhs;
let af: *tfield = nil;
if (ab != nil) {
let abu: *tinfo = ab.type_: *tinfo;
abu = tichase(abu);
if (abu != nil && abu.kind == tykind.TY_STRUCT) {
let fl: *tfield = abu.fields;
for (fl != nil) {
if (streq(fl.name, ch.str)) { af = fl; break; };
fl = fl.tnext;
};
};
};
if (af == nil) { aok = false; break; };
afld += af.offset: i32;
ch = ab;
};
if (aok && ch.kind == nkind.N_INDEX) {
let ab: *node = ch.lhs;
let aet: *tinfo = ch.type_: *tinfo;
aet = tichase(aet);
let abu: *tinfo = nil;
if (ab != nil) {
abu = ab.type_: *tinfo;
abu = tichase(abu);
};
if (ab == nil || abu == nil || aet == nil
|| (abu.kind != tykind.TY_SLICE && abu.kind != tykind.TY_ARRAY)) {
aok = false;
} else {
abaseslice = abu.kind == tykind.TY_SLICE;
aidxesz = aet.size: i32;
aidx = ch.rhs;
ch = ab;
};
};
if (aok) {
if (ch.kind == nkind.N_IDENT) {
let rl: *local = localfindnode(c, ch.str);
if (rl != nil) {
arootoff = rl.off;
} else {
let gok: bool = isletvar(c, ch.str);
if (!gok) {
if (defvarstructinfo(c, ch.str) != nil) { gok = true; };
};
if (!gok) {
let dtn: *node = defvartnode(c, ch.str);
if (dtn != nil) {
if (dtn.kind == nkind.N_TARRAY) { gok = true; };
};
};
if (!gok) { aok = false; };
};
} else {
if (ch.kind != nkind.N_UN || ch.op != tkind.TK_STAR) {
aok = false;
};
};
};
if (!aok) {
let m34p: str = "#34: append() struct element source shape unsupported (rule-7)\n";
os.write(2, m34p.ptr, m34p.len: u64);
os.exit(1);
};
aroot = ch;
if (aroot.kind == nkind.N_UN) {
asroot = localadd(c, "@appendsroot", 8, nil);
cgexpr(c, aroot.lhs);
emitline("\tMOVQ\tAX, ");
emitoff(asroot: i64);
emitline("(BP)\n");
};
if (aidx != nil) {
asoff = localadd(c, "@appendsoff", 8, nil);
cgexpr(c, aidx);
if (aidxesz > 1) {
emitline("\tMOVQ\t$");
emitint(aidxesz: i64);
emitline(", CX\n");
emitline("\tIMULQ\tCX, AX\n");
};
emitline("\tMOVQ\tAX, ");
emitoff(asoff: i64);
emitline("(BP)\n");
};
aplace = true;
};
if (eltagged) {
// Fresh slot per SITE, not the shared per-size
// scratch: the box must stay live across
// rt_ensure, and a nested append inside the
// value expression would clobber a dedup'd
// slot (the @apphdrscr rationale; #25/#31).
let tgscr: i32 = localalloc(c, "@apptagscr", esz, nil);
emitline("\tXORQ\tAX, AX\n");
let zk: i32 = 0;
for (zk < esz) {
emitline("\tMOVQ\tAX, ");
emitoff((tgscr + zk): i64);
emitline("(BP)\n");
zk += 8;
};
cgwidentaggedstore(c, esubti, vn, "BP", tgscr, esz);
cgappendgrow(c, sndirect, sn_off, snscr, esz);
cgappendslot(c, sndirect, sn_off, snscr, esz, "BX");
let ck: i32 = 0;
for (ck < esz) {
emitline("\tMOVQ\t");
emitoff((tgscr + ck): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(ck: i64, "BX");
emitline("\n");
ck += 8;
};
vn = vn.next;
continue;
};
if (vn.kind == nkind.N_STRUCTLIT) {
// #59 (#50's eval-order kin): resolve the struct
// info, then fill the literal into a fresh
// per-SITE scratch (must stay live across
// rt_ensure, and a nested append in a field expr
// would clobber a dedup'd slot — the @apptagscr
// rationale, #25/#31) BEFORE the grow, so the
// field exprs see the pre-grow len. Then grow,
// slot, raw-copy scratch->slot (mirror the #50
// tagged arm above).
let esi: *structinfo = structlookupchain(c, etnode);
if (esi == nil && !sndirect && esubnamed != nil) {
// FA1: no declared tnode to chain through —
// the stamped NAMED element tinfo carries the
// same leaf structlookupchain would resolve.
if (esubnamed.kind == tykind.TY_NAMED) {
esi = structlookup(c, esubnamed.name);
};
};
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);
};
let stscr: i32 = localalloc(c, "@appendstructscr", esz, nil);
cgstructlitfillbp(c, esi, vn, stscr);
cgappendgrow(c, sndirect, sn_off, snscr, esz);
cgappendslot(c, sndirect, sn_off, snscr, esz, "BX");
// Descending 8/4/2/1 ladder, not an 8B-word
// loop: a plain struct's esz rounds to maxalign
// (check.c:916), not 8, so a sub-8B / non-8B-
// multiple element packs at its own stride — an
// 8B copy of the last slot writes past the slice
// buffer (the tagged arm above is safe only
// because boxes are 8B-padded; #59). Mirrors the
// N_IDENT source arm below.
let ck: i32 = 0;
for (ck + 8 <= esz) {
emitline("\tMOVQ\t");
emitoff((stscr + 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((stscr + 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((stscr + 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((stscr + ck): i64);
emitline("(BP), AX\n");
emitline("\tMOVB\tAX, ");
emitdispreg(ck: i64, "BX");
emitline("\n");
ck += 1;
};
vn = vn.next;
continue;
};
cgappendgrow(c, sndirect, sn_off, snscr, esz);
cgappendslot(c, sndirect, sn_off, snscr, esz, "BX");
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;
};
if (aplace) {
// phase 2: dst slot to @appendscr, base from
// the live storage, stashed offsets back on
// top.
let pscroff: i32 = localadd(c, "@appendscr", 8, nil);
emitline("\tMOVQ\tBX, ");
emitoff(pscroff: i64);
emitline("(BP)\n");
if (aroot.kind == nkind.N_IDENT) {
if (arootoff != 0) {
emitline("\tLEAQ\t");
emitoff(arootoff: i64);
emitline("(BP), BX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, aroot.str);
emitline("(SB), BX\n");
};
} else {
emitline("\tMOVQ\t");
emitoff(asroot: i64);
emitline("(BP), BX\n");
};
if (aidx != nil) {
if (abaseslice) {
emitline("\tMOVQ\t(BX), BX\n");
};
emitline("\tMOVQ\t");
emitoff(asoff: i64);
emitline("(BP), AX\n");
emitline("\tADDQ\tAX, BX\n");
};
if (afld != 0) {
emitline("\tADDQ\t$");
emitint(afld: i64);
emitline(", BX\n");
};
emitline("\tMOVQ\t");
emitoff(pscroff: i64);
emitline("(BP), DX\n");
let pk: i32 = 0;
for (pk + 8 <= esz) {
emitline("\tMOVQ\t");
emitdispreg(pk: i64, "BX");
emitline(", AX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(pk: i64, "DX");
emitline("\n");
pk += 8;
};
if (pk + 4 <= esz) {
emitline("\tMOVL\t");
emitdispreg(pk: i64, "BX");
emitline(", AX\n");
emitline("\tMOVL\tAX, ");
emitdispreg(pk: i64, "DX");
emitline("\n");
pk += 4;
};
if (pk + 2 <= esz) {
emitline("\tMOVW\t");
emitdispreg(pk: i64, "BX");
emitline(", AX\n");
emitline("\tMOVW\tAX, ");
emitdispreg(pk: i64, "DX");
emitline("\n");
pk += 2;
};
if (pk + 1 <= esz) {
emitline("\tMOVB\t");
emitdispreg(pk: i64, "BX");
emitline(", AX\n");
emitline("\tMOVB\tAX, ");
emitdispreg(pk: i64, "DX");
emitline("\n");
pk += 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");
cgappendgrow(c, sndirect, sn_off, snscr, esz);
cgappendslot(c, sndirect, sn_off, snscr, esz, "BX");
emitline("\tPOPQ\tAX\n");
emitline("\t"); emitline(store_op); emitline("\tAX, (BX)\n");
vn = vn.next;
};
return;
};
// cgdelete — Hare `delete(xs[i])`: single-element slice removal, the
// delete-half of #35. Shift [i+1..len) down one stride, len -= 1, cap
// unchanged. The move is a same-type whole-stride byte copy: src and
// dst are elements of the SAME slice, so no boxing/coercion exists for
// any element kind (str/slice 24B header, tagged box, struct body) —
// one word-copy loop serves all kinds, unlike append's value-store
// dispatch (#34) which boxes from a foreign source. Ascending j keeps
// src (j+1) ahead of dst (j), the safe memmove-down direction. Mirrors
// cmd/w6c/cgen.c's N_CALL delete arm instruction-for-instruction
// (rule-10 byte-id).
fn cgdelete(c: *cgen, n: *node) void = {
let d: *node = n.list; // N_INDEX, checker-validated
let base: *node = d.lhs;
// esz off the STAMPED base tinfo (#34/#48 discipline — never the
// value node). Peel TY_NAMED on indexable AND element, mirroring
// cstage's type_chase_named on both (#8 family).
let sti: *tinfo = base.type_: *tinfo;
sti = tichase(sti);
let esub: *tinfo = nil;
if (sti != nil) { esub = sti.sub; };
esub = tichase(esub);
let esz: i32 = 0;
if (esub != nil) { esz = esub.size: i32; };
if (esz <= 0) {
let m35a: str = "#35: delete() element size unresolved (rule-7)\n";
os.write(2, m35a.ptr, m35a.len: u64);
os.exit(1);
};
let hdr_lea: bool = false;
let hdr_off: i32 = 0;
let hdr_ok: bool = false;
if (base.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, base.str);
if (lc != nil) {
hdr_lea = true;
hdr_off = lc.off;
hdr_ok = true;
};
};
// (*p)[i]: the header lives behind a local ptr-to-slice — the
// regex fold-2b delete_thread shape (threads: *[]thread).
if (!hdr_ok && base.kind == nkind.N_UN) {
if (base.op == tkind.TK_STAR && base.lhs != nil) {
if (base.lhs.kind == nkind.N_IDENT) {
let pc: *local = localfindnode(c, base.lhs.str);
if (pc != nil) {
hdr_off = pc.off;
hdr_ok = true;
};
};
};
};
if (!hdr_ok) {
let m35b: str = "#35: delete() base shape unsupported (rule-7: local slice ident or deref-of-local only)\n";
os.write(2, m35b.ptr, m35b.len: u64);
os.exit(1);
};
cgexpr(c, d.rhs); // AX = i
emitline("\tPUSHQ\tAX\n");
if (hdr_lea) {
emitline("\tLEAQ\t");
} else {
emitline("\tMOVQ\t");
};
emitoff(hdr_off: i64);
emitline("(BP), AX\n");
emitline("\tPUSHQ\tAX\n");
let dll: str = mklabel(c, "del_l");
let dle: str = mklabel(c, "del_e");
emitlabel(dll);
emitline("\tMOVQ\t(SP), DX\n");
emitline("\tMOVQ\t8(SP), CX\n");
emitline("\tMOVQ\t8(DX), BX\n");
emitline("\tSUBQ\t$1, BX\n");
emitline("\tCMPQ\tBX, CX\n");
emitline("\tJGE\t"); emitline(dle); emitline("\n");
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", AX\n");
emitline("\tIMULQ\tAX, CX\n");
};
emitline("\tMOVQ\t(DX), BX\n");
emitline("\tADDQ\tCX, BX\n");
let dk: i32 = 0;
for (dk + 8 <= esz) {
emitline("\tMOVQ\t");
emitdispreg((esz + dk): i64, "BX");
emitline(", AX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(dk: i64, "BX");
emitline("\n");
dk += 8;
};
if (dk + 4 <= esz) {
emitline("\tMOVL\t");
emitdispreg((esz + dk): i64, "BX");
emitline(", AX\n");
emitline("\tMOVL\tAX, ");
emitdispreg(dk: i64, "BX");
emitline("\n");
dk += 4;
};
if (dk + 2 <= esz) {
emitline("\tMOVW\t");
emitdispreg((esz + dk): i64, "BX");
emitline(", AX\n");
emitline("\tMOVW\tAX, ");
emitdispreg(dk: i64, "BX");
emitline("\n");
dk += 2;
};
if (dk + 1 <= esz) {
emitline("\tMOVB\t");
emitdispreg((esz + dk): i64, "BX");
emitline(", AX\n");
emitline("\tMOVB\tAX, ");
emitdispreg(dk: i64, "BX");
emitline("\n");
dk += 1;
};
emitline("\tADDQ\t$1, 8(SP)\n");
emitline("\tJMP\t"); emitline(dll); emitline("\n");
emitlabel(dle);
emitline("\tMOVQ\t(SP), DX\n");
emitline("\tSUBQ\t$1, 8(DX)\n");
emitline("\tADDQ\t$16, SP\n");
};
// cgdeleterange — Hare `delete(xs[lo..hi])` (ww spells the range
// `xs[lo:hi]`): range slice removal, the fold-5a prereq P2
// (ref/hare/regex/regex.ha:333 delete(jump_idxs[group_level][..]);
// harec ref/harec/src/check.c:1994 EXPR_SLICE). Shift [hi..len) down
// count = hi-lo strides, len -= count, cap unchanged; lo defaults 0,
// hi defaults len. delete(xs[:]) never enters the copy loop (lo+count
// == len at entry) and zeroes len. The per-element move is cgdelete's
// same-slice whole-stride word copy with a DYNAMIC src offset
// (count*esz via a src register) instead of the constant one-stride.
// Ascending j keeps src >= dst, the safe memmove-down direction.
// Bounds are implicit (no range check, matching cgdelete and the rest
// of cgen). Mirrors cmd/w6c/cgen.c's N_CALL delete range arm
// instruction-for-instruction (rule-10 byte-id).
fn cgdeleterange(c: *cgen, n: *node) void = {
let d: *node = n.list; // N_SLICE, checker-validated
let base: *node = d.lhs;
// esz off the STAMPED base tinfo (#34/#48 discipline — never the
// value node). Peel TY_NAMED on indexable AND element, mirroring
// cstage's type_chase_named on both (#8 family).
let sti: *tinfo = base.type_: *tinfo;
sti = tichase(sti);
let esub: *tinfo = nil;
if (sti != nil) { esub = sti.sub; };
esub = tichase(esub);
let esz: i32 = 0;
if (esub != nil) { esz = esub.size: i32; };
if (esz <= 0) {
let m35a: str = "#35: delete() element size unresolved (rule-7)\n";
os.write(2, m35a.ptr, m35a.len: u64);
os.exit(1);
};
let hdr_lea: bool = false;
let hdr_off: i32 = 0;
let hdr_ok: bool = false;
let hdr_idx: bool = false;
let osz: i32 = 0;
if (base.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, base.str);
if (lc != nil) {
hdr_lea = true;
hdr_off = lc.off;
hdr_ok = true;
};
};
// (*p)[lo:hi]: header behind a local ptr-to-slice — cgdelete's
// regex_shape twin.
if (!hdr_ok && base.kind == nkind.N_UN) {
if (base.op == tkind.TK_STAR && base.lhs != nil) {
if (base.lhs.kind == nkind.N_IDENT) {
let pc: *local = localfindnode(c, base.lhs.str);
if (pc != nil) {
hdr_off = pc.off;
hdr_ok = true;
};
};
};
};
// xs[g][lo:hi]: the header IS element g of an outer local slice —
// the fold-5a consumer shape (ref/hare/regex/regex.ha:333
// delete(jump_idxs[group_level][..])). Outer stride = the inner
// header type's own table size (sti).
if (!hdr_ok && base.kind == nkind.N_INDEX) {
if (base.lhs != nil) {
if (base.lhs.kind == nkind.N_IDENT) {
let oc: *local = localfindnode(c, base.lhs.str);
if (oc != nil) {
hdr_idx = true;
hdr_off = oc.off;
if (sti != nil) { osz = sti.size: i32; };
if (osz <= 0) {
let m35c: str = "#35: delete() outer element size unresolved (rule-7)\n";
os.write(2, m35c.ptr, m35c.len: u64);
os.exit(1);
};
hdr_ok = true;
};
};
};
};
if (!hdr_ok) {
let m35b: str = "#35: delete() range base shape unsupported (rule-7: local slice ident, deref-of-local, or indexed local slice only)\n";
os.write(2, m35b.ptr, m35b.len: u64);
os.exit(1);
};
if (hdr_idx) {
cgexpr(c, base.rhs);
if (osz > 1) {
emitline("\tMOVQ\t$");
emitint(osz: i64);
emitline(", CX\n");
emitline("\tIMULQ\tCX, AX\n");
};
emitline("\tMOVQ\t");
emitoff(hdr_off: i64);
emitline("(BP), CX\n");
emitline("\tADDQ\tCX, AX\n");
} else {
if (hdr_lea) {
emitline("\tLEAQ\t");
} else {
emitline("\tMOVQ\t");
};
emitoff(hdr_off: i64);
emitline("(BP), AX\n");
};
emitline("\tPUSHQ\tAX\n");
if (d.rhs != nil) {
cgexpr(c, d.rhs);
} else {
emitline("\tMOVQ\t$0, AX\n");
};
emitline("\tPUSHQ\tAX\n");
if (d.cond != nil) {
cgexpr(c, d.cond);
} else {
emitline("\tMOVQ\t8(SP), CX\n");
emitline("\tMOVQ\t8(CX), AX\n");
};
emitline("\tMOVQ\t(SP), CX\n");
emitline("\tSUBQ\tCX, AX\n");
emitline("\tPUSHQ\tAX\n");
let rll: str = mklabel(c, "rdl_l");
let rle: str = mklabel(c, "rdl_e");
emitlabel(rll);
emitline("\tMOVQ\t16(SP), DX\n");
emitline("\tMOVQ\t8(SP), CX\n");
emitline("\tMOVQ\t(SP), AX\n");
emitline("\tADDQ\tCX, AX\n");
emitline("\tMOVQ\t8(DX), BX\n");
emitline("\tCMPQ\tBX, AX\n");
emitline("\tJGE\t"); emitline(rle); emitline("\n");
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", AX\n");
emitline("\tIMULQ\tAX, CX\n");
};
emitline("\tMOVQ\t(DX), BX\n");
emitline("\tADDQ\tCX, BX\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("\tADDQ\tBX, CX\n");
let rk: i32 = 0;
for (rk + 8 <= esz) {
emitline("\tMOVQ\t");
emitdispreg(rk: i64, "CX");
emitline(", AX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(rk: i64, "BX");
emitline("\n");
rk += 8;
};
if (rk + 4 <= esz) {
emitline("\tMOVL\t");
emitdispreg(rk: i64, "CX");
emitline(", AX\n");
emitline("\tMOVL\tAX, ");
emitdispreg(rk: i64, "BX");
emitline("\n");
rk += 4;
};
if (rk + 2 <= esz) {
emitline("\tMOVW\t");
emitdispreg(rk: i64, "CX");
emitline(", AX\n");
emitline("\tMOVW\tAX, ");
emitdispreg(rk: i64, "BX");
emitline("\n");
rk += 2;
};
if (rk + 1 <= esz) {
emitline("\tMOVB\t");
emitdispreg(rk: i64, "CX");
emitline(", AX\n");
emitline("\tMOVB\tAX, ");
emitdispreg(rk: i64, "BX");
emitline("\n");
rk += 1;
};
emitline("\tADDQ\t$1, 8(SP)\n");
emitline("\tJMP\t"); emitline(rll); emitline("\n");
emitlabel(rle);
emitline("\tMOVQ\t16(SP), DX\n");
emitline("\tMOVQ\t(SP), AX\n");
emitline("\tMOVQ\t8(DX), BX\n");
emitline("\tSUBQ\tAX, BX\n");
emitline("\tMOVQ\tBX, 8(DX)\n");
emitline("\tADDQ\t$24, SP\n");
};
// cginsert — Hare `insert(xs[idx], v)`: delete()'s twin, the insert-half
// of #35. Insert v BEFORE idx; idx==len is a legal end-insert. Lowered
// as a DESUGAR to append(xs, v) + a rotate-right of [idx, len): cgappend
// contributes grow (rt_ensure) and the whole #34 value-store dispatch
// (scalar / str-slice header / tagged widen / struct fill) verbatim —
// one boxing choke-point, byte-id by construction — landing v at slot
// len-1; the rotate then moves it home through an esz frame scratch.
// The rotate is cgdelete's shift loop in reverse (descending j keeps
// src j behind dst j+1, the safe memmove-up direction) and, like
// delete's, is a same-slice whole-stride raw byte move — no boxing
// exists for any element kind. idx evaluates BEFORE the grow (Hare's
// left-to-right operand order: insert(xs[len(xs)], v) sees the pre-grow
// len); v's evaluation point inherits append's per-kind rules. Bounds
// are implicit (no index check, matching delete). Mirrors cmd/w6c/
// cgen.c's N_CALL insert arm instruction-for-instruction (rule-10
// byte-id).
fn cginsert(c: *cgen, n: *node) void = {
let d: *node = n.list; // N_INDEX, checker-validated
let base: *node = d.lhs;
let v: *node = d.next;
// esz off the STAMPED base tinfo (#34/#48 discipline — never the
// value node). Peel TY_NAMED on indexable AND element, mirroring
// cstage's type_chase_named on both (#8 family).
let sti: *tinfo = base.type_: *tinfo;
sti = tichase(sti);
let esub: *tinfo = nil;
if (sti != nil) { esub = sti.sub; };
esub = tichase(esub);
let esz: i32 = 0;
if (esub != nil) { esz = esub.size: i32; };
if (esz <= 0) {
let m35c: str = "#35: insert() element size unresolved (rule-7)\n";
os.write(2, m35c.ptr, m35c.len: u64);
os.exit(1);
};
let hdr_lea: bool = false;
let hdr_off: i32 = 0;
let hdr_ok: bool = false;
if (base.kind == nkind.N_IDENT) {
let lc: *local = localfindnode(c, base.str);
if (lc != nil) {
hdr_lea = true;
hdr_off = lc.off;
hdr_ok = true;
};
};
// (*p)[i]: the header lives behind a local ptr-to-slice —
// delete's regex_shape twin.
if (!hdr_ok && base.kind == nkind.N_UN) {
if (base.op == tkind.TK_STAR && base.lhs != nil) {
if (base.lhs.kind == nkind.N_IDENT) {
let pc: *local = localfindnode(c, base.lhs.str);
if (pc != nil) {
hdr_off = pc.off;
hdr_ok = true;
};
};
};
};
if (!hdr_ok) {
let m35d: str = "#35: insert() base shape unsupported (rule-7: local slice ident or deref-of-local only)\n";
os.write(2, m35d.ptr, m35d.len: u64);
os.exit(1);
};
// Fresh esz-sized slot per SITE (esz varies; an @-name dedup
// would mis-share across element types). Allocated BEFORE the
// cgappend body's own scratch allocs — cstage order.
let insscr: i32 = localalloc(c, "@insscr", esz, nil);
cgexpr(c, d.rhs); // AX = idx
emitline("\tPUSHQ\tAX\n");
// Desugar in place and route through cgappend: cgen is
// single-pass, base is an lhs node (never on a sibling chain),
// and the checker has already validated this call — the mutation
// is dead after this emission. The callee rename keeps the AST
// consistent with cstage's re-dispatch.
n.lhs.str = "append";
n.list = base;
base.next = v;
cgappend(c, n);
if (hdr_lea) {
emitline("\tLEAQ\t");
} else {
emitline("\tMOVQ\t");
};
emitoff(hdr_off: i64);
emitline("(BP), AX\n");
emitline("\tPUSHQ\tAX\n");
emitline("\tMOVQ\tAX, DX\n");
emitline("\tMOVQ\t8(DX), 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(DX), BX\n");
emitline("\tADDQ\tCX, BX\n");
let ik: i32 = 0;
for (ik + 8 <= esz) {
emitline("\tMOVQ\t");
emitdispreg(ik: i64, "BX");
emitline(", AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((insscr + ik): i64);
emitline("(BP)\n");
ik += 8;
};
if (ik + 4 <= esz) {
emitline("\tMOVL\t");
emitdispreg(ik: i64, "BX");
emitline(", AX\n");
emitline("\tMOVL\tAX, ");
emitoff((insscr + ik): i64);
emitline("(BP)\n");
ik += 4;
};
if (ik + 2 <= esz) {
emitline("\tMOVW\t");
emitdispreg(ik: i64, "BX");
emitline(", AX\n");
emitline("\tMOVW\tAX, ");
emitoff((insscr + ik): i64);
emitline("(BP)\n");
ik += 2;
};
if (ik + 1 <= esz) {
emitline("\tMOVB\t");
emitdispreg(ik: i64, "BX");
emitline(", AX\n");
emitline("\tMOVB\tAX, ");
emitoff((insscr + ik): i64);
emitline("(BP)\n");
ik += 1;
};
emitline("\tMOVQ\t8(DX), AX\n");
emitline("\tSUBQ\t$2, AX\n");
emitline("\tPUSHQ\tAX\n");
let ill: str = mklabel(c, "ins_l");
let ile: str = mklabel(c, "ins_e");
emitlabel(ill);
emitline("\tMOVQ\t(SP), CX\n");
emitline("\tMOVQ\t16(SP), DX\n");
emitline("\tCMPQ\tDX, CX\n");
emitline("\tJL\t"); emitline(ile); emitline("\n");
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", AX\n");
emitline("\tIMULQ\tAX, CX\n");
};
emitline("\tMOVQ\t8(SP), DX\n");
emitline("\tMOVQ\t(DX), BX\n");
emitline("\tADDQ\tCX, BX\n");
ik = 0;
for (ik + 8 <= esz) {
emitline("\tMOVQ\t");
emitdispreg(ik: i64, "BX");
emitline(", AX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg((esz + ik): i64, "BX");
emitline("\n");
ik += 8;
};
if (ik + 4 <= esz) {
emitline("\tMOVL\t");
emitdispreg(ik: i64, "BX");
emitline(", AX\n");
emitline("\tMOVL\tAX, ");
emitdispreg((esz + ik): i64, "BX");
emitline("\n");
ik += 4;
};
if (ik + 2 <= esz) {
emitline("\tMOVW\t");
emitdispreg(ik: i64, "BX");
emitline(", AX\n");
emitline("\tMOVW\tAX, ");
emitdispreg((esz + ik): i64, "BX");
emitline("\n");
ik += 2;
};
if (ik + 1 <= esz) {
emitline("\tMOVB\t");
emitdispreg(ik: i64, "BX");
emitline(", AX\n");
emitline("\tMOVB\tAX, ");
emitdispreg((esz + ik): i64, "BX");
emitline("\n");
ik += 1;
};
emitline("\tSUBQ\t$1, (SP)\n");
emitline("\tJMP\t"); emitline(ill); emitline("\n");
emitlabel(ile);
emitline("\tMOVQ\t16(SP), CX\n");
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", AX\n");
emitline("\tIMULQ\tAX, CX\n");
};
emitline("\tMOVQ\t8(SP), DX\n");
emitline("\tMOVQ\t(DX), BX\n");
emitline("\tADDQ\tCX, BX\n");
ik = 0;
for (ik + 8 <= esz) {
emitline("\tMOVQ\t");
emitoff((insscr + ik): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(ik: i64, "BX");
emitline("\n");
ik += 8;
};
if (ik + 4 <= esz) {
emitline("\tMOVL\t");
emitoff((insscr + ik): i64);
emitline("(BP), AX\n");
emitline("\tMOVL\tAX, ");
emitdispreg(ik: i64, "BX");
emitline("\n");
ik += 4;
};
if (ik + 2 <= esz) {
emitline("\tMOVW\t");
emitoff((insscr + ik): i64);
emitline("(BP), AX\n");
emitline("\tMOVW\tAX, ");
emitdispreg(ik: i64, "BX");
emitline("\n");
ik += 2;
};
if (ik + 1 <= esz) {
emitline("\tMOVB\t");
emitoff((insscr + ik): i64);
emitline("(BP), AX\n");
emitline("\tMOVB\tAX, ");
emitdispreg(ik: i64, "BX");
emitline("\n");
ik += 1;
};
emitline("\tADDQ\t$24, SP\n");
};
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) {
// abort([msg]) / assert(cond[, msg]) — checker-tagged
// builtins (callee TY_ERR is the routing key, cstage's
// `lhs->type == ty_err` at cmd/w6c/cgen.c:6618,6645);
// lower to rt_abort. A user-shadowed abort/assert is
// untagged and stays on the regular call path (#58).
let bti: *tinfo = callee.type_: *tinfo;
if (bti != nil && bti.kind == tykind.TY_ERR) {
if (streq(callee.str, "abort")) {
if (n.list != nil) {
cgexpr(c, n.list);
emitline("\tMOVQ\tAX, DI\n");
emitline("\tMOVQ\tBX, SI\n");
} else {
emitline("\tMOVQ\t$0, DI\n");
emitline("\tMOVQ\t$0, SI\n");
};
emitline("\tCALL\trt_abort(SB)\n");
return;
};
if (streq(callee.str, "assert") && n.list != nil) {
cgexpr(c, n.list);
let skip: str = mklabel(c, "as");
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJNE\t");
emitline(skip);
emitline("\n");
let msg: *node = n.list.next;
if (msg != nil) {
cgexpr(c, msg);
emitline("\tMOVQ\tAX, DI\n");
emitline("\tMOVQ\tBX, SI\n");
} else {
emitline("\tMOVQ\t$0, DI\n");
emitline("\tMOVQ\t$0, SI\n");
};
emitline("\tCALL\trt_abort(SB)\n");
emitlabel(skip);
return;
};
};
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 N_CALL "len"
// arm. 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).
//
// Dispatch (#10/#41): enumerated fast-paths keep their
// pre-fix asm (ident local/global, #235 tuple element, #19
// indexed element, TY_ARRAY const fold), then ONE uniform
// header-place route via cgplaceaddr (.len at place+8) for
// every other slice/str place — the arm enumeration leaked
// four siblings (#235 → #19 → F2 → FA2/FB1), each new operand
// shape falling to a cgexpr fallback that returned the slice
// DATA POINTER as the length. Non-place operands (call
// result, slicing expr, string literal — previously the same
// silent ptr-garbage) die LOUD per rule 7.
if (streq(callee.str, "len")) {
if (n.list != nil) {
let a: *node = n.list;
let at: *tinfo = a.type_: *tinfo;
let u: *tinfo = at;
u = tichase(u);
let hdrish: bool = false;
if (u != nil) {
if (u.kind == tykind.TY_SLICE
|| u.kind == tykind.TY_STR) {
hdrish = true;
};
};
if (hdrish && 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;
};
// non-local non-let ident (DATA-backed
// def): the old arm fell to the silent
// cgexpr fallback. Falls to the
// resolver route below.
};
// #235: len() of a tuple-element slice/str
// (`len(t.N)`). Kept as an enumerated arm:
// tuples are not resolver-addressable
// (cgplaceaddr has no TY_TUPLE hop — that gap
// is #238). 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 (hdrish && 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 {
// C-t0/#22: slot
// stride (tupeslot).
foff += tupeslotn(tp.lhs);
tp = tp.next;
i += 1;
};
};
if (tp != nil) {
emitline("\tMOVQ\t");
emitoff((lc.off + foff + 8): i64);
emitline("(BP), AX\n");
return;
};
};
};
};
};
// C-t3 (#48): GLOBAL tuple len(g.N) —
// LEAQ name(SB) into CX, .len word at
// the SLOT offset + 8. Graduates the
// C5 loud-stop this shape previously
// hit; twin of the cgdot global-tuple
// arm and cstage's #235 global base.
if (lc == nil) {
let gtn: *node = letvartnode(c, a.lhs.str);
for (gtn != nil && gtn.kind == nkind.N_TNAME) {
gtn = aliaslookup(c, gtn.str);
};
if (gtn != nil) {
if (gtn.kind == nkind.N_TTUPLE) {
let gidx: i32 = fldnumidx(a.str);
if (gidx >= 0) {
let gtp: *node = gtn.list;
let gfoff: i32 = 0;
let gi: i32 = 0;
for (gi < gidx) {
if (gtp == nil) { gi = gidx; }
else {
// C-t0/#22: slot
// stride (tupeslot).
gfoff += tupeslotn(gtp.lhs);
gtp = gtp.next;
gi += 1;
};
};
if (gtp != nil) {
emitline("\tLEAQ\t");
emitsymname(c, a.lhs.str);
emitline("(SB), CX\n");
emitline("\tMOVQ\t");
emitdispreg((gfoff + 8): i64, "CX");
emitline(", AX\n");
return;
};
};
};
};
};
// struct-field N_DOT (`len(s.field)`): the
// old fallback returned .ptr as the length.
// Falls to the resolver route below.
};
// #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 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 (hdrish && a.kind == nkind.N_INDEX) {
cgexpr(c, a);
emitline("\tMOVQ\tBX, AX\n");
return;
};
if (u != nil) {
if (u.kind == tykind.TY_ARRAY) {
emitline("\tMOVQ\t$");
emitint(u.alen: i64);
emitline(", AX\n");
return;
};
};
// #10 (F2) + #41 (FA2/FB1): ONE uniform
// header-place route for every other slice/str
// place — resolve the operand's header address
// (cgplaceaddr: deref / index / dot spines) and
// read the .len word at +8.
if (hdrish) {
if (cgplaceaddr(c, a, "BX")) {
emitline("\tMOVQ\t8(BX), AX\n");
return;
};
};
let mlen: str = "#10/#41: len() operand shape not place-resolvable (rule-7)\n";
os.write(2, mlen.ptr, mlen.len: u64);
os.exit(1);
};
};
// 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;
};
};
};
// delete(xs[i]) / delete(xs[lo:hi]) — #35
// delete-half + fold-5a P2 range form; mirror of
// cstage cgen.c's N_CALL delete arm (which branches
// on d->kind == N_SLICE internally).
if (streq(callee.str, "delete")) {
if (n.list != nil) {
if (n.list.next == nil) {
if (n.list.kind == nkind.N_SLICE) {
cgdeleterange(c, n);
return;
};
cgdelete(c, n);
return;
};
};
};
// insert(xs[idx], v) — #35 insert-half; mirror of
// cstage cgen.c's N_CALL insert arm.
if (streq(callee.str, "insert")) {
if (n.list != nil) {
if (n.list.next != nil) {
if (n.list.next.next == nil) {
cginsert(c, n);
return;
};
};
};
};
};
};
// Look up the callee's declared params for tagged-union widening.
// fn-pointer calls (callee is a local) don't get widening — the
// user must build the tagged value explicitly.
//
// 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 = aliasprimsize(c, velem.str);
if (ps > 0) { esz = ps; }
else { esz = slotsize(c, velem); };
} else {
esz = slotsize(c, velem);
};
};
if (esz < 1) { esz = 1; };
// #38b: a >48B tagged variadic ELEMENT would
// need the memory convention inside the vararg
// gather buffer — unwired (rule 7). cstage twin
// guards before its v_is_tagged gather.
if (velem != nil) {
if (taggedmemargsize(velem.type_: *tinfo) > 0) {
let mv: str = "#38b: >48B tagged variadic element unwired\n";
os.write(2, mv.ptr, mv.len: u64);
os.exit(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; };
};
};
};
// #38b: two-phase push — MEMORY-class (>48B tagged) args staged
// first so they sit BELOW every register-class word; the pop loop
// drains a strict prefix and never touches them. memwords feeds
// the caller-cleanup ADDQ (with the mix guard below).
let memwords: i32 = pushargsrev(c, n.list, calleeparams, true);
let nargs: i32 = pushargsrev(c, n.list, calleeparams, false);
// 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 dparam: *node = calleeparams;
let popped: i32 = 0;
let stackslots: i32 = 0;
for (a != nil) {
// #38b: MEMORY-class arg — its words sit below the pop
// region and stay on the stack for the callee; nothing to
// drain. Same param-keyed-else-arg-keyed detection as
// pushargsrev (a widened concrete arg is mem-class only
// via its param).
let dmemsz: i32 = 0;
if (dparam != nil) { if (dparam.kind == nkind.N_PARAM) {
if (dparam.op != tkind.TK_ELLIPSIS) {
if (dparam.lhs != nil) {
dmemsz = taggedmemargsize(dparam.lhs.type_: *tinfo);
};
};
}; };
if (dmemsz == 0) {
dmemsz = taggedmemargsize(a.type_: *tinfo);
};
if (dmemsz > 0) {
if (dparam != nil) { dparam = dparam.next; };
a = a.next;
continue;
};
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 {
// #68: drain over the PARAM tuple element widths for a
// tuple LITERAL arg (the declared-tagged box words pop
// together with the i64 that follows), mirroring the
// param-aware send; else the source tuple type.
let dptt: *node = nil;
if (a.kind == nkind.N_TUPLE) { if (dparam != nil) {
if (dparam.kind == nkind.N_PARAM && dparam.lhs != nil) {
let dtn2: *node = dparam.lhs;
for (dtn2 != nil && dtn2.kind == nkind.N_TNAME) {
dtn2 = aliaslookup(c, dtn2.str);
};
if (dtn2 != nil) { if (dtn2.kind == nkind.N_TTUPLE) { dptt = dtn2; }; };
};
}; };
let tuparg: *node = dptt;
if (tuparg == nil) { tuparg = nodetuplearg(c, a); };
if (tuparg != nil) {
// #163/#32: 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, a declared-tagged box its eslot words
// (#68). Reg overflow loud-stops (rule 7); the
// partial-spill stitch is out of scope (twin of #164).
// C-t2: nodetuplearg admits ident/literal/unwrap
// sources; a literal's elements are VALUE exprs,
// classified the way the @tupargscr restage classified
// them (kind-discriminated twin walk). The param-aware
// path (dptt) walks declared element TYPE nodes.
let tuplit: bool = false;
if (dptt == nil) { if (tuparg.kind == nkind.N_TUPLE) { tuplit = true; }; };
let p: *node = tuparg.list;
for (p != nil) {
let et: *node = p.lhs;
if (tuplit) { et = p; };
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 {
// eslot — full slot split; on the declared
// path tupeslotn reads the element TYPE node
// directly (#68 box-aware), else wide-vs-scalar
// off the literal VALUE node (#22 accessor scale).
let eb: i32 = 1;
if (tuplit) {
let wide: bool = nodeisstr(c, et) || nodeisslice(c, et);
if (wide) { eb = (tyslicesize() / 8i64): i32; };
} else {
eb = tupeslotn(et) / 8;
};
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;
};
};
};
};
if (dparam != nil) { dparam = dparam.next; };
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;
};
// #38b: MEMORY-class args and register-overflow spill words cannot
// coexist — the callee's positive-BP cursor walks params in
// declaration order, but the residual region puts spill words
// below every mem copy. Loud-stop (rule 7); cgfnparams holds the
// mirror check. The merged count feeds the caller-cleanup ADDQ.
if (memwords > 0 && stackslots > 0) {
let mm: str = "#38b: >48B tagged arg mixed with register-overflow stack args unwired\n";
os.write(2, mm.ptr, mm.len: u64);
os.exit(1);
};
stackslots += memwords;
// `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;
// #145 (c1.5a): bulk slice-copy-assign `s.arr[lo:hi] = bs` (LHS is
// N_SLICE). No legacy cgassign arm catches N_SLICE — the statement
// silently emitted nothing (both stages, byte-id-green, #263-class).
// Twin of cstage cgen.c N_ASSIGN N_SLICE arm (full WHY there).
// cgexpr(lhs) routes to cgslice and leaves AX = dst ptr (base+lo*esz),
// BX = hi-lo (element count), CX = cap; slicebaseesz gives the SAME
// esz cgslice scaled the ptr by, so BX*esz is the byte count. Then a
// runtime-counted byte-granular copy from bs.ptr — byte loop because
// the length is RUNTIME (w6a has no REP/MOVSB). Only plain `=`. The
// Hare len(bs)==hi-lo assert is task #149 (rule-7: documented, not a
// c2 blocker — appendlit's lengths are equal by construction).
if (lhs != nil) { if (lhs.kind == nkind.N_SLICE
&& n.op == tkind.TK_ASSIGN) {
let esz: i32 = slicebaseesz(c, lhs.lhs);
cgexpr(c, lhs);
if (esz > 1) {
emitline("\tMOVQ\t$");
emitint(esz: i64);
emitline(", DX\n");
emitline("\tIMULQ\tDX, BX\n");
};
emitline("\tPUSHQ\tAX\n");
emitline("\tPUSHQ\tBX\n");
cgexpr(c, n.rhs);
emitline("\tMOVQ\tAX, SI\n");
emitline("\tPOPQ\tCX\n");
emitline("\tPOPQ\tDI\n");
let top: str = mklabel(c, "scpy");
let end: str = mklabel(c, "scpe");
emitlabel(top);
emitline("\tCMPQ\t$0, CX\n");
emitline("\tJLE\t"); emitline(end); emitline("\n");
emitline("\tMOVB\t(SI), AX\n");
emitline("\tMOVB\tAX, (DI)\n");
emitline("\tADDQ\t$1, SI\n");
emitline("\tADDQ\t$1, DI\n");
emitline("\tSUBQ\t$1, CX\n");
emitline("\tJMP\t"); emitline(top); emitline("\n");
emitlabel(end);
return;
};};
// #20 (task): struct-lit rhs into an INDEXED struct element —
// `a[i] = pt{...}`, `(*ts)[i].caps[k] = capture{...}` — a
// DEREF place (`*p = pt{...}`) or an indexed-base FIELD place
// (`a[i].f = pt{...}`, same class) skips the legacy arms and
// routes to the resolver aggregate arm below (the single
// @placescr funnel). The legacy arms' rhs handling (#270-1b
// ident/dot/deref gate; deref scalar store; the a[i].f
// fldstoreop tail) let the lit fall to a scalar tail:
// cgexpr(N_STRUCTLIT) emits nothing (AX=0) and one MOVQ zeroed
// the place's first word — every field silently dropped, a
// leading str header trashed.
let placeslit: bool = false;
let placedotidx: bool = false;
if (lhs != nil) {
if (lhs.kind == nkind.N_DOT && lhs.lhs != nil) {
if (lhs.lhs.kind == nkind.N_INDEX) {
placedotidx = true;
};
};
if ((lhs.kind == nkind.N_INDEX
|| (lhs.kind == nkind.N_UN && lhs.op == tkind.TK_STAR)
|| placedotidx)
&& n.op == tkind.TK_ASSIGN && n.rhs != nil) {
if (n.rhs.kind == nkind.N_STRUCTLIT) {
let iet: *tinfo = lhs.type_: *tinfo;
iet = tichase(iet);
if (iet != nil) {
if (iet.kind == tykind.TY_STRUCT) {
placeslit = true;
};
};
};
};
};
// #49 (#31-A fold): an aggregate pointee diverts the whole
// deref-assign to the resolver aggregate arm below — the scalar
// tail there stored ONE word of `*p = s` (#31-A); tuple-lit
// (#31-E) and call (#31-G) rhs now die loud there instead of
// silently truncating. str/slice pointees keep their 3-word arm
// (byte-id-pinned). Mirror of cstage deref_agg.
let derefagg: bool = false;
if (lhs != nil) {
if (lhs.kind == nkind.N_UN && lhs.op == tkind.TK_STAR
&& n.op == tkind.TK_ASSIGN) {
let du: *tinfo = lhs.type_: *tinfo;
du = tichase(du);
if (du != nil) {
if (du.kind == tykind.TY_STRUCT
|| du.kind == tykind.TY_ARRAY
|| du.kind == tykind.TY_TUPLE) {
derefagg = true;
};
};
};
};
// 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;
};
};
};
};
// Task #32: an array-LITERAL rhs at assignment is unwired for
// EVERY place kind (ident reassign, index, deref, dot) — only
// decl-init fills. Pre-#32 the same scalar tail zeroed one
// word silently; die loud until the fill lands. Slice-typed
// places are already loud in the checker.
if (n.op == tkind.TK_ASSIGN && lhs != nil && n.rhs != nil) {
if (n.rhs.kind == nkind.N_ARRLIT) {
let alt: *tinfo = lhs.type_: *tinfo;
alt = tichase(alt);
if (alt != nil) {
if (alt.kind == tykind.TY_ARRAY) {
let mal: str = "array-literal store at assignment unwired (task #32)\n";
os.write(2, mal.ptr, mal.len: u64);
os.exit(1);
};
};
};
};
// 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;
aru = tichase(aru);
let alu: *tinfo = lc.tnode.type_: *tinfo;
alu = tichase(alu);
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;
gru = tichase(gru);
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.
// Retained gap: an aggregate >8B rhs (ident, tuple-lit, call)
// truncates to one word here — task #31 A/E/G; struct-lit
// diverts at the placeslit gate, array-lit dies loud (#32).
if (lhs != nil) {
if (lhs.kind == nkind.N_UN) {
if (lhs.op == tkind.TK_STAR) {
if (n.op == tkind.TK_ASSIGN && !placeslit
&& !derefagg) {
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 {
// primsize-ok (#101/#109): this site OWNS its own ps==0
// typenodeprimresolved chase below — routing through
// aliasprimsize would double-resolve and regress #11.
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 && !placeslit) {
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;
let basealias: bool = false;
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);
// idxelemtn drills `*[N]T` to the pointee
// array's element (#61): an undrilled elemtn
// made the width chooser believe the element
// IS the whole array (N*8B aggregate copy
// from an 8B source — frame smash).
elemtn = idxelemtn(baselocal.tnode);
} 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);
// idxelemtn: `*[N]T` drill, see the
// local branch above (#61).
elemtn = idxelemtn(tn);
if (tn.kind == nkind.N_TARRAY) {
isglobalarr = true;
} else {
isglobalptr = true;
};
};
};
} else { if (base.kind == nkind.N_DOT
|| (base.kind == nkind.N_UN
&& base.op == tkind.TK_STAR)) {
// 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). N_UN deref base
// (`(*p)[i] = v`, #61 C): same stamped source.
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;
};
};};};
};
// #60 (write spine): alias-NAMED N_IDENT base — the
// tnode walk above is blind (esz 1-sentinel, elemtn
// nil → pointer-treated base, wrong-stride store).
// Adopt the stamped-element idiom of the N_DOT/
// N_INDEX arms (lhs.type_ IS the element tinfo);
// cstage N_INDEX store reads idx_eff(base->type)->sub
// uniformly (cmd/w6c/cgen.c:3517-3518).
if (base != nil) {
if (base.kind == nkind.N_IDENT) {
let bt60: *tinfo = base.type_: *tinfo;
if (bt60 != nil) {
if (bt60.kind == tykind.TY_NAMED) { basealias = true; };
};
if (basealias) {
let et60: *tinfo = tichase(lhs.type_: *tinfo);
if (et60 != nil) {
esz = et60.size: i32;
elemtn = lhs;
};
// see cgindex twin: cs-aligned, runtime-
// unreachable until #77/#78 global DATA.
if (isglobalarr || isglobalptr) {
let bu60: *tinfo = tichase(bt60);
if (bu60 != nil) {
isglobalarr = bu60.kind == tykind.TY_ARRAY;
isglobalptr = !isglobalarr;
};
};
};
};
};
// 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;
};
};
// #60: alias-NAMED base — chased kind
// (see cgindex twin).
if (basealias) {
let bu60: *tinfo = tichase(base.type_: *tinfo);
if (bu60 != nil) { isarr = bu60.kind == tykind.TY_ARRAY; };
};
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;
};
// #121 (write-face of leg-b): a tuple-LITERAL rhs into
// an indexed element `a[i] = (3,4)`. A literal has no
// source ADDRESS, so the ident/dot/deref copy arm below
// can't reach it — it fell to the 1-word scalar store
// tail (word0 only; the read-luck masked it until leg-b's
// correct read). Materialise the literal into a frame
// scratch via the cglet in-cap path (cgtuplelittocursor +
// tupstore), then word-copy scratch → &a[i]. NARROW:
// N_TTUPLE-literal rhs, N_IDENT base (idxelemtn gives the
// element N_TTUPLE), in-cap. Mirror of cstage cgen.c #121
// store arm; the materialise + base-resolve are the
// cglet / #270-1b byte-id twins.
if (n.rhs.kind == nkind.N_TUPLE && esz > 8
&& base != nil && base.kind == nkind.N_IDENT
&& elemtn != nil && elemtn.kind == nkind.N_TTUPLE
&& sretretsize(c, elemtn) == 0) {
let scr121: i32 = tagscradd(c, esz);
cgtuplelittocursor(c, n.rhs, elemtn);
let gpc: i32 = 0;
let ssc: i32 = 0;
let eo: i32 = 0;
let q121: *node = elemtn.list;
for (q121 != nil) {
let qt: *node = q121.lhs;
let isflt: bool = isfloattype(c, qt);
let es: i32 = tupeslotn(qt);
tupstore(c, gpc, ssc, scr121 + eo, es, qt);
if (isflt) { ssc += 1; }
else { gpc += es / 8; };
eo += es;
q121 = q121.next;
};
// dest &a[i] → BX (#270-1b base resolve)
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 tn2: *node = baselocal.tnode;
let isarr2: bool = false;
if (tn2 != nil) { if (tn2.kind == nkind.N_TARRAY) { isarr2 = true; }; };
if (basealias) {
let bu60: *tinfo = tichase(base.type_: *tinfo);
if (bu60 != nil) { isarr2 = bu60.kind == tykind.TY_ARRAY; };
};
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")) {
} else {
cgexpr(c, base);
emitline("\tMOVQ\tAX, BX\n");
};};};};
emitline("\tPOPQ\tAX\n");
emitline("\tADDQ\tAX, BX\n");
let kk2: i32 = 0;
for (kk2 < esz) {
emitline("\tMOVQ\t");
emitoff((scr121 + kk2): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff(kk2: i64);
emitline("(BX)\n");
kk2 += 8;
};
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); struct-lit sources divert at the placeslit
// gate above (#20), array-lit dies loud (task #32), and
// a by-value call result still falls to the scalar tail
// — RAX-only store, task #31-G. 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; }; };
// #60: alias-NAMED base — chased kind (see cgindex twin).
if (basealias) {
let bu60: *tinfo = tichase(base.type_: *tinfo);
if (bu60 != nil) { isarr2 = bu60.kind == tykind.TY_ARRAY; };
};
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; }; };
// #60: alias-NAMED base — chased kind (see cgindex twin).
if (basealias) {
let bu60: *tinfo = tichase(base.type_: *tinfo);
if (bu60 != nil) { isarray = bu60.kind == tykind.TY_ARRAY; };
};
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);
// idxelemtn drills `*[N]T` to the pointee
// array's element (#61): an undrilled elemtn
// made the width chooser believe the element
// IS the whole array (N*8B aggregate copy
// from an 8B source — frame smash).
elemtn = idxelemtn(baselocal.tnode);
} 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);
// idxelemtn: `*[N]T` drill, see the
// local branch above (#61).
elemtn = idxelemtn(tn);
if (tn.kind == nkind.N_TARRAY) {
isglobalarr = true;
} else {
isglobalptr = true;
};
};
};
} else { if (base.kind == nkind.N_DOT
|| (base.kind == nkind.N_UN
&& base.op == tkind.TK_STAR)) {
// N_UN deref base (`(*p)[i] OP= v`, #61 C):
// same stamped source as the store arm.
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;
};
};};};
};
// #60 (compound spine): alias-NAMED N_IDENT base —
// same stamped-element adoption as the store arm.
let basealias: bool = false;
if (base != nil) {
if (base.kind == nkind.N_IDENT) {
let bt60: *tinfo = base.type_: *tinfo;
if (bt60 != nil) {
if (bt60.kind == tykind.TY_NAMED) { basealias = true; };
};
if (basealias) {
let et60: *tinfo = tichase(lhs.type_: *tinfo);
if (et60 != nil) {
esz = et60.size: i32;
elemtn = lhs;
};
if (isglobalarr || isglobalptr) {
let bu60: *tinfo = tichase(bt60);
if (bu60 != nil) {
isglobalarr = bu60.kind == tykind.TY_ARRAY;
isglobalptr = !isglobalarr;
};
};
};
};
};
// #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; }; };
// #60: alias-NAMED base — chased kind (see cgindex twin).
if (basealias) {
let bu60: *tinfo = tichase(base.type_: *tinfo);
if (bu60 != nil) { isarray = bu60.kind == tykind.TY_ARRAY; };
};
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 && !placeslit) {
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;
// idxelemtn: `*[N]T` drills to the pointee
// array's element (#61).
let elemt: *node = idxelemtn(tn);
let baseisarray: bool = tn.kind == nkind.N_TARRAY;
let snode: *node = nil;
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) {
snode = inner;
viaptr = true;
};};
} else { if (elemt.kind == nkind.N_TNAME) {
snode = elemt;
};};
};
// #102 (ken B6-c3 re-attribution): an alias-NAMED
// element misses the bare name-keyed lookup, so
// `arr[i].f = v` fell to the generic place route —
// runtime-correct but byte-divergent from the
// dedicated shape cs pins post-B6-c3 (the READ twin
// above already chases via tichase, task #8).
// structlookupchain (#22) chases the alias chain;
// plain rows short-circuit at its structlookup
// head, byte-id by construction. esz stays sound:
// elemsizeofc reads the chased stamped tinfo (#8).
if (snode != nil) {
let si: *structinfo = structlookupchain(c, snode);
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;
};
// #58: a TAGGED field of an indexed
// array element (`xs[i].f = v`). The
// scalar store below would write the
// raw unboxed rhs into the TAG slot —
// never boxing, never writing the
// payload (box-corruption, the #38a
// write-twin). BOX (mirror the #24
// tagged-field-assign tag lookup,
// taggedvariantindext) + STORE spine
// (mirror the co-located str/slice
// 3-word arm above): cgexpr the
// payload, spill across the index/
// address computation, compute
// &xs[i]->BX, store the variant tag
// (constant) at foff+0 and the scalar
// payload at foff+8. Only a SCALAR-
// payload variant (box <=16B) store
// is wired here. A >16B / multi-word /
// float-payload union field IS
// constructible (a wide box, built via
// a NARROW variant — not unbuildable as
// earlier triage assumed; #54/#23 fires
// only on STRUCT-LITERAL payloads), but
// its box+memcpy store arm is not yet
// wired, so it LOUD-STOPS rather than
// silently corrupting the box (rule 7,
// the #41 untested-arm trap), byte-id-
// neutral. Reachable + pinned expect-
// loud (test/wcc/944 cfail rows). When
// #114 wires them, that commit replaces
// these stops with the real box+memcpy
// emission + value pin rows. Mirrors
// cstage cgen.c.
if (istaggedtype(c, fi.tnode)) {
let bsz: i32 = slotsize(c, fi.tnode);
if (bsz > TUPLE_GPCAP * 8) {
let m58s: str = "#58: >32B tagged-field indexed store unreachable until #114\n";
os.write(2, m58s.ptr, m58s.len: u64);
os.exit(1);
};
if (bsz > 16) {
let m58m: str = "#58: multi-word tagged-field indexed store unreachable until #114\n";
os.write(2, m58m.ptr, m58m.len: u64);
os.exit(1);
};
if (typeisfloat(n.rhs.type_: *tinfo)) {
let m58f: str = "#58: float-payload tagged-field indexed store unreachable until #114\n";
os.write(2, m58f.ptr, m58f.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");
};
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 v58tag: i32 = taggedvariantindext(c, fi.tnode.type_: *tinfo, n.rhs);
if (v58tag < 0) { v58tag = 0; };
emitline("\tMOVQ\t$");
emitint(v58tag: i64);
emitline(", ");
emitdispreg(fi.foff: i64, "BX");
emitline("\n");
emitline("\tMOVQ\tAX, ");
emitdispreg((fi.foff + 8): i64, "BX");
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
&& aliasprimsize(c, 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
&& aliasprimsize(c, 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
&& aliasprimsize(c, 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
&& aliasprimsize(c, 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
&& aliasprimsize(c, 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
&& aliasprimsize(c, 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
&& aliasprimsize(c, 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
&& aliasprimsize(c, 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
&& aliasprimsize(c, 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;
};};
};
// #129: tagged-union field on global struct. LEAQ
// base(SB) into BX then the shared widener handles
// every rhs shape. Mirrors cstage cgen.c:4902
// is_global arm. Without this the generic TK_ASSIGN
// below truncates to 1 word, silently dropping tag
// and payload.
if (n.op == tkind.TK_ASSIGN
&& istaggedtype(c, fi.tnode)) {
let fsz: i32 = slotsize(c, fi.tnode);
emitline("\tLEAQ\t");
emitsymname(c, bn);
emitline("(SB), BX\n");
cgwidentaggedstore(c,
fi.tnode.type_: *tinfo,
n.rhs, "BX", fi.foff, fsz);
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;
ct = tichase(ct);
let okp: bool = false;
if (ct != nil) { if (ct.kind == tykind.TY_PTR) {
let cs: *tinfo = ct.sub;
cs = tichase(cs);
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;
};
it = tichase(it);
if (it != nil) { if (it.kind == tykind.TY_PTR) {
let st: *tinfo = it.sub;
st = tichase(st);
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) {
// tagged leaf (#38a): eval the *struct
// base into BX, then the shared widener
// (it spills BX across its internal
// cgexpr) — same base-then-widen order
// as the single-dot via-ptr arm. The
// scalar tail below stored ONE sized
// word at the field offset: the rhs
// landed in the TAG slot (ken b8).
if (typeistagged(ft)) {
let flu: *tinfo = ft;
flu = tichase(flu);
cgexpr(c, base);
emitline("\tMOVQ\tAX, BX\n");
cgwidentaggedstore(c, flu, n.rhs,
"BX", tf.offset: i32,
flu.size: i32);
return;
};
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;
};
// tagged leaf (#38a): full slot rewrite via the shared
// widener — the single-dot tagged-field arm verbatim
// (cgwidentaggedstore spills the BX base itself). The
// scalar tail below stored ONE sized word at the field
// offset: the rhs landed in the TAG slot and the payload
// kept its old bytes (ken x5d: `o.r.min = 8: size` left
// `is size` false). Only plain `=` reaches this walker
// (TK_ASSIGN gate above).
if (typeistagged(leaftype)) {
let wlu: *tinfo = leaftype;
wlu = tichase(wlu);
let wtsz: i32 = wlu.size: i32;
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");
};
cgwidentaggedstore(c, wlu, n.rhs,
"BX", totaloff, wtsz);
} else {
cgwidentaggedstore(c, wlu, n.rhs,
"BP", rootoff + totaloff, wtsz);
};
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;
lp = tichase(lp);
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 (aliasprimsize(c, 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.
// C1: a name that is neither a local nor a
// let dies LOUD — the pre-C1 return dropped
// the whole statement silently (cstage twin:
// the N_ASSIGN IDENT-tail fatal).
if (!isletvar(c, nm)) {
let mi1: str = "unsupported assign target: unresolved identifier '";
os.write(2, mi1.ptr, mi1.len: u64);
os.write(2, nm.ptr, nm.len: u64);
let mi2: str = "'\n";
os.write(2, mi2.ptr, mi2.len: u64);
os.exit(1);
};
// 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;
gat = tichase(gat);
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;
aat = tichase(aat);
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;
};
};
// #49 (global twin): aggregate module-let reassign —
// `g = a` / `g = pt{...}`. Same funnel as the local
// arm: structlit → mode-2 (DST_GLOBAL) fill; call →
// loud (#276: ≤24B struct global receive was a
// documented symmetric fall-through, now loud);
// addressable rhs → aggargsrcaddr + LEAQ g(SB), BX
// + aggcopy. Pre-#49 every shape fell to the scalar
// tail below: one MOVQ AX, g(SB).
if (n.op == tkind.TK_ASSIGN && lvftn != nil) {
let gau: *tinfo = lvftn.type_: *tinfo;
gau = tichase(gau);
if (gau != nil) {
if (gau.kind == tykind.TY_STRUCT
|| gau.kind == tykind.TY_ARRAY
|| gau.kind == tykind.TY_TUPLE) {
if (n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT) {
let gsi49: *structinfo = nil;
if (lvftn.kind == nkind.N_TNAME) {
gsi49 = structlookup(c, lvftn.str);
};
if (gsi49 == nil) {
// wwstage-only bail (anonymous
// type; the @placescr precedent).
let m49d: str = "assign: structlit layout unresolved (rule-7)\n";
os.write(2, m49d.ptr, m49d.len: u64);
os.exit(1);
};
cgstructlitfill(c, gsi49, n.rhs, 2, 0, nm, 0);
return;
};
if (n.rhs != nil && n.rhs.kind == nkind.N_CALL) {
let m49e: str = "assign: aggregate call receive shape unwired (task #49/#276/rule-7)\n";
os.write(2, m49e.ptr, m49e.len: u64);
os.exit(1);
};
if (aggargsrcaddr(c, n.rhs, "SI")) {
emitline("\tLEAQ\t");
emitsymname(c, nm);
emitline("(SB), BX\n");
aggcopy(c, gau.size: i32);
return;
};
let m49f: str = "assign: aggregate rhs shape unwired (task #49/rule-7)\n";
os.write(2, m49f.ptr, m49f.len: u64);
os.exit(1);
};
};
};
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; };
acati = tichase(acati);
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;
};
// #49: aggregate (struct/array/tuple) IDENT
// reassignment — any rhs shape that missed the
// dedicated arms above (struct-lit fill, call
// receive, sret) funnels through the ONE mem-to-mem
// copy (aggargsrcaddr → SI, dst → BX, aggcopy), or
// dies loud. Pre-#49 it fell to the scalar tail
// below and word0-copied `b = a` (cstage cgen.c
// N_ASSIGN aggregate-ident twin). Kind keys off the
// checker-STAMPED tinfo (#209/#211 discipline).
if (n.op == tkind.TK_ASSIGN) {
let agu: *tinfo = nil;
if (lcn != nil) {
if (lcn.tnode != nil) {
agu = lcn.tnode.type_: *tinfo;
};
};
agu = tichase(agu);
if (agu != nil) {
if (agu.kind == tykind.TY_STRUCT
|| agu.kind == tykind.TY_ARRAY
|| agu.kind == tykind.TY_TUPLE) {
if (n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT) {
// wwstage-only bail: the fill is
// structinfo-keyed; a literal
// reaching past the name-keyed arm
// above has no registry entry
// (anonymous struct type). Loud,
// rule 7 (the resolver @placescr
// precedent); cstage fills from
// Type directly.
let m49a: str = "assign: structlit layout unresolved (rule-7)\n";
os.write(2, m49a.ptr, m49a.len: u64);
os.exit(1);
};
if (n.rhs != nil && n.rhs.kind == nkind.N_CALL) {
let m49b: str = "assign: aggregate call receive shape unwired (task #49/#276/rule-7)\n";
os.write(2, m49b.ptr, m49b.len: u64);
os.exit(1);
};
if (aggargsrcaddr(c, n.rhs, "SI")) {
emitline("\tLEAQ\t");
emitoff(off: i64);
emitline("(BP), BX\n");
aggcopy(c, agu.size: i32);
return;
};
let m49c: str = "assign: aggregate rhs shape unwired (task #49/rule-7)\n";
os.write(2, m49c.ptr, m49c.len: u64);
os.exit(1);
};
};
};
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;
};
};
// F6 (cgplaceaddr, commit C1): an N_DOT lvalue none of the
// enumerated arms above matched — today the deref-rooted spine
// `(*p)[i].f = v` / `OP= v`. Base-address derivation routes
// through cgplaceaddr; the load/store emission stays here. Any
// N_DOT shape the resolver can't address dies LOUD below: the
// pre-C1 fall-off-the-function tail silently emitted NOTHING
// (rhs unevaluated). Mirror of the cstage cgen.c N_ASSIGN arm.
// #20 (task): N_INDEX and N_UN(STAR) lvalues enroll too — only
// the struct-lit-rhs diversion above reaches here (every other
// indexed/deref shape returned from its legacy arm), and the
// C1.25 aggregate branch fills via @placescr.
if (lhs != nil) {
if (lhs.kind == nkind.N_DOT || lhs.kind == nkind.N_INDEX
|| (lhs.kind == nkind.N_UN && lhs.op == tkind.TK_STAR)) {
let ft: *tinfo = lhs.type_: *tinfo;
let fu: *tinfo = ft;
fu = tichase(fu);
let fsz: i32 = 8;
if (ft != nil) { fsz = ft.size: i32; };
if (typeisfloat(ft)) {
let mf: str = "assign-resolver: float field not wired (rule-7)\n";
os.write(2, mf.ptr, mf.len: u64);
os.exit(1);
};
if (fu != nil) {
if (fu.kind == tykind.TY_TAGGED) {
let mt: str = "assign-resolver: tagged field not wired (rule-7)\n";
os.write(2, mt.ptr, mt.len: u64);
os.exit(1);
};
// C1.25 (#23): aggregate field STORE through the
// resolver — run_thread's 40B capture store
// `(*ts)[i].root_capture = capture{...}`. Dest
// address from cgplaceaddr (BX), source address
// in SI per rhs shape, then the #270-1b
// word-copy tail (SI)→(BX). Pre-C1 a SILENT
// no-op; C1 made it loud; this wires it
// (loud-first, wire-next). Compound on an
// aggregate is meaningless and stays loud.
// Mirror of the cstage cgen.c C1.25 arm.
if (fu.kind == tykind.TY_STRUCT
|| fu.kind == tykind.TY_ARRAY
|| fu.kind == tykind.TY_TUPLE) {
if (n.op != tkind.TK_ASSIGN) {
let mac: str = "assign-resolver: compound on aggregate field not wired (rule-7)\n";
os.write(2, mac.ptr, mac.len: u64);
os.exit(1);
};
if (n.rhs != nil) {
if (n.rhs.kind == nkind.N_CALL) {
// sret-class needs a runtime-RDI dest
// (the #234-tail deferral); the ≤24B
// reg-return receive is task #24. The
// callee return type equals the field
// type (checker-guaranteed), so
// callsretsize gives cstage's
// cg_sret_retsize(ft) verdict.
if (callsretsize(c, n.rhs) > 0) {
let mas: str = "assign-resolver: sret call into aggregate field unwired (#234-tail/rule-7)\n";
os.write(2, mas.ptr, mas.len: u64);
os.exit(1);
};
let ma24: str = "assign-resolver: call result into aggregate field unwired (task #24/rule-7)\n";
os.write(2, ma24.ptr, ma24.len: u64);
os.exit(1);
};
};
let placed: bool = false;
let isslit: bool = false;
if (n.rhs != nil) {
if (n.rhs.kind == nkind.N_STRUCTLIT
&& fu.kind == tykind.TY_STRUCT) {
isslit = true;
};
};
if (isslit) {
// @placescr — FRESH slot PER USE (the
// @slicescr discipline via localalloc,
// NOT the cached @tagscr table: a
// cached slot is the #31 multi-live
// corruption trap; rob ruling). Funnel
// contract, #44 discipline: this arm is
// the ONLY @placescr alloc site. Fill
// handles nested literals (#18), tagged
// fields, TK_ELLIPSIS autofill; the
// value sits in memory, so the resolver
// below may clobber AX/CX freely.
let sname: str;
sname.ptr = nil; sname.len = 0;
if (ft.kind == tykind.TY_NAMED) {
sname = ft.name;
};
let si: *structinfo = nil;
if (sname.len > 0) {
si = structlookup(c, sname);
};
if (si == nil) {
// wwstage-only bail: the fill is
// structinfo-keyed, so an anonymous-
// struct field type has no registry
// entry (cstage fills from Type
// directly). Loud, rule 7.
let man: str = "assign-resolver: structlit field layout unresolved (rule-7)\n";
os.write(2, man.ptr, man.len: u64);
os.exit(1);
};
let scr: i32 = localalloc(c, "@placescr", fsz, nil);
cgstructlitfillbp(c, si, n.rhs, scr);
placed = cgplaceaddr(c, lhs, "BX");
if (placed) {
emitline("\tLEAQ\t");
emitoff(scr: i64);
emitline("(BP), SI\n");
};
} else {
// Addressable source — ident / global /
// N_DOT chain / deref — via the closed
// #265/#268 dispatch. Its N_INDEX arm
// clobbers BX, so the dest spills around
// it (the #270-1b order). Literal
// arrays/tuples have no storage address
// and stay loud.
placed = cgplaceaddr(c, lhs, "BX");
if (placed) {
emitline("\tPUSHQ\tBX\n");
if (!aggargsrcaddr(c, n.rhs, "SI")) {
let mar: str = "assign-resolver: aggregate rhs shape unwired (rule-7)\n";
os.write(2, mar.ptr, mar.len: u64);
os.exit(1);
};
emitline("\tPOPQ\tBX\n");
};
};
if (!placed) {
let mau: str = "unsupported assign target shape\n";
os.write(2, mau.ptr, mau.len: u64);
os.exit(1);
};
aggcopy(c, fsz);
return;
};
};
let fstrsl: bool = false;
if (fu != nil) {
if (fu.kind == tykind.TY_STR
|| fu.kind == tykind.TY_SLICE) {
fstrsl = true;
};
};
if (fstrsl) {
if (n.op != tkind.TK_ASSIGN) {
let ms: str = "assign-resolver: compound on str/slice field not wired (rule-7)\n";
os.write(2, ms.ptr, ms.len: u64);
os.exit(1);
};
// str IS []u8: store the whole {ptr,len,cap}
// triple from (AX,BX,CX); the place address
// goes in DX so the three pops survive
// (#1/Phase 3).
cgexpr(c, n.rhs);
emitline("\tPUSHQ\tCX\n");
emitline("\tPUSHQ\tBX\n");
emitline("\tPUSHQ\tAX\n");
if (cgplaceaddr(c, lhs, "DX")) {
emitline("\tPOPQ\tAX\n");
emitline("\tPOPQ\tBX\n");
emitline("\tPOPQ\tCX\n");
emitline("\tMOVQ\tAX, (DX)\n");
emitline("\tMOVQ\tBX, 8(DX)\n");
emitline("\tMOVQ\tCX, 16(DX)\n");
return;
};
} else { if (n.op == tkind.TK_ASSIGN) {
cgexpr(c, n.rhs);
emitline("\tPUSHQ\tAX\n");
if (cgplaceaddr(c, lhs, "BX")) {
emitline("\tPOPQ\tAX\n");
let sop: str = "MOVQ";
if (fsz == 1) { sop = "MOVB"; };
if (fsz == 2) { sop = "MOVW"; };
if (fsz == 4) { sop = "MOVL"; };
emitline("\t");
emitline(sop);
emitline("\tAX, (BX)\n");
return;
};
} else {
// Compound: AX=old, CX=rhs, BX=addr — the same
// register roles as the chained-ptr-field
// compound template above.
cgexpr(c, n.rhs);
emitline("\tPUSHQ\tAX\n");
if (cgplaceaddr(c, lhs, "BX")) {
let lop: str = loadopsz(typeissigned(ft), fsz);
emitline("\t");
emitline(lop);
emitline("\t(BX), AX\n");
emitline("\tPOPQ\tCX\n");
let unsignd: bool = typeisunsigned(ft);
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) { 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) { 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) { emitline("\tSHRQ\tCX, AX\n"); }
else { emitline("\tSARQ\tCX, AX\n"); };
wired = true;
};
if (!wired) {
let mu: str = "assign-resolver: unknown compound op (rule-7)\n";
os.write(2, mu.ptr, mu.len: u64);
os.exit(1);
};
let sop: str = "MOVQ";
if (fsz == 1) { sop = "MOVB"; };
if (fsz == 2) { sop = "MOVW"; };
if (fsz == 4) { sop = "MOVL"; };
emitline("\t");
emitline(sop);
emitline("\tAX, (BX)\n");
return;
};
}; };
let mtl: str = "unsupported assign target shape\n";
os.write(2, mtl.ptr, mtl.len: u64);
os.exit(1);
};
};
// C1 residual (task #22): a non-DOT lvalue no arm above matched
// still falls out SILENT here — known member: the str-base element
// store family (`s[i] = v`: cstage drops, wwstage emits MOVB;
// pre-existing gate-blind divergence) plus tuple-member writes.
// The tail goes loud for the remaining kinds with #22.
return;
};