Files
ww/selfhost/cmd/wcc/cgenexpr.ww
Hojun-Cho 078708770b cgen: route aggregate field-to-field assignment through the aggregate copier
The direct-field assignment arms enumerate CALL, STRUCTLIT, and local
IDENT producers; an addressable N_DOT/N_INDEX/deref rhs fell through to
the scalar tail, so a 16-byte struct field copied only its first word.
Resolve both places through the existing address funnels and use the
tail-aware aggregate copier. Both stages.
2026-08-07 22:59:52 +09:00

13140 lines
471 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// selfhost/cmd/wcc/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 syntax;
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: *syntax.node) void = {
if (n == nil) { return; };
let k: syntax.nkind = n.kind;
switch (k) {
case syntax.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 syntax.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 syntax.nkind.N_RUNELIT:
emitline("\tMOVQ\t$");
emitint(n.uval: i64);
emitline(", AX\n");
return;
case syntax.nkind.N_STRLIT: cgstrlit(c, n); return;
case syntax.nkind.N_TRUE:
emitline("\tMOVQ\t$1, AX\n");
return;
case syntax.nkind.N_FALSE:
emitline("\tMOVQ\t$0, AX\n");
return;
case syntax.nkind.N_NIL:
emitline("\tMOVQ\t$0, AX\n");
return;
case syntax.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 syntax.nkind.N_IDENT: cgident(c, n); return;
case syntax.nkind.N_INDEX: cgindex(c, n); return;
case syntax.nkind.N_SLICE: cgslice(c, n); return;
case syntax.nkind.N_MATCH: cgmatch(c, n); return;
case syntax.nkind.N_CAST: cgcast(c, n); return;
case syntax.nkind.N_DOT: cgdot(c, n); return;
case syntax.nkind.N_UN: cgun(c, n); return;
case syntax.nkind.N_BIN: cgbin(c, n); return;
case syntax.nkind.N_CALL: cgcall(c, n); return;
case syntax.nkind.N_ASSIGN: cgassign(c, n); return;
case syntax.nkind.N_TRYPROP: cgtryprop(c, n); return;
case syntax.nkind.N_TRYUNW: cgtryunw(c, n); return;
case syntax.nkind.N_TYPETEST: cgtypetest(c, n); return;
case syntax.nkind.N_TYPEASSERT: cgtypeassert(c, n); return;
case syntax.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: *syntax.node, vt: *syntax.node) i32 = {
if (tagged == nil) { return -1; };
if (vt == nil) { return -1; };
if (tagged.kind != syntax.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: *syntax.tinfo = tagged.type_: *syntax.tinfo;
sti = tichase(sti);
if (sti != nil && sti.kind == syntax.tykind.TY_TAGGED && vt.type_ != nil) {
return flatvariantidxt(sti, vt.type_: *syntax.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 == syntax.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);
};
// successtag — index of the success variant of a tagged union. Mirrors
// cstage cg_tagged_success_tag (cmd/w6c/cgen.c:857): if any variant is an
// error (`!T`), the success tag is the first NON-error variant's index;
// else 0 (legacy/no-error). Drives the `?`/`!` success-tag CMPQ + the
// payload-shift variant lookup (#52/#216 — replaces the hardcoded tag-0).
fn successtag(ou: *syntax.tinfo) i64 = {
let u: *syntax.tinfo = tichase(ou);
if (u == nil) { return 0i64; };
if (u.kind != syntax.tykind.TY_TAGGED) { return 0i64; };
let haserr: bool = false;
let p: *syntax.tparam = u.params;
for (p != nil) { if (p.iserror) { haserr = true; }; p = p.tnext; };
if (!haserr) { return 0i64; };
let idx: i64 = 0i64;
p = u.params;
for (p != nil) {
if (!p.iserror) { return idx; };
idx += 1i64;
p = p.tnext;
};
return 0i64;
};
// successvariant — the success variant's type_ (the param at successtag).
// Lets the #241 tuple/tagged payload-shift sites inspect the SUCCESS
// variant's shape, not the (error-first) first param.
fn successvariant(ou: *syntax.tinfo) *syntax.tinfo = {
let u: *syntax.tinfo = tichase(ou);
if (u == nil) { return nil; };
if (u.kind != syntax.tykind.TY_TAGGED) { return nil; };
let st: i64 = successtag(ou);
let idx: i64 = 0i64;
let p: *syntax.tparam = u.params;
for (p != nil) {
if (idx == st) { return p.type_; };
idx += 1i64;
p = p.tnext;
};
return nil;
};
// cgtrytupleshift — #241: if the `?`/`!` operand's success variant 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
// param at successtag (dynamic, #52 — not the hardcoded first param),
// matching the dynamic CMPQ $successtag success-tag convention.
fn cgtrytupleshift(c: *cgen, n: *syntax.node) bool = {
if (n.lhs == nil) { return false; };
let ou: *syntax.tinfo = n.lhs.type_: *syntax.tinfo;
ou = tichase(ou);
if (ou == nil) { return false; };
if (ou.kind != syntax.tykind.TY_TAGGED) { return false; };
if (ou.params == nil) { return false; };
let sv: *syntax.tinfo = successvariant(ou);
sv = tichase(sv);
if (sv == nil) { return false; };
if (sv.kind != syntax.tykind.TY_TUPLE) { return false; };
cgtaggedtuplepayloadshift(c, sv);
return true;
};
// cgtrytaggedshift — Family C (#35, unwrap source): if the `?`/`!`
// operand's success variant (at successtag, #52) 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: *syntax.node) bool = {
if (n.lhs == nil) { return false; };
let ou: *syntax.tinfo = n.lhs.type_: *syntax.tinfo;
ou = tichase(ou);
if (ou == nil) { return false; };
if (ou.kind != syntax.tykind.TY_TAGGED) { return false; };
if (ou.params == nil) { return false; };
let sv: *syntax.tinfo = successvariant(ou);
sv = tichase(sv);
if (sv == nil) { return false; };
// #12: a general-aggregate (struct/array) success variant rides the
// SAME in-cap {AX=w0,DX=w1,CX=w2} payload shuffle as the nested-TAGGED
// box — the union return packs the payload as raw GP words past the
// outer tag. Pre-#12 it matched no arm and fell to the bare MOVQ DX,AX
// below, materialising only w0 (w1/w2 dropped) — a SILENT both-stage
// word-drop. A float-bearing aggregate rides X0/X1 instead (the SSE
// return-class), which this GP cursor cannot reach, so LOUD-STOP it
// (mirror #11/#165). Mirrors cstage N_TRYPROP/N_TRYUNW.
let agg: bool = false;
if (sv.kind == syntax.tykind.TY_TAGGED && sv.nullable == 0) { agg = true; };
if (sv.kind == syntax.tykind.TY_STRUCT) { agg = true; };
if (sv.kind == syntax.tykind.TY_ARRAY) { agg = true; };
if (!agg) { return false; };
if (tinfoaggfloat(sv)) {
let m12f: str = "#12/#165: float-bearing aggregate success variant unwrap (S|e)! rides SSE X0/X1; GP cursor unwired\n";
os.write(2, m12f.ptr, m12f.len: u64);
os.exit(1);
};
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.
// cgdotfieldcombine — single-dot field compound combine. The old
// field value is in BX, the rhs in AX; the result is left in AX.
// PLUSEQ/MINUSEQ preserve the pre-#34 emission (byte-id); the other 8
// ops were silently DROPPED (the arm fell through to a plain store of
// the rhs → `s.f = rhs`, #34/#263). SLASHEQ/PERCENTEQ/LSHIFTEQ/RSHIFTEQ
// need the lhs in AX and the divisor/count in CX, so swap (rhs AX→CX,
// old BX→AX) first. Signed RSHIFTEQ uses SARQ, unsigned SHRQ (#136).
// Mirrors cstage cg_dotfield_combine — both stages emit identical asm.
fn cgdotfieldcombine(c: *cgen, op: syntax.tkind, unsignd: bool) void = {
if (op == syntax.tkind.TK_PLUSEQ) { emitline("\tADDQ\tBX, AX\n"); return; };
if (op == syntax.tkind.TK_MINUSEQ) {
emitline("\tSUBQ\tAX, BX\n");
emitline("\tMOVQ\tBX, AX\n");
return;
};
if (op == syntax.tkind.TK_STAREQ) { emitline("\tIMULQ\tBX, AX\n"); return; };
if (op == syntax.tkind.TK_AMPEQ) { emitline("\tANDQ\tBX, AX\n"); return; };
if (op == syntax.tkind.TK_PIPEEQ) { emitline("\tORQ\tBX, AX\n"); return; };
if (op == syntax.tkind.TK_CARETEQ) { emitline("\tXORQ\tBX, AX\n"); return; };
if (op == syntax.tkind.TK_SLASHEQ) {
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"); };
return;
};
if (op == syntax.tkind.TK_PERCENTEQ) {
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"); };
emitline("\tMOVQ\tDX, AX\n");
return;
};
if (op == syntax.tkind.TK_LSHIFTEQ) {
emitline("\tMOVQ\tAX, CX\n");
emitline("\tMOVQ\tBX, AX\n");
emitline("\tSHLQ\tCX, AX\n");
return;
};
if (op == syntax.tkind.TK_RSHIFTEQ) {
emitline("\tMOVQ\tAX, CX\n");
emitline("\tMOVQ\tBX, AX\n");
if (unsignd) { emitline("\tSHRQ\tCX, AX\n"); }
else { emitline("\tSARQ\tCX, AX\n"); };
return;
};
let m: str = "single-dot field compound: unknown op (#34/rule-7)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
// cgdotfieldhardstop — loud-stop a single-dot field compound on a
// non-integer field (float/str/slice/tagged): the combine arm only
// speaks integer ABI; pre-#34 these silently became `s.f = rhs`.
// Mirrors the cstage cg_dotfield_hardstop gate. (#34/rule-7)
fn cgdotfieldhardstop(c: *cgen, ftn: *syntax.node) void = {
if (istaggedtype(c, ftn)) {
let m: str = "single-dot field compound on tagged field not wired (#34/rule-7)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
if (isstrtype(c, ftn)) {
let m: str = "single-dot field compound on str field not wired (#34/rule-7)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
if (isslicetype(c, ftn)) {
let m: str = "single-dot field compound on slice field not wired (#34/rule-7)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
if (isfloattype(c, ftn)) {
let m: str = "single-dot field compound on float field not wired (#34/rule-7)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
};
// cgdotfieldhardstoptn — tinfo twin of cgdotfieldhardstop (#31): the
// single-dot field compound combine speaks integer ABI only, so a
// tagged/str/slice/float field compound loud-stops. Keyed off the
// checker-STAMPED field tinfo (the receiver-layout tf walk), not a
// tnode. Messages verbatim from cgdotfieldhardstop. (#34/rule-7)
fn cgdotfieldhardstoptn(c: *cgen, ft: *syntax.tinfo) void = {
if (syntax.typeistagged(ft)) {
let m: str = "single-dot field compound on tagged field not wired (#34/rule-7)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
if (syntax.typeisstr(ft)) {
let m: str = "single-dot field compound on str field not wired (#34/rule-7)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
if (syntax.typeisslice(ft)) {
let m: str = "single-dot field compound on slice field not wired (#34/rule-7)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
if (syntax.typeisfloat(ft)) {
let m: str = "single-dot field compound on float field not wired (#34/rule-7)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
};
fn cgtryunwcursor(c: *cgen, n: *syntax.node, opname: str) void = {
let u: *syntax.tinfo = nil;
if (n.lhs != nil) { u = n.lhs.type_: *syntax.tinfo; };
u = tichase(u);
let utag: bool = false;
if (u != nil) {
if (u.kind == syntax.tykind.TY_TAGGED && u.nullable == 0) {
utag = true;
};
};
if (utag && u.size: i32 > TUPLE_GPCAP * 8
&& n.lhs.kind != syntax.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 == syntax.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 is dynamic via successtag (#52/#216): the first non-error
// variant's index (cstage cg_tagged_success_tag parity), 0 when success-first.
fn cgtryprop(c: *cgen, n: *syntax.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 == syntax.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, "?");
// Nullable `(*T | void)`: AX IS the pointer, not a tag. Non-null
// = success (any *T variant), null = error → propagate (RET with
// AX=0). The pre-fix path compared AX against successtag and so
// treated null as success (inverted). Mirrors cstage cgen.c
// N_TRYPROP nullable arm; ww's own cgtypetest carries the twin
// (cgenexpr.ww nullable fold). (task #15/F4)
if (isnullabletype(n.lhs)) {
let nl: str = mklabel(c, "tryprop_ok");
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJNE\t");
emitline(nl);
emitline("\n");
emitline("\tMOVQ\tBP, SP\n\tPOPQ\tBP\n\tRET\n");
emitlabel(nl);
return;
};
// AX = tag. If not the success tag, this is an error; pop frame and
// RET. Success tag is dynamic (successtag / cstage cg_tagged_success_tag
// parity, #52): 0 for success-first, the first non-error index for an
// error-first union.
let cl: str = mklabel(c, "tryprop_ok");
let propu: *syntax.tinfo = nil;
if (n.lhs != nil) { propu = n.lhs.type_: *syntax.tinfo; };
emitline("\tCMPQ\t$");
emitint(successtag(propu));
emitline(", 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: *syntax.tinfo = nil;
if (n.lhs != nil) { u = n.lhs.type_: *syntax.tinfo; };
u = tichase(u);
let r: *syntax.tinfo = nil;
if (c.fnret != nil) { r = c.fnret.type_: *syntax.tinfo; };
r = tichase(r);
if (u != nil && r != nil && r.kind == syntax.tykind.TY_TAGGED && u.params != nil) {
let propret: str;
propret.ptr = nil; propret.len = 0;
let haveret: bool = false;
let p: *syntax.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 = successtag (#52).
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.
// #16: read the STAMPED operand's success-variant type (any source
// shape, any variant order), not a name-keyed call-only lookup of
// the first variant. Mirrors cstage cgen.c:10459-10466
// (success_is_str = type_isstr(succ_t at cg_tagged_success_tag)).
// #6 (Mech B): a slice success variant shares str's 24B {ptr,len,cap}
// header in the tagged ABI {DX,CX,R8}; widen the gate so slice rides
// the same shuffle. A struct/aggregate success uses a different
// {AX,DX,CX} ABI (separate latent, task #12) — keep str||slice-specific.
let succisstr: bool = false;
if (n.lhs != nil) {
let sv: *syntax.tinfo = successvariant(n.lhs.type_: *syntax.tinfo);
succisstr = syntax.typeisstr(sv) || syntax.typeisslice(sv);
};
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). Success tag
// is dynamic via successtag (#52): the first non-error variant's index
// (cstage cg_tagged_success_tag parity), 0 when success-first.
fn cgtryunw(c: *cgen, n: *syntax.node) void = {
// #38b residual (rule 7): see the cgtryprop twin.
if (n.lhs != nil) {
if (n.lhs.kind == syntax.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, "!");
// Nullable `(*T | void)`: AX IS the pointer. Non-null = success
// (leave AX as-is), null = error → abort exit(1). The pre-fix
// path compared AX against successtag, treating null as success
// (inverted). Mirrors cstage cgen.c N_TRYUNW nullable arm; ww's
// own cgtypetest carries the twin. (task #15/F4)
if (isnullabletype(n.lhs)) {
let nl: str = mklabel(c, "tryunw_ok");
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJNE\t");
emitline(nl);
emitline("\n");
emitline("\tMOVQ\t$1, DI\n\tMOVQ\t$60, AX\n\tSYSCALL\n");
emitlabel(nl);
return;
};
let cl: str = mklabel(c, "tryunw_ok");
// Success tag is dynamic (successtag / cstage cg_tagged_success_tag
// parity, #52): 0 for success-first, the first non-error index for an
// error-first union.
let unwu: *syntax.tinfo = nil;
if (n.lhs != nil) { unwu = n.lhs.type_: *syntax.tinfo; };
emitline("\tCMPQ\t$");
emitint(successtag(unwu));
emitline(", 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.)
// #16: stamped success-variant type, any source/order (twin of
// cgtryprop; cstage cgen.c:10595-10602).
// #6 (Mech B): slice success shares str's 24B header in the tagged
// ABI {DX,CX,R8} — widen the gate (twin of cgtryprop). Struct/aggregate
// success uses a different {AX,DX,CX} ABI (task #12) — str||slice only.
let succisstr: bool = false;
if (n.lhs != nil) {
let sv: *syntax.tinfo = successvariant(n.lhs.type_: *syntax.tinfo);
succisstr = syntax.typeisstr(sv) || syntax.typeisslice(sv);
};
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: *syntax.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: *syntax.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 == syntax.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: *syntax.node = nil;
let nonident: bool = false;
let globalident: bool = false;
if (lhs != nil) {
if (lhs.kind == syntax.nkind.N_IDENT) {
let lc: *local = localfindnode(c, lhs.str);
if (lc != nil) {
scrutoff = lc.off;
scrutt = resolvetagged(c, lc.tnode);
} else {
// #18 is-half (align-UP): global tagged ident — tag word at
// g(SB)+0, no BP slot. cstage N_TYPETEST is uniformly cgexpr
// (MOVQ g(SB),AX); pre-fix the !nonident emit read 0(BP)=saved BP.
let gt: *syntax.node = letvartnode(c, lhs.str);
scrutt = resolvetagged(c, gt);
globalident = true;
};
} 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: *syntax.tinfo = lhs.type_: *syntax.tinfo;
icu = tichase(icu);
if (lhs.kind == syntax.nkind.N_CAST && icu != nil
&& icu.kind == syntax.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: *syntax.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 == syntax.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) {
if (globalident) {
emitline("\tMOVQ\t");
emitsymname(c, lhs.str);
emitline("(SB), AX\n");
} else {
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;
};
fn cgtypeassert(c: *cgen, n: *syntax.node) void = {
// `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: *syntax.node = taggedidcastpeel(c, n.lhs);
// Enum ↔ integer: reinterpret-only — the value already occupies AX
// (or AX:BX for str variants, irrelevant here); no tag/unwrap. Gate
// on the stamped operand / result TYPE, not node shape: a constant-
// folded enum member (`flag.NOESCAPE`) reaches cgen as an int-literal
// node carrying the enum type_, which a node-shape probe missed →
// spurious tagged-assertion + exit(1) (#27b). Mirrors cstage
// cmd/w6c/cgen.c N_TYPEASSERT (type_chase_named on operand + result).
{
let su: *syntax.tinfo = nil;
if (lhs != nil) { su = tichase(lhs.type_: *syntax.tinfo); };
let vu: *syntax.tinfo = tichase(n.type_: *syntax.tinfo);
let lenum: bool = false;
let renum: bool = false;
if (su != nil) { if (su.kind == syntax.tykind.TY_ENUM) { lenum = true; }; };
if (vu != nil) { if (vu.kind == syntax.tykind.TY_ENUM) { renum = true; }; };
if (lenum || renum) {
cgexpr(c, lhs);
return;
};
};
// #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 == syntax.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: *syntax.node = nil;
if (lhs != nil) {
if (lhs.kind == syntax.nkind.N_IDENT) {
let lc: *local = localfindnode(c, lhs.str);
if (lc != nil) {
scrutoff = lc.off;
scrutt = resolvetagged(c, lc.tnode);
} else {
// #18 as-half (#263 ww-runtime-correct; cstage N_TYPEASSERT on a
// global tagged ident spills uninitialized DX as the payload —
// task #46). No BP slot: copy the box words from g(SB) into a
// fresh @asrt_spill so the tag-check + payload load below index
// off memory like a local. cs!=ww residual until #46 lands.
let gt: *syntax.node = letvartnode(c, lhs.str);
scrutt = resolvetagged(c, gt);
let gsz: i32 = matchspillsz(c, scrutt);
scrutoff = localalloc(c, "@asrt_spill", gsz, nil);
emitline("\tLEAQ\t");
emitsymname(c, lhs.str);
emitline("(SB), AX\n");
let gk: i32 = 0;
for (gk < gsz) {
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.). 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: *syntax.tinfo = lhs.type_: *syntax.tinfo;
acu = tichase(acu);
if (lhs.kind == syntax.nkind.N_CAST && acu != nil
&& acu.kind == syntax.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);
let okl: str = mklabel(c, "asrt_ok");
// Nullable `(*T | void)`: the slot word IS the pointer, not a tag.
// The *T variant asserts non-null, the void variant asserts null;
// AX keeps the pointer on the ok path (no slot+8 unwrap — the 8B
// nullable slot has no second word). The pre-fix path compared the
// POINTER against `want` (so a real pointer aborted, null passed)
// and unwrapped a frame word past the slot. `want` stays RAW (no
// clamp), mirroring cstage cgen.c N_TYPEASSERT nullable arm and
// ww's own cgtypetest nullable fold (cgenexpr.ww). (task #17/F4)
if (isnullabletype(scrutt)) {
let ptrtag: i32 = nullableptrtag(scrutt);
emitline("\tMOVQ\t");
emitoff(scrutoff: i64);
emitline("(BP), AX\n");
emitline("\tCMPQ\t$0, AX\n");
if (want == ptrtag) {
emitline("\tJNE\t");
} else {
emitline("\tJE\t");
};
emitline(okl);
emitline("\n\tMOVQ\t$1, DI\n\tMOVQ\t$60, AX\n\tSYSCALL\n");
emitlabel(okl);
return;
};
if (want < 0) { want = 0; };
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: *syntax.node) void = {
let srcfk: i32 = 0;
if (n.lhs != nil) {
let st: *syntax.tinfo = n.lhs.type_: *syntax.tinfo;
if (syntax.typeisf32(st)) { srcfk = 1; }
else { if (syntax.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. Type-keyed on the stamped src/dst types (dst-is-slice +
// src-is-str), mirroring cstage cgen.c N_CAST (type_chase_named →
// TY_SLICE/TY_STR). The prior local-ident-source gate (#19) missed
// every non-local str source — global ident, struct field, call
// result — leaving CX = the str's stale word-16 garbage cap.
if (isslicetype(c, n.rhs)) {
if (isstrtype(c, n.lhs)) { 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: *syntax.node = n.rhs;
for (leaf_tn != nil) {
let lk: syntax.nkind = leaf_tn.kind;
if (lk == syntax.nkind.N_TBANG) { leaf_tn = leaf_tn.lhs; }
else { if (lk == syntax.nkind.N_TENUM) { leaf_tn = leaf_tn.lhs; }
else { if (lk == syntax.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: *syntax.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 == syntax.nkind.N_TNAME) {
is_bool = syntax.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: *syntax.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: *syntax.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: *syntax.tinfo = nil;
if (lc.tnode != nil) { itu = lc.tnode.type_: *syntax.tinfo; };
itu = tichase(itu);
if (itu != nil) { if (itu.kind == syntax.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: *syntax.node = deflookuprhs(c, nm);
if (drhs != nil) {
if (drhs.kind == syntax.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");
emitfnname(c, nm, c.curmod);
emitline("(SB), CX\n");
emitline("\t");
emitline(mov);
emitline("\t(CX), X0\n");
return;
};
emitline("\tMOVQ\t");
emitfnname(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: *syntax.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: *syntax.node = letvartnode(c, nm);
for (gtt != nil && gtt.kind == syntax.nkind.N_TNAME) {
gtt = aliaslookup(c, gtt.str);
};
if (gtt != nil) {
if (gtt.kind == syntax.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");
emitfnname(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: *syntax.node = nil;
let lv: *letvar = c.lets;
for (lv != nil) {
if (syntax.streq(lv.name, nm)) {
// #135: an inferred-float global's tnode is the
// defaultinferredlets-renamed "f64"/"f32" N_TNAME whose
// .type_ is unstamped (cgen can't build tinfo), so the
// isfloattype stamp-read misses it. Fall back to the
// name keyword (the letfloatprim SSoT letemitsize uses)
// so the float load fires for inferred as for explicit.
let isf: bool = isfloattype(c, lv.tnode);
let is32: bool = isf32type(c, lv.tnode);
if (!isf && lv.tnode != nil
&& lv.tnode.kind == syntax.nkind.N_TNAME) {
let fsz: i32 = letfloatprim(lv.tnode.str);
if (fsz > 0) { isf = true; };
if (fsz == 4) { is32 = true; };
};
if (isf) {
let mov: str = "MOVSD";
if (is32) { mov = "MOVSS"; };
emitline("\tLEAQ\t");
emitfnname(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 (syntax.streq(glop, "MOVQ")) {
emitline("\tMOVQ\t");
emitfnname(c, nm, c.curmod);
emitline("(SB), AX\n");
} else {
emitline("\tLEAQ\t");
emitfnname(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 (!syntax.streq(base, "BX")) { emitmovqload(8i64, base, "BX"); };
if (!syntax.streq(base, "CX")) { emitmovqload(16i64, base, "CX"); };
if (!syntax.streq(base, "AX")) { emitmovqload(0i64, base, "AX"); };
if (syntax.streq(base, "BX")) { emitmovqload(8i64, base, "BX"); };
if (syntax.streq(base, "CX")) { emitmovqload(16i64, base, "CX"); };
if (syntax.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: *syntax.node, dstreg: str) bool = {
if (n == nil) { return false; };
if (n.kind == syntax.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 != syntax.nkind.N_DOT) { return false; };
let x: *syntax.node = n.lhs;
if (x == nil) { return false; };
let xu: *syntax.tinfo = x.type_: *syntax.tinfo;
xu = tichase(xu);
if (xu == nil) { return false; };
let xviaptr: bool = false;
let st: *syntax.tinfo = nil;
if (xu.kind == syntax.tykind.TY_PTR) {
let p: *syntax.tinfo = xu.sub;
p = tichase(p);
if (p != nil) { if (p.kind == syntax.tykind.TY_STRUCT) {
st = p;
xviaptr = true;
}; };
} else { if (xu.kind == syntax.tykind.TY_STRUCT) {
st = xu;
}; };
if (st == nil) { return false; };
let f: *syntax.tfield = st.fields;
let foff: i64 = -1;
for (f != nil) {
if (syntax.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: *syntax.node, dstreg: str) bool = {
if (base == nil) { return false; };
if (base.kind != syntax.nkind.N_DOT) { return false; };
let inner: *syntax.node = base.lhs;
if (inner == nil) { return false; };
let chained: bool = (inner.kind == syntax.nkind.N_DOT);
if (inner.kind != syntax.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) {
// #128b is for a module-QUALIFIER inner (mod.arr): inner has no
// usable struct type. cstage gates it on inner->type==NULL||ty_err
// (cgen.c:2109). Without the gate a typed-struct global inner whose
// FIELD shares a name with an unrelated global array hijacks it
// (gs.fld -> LEAQ fld(SB)) — #20. A typed inner falls to #249 below.
let ibu: *syntax.tinfo = tichase(inner.type_: *syntax.tinfo);
if (ibu == nil || ibu.kind == syntax.tykind.TY_ERR) {
let gt: *syntax.node = letvartnode(c, base.str);
if (gt != nil && gt.kind == syntax.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: *syntax.tinfo = inner.type_: *syntax.tinfo;
bu = tichase(bu);
if (bu == nil) { return false; };
let viaptr: bool = false;
let structt: *syntax.tinfo = nil;
if (bu.kind == syntax.tykind.TY_PTR) {
let st: *syntax.tinfo = bu.sub;
st = tichase(st);
if (st != nil) { if (st.kind == syntax.tykind.TY_STRUCT) {
structt = st;
viaptr = true;
}; };
} else { if (bu.kind == syntax.tykind.TY_STRUCT) {
structt = bu;
}; };
if (structt == nil) { return false; };
let f: *syntax.tfield = structt.fields;
let foff: i64 = -1;
let ft: *syntax.tinfo = nil;
for (f != nil) {
if (syntax.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 != syntax.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: *syntax.node, dst: str) bool = {
if (src.kind == syntax.nkind.N_UN) {
if (src.op == syntax.tkind.TK_STAR) {
cgexpr(c, src.lhs);
if (!syntax.streq(dst, "AX")) {
emitline("\tMOVQ\tAX, ");
emitline(dst);
emitline("\n");
};
return true;
};
};
if (src.kind == syntax.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 == syntax.nkind.N_DOT) {
return dotchainaddr(c, src, dst);
};
if (src.kind == syntax.nkind.N_INDEX) {
let base: *syntax.node = src.lhs;
let idx: *syntax.node = src.rhs;
if (base == nil) { return false; };
if (base.kind != syntax.nkind.N_IDENT) { return false; };
let bu: *syntax.tinfo = base.type_: *syntax.tinfo;
bu = tichase(bu);
if (bu == nil) { return false; };
if (bu.kind != syntax.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 (!syntax.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;
};
};
// cgaggregstore — ww twin of cstage cgen.c cg_agg_reg_store. The ONE
// register-cursor aggregate materialise: an in-cap (<=24B, GP-class)
// struct/array/tuple already held in the {AX,DX,CX} return cursor is
// stored into basereg+disp. full=sz/8 exact-8B MOVQ words land straight
// in; the sz%8 tail is one sized MOVB/MOVW/MOVL for {1,2,4} (BOTH
// branches). Extracted from the cstage site-E #10 template so every
// narrow-tail materialise funnels through one place — close-by-
// construction (task #14).
//
// dest_padded forks ONLY the {3,5,6,7} tail (no GP sub-register exists
// for those widths and w6a has no shift):
// dest_padded == "the dest is a ceil-8/round8 slot (a scratch, or a
// #75 let/local aggregate slot) so an 8B tail over-store stays
// in-bounds." Post-#9: packed struct fields + array elements => false;
// let/local/scratch slots => true. It CANNOT be derived — paddedness
// is routing knowledge the caller owns.
// true -> a single full MOVQ of the cursor tail eightbyte (= the
// #10 template; the over-store lands in the slot's pad).
// false -> MOVQ the cursor tail eightbyte into the helper's own
// ceil-8 @tagscr pad, then a sized aggcopy of the tail bytes
// into the dest so a packed field / array element is never
// overrun.
// PRECONDITION (caller-owned, stays per-site): the value is already in
// AX/DX/CX, the dest base is resolved into basereg+disp, and the
// float-class (#165) / over-cap-sret (#234) loud-stops have already
// fired. Clobbers SI/BX/AX only on the dest_padded==false detour (the
// value words are in memory by then).
fn cgaggregstore(c: *cgen, basereg: str, disp: i32, sz: i32, dest_padded: bool) void = {
let full: i32 = sz / 8;
let tail: i32 = sz - full * 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((disp + i * 8): i64, basereg);
emitline("\n");
i += 1;
};
if (tail == 0) { return; };
let treg: str = "AX";
if (full == 1) { treg = "DX"; };
if (full == 2) { treg = "CX"; };
if (tail == 1 || tail == 2 || tail == 4) {
let top: str = "MOVB";
if (tail == 4) { top = "MOVL"; };
if (tail == 2) { top = "MOVW"; };
emitline("\t");
emitline(top);
emitline("\t");
emitline(treg);
emitline(", ");
emitdispreg((disp + full * 8): i64, basereg);
emitline("\n");
return;
};
if (dest_padded) {
emitline("\tMOVQ\t");
emitline(treg);
emitline(", ");
emitdispreg((disp + full * 8): i64, basereg);
emitline("\n");
return;
};
let pad: i32 = tagscradd(c, 8);
emitline("\tMOVQ\t");
emitline(treg);
emitline(", ");
emitdispreg(pad: i64, "BP");
emitline("\n");
emitline("\tLEAQ\t");
emitdispreg(pad: i64, "BP");
emitline(", SI\n");
emitline("\tLEAQ\t");
emitdispreg((disp + full * 8): i64, basereg);
emitline(", BX\n");
aggcopy(c, tail);
};
// copysrcnatsize — natural byte width of a whole-struct copy's SOURCE
// operand, read from the source node's stamped tinfo (tichase peels
// NAMED). #71: the four whole-struct field-copy sites must move this many
// bytes, NOT structinfo.totsize (slot-padded, round-8) which over-copies
// into slot padding and clobbers a natural-offset successor once #44 packs
// it there. Reads the source NODE's type, never fi.foff/fi.fsz or
// structinfo, so #71 stays independent of #44's registerstruct offset
// change. Equals cstage's `str_fu->size` (cmd/w6c/cgen.c:5302 SSoT) — the
// field struct's aligned r.size (check.ww N_TSTRUCT), distinct from the
// existing structnaturalsize (which reads structinfo max(foff+fsz), a
// #44-coupled source). The ragged-tail completeness shared by both stages'
// field copies is a separate class, tracked under #73 / the aggcopy
// emitter choke-point.
fn copysrcnatsize(c: *cgen, src: *syntax.node) i32 = {
if (src == nil || src.type_ == nil) {
let msg: str = "#71: whole-struct copy source has no stamped tinfo\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
let ti: *syntax.tinfo = tichase(src.type_: *syntax.tinfo);
if (ti == nil) {
let msg: str = "#71: whole-struct copy source tinfo chase yielded nil\n";
os.write(2, msg.ptr, msg.len: u64);
os.exit(1);
};
return ti.size: i32;
};
// 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: *syntax.node, dstreg: str) bool = {
if (n == nil) { return false; };
if (n.kind == syntax.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: *syntax.node = defvartnode(c, n.str);
if (dtn != nil) {
if (dtn.kind == syntax.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 == syntax.nkind.N_UN) {
if (n.op != syntax.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 == syntax.nkind.N_INDEX) {
let base: *syntax.node = n.lhs;
let idx: *syntax.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: *syntax.tinfo = base.type_: *syntax.tinfo;
bu = tichase(bu);
if (bu == nil) { return false; };
if (bu.kind != syntax.tykind.TY_SLICE && bu.kind != syntax.tykind.TY_ARRAY) {
return false;
};
let et: *syntax.tinfo = n.type_: *syntax.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 == syntax.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 == syntax.nkind.N_DOT) {
let base: *syntax.node = n.lhs;
if (base == nil) { return false; };
let bu: *syntax.tinfo = base.type_: *syntax.tinfo;
bu = tichase(bu);
if (bu == nil) { return false; };
let viaptr: bool = false;
let st: *syntax.tinfo = nil;
if (bu.kind == syntax.tykind.TY_PTR) {
let p: *syntax.tinfo = bu.sub;
p = tichase(p);
if (p != nil) { if (p.kind == syntax.tykind.TY_STRUCT) {
st = p;
viaptr = true;
}; };
} else { if (bu.kind == syntax.tykind.TY_STRUCT) {
st = bu;
}; };
if (st == nil) { return false; };
let f: *syntax.tfield = st.fields;
let foff: i64 = -1;
for (f != nil) {
if (syntax.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: *syntax.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: *syntax.node = n.lhs;
let idx: *syntax.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 == syntax.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: *syntax.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 == syntax.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: *syntax.tinfo = base.type_: *syntax.tinfo;
if (bt60 != nil) {
if (bt60.kind == syntax.tykind.TY_NAMED) { basealias = true; };
};
if (basealias) {
let et60: *syntax.tinfo = tichase(n.type_: *syntax.tinfo);
if (et60 != nil) {
esz = et60.size: i32;
signed_elem = syntax.typeissigned(et60);
float_elem = syntax.typeisfloat(et60);
f32_elem = syntax.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: *syntax.tinfo = tichase(bt60);
if (bu60 != nil) {
isglobalarr = bu60.kind == syntax.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_: *syntax.tinfo);
};
} else { if (base.kind == syntax.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.
// #22 (F7-c3): also stamp elemisstr/elemisslice off the same
// chased element tinfo. Without them a chained index whose
// element is a str/slice (`m[i][k]` over [N][M]str) loaded
// only the ptr word — the 24B/16B header (len/cap) was
// dropped (stale BX/CX) → garbage .len downstream. cstage's
// idx_eff path classifies the element uniformly via
// type_isstr/type_isslice (cmd/w6c/cgen.c); align ww UP by
// reading the SAME stamp the esz read above uses. CLASS-N:
// the corpus has no chained str/slice element, so the prior
// byte-id is preserved (this only fires on the missed shape).
let et: *syntax.tinfo = n.type_: *syntax.tinfo;
if (et != nil) {
esz = et.size: i32;
signed_elem = syntax.typeissigned(et);
elemisstr = syntax.typeisstr(et);
elemisslice = syntax.typeisslice(et);
float_elem = syntax.typeisfloat(et);
f32_elem = syntax.typeisf32(et);
elem_isarray = tinfoisarray(et);
};
} else {
// Every OTHER non-ident base — N_DOT (`s.arr[i]`), N_UN-deref
// (`(*p)[i]`, #61), N_CAST (`(e:*[N]T)[i]`, #19old), N_CALL
// (`f()[i]`), N_SLICE (`s[a:b][i]`), N_TYPEASSERT
// (`(v as *[N]T)[i]`), … — derives esz/stride/load-width/
// signedness from the checker-stamped index-RESULT tinfo
// n.type_ (the element T: u32->4). cstage reads it UNIFORMLY
// via idx_eff(base->type)->sub->size with NO node-kind gate
// (cmd/w6c/cgen.c:3517-18); wwstage's prior node-kind whitelist
// (DOT/UN/CAST only) silently left N_CALL/N_SLICE/N_TYPEASSERT
// (and any future base kind) at the 8B default — wrong stride
// AND full-word MOVQ load for narrow elements. One stamped-tinfo
// read mirrors cstage and closes the class by construction
// (#19old + its CALL/SLICE/TYPEASSERT residuals). esz via
// dt.size (type table, rule-13). Signedness from the same tinfo
// so a signed-narrow element sign-extends on load (loadopsz keys
// on (signed,sz); cstage's fldloadop reads it from the element
// type — align ww up, #255). The base ADDRESS materialization
// below is unchanged; only the stride/load-width was wrong.
let dt: *syntax.tinfo = n.type_: *syntax.tinfo;
if (dt != nil) { esz = dt.size: i32; signed_elem = syntax.typeissigned(dt); elemisstr = syntax.typeisstr(dt); elemisslice = syntax.typeisslice(dt); float_elem = syntax.typeisfloat(dt); f32_elem = syntax.typeisf32(dt); };
elem_isarray = tinfoisarray(dt);
};};
};
// 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 == syntax.nkind.N_IDENT) {
let bl: *local = baselocal;
let etn: *syntax.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));
// #8/GAP-B: a def-global str/slice-array base lives
// in c.defs, not c.lets — letvartnode misses it →
// etn nil → elem mis-classified scalar, dropping the
// 3-word slice-header load (returns element ADDR not
// .len; cstage's Type-based classify loads the full
// header, the proven let form). defvartnode is the
// def-side sister — same fallback as the base-address
// resolution at cgenexpr.ww:1762.
if (etn == nil) {
etn = idxelemtn(defvartnode(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);
};
// Any NON-ident base (`x.o[i]`, chained `m[i][j]`, `(*p)[i]`,
// call `f()[i]`, slice `s[a:b][i]`, typeassert …): 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 a base-kind whitelist, 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 the classify an N_DOT-base tagged element fell to
// the scalar loadopsz path and dropped the tag/payload-high word;
// #23 (F7-c6): the prior N_DOT/N_INDEX/N_UN whitelist still missed
// an N_CALL / N_SLICE base (`f()[i]` / `s[a:b][i]`) → lone
// `MOVQ (AX),AX` (tag only) vs cstage's full 4-word cursor
// (cs rc=50 / ww rc=40). Dropping the whitelist for the stamp read
// closes the base-kind class by construction.
if (base.kind != syntax.nkind.N_IDENT) {
let dt: *syntax.tinfo = n.type_: *syntax.tinfo;
if (dt != nil) {
if (syntax.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: *syntax.tinfo = tichase(n.type_: *syntax.tinfo);
if (eti != nil) { if (eti.kind == syntax.tykind.TY_TUPLE) {
elem_tuple = true;
// ww tuples store elements in .tupleelems, not .params.
let tpw: *syntax.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: *syntax.node = baselocal.tnode;
let isarray: bool = false;
if (tn != nil) { if (tn.kind == syntax.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: *syntax.tinfo = tichase(base.type_: *syntax.tinfo);
if (bu60 != nil) { isarray = bu60.kind == syntax.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: *syntax.node, dst: str) bool = {
if (base == nil) { return false; };
if (base.kind != syntax.nkind.N_IDENT) { return false; };
let baselocal: *local = localfindnode(c, base.str);
if (baselocal != nil) {
let tn: *syntax.node = baselocal.tnode;
if (tn == nil) { return false; };
if (tn.kind == syntax.nkind.N_TARRAY) {
let lenn: *syntax.node = tn.rhs;
if (lenn != nil && lenn.kind == syntax.nkind.N_INTLIT) {
emitline("\tMOVQ\t$");
emituint(lenn.uval);
emitline(", ");
emitline(dst);
emitline("\n");
return true;
};
// #21: a def/const array dim — non-N_INTLIT — reads its cap
// from the stamped array tinfo (rule-13), the cap twin of the
// default-hi fix; pre-fix cgbasecap returned false here and the
// caller fell to cap=len (cs computes base_cap-lo from bu->alen).
let abt: *syntax.tinfo = tichase(base.type_: *syntax.tinfo);
if (abt != nil && abt.kind == syntax.tykind.TY_ARRAY) {
emitline("\tMOVQ\t$");
emitint(abt.alen: i64);
emitline(", ");
emitline(dst);
emitline("\n");
return true;
};
return false;
};
if (tn.kind == syntax.nkind.N_TSLICE) {
emitline("\tMOVQ\t");
emitoff((baselocal.off + 16): i64);
emitline("(BP), ");
emitline(dst);
emitline("\n");
return true;
};
if (tn.kind == syntax.nkind.N_TNAME) {
if (syntax.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: *syntax.tinfo = base.type_: *syntax.tinfo;
if (bt60 != nil) { if (bt60.kind == syntax.tykind.TY_NAMED) {
let bu60: *syntax.tinfo = tichase(bt60);
if (bu60 != nil) {
if (bu60.kind == syntax.tykind.TY_ARRAY) {
emitline("\tMOVQ\t$");
emitint(bu60.alen: i64);
emitline(", ");
emitline(dst);
emitline("\n");
return true;
};
if (bu60.kind == syntax.tykind.TY_SLICE
|| bu60.kind == syntax.tykind.TY_STR) {
emitline("\tMOVQ\t");
emitoff((baselocal.off + 16): i64);
emitline("(BP), ");
emitline(dst);
emitline("\n");
return true;
};
};
};};
return false;
};
let gt: *syntax.node = letvartnode(c, base.str);
if (gt == nil) { return false; };
if (gt.kind == syntax.nkind.N_TARRAY) {
let lenn: *syntax.node = gt.rhs;
if (lenn != nil && lenn.kind == syntax.nkind.N_INTLIT) {
emitline("\tMOVQ\t$");
emituint(lenn.uval);
emitline(", ");
emitline(dst);
emitline("\n");
return true;
};
// #21: def/const global array dim cap — twin of the local arm.
let abt: *syntax.tinfo = tichase(base.type_: *syntax.tinfo);
if (abt != nil && abt.kind == syntax.tykind.TY_ARRAY) {
emitline("\tMOVQ\t$");
emitint(abt.alen: i64);
emitline(", ");
emitline(dst);
emitline("\n");
return true;
};
return false;
};
if (gt.kind == syntax.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: *syntax.tinfo = base.type_: *syntax.tinfo;
if (gbt60 != nil) { if (gbt60.kind == syntax.tykind.TY_NAMED) {
let gbu60: *syntax.tinfo = tichase(gbt60);
if (gbu60 != nil) {
if (gbu60.kind == syntax.tykind.TY_ARRAY) {
emitline("\tMOVQ\t$");
emitint(gbu60.alen: i64);
emitline(", ");
emitline(dst);
emitline("\n");
return true;
};
if (gbu60.kind == syntax.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: *syntax.node) i32 = {
if (base == nil) { return 1; };
let esz: i32 = 1;
if (base.kind == syntax.nkind.N_IDENT) {
let bl: *local = localfindnode(c, base.str);
if (bl != nil) {
esz = elemsizeofc(c, bl.tnode);
} else {
let gt: *syntax.node = letvartnode(c, base.str);
if (gt != nil) { esz = elemsizeofc(c, gt); };
};
let bt: *syntax.tinfo = base.type_: *syntax.tinfo;
if (bt != nil) { if (bt.kind == syntax.tykind.TY_NAMED) {
let bu60: *syntax.tinfo = tichase(bt);
let es60: *syntax.tinfo = tichase(bu60.sub);
if (es60 != nil) { esz = es60.size: i32; };
};};
} else { if (base.kind == syntax.nkind.N_DOT) {
let db: *syntax.tinfo = tichase(base.type_: *syntax.tinfo);
if (db != nil) { if (db.sub != nil) { esz = db.sub.size: i32; }; };
} else { if (base.kind == syntax.nkind.N_ARRLIT) {
esz = elemsizeofc(c, base.lhs);
};};};
return esz;
};
fn cgslice(c: *cgen, n: *syntax.node) void = {
let base: *syntax.node = n.lhs;
let lo: *syntax.node = n.rhs;
let hi: *syntax.node = n.cond;
let baselocal: *local = nil;
let globaltn: *syntax.node = nil;
let globalname: str;
globalname.ptr = nil; globalname.len = 0;
if (base != nil) {
if (base.kind == syntax.nkind.N_IDENT) {
baselocal = localfindnode(c, base.str);
if (baselocal == nil) {
let gt: *syntax.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: *syntax.tinfo = nil;
if (base != nil) { if (base.kind == syntax.nkind.N_DOT) {
dotbu = base.type_: *syntax.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: *syntax.node = nil;
if (base != nil) { if (base.kind == syntax.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: *syntax.tinfo = nil;
if (base != nil) { if (base.kind == syntax.nkind.N_IDENT) {
let bt60: *syntax.tinfo = base.type_: *syntax.tinfo;
if (bt60 != nil) { if (bt60.kind == syntax.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: *syntax.tinfo = tichase(bu60.sub);
if (es60 != nil) { esz = es60.size: i32; };
};
// base address
if (baselocal != nil) {
let tn: *syntax.node = baselocal.tnode;
let isarray: bool = false;
if (tn != nil) {
if (tn.kind == syntax.nkind.N_TARRAY) { isarray = true; };
};
// #60: alias-NAMED base — chased kind (see cgindex twin).
if (bu60 != nil) { isarray = bu60.kind == syntax.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 == syntax.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 == syntax.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 == syntax.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 == syntax.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: *syntax.node = baselocal.tnode;
let handled: bool = false;
if (tn != nil) {
if (tn.kind == syntax.nkind.N_TARRAY) {
let lenn: *syntax.node = tn.rhs;
if (lenn != nil && lenn.kind == syntax.nkind.N_INTLIT) {
emitline("\tMOVQ\t$");
emituint(lenn.uval);
emitline(", AX\n");
handled = true;
} else {
// #21: a def/const array dim (`[MAX]u8`) is not an
// N_INTLIT node, so the literal read above misses it
// (MOVQ $0 default-hi -> len 0 / underflow, exit 255).
// Read the resolved length from the stamped array
// tinfo (rule-13), mirroring cstage's bu->alen.
let abt: *syntax.tinfo = tichase(base.type_: *syntax.tinfo);
if (abt != nil && abt.kind == syntax.tykind.TY_ARRAY) {
emitline("\tMOVQ\t$");
emitint(abt.alen: i64);
emitline(", AX\n");
handled = true;
};
};
} else { if (tn.kind == syntax.nkind.N_TSLICE) {
emitline("\tMOVQ\t");
emitoff((baselocal.off + 8): i64);
emitline("(BP), AX\n");
handled = true;
} else { if (tn.kind == syntax.nkind.N_TNAME) {
if (syntax.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 == syntax.tykind.TY_ARRAY) {
emitline("\tMOVQ\t$");
emitint(bu60.alen: i64);
emitline(", AX\n");
handled = true;
} else { if (bu60.kind == syntax.tykind.TY_SLICE
|| bu60.kind == syntax.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 == syntax.nkind.N_TARRAY) {
let lenn: *syntax.node = globaltn.rhs;
if (lenn != nil && lenn.kind == syntax.nkind.N_INTLIT) {
emitline("\tMOVQ\t$");
emituint(lenn.uval);
emitline(", AX\n");
handled = true;
} else {
// #21: a def/const global array dim — non-N_INTLIT, read the
// resolved length off the stamped array tinfo (rule-13), the
// twin of the local arm above (cstage's bu->alen).
let abt: *syntax.tinfo = tichase(base.type_: *syntax.tinfo);
if (abt != nil && abt.kind == syntax.tykind.TY_ARRAY) {
emitline("\tMOVQ\t$");
emitint(abt.alen: i64);
emitline(", AX\n");
handled = true;
};
};
} else { if (globaltn.kind == syntax.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 == syntax.tykind.TY_ARRAY) {
emitline("\tMOVQ\t$");
emitint(bu60.alen: i64);
emitline(", AX\n");
handled = true;
} else { if (bu60.kind == syntax.tykind.TY_SLICE
|| bu60.kind == syntax.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 == syntax.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 == syntax.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: *syntax.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: *syntax.node = taggedidcastpeel(c, n.lhs);
let scrutoff: i32 = 0;
let scrutt: *syntax.node = nil;
if (scrut != nil) {
if (scrut.kind == syntax.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: *syntax.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");
emitfnname(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 {
// M1 (#25): match on a tagged field of a BARE-ident VALUE
// struct reads the box IN PLACE at base.off + field.offset
// — the box (tag@+0, word0@+8, word1@+16) is contiguous in
// the parent frame, so no @match_spill copy. Verbatim mirror
// of cstage N_MATCH's in-place arm (cmd/w6c/cgen.c:10241-
// 10296): gate on N_DOT with a bare-IDENT base whose stamped
// type chases to TY_STRUCT and whose field resolves by name.
// A *ptr-field base (`match (h.e)`, h:*struct) chases to
// TY_PTR, the by-name scan misses, and it falls to the spill
// arm below — exactly as cstage, no separate deref guard.
// #29: the in-place arm is also gated on a CONFIRMED-LOCAL
// base (bglobal below). A global VALUE-struct base
// (`match (g.field)`) has localfind==0, so the M1 base.off
// would land in the saved-BP/return-addr region — both
// stages emitted `MOVQ (BP),AX` (gate-blind both-wrong). The
// global-base predicate `localfind==0 && isletvar` (cstage
// twin: let_islet, cgen.c:2000) routes it through to the
// spill `else`, which resolves g(SB). align-DOWN to cstage
// (rule 10): both stages emit TEXT $32 on the local-field
// case and the same g(SB) spill on the global-field case.
let mfld: *syntax.tfield = nil;
if (scrut.kind == syntax.nkind.N_DOT && scrut.lhs != nil
&& scrut.lhs.kind == syntax.nkind.N_IDENT
&& scrut.lhs.type_ != nil) {
let bu: *syntax.tinfo = tichase(scrut.lhs.type_: *syntax.tinfo);
if (bu != nil && bu.kind == syntax.tykind.TY_STRUCT) {
let fl: *syntax.tfield = bu.fields;
for (fl != nil) {
if (syntax.streq(fl.name, scrut.str)) {
mfld = fl;
break;
};
fl = fl.tnext;
};
};
};
let bglobal: bool = false;
if (mfld != nil) {
bglobal = (localfind(c, scrut.lhs.str) == 0)
&& isletvar(c, scrut.lhs.str);
};
if (mfld != nil && !bglobal) {
let foff: i32 = mfld.offset: i32;
scrutoff = localfind(c, scrut.lhs.str) + foff;
scrutt = matchscrutt(c, scrut);
} 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 == syntax.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: *syntax.tinfo = scrut.type_: *syntax.tinfo;
ms37 = tichase(ms37);
if (ms37 != nil && ms37.kind == syntax.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 == syntax.nkind.N_CAST && ms37 != nil
&& ms37.kind == syntax.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: *syntax.node = n.list;
for (cs != nil) {
let nxt: str = mklabel(c, "match_next");
let pat: *syntax.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 == syntax.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: *syntax.tinfo = pat.type_: *syntax.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 (syntax.typeisslice(pattype)) {
r = flatslicevariantidx(c, scrutt, pat.lhs);
} else {
r = flatvariantidxt(scrutt.type_: *syntax.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 == syntax.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 cgptrfieldload(c: *cgen, fi: *fieldinfo) void = {
// #15 — load struct field `fi` from a *struct base already in BX
// (a local ptr's MOVQ off(BP) value, or a module-global ptr's
// MOVQ name(SB) value). Shared by the local and global *struct
// field-read arms in cgdot; mirrors cstage cgen.c N_DOT *struct
// field tail. BX is never a load target, so order is harmless.
if (istaggedtype(c, fi.tnode)) {
let tsz: i32 = slotsize(c, fi.tnode);
// cxlast=false: BX is not a cursor target here, so match
// cstage's direct *struct-ptr arm and load CX@+16 before
// R8@+24 in strict offset order (#17).
cgloadtaggedfield(c, "BX", fi.foff, tsz, false);
return;
};
// str IS []u8 — same 3-word {ptr,len,cap} as a slice field: load
// (ptr, len, cap) into (AX, BX, CX); .len LAST so the earlier reads
// still index off BX (#1/Phase 3 collapse).
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");
return;
};
if (isfloattype(c, fi.tnode)) {
// f64/f32 via *struct: route through X0.
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");
return;
};
let op: str = fieldloadop(c, fi);
emitline("\t");
emitline(op);
emitline("\t");
emitdispreg(fi.foff: i64, "BX");
emitline(", AX\n");
};
// cgptrfieldloadtf — tfield twin of cgptrfieldload (#31): load struct
// field `tf` from a *struct base already in BX, resolving offset + field
// type off the checker-STAMPED tfield (tf.offset/tf.type_) instead of the
// name-keyed fieldinfo. The read-side choke-point shared by the R1 (local)
// and R2 (global) *struct field-read arms; a cross-module same-leaf
// collision that mis-resolved structlookupchain(inner) cannot reach a
// stamped tinfo. Emission is byte-identical to cgptrfieldload (the #21 R0
// substitution table): syntax.typeis*(tf.type_) replaces is*type(fi.tnode),
// tichase(tf.type_).size replaces slotsize/fsz. BX is never a load target,
// so the str/slice triple writes BX (the base) LAST.
fn cgptrfieldloadtf(c: *cgen, tf: *syntax.tfield) void = {
let foff: i32 = tf.offset: i32;
let ftraw: *syntax.tinfo = tf.type_;
if (syntax.typeistagged(ftraw)) {
let ftc: *syntax.tinfo = tichase(ftraw);
let tsz: i32 = 0;
if (ftc != nil) { tsz = ftc.size: i32; };
cgloadtaggedfield(c, "BX", foff, tsz, false);
return;
};
if (syntax.typeisstr(ftraw) || syntax.typeisslice(ftraw)) {
emitline("\tMOVQ\t");
emitdispreg(foff: i64, "BX");
emitline(", AX\n");
emitline("\tMOVQ\t");
emitdispreg((foff + 16): i64, "BX");
emitline(", CX\n");
emitline("\tMOVQ\t");
emitdispreg((foff + 8): i64, "BX");
emitline(", BX\n");
return;
};
if (syntax.typeisfloat(ftraw)) {
let mov: str = "MOVSD";
if (syntax.typeisf32(ftraw)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\t");
emitdispreg(foff: i64, "BX");
emitline(", X0\n");
return;
};
let ftc: *syntax.tinfo = tichase(ftraw);
let fsz: i32 = 0;
if (ftc != nil) { fsz = ftc.size: i32; };
let op: str = loadopsz(syntax.typeissigned(ftraw), fsz);
emitline("\t");
emitline(op);
emitline("\t");
emitdispreg(foff: i64, "BX");
emitline(", AX\n");
};
// emitchainbase — load the chained-DOT root BASE into CX for the viacx
// (global or `*T`-root) spine. Mirrors cstage cgen.c's uniform base
// resolution: a global is reached via LEAQ name(SB),CX; a `*struct`
// ROOT then derefs that address once (MOVQ (CX),CX) — covering the
// global `*struct` root, where BOTH flags hold (#16) — while a LOCAL
// `*T` root loads its frame slot (MOVQ off(BP),CX). The leaf load then
// indexes at totaloff off CX.
fn emitchainbase(c: *cgen, ptrroot: bool, isglobal: bool,
rootoff: i32, rootname: str) void = {
if (isglobal) {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), CX\n");
if (ptrroot) {
emitline("\tMOVQ\t(CX), CX\n");
};
} else {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), CX\n");
};
};
fn cgdot(c: *cgen, n: *syntax.node) void = {
let lhs: *syntax.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: *syntax.node = lhs;
if (dotlhs != nil) {
if (dotlhs.kind == syntax.nkind.N_UN) {
if (dotlhs.op == syntax.tkind.TK_STAR) {
if (dotlhs.lhs != nil) {
if (dotlhs.lhs.kind == syntax.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 == syntax.nkind.N_IDENT) {
etname = lhs.str;
};
if (lhs.kind == syntax.nkind.N_DOT) {
if (lhs.lhs != nil) {
if (lhs.lhs.kind == syntax.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 == syntax.nkind.N_IDENT) {
let nm: str = dotlhs.str;
let lc: *local = localfindnode(c, nm);
if (lc != nil) {
let tn: *syntax.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. #21/#224: the direct-struct arm
// below no longer name-keys — it resolves field offsets off
// the stamped struct tinfo (dotlhs.type_), closing the
// cross-module same-leaf STRUCT mis-read there. This peel +
// same-module break still scopes the *struct (N_TPTR) and
// array arms, which remain structlookupchain-keyed (out of
// the #21 scoped slice).
for (tn != nil && tn.kind == syntax.nkind.N_TNAME) {
if (structsamemod(c, tn.str) != nil) { break; };
let nx: *syntax.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: syntax.nkind = tn.kind;
// Pointer-to-struct: deref then field load.
if (lkind == syntax.nkind.N_TPTR) {
// #31: resolve the *struct field OFFSET + type off the
// checker-STAMPED receiver tinfo (tichase(dotlhs.type_)
// → .sub pointee), NOT the name-keyed
// structlookupchain(inner). Under a cross-module same-
// leaf collision inner is a bare leaf that mis-resolves
// to a FOREIGN same-leaf struct → wrong offset/load-op
// (the inferred-local READ row, ww=28). The stamped tinfo
// carries the right layout; mirror cstage
// type_chase_named(bu->sub)->fields (cgen.c:506-512) +
// the #21 R0 template. Non-struct pointees (*str/*slice/
// *[N]T) are not TY_STRUCT → fall through to the pseudo-
// field arms below (guard, never a bare-leaf fallback).
// Read choke-point cgptrfieldloadtf shared with the R2
// global arm. Stage the *struct base in BX (not a load
// target), then field-load.
let sti: *syntax.tinfo = nil;
if (dotlhs != nil) { sti = tichase(dotlhs.type_: *syntax.tinfo); };
if (sti != nil && sti.kind == syntax.tykind.TY_PTR) { sti = tichase(sti.sub); };
if (sti != nil) { if (sti.kind == syntax.tykind.TY_STRUCT) {
let tf: *syntax.tfield = sti.fields;
for (tf != nil) {
if (syntax.streq(tf.name, fld)) {
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
cgptrfieldloadtf(c, tf);
return;
};
tf = tf.tnext;
};
}; };
};
// Direct struct local: field load at off+foff.
if (lkind == syntax.nkind.N_TNAME) {
// #21/#224: resolve the field OFFSET + field type off
// the checker-STAMPED struct tinfo
// (tichase(dotlhs.type_).fields), NOT the name-keyed
// structlookupchain(tn). On a cross-module same-leaf
// collision lc.tnode is a bare leaf that structlookup
// mis-resolves to a FOREIGN same-leaf struct → fields
// read at the WRONG offsets / wrong load-op (the #224
// direct-struct arm flagged at the peel-loop comment
// above). The stamped tinfo carries the right layout
// regardless of leaf collision; mirrors cstage's
// `t->fields` walk (cgen.c N_DOT, type-keyed) — align
// ww UP. tfield {name, type_, offset} is the tinfo twin
// of fieldinfo {fname, tnode, foff}; the dispatch keys
// off syntax.typeis* on the field tinfo, byte-id with
// the prior is*type(fi.tnode)=typeis*(fi.tnode.type_).
let sbu: *syntax.tinfo = nil;
if (dotlhs != nil) { sbu = tichase(dotlhs.type_: *syntax.tinfo); };
if (sbu != nil) { if (sbu.kind == syntax.tykind.TY_STRUCT) {
let tf: *syntax.tfield = sbu.fields;
for (tf != nil) {
if (syntax.streq(tf.name, fld)) {
let foff: i32 = tf.offset: i32;
let ftraw: *syntax.tinfo = tf.type_;
// tagged-union field: AX=tag, DX=word0,
// CX=word1[, R8=word2]; slot = ti.size
// (slotsize's TAGGED arm, cgenutil.ww:2680).
if (syntax.typeistagged(ftraw)) {
let ftc: *syntax.tinfo = tichase(ftraw);
let tsz: i32 = 0;
if (ftc != nil) { tsz = ftc.size: i32; };
cgloadtaggedfield(c, "BP",
lc.off + foff, tsz, true);
return;
};
// str IS []u8 — 3-word {ptr,len,cap} into
// (AX,BX,CX). Base is BP so order is harmless.
if (syntax.typeisstr(ftraw) || syntax.typeisslice(ftraw)) {
emitline("\tMOVQ\t");
emitoff((lc.off + foff): 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");
} else { if (syntax.typeisfloat(ftraw)) {
// f64/f32 field: route through X0.
let mov: str = "MOVSD";
if (syntax.typeisf32(ftraw)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\t");
emitoff((lc.off + foff): i64);
emitline("(BP), X0\n");
} else {
let ftc: *syntax.tinfo = tichase(ftraw);
let fsz: i32 = 0;
if (ftc != nil) { fsz = ftc.size: i32; };
let op: str = loadopsz(syntax.typeissigned(ftraw), fsz);
emitline("\t");
emitline(op);
emitline("\t");
emitoff((lc.off + foff): i64);
emitline("(BP), AX\n");
}; };
return;
};
tf = tf.tnext;
};
}; };
};
// Array pseudo-fields: `.ptr` is the array's
// address (LEAQ); `.len` is the static element
// count (immediate).
if (lkind == syntax.nkind.N_TARRAY) {
if (syntax.streq(fld, "ptr")) {
emitline("\tLEAQ\t");
emitoff(lc.off: i64);
emitline("(BP), AX\n");
return;
};
if (syntax.streq(fld, "len")) {
let lenn: *syntax.node = tn.rhs;
let alen: i64 = 0i64;
if (lenn != nil && lenn.kind == syntax.nkind.N_INTLIT) {
alen = lenn.uval: i64;
} else {
// #56: def/const dim — resolve from the
// stamped array tinfo (rule-13), the field-
// read twin of the #21 cgslice fix. The dim
// node is an N_IDENT(def), not N_INTLIT, so
// the literal read above defaults 0; cstage
// reads the resolved bu->alen.
let abt: *syntax.tinfo = tichase(tn.type_: *syntax.tinfo);
if (abt != nil && abt.kind == syntax.tykind.TY_ARRAY) {
alen = abt.alen: 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 == syntax.nkind.N_TTUPLE) {
let idx: i32 = fldnumidx(fld);
if (idx >= 0) {
let tp: *syntax.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: *syntax.node = tp.lhs;
// str IS []u8, and a slice is the same 24B
// {ptr,len,cap} header — load all three words
// into (AX, BX, CX). #28: pre-fix the gate was
// str-only, so a SLICE tuple element fell to
// the scalar tail below (one ptr word; len/cap
// took stale registers). base is BP (frame),
// so the triple order has no clobber risk.
if (isstrtype(c, tpt) || isslicetype(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: *syntax.tinfo = tpt.type_: *syntax.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 (syntax.streq(fld, "ptr")) { delta = 0; };
if (syntax.streq(fld, "len")) { delta = 8; };
if (syntax.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 == syntax.nkind.N_TPTR) {
let inner: *syntax.node = tn.lhs;
let innerkind: syntax.nkind = syntax.nkind.N_NONE;
if (inner != nil) { innerkind = inner.kind; };
let innerstr: bool = false;
if (innerkind == syntax.nkind.N_TNAME) {
if (syntax.streq(inner.str, "str")) { innerstr = true; };
};
if (innerkind == syntax.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;
};
};
};
};
// #15 — module-GLOBAL ptr receiver `gp.f` (no local slot, so the
// local arm above is skipped): load the pointer VALUE from name(SB)
// into BX, then field load. Read twin of the #6 store fix; mirrors
// cstage cgen.c N_DOT pointer-to-{struct,slice/str} SB base load. A
// global VALUE struct/slice/array is served by the dedicated global
// arms below, so only a *T receiver lands here.
if (dotlhs != nil) {
if (dotlhs.kind == syntax.nkind.N_IDENT) {
// localfindnode==nil mirrors cstage's off==0 guard: a
// LOCAL ptr (incl. one shadowing a global let) stays on
// the BP-relative local arm above; only a true module
// global lands here.
if (localfindnode(c, dotlhs.str) == nil
&& isletvar(c, dotlhs.str)) {
let gnm: str = dotlhs.str;
let gtn: *syntax.node = letvartnode(c, gnm);
// Peel a NAMED alias chain to expose N_TPTR, the
// same module-aware peel as the local arm (#191/#223).
for (gtn != nil && gtn.kind == syntax.nkind.N_TNAME) {
if (structsamemod(c, gtn.str) != nil) { break; };
let nx: *syntax.node = aliassamemod(c, gtn.str);
if (nx == nil) {
if (structlookup(c, gtn.str) != nil) { break; };
nx = aliaslookup(c, gtn.str);
if (nx == nil) { break; };
};
gtn = nx;
};
if (gtn != nil) {
if (gtn.kind == syntax.nkind.N_TPTR) {
let inner: *syntax.node = gtn.lhs;
// #31: stamped-tinfo *struct field resolution
// (tichase(dotlhs.type_)->.sub), the read choke-point
// twin of the R1 local arm -- see cgptrfieldloadtf. A
// global *struct decl is qualified (let gp: *m1.pair)
// -> non-reddenable; converted for close-by-construction
// (byte-id with the name-keyed path). Non-struct
// pointees fall through to the str/slice arm below.
let sti: *syntax.tinfo = nil;
if (dotlhs != nil) { sti = tichase(dotlhs.type_: *syntax.tinfo); };
if (sti != nil && sti.kind == syntax.tykind.TY_PTR) { sti = tichase(sti.sub); };
if (sti != nil) { if (sti.kind == syntax.tykind.TY_STRUCT) {
let tf: *syntax.tfield = sti.fields;
for (tf != nil) {
if (syntax.streq(tf.name, fld)) {
emitline("\tMOVQ\t");
emitsymname(c, gnm);
emitline("(SB), BX\n");
cgptrfieldloadtf(c, tf);
return;
};
tf = tf.tnext;
};
}; };
// Pointer to str/slice (`*[]u8`, `*str`): deref
// name(SB), then load at delta within the header.
let delta: i32 = -1;
if (syntax.streq(fld, "ptr")) { delta = 0; };
if (syntax.streq(fld, "len")) { delta = 8; };
if (syntax.streq(fld, "cap")) { delta = 16; };
if (delta >= 0) {
let innerkind: syntax.nkind = syntax.nkind.N_NONE;
if (inner != nil) { innerkind = inner.kind; };
let innerstr: bool = false;
if (innerkind == syntax.nkind.N_TNAME) {
if (syntax.streq(inner.str, "str")) { innerstr = true; };
};
if (innerkind == syntax.nkind.N_TSLICE) { innerstr = true; };
if (innerstr) {
emitline("\tMOVQ\t");
emitsymname(c, gnm);
emitline("(SB), BX\n");
emitline("\tMOVQ\t");
emitdispreg(delta: i64, "BX");
emitline(", 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 == syntax.nkind.N_IDENT) {
let drhs: *syntax.node = deflookuprhs(c, lhs.str);
if (drhs != nil) {
if (drhs.kind == syntax.nkind.N_STRLIT) {
let bytes: str = drhs.str;
if (syntax.streq(fld, "ptr")) {
let lab: str = internstrlit(c, bytes);
emitline("\tLEAQ\t");
emitbytes( lab.ptr, lab.len: u64);
emitline("(SB), AX\n");
return;
};
if (syntax.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 == syntax.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 (syntax.streq(fld, "ptr")) { delta = 0; };
if (syntax.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 (syntax.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 == syntax.nkind.N_IDENT) {
let gtn: *syntax.node = letvartnode(c, lhs.str);
if (gtn != nil) {
if (gtn.kind == syntax.nkind.N_TARRAY) {
if (syntax.streq(fld, "ptr")) {
emitline("\tLEAQ\t");
emitsymname(c, lhs.str);
emitline("(SB), AX\n");
return;
};
if (syntax.streq(fld, "len")) {
let lenn: *syntax.node = gtn.rhs;
let alen: i64 = 0i64;
if (lenn != nil && lenn.kind == syntax.nkind.N_INTLIT) {
alen = lenn.uval: i64;
} else {
// #56: def/const dim on a let-global array —
// resolve from the stamped array tinfo
// (rule-13), twin of the local arm above.
let abt: *syntax.tinfo = tichase(gtn.type_: *syntax.tinfo);
if (abt != nil && abt.kind == syntax.tykind.TY_ARRAY) {
alen = abt.alen: 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 == syntax.nkind.N_IDENT) {
if (syntax.streq(fld, "ptr")) {
let dtn: *syntax.node = defvartnode(c, lhs.str);
if (dtn != nil) {
if (dtn.kind == syntax.nkind.N_TARRAY) {
emitline("\tLEAQ\t");
emitsymname(c, lhs.str);
emitline("(SB), AX\n");
return;
};
};
};
if (syntax.streq(fld, "len")) {
let dtn: *syntax.node = defvartnode(c, lhs.str);
if (dtn != nil) {
if (dtn.kind == syntax.nkind.N_TARRAY) {
let lenn: *syntax.node = dtn.rhs;
let alen: i64 = 0i64;
if (lenn != nil && lenn.kind == syntax.nkind.N_INTLIT) {
alen = lenn.uval: i64;
} else {
// #56: def/const dim on a def array —
// resolve from the stamped array tinfo
// (rule-13), twin of the let-global arm above.
let abt: *syntax.tinfo = tichase(dtn.type_: *syntax.tinfo);
if (abt != nil && abt.kind == syntax.tykind.TY_ARRAY) {
alen = abt.alen: 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 == syntax.nkind.N_IDENT) {
let gtt: *syntax.node = letvartnode(c, lhs.str);
for (gtt != nil && gtt.kind == syntax.nkind.N_TNAME) {
gtt = aliaslookup(c, gtt.str);
};
if (gtt != nil) {
if (gtt.kind == syntax.nkind.N_TTUPLE) {
let gidx: i32 = fldnumidx(fld);
if (gidx >= 0) {
let gtp: *syntax.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: *syntax.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: *syntax.tinfo = gpt.type_: *syntax.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 == syntax.nkind.N_IDENT) {
// #31: resolve the global value-struct field OFFSET + type
// off the checker-STAMPED ident tinfo (tichase(lhs.type_)),
// NOT name-keyed letvarstructinfo/defvarstructinfo. A global
// decl is always qualified (let g: m1.pair) -> non-reddenable;
// converted for close-by-construction (byte-id). Covers both
// let- and def-struct globals (both stamp lhs.type_). Mirrors
// cstage type-keyed N_DOT global arm + the #21 R0 template.
let sti: *syntax.tinfo = tichase(lhs.type_: *syntax.tinfo);
if (sti != nil) { if (sti.kind == syntax.tykind.TY_STRUCT) {
let tf: *syntax.tfield = sti.fields;
for (tf != nil) {
if (syntax.streq(tf.name, fld)) {
let foff: i32 = tf.offset: i32;
let ftraw: *syntax.tinfo = tf.type_;
emitline("\tLEAQ\t");
emitsymname(c, lhs.str);
emitline("(SB), CX\n");
if (syntax.typeistagged(ftraw)) {
let ftc: *syntax.tinfo = tichase(ftraw);
let tsz: i32 = 0;
if (ftc != nil) { tsz = ftc.size: i32; };
cgloadtaggedfield(c, "CX", foff, tsz, true);
return;
};
if (syntax.typeisstr(ftraw)) {
emitline("\tMOVQ\t");
emitdispreg(foff: i64, "CX");
emitline(", AX\n");
emitline("\tMOVQ\t");
emitdispreg((foff + 8): i64, "CX");
emitline(", BX\n");
emitline("\tMOVQ\t");
emitdispreg((foff + 16): i64, "CX");
emitline(", CX\n");
} else { if (syntax.typeisfloat(ftraw)) {
let mov: str = "MOVSD";
if (syntax.typeisf32(ftraw)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\t");
emitdispreg(foff: i64, "CX");
emitline(", X0\n");
} else {
let ftc: *syntax.tinfo = tichase(ftraw);
let fsz: i32 = 0;
if (ftc != nil) { fsz = ftc.size: i32; };
let op: str = loadopsz(syntax.typeissigned(ftraw), fsz);
emitline("\t");
emitline(op);
emitline("\t");
emitdispreg(foff: i64, "CX");
emitline(", AX\n");
}; };
return;
};
tf = tf.tnext;
};
}; };
};
};
// `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 == syntax.nkind.N_INDEX) {
let idxbase: *syntax.node = lhs.lhs;
if (idxbase != nil) { if (idxbase.kind == syntax.nkind.N_IDENT) {
let elemt: *syntax.tinfo = lhs.type_: *syntax.tinfo;
let elemu: *syntax.tinfo = elemt;
elemu = tichase(elemu);
let st: *syntax.tinfo = nil;
let viaptr: bool = false;
if (elemu != nil) {
if (elemu.kind == syntax.tykind.TY_PTR) {
let pin: *syntax.tinfo = elemu.sub;
pin = tichase(pin);
if (pin != nil) { if (pin.kind == syntax.tykind.TY_STRUCT) {
st = pin;
viaptr = true;
};};
} else { if (elemu.kind == syntax.tykind.TY_STRUCT) {
st = elemu;
};};
};
if (st != nil) {
let fnd: *syntax.tfield = nil;
let fwalk: *syntax.tfield = st.fields;
for (fwalk != nil) {
if (syntax.streq(fwalk.name, fld)) { fnd = fwalk; break; };
fwalk = fwalk.tnext;
};
let bu: *syntax.tinfo = idxbase.type_: *syntax.tinfo;
bu = tichase(bu);
let baseisarray: bool = false;
let baseok: bool = false;
if (bu != nil) {
if (bu.kind == syntax.tykind.TY_ARRAY) { baseisarray = true; baseok = true; };
if (bu.kind == syntax.tykind.TY_SLICE) { baseok = true; };
if (bu.kind == syntax.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: *syntax.node = defvartnode(c, idxbase.str);
if (dtn != nil) {
if (dtn.kind == syntax.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: *syntax.tinfo = fnd.type_;
let fu: *syntax.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 == syntax.tykind.TY_ARRAY) {
if (foff != 0) {
emitline("\tADDQ\t$");
emitint(foff);
emitline(", AX\n");
};
return;
};
if (fu != nil && (fu.kind == syntax.tykind.TY_STR
|| fu.kind == syntax.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 (syntax.typeisfloat(ft)) {
let mov: str = "MOVSD";
if (syntax.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 == syntax.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(syntax.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 == syntax.nkind.N_INDEX) {
let eu: *syntax.tinfo = tichase(lhs.type_: *syntax.tinfo);
if (eu != nil) { if (eu.kind == syntax.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: *syntax.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: *syntax.tinfo = tp.type_;
let fu: *syntax.tinfo = tichase(ft);
if (fu != nil && (fu.kind == syntax.tykind.TY_TAGGED
|| fu.kind == syntax.tykind.TY_STRUCT
|| fu.kind == syntax.tykind.TY_TUPLE
|| fu.kind == syntax.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 (syntax.typeisfloat(ft)) {
let mov: str = "MOVSD";
if (syntax.typeisf32(ft)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\t");
emitdispreg(foff: i64, "AX");
emitline(", X0\n");
return;
};
if (fu != nil && (fu.kind == syntax.tykind.TY_STR
|| fu.kind == syntax.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(syntax.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 == syntax.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: *syntax.node = fnretlookupmod(c, fld, usehint(c, lhs.str));
if (frt != nil) {
emitline("\tLEAQ\t");
emitfnname(c, fld, usehint(c, 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: *syntax.node = deflookuprhsmod(c, fld, usehint(c, lhs.str));
if (drhs != nil) {
if (drhs.kind == syntax.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 (syntax.streq(mqop, "MOVQ")) {
emitline("\tMOVQ\t");
emitfnname(c, fld, usehint(c, lhs.str));
emitline("(SB), AX\n");
} else {
emitline("\tLEAQ\t");
emitfnname(c, fld, usehint(c, 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 == syntax.nkind.N_DOT) {
let rootname: str = "";
let rootoff: i32 = 0;
let totaloff: i32 = 0;
let leaftype: *syntax.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) {
emitchainbase(c, ptrroot, isglobal,
rootoff, rootname);
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 (syntax.typeistagged(leaftype)) {
let tlu: *syntax.tinfo = leaftype;
tlu = tichase(tlu);
let ttsz: i32 = tlu.size: i32;
if (viacx) {
emitchainbase(c, ptrroot, isglobal,
rootoff, rootname);
cgloadtaggedfield(c, "CX", totaloff, ttsz, true);
} else {
cgloadtaggedfield(c, "BP",
rootoff + totaloff, ttsz, true);
};
return;
};
if (syntax.typeisstr(leaftype) || syntax.typeisslice(leaftype)) {
// str/slice leaf: load all three header words into
// (AX=ptr, BX=len, CX=cap). str IS []u8 — the same 24B
// {ptr,len,cap} header. #29: the str leaf used to load
// only ptr+len here (cap dropped → a junk strlit before
// the chain left CX stale); merged into the slice arm so
// both load the full triple, both stages (#263). 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 do not alias so order is
// free.
if (viacx) {
emitchainbase(c, ptrroot, isglobal,
rootoff, rootname);
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 (syntax.typeisfloat(leaftype)) {
let mov: str = "MOVSD";
if (syntax.typeisf32(leaftype)) { mov = "MOVSS"; };
if (viacx) {
emitchainbase(c, ptrroot, isglobal,
rootoff, rootname);
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(syntax.typeissigned(leaftype),
leaftype.slotsize: i32);
if (viacx) {
emitchainbase(c, ptrroot, isglobal,
rootoff, rootname);
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: *syntax.tinfo = nil;
if (lhs != nil) { pbu = lhs.type_: *syntax.tinfo; };
pbu = tichase(pbu);
let pbok: bool = false;
if (pbu != nil) {
if (pbu.kind == syntax.tykind.TY_SLICE
|| pbu.kind == syntax.tykind.TY_STR
|| pbu.kind == syntax.tykind.TY_UNTYPED_STR) { pbok = true; };
};
if (lhs != nil) {
if (lhs.kind == syntax.nkind.N_STRLIT) { pbok = true; };
};
if (pbok) {
if (syntax.streq(fld, "ptr")) { cgexpr(c, lhs); return; };
if (syntax.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 (syntax.streq(fld, "cap")) {
cgexpr(c, lhs);
if (lhs != nil && lhs.kind != syntax.nkind.N_STRLIT) {
if (pbu != nil && (pbu.kind == syntax.tykind.TY_SLICE
|| pbu.kind == syntax.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 == syntax.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: *syntax.node = lhs;
let allptr: bool = true;
for (croot != nil && croot.kind == syntax.nkind.N_DOT) {
let ct: *syntax.tinfo = croot.type_: *syntax.tinfo;
ct = tichase(ct);
let okp: bool = false;
if (ct != nil) { if (ct.kind == syntax.tykind.TY_PTR) {
let cs: *syntax.tinfo = ct.sub;
cs = tichase(cs);
if (cs != nil) { if (cs.kind == syntax.tykind.TY_STRUCT) { okp = true; }; };
}; };
if (!okp) { allptr = false; };
croot = croot.lhs;
};
let it: *syntax.tinfo = nil;
if (allptr && croot != nil && croot.kind == syntax.nkind.N_IDENT &&
localfindnode(c, croot.str) != nil) {
it = lhs.type_: *syntax.tinfo;
};
it = tichase(it);
if (it != nil) { if (it.kind == syntax.tykind.TY_PTR) {
let st: *syntax.tinfo = it.sub;
st = tichase(st);
if (st != nil) { if (st.kind == syntax.tykind.TY_STRUCT) {
let tf: *syntax.tfield = st.fields;
for (tf != nil) {
if (syntax.streq(tf.name, fld)) {
let ft: *syntax.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 (syntax.typeistagged(ft)) {
let plu: *syntax.tinfo = ft;
plu = tichase(plu);
emitline("\tMOVQ\tAX, BX\n");
cgloadtaggedfield(c, "BX",
tf.offset: i32,
plu.size: i32, true);
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 (syntax.typeisstr(ft) || syntax.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 (syntax.typeisfloat(ft)) {
let mov: str = "MOVSD";
if (syntax.typeisf32(ft)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\t");
emitdispreg(tf.offset: i64, "AX");
emitline(", X0\n");
return;
};
let lop: str = loadopsz(syntax.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 == syntax.nkind.N_DOT) {
let inner: *syntax.node = lhs.lhs;
let innerfld: str = lhs.str;
if (inner != nil) { if (inner.kind == syntax.nkind.N_IDENT) {
let lc: *local = localfindnode(c, inner.str);
if (lc != nil) { if (lc.tnode != nil) {
let tn: *syntax.node = lc.tnode;
let lkind: syntax.nkind = tn.kind;
let outname: str;
outname.ptr = nil; outname.len = 0;
let isptr: bool = false;
if (lkind == syntax.nkind.N_TNAME) { outname = tn.str; };
if (lkind == syntax.nkind.N_TPTR) {
let pe: *syntax.node = tn.lhs;
if (pe != nil) { if (pe.kind == syntax.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 (syntax.streq(ofi.fname, innerfld)) {
let oft: *syntax.node = ofi.tnode;
if (oft != nil) { if (oft.kind == syntax.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 (syntax.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 == syntax.nkind.N_DOT) {
let sbt: *syntax.tinfo = lhs.type_: *syntax.tinfo;
if (sbt == nil || sbt.kind == syntax.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 == syntax.nkind.N_DOT) {
let pgu: *syntax.tinfo = lhs.type_: *syntax.tinfo;
pgu = tichase(pgu);
if (pgu != nil) {
if (pgu.kind == syntax.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: *syntax.tinfo = n.type_: *syntax.tinfo;
let rdu: *syntax.tinfo = rdt;
rdu = tichase(rdu);
if (rdu != nil) {
if (rdu.kind == syntax.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 == syntax.tykind.TY_STRUCT
|| rdu.kind == syntax.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 (syntax.typeisfloat(rdt)) {
let mov: str = "MOVSD";
if (syntax.typeisf32(rdt)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\t(BX), X0\n");
return;
};
if (rdu != nil && rdu.kind == syntax.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 == syntax.tykind.TY_STR
|| rdu.kind == syntax.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(syntax.typeissigned(rdt), rdt.slotsize: i32);
};
emitline("\t");
emitline(lop);
emitline("\t(BX), AX\n");
return;
};
};
fn cgun(c: *cgen, n: *syntax.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: *syntax.tinfo = n.lhs.type_: *syntax.tinfo;
if (syntax.typeisf32(lt)) { fk = 1; }
else { if (syntax.typeisfloat(lt)) { fk = 2; }; };
};
if (n.op == syntax.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 == syntax.tkind.TK_AMP) {
let opnd: *syntax.node = n.lhs;
if (opnd != nil) {
if (opnd.kind == syntax.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 == syntax.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 == syntax.nkind.N_DOT) {
let rootname: str = "";
let rootoff: i32 = 0;
let totaloff: i32 = 0;
let leaftype: *syntax.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 == syntax.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: *syntax.node = lc.tnode;
let lkind: syntax.nkind = syntax.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 == syntax.nkind.N_TPTR) {
// #31: &p.f offset off the stamped *struct tinfo
// (tichase(opnd.lhs.type_)->.sub), NOT structlookupchain(inner).
// Twin of the R1 read arm (inferred-local ADDR row). Non-struct
// pointees fall through to the slice/str pseudo arm below.
let sti: *syntax.tinfo = nil;
if (opnd.lhs != nil) { sti = tichase(opnd.lhs.type_: *syntax.tinfo); };
if (sti != nil && sti.kind == syntax.tykind.TY_PTR) { sti = tichase(sti.sub); };
if (sti != nil) { if (sti.kind == syntax.tykind.TY_STRUCT) {
let tf: *syntax.tfield = sti.fields;
for (tf != nil) {
if (syntax.streq(tf.name, fld)) {
let foff: i32 = tf.offset: i32;
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), AX\n");
emitline("\tLEAQ\t");
emitdispreg(foff: i64, "AX");
emitline(", AX\n");
return;
};
tf = tf.tnext;
};
}; };
};
// 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 == syntax.nkind.N_TNAME) {
// #31: &o.f offset off the stamped value-struct tinfo
// (tichase(opnd.lhs.type_)), NOT structlookupchain(tn). Non-
// struct (str alias) falls through to the slice/str arm below.
let sti: *syntax.tinfo = nil;
if (opnd.lhs != nil) { sti = tichase(opnd.lhs.type_: *syntax.tinfo); };
if (sti != nil) { if (sti.kind == syntax.tykind.TY_STRUCT) {
let tf: *syntax.tfield = sti.fields;
for (tf != nil) {
if (syntax.streq(tf.name, fld)) {
let foff: i32 = tf.offset: i32;
emitline("\tLEAQ\t");
emitoff((lc.off + foff): i64);
emitline("(BP), AX\n");
return;
};
tf = tf.tnext;
};
}; };
};
// 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 (syntax.streq(fld, "ptr")) { delta = 0; };
if (syntax.streq(fld, "len")) { delta = 8; };
if (syntax.streq(fld, "cap")) { delta = 16; };
if (delta >= 0) {
let isslor: bool = false;
if (lkind == syntax.nkind.N_TSLICE) { isslor = true; };
if (lkind == syntax.nkind.N_TNAME) {
if (syntax.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)) {
// #31: global &g.f offset off the stamped value-struct tinfo
// (tichase(opnd.lhs.type_)), NOT name-keyed letvarstructinfo.
// Global decls are qualified -> non-reddenable; converted for
// close-by-construction (byte-id).
let gsti: *syntax.tinfo = nil;
if (opnd.lhs != nil) { gsti = tichase(opnd.lhs.type_: *syntax.tinfo); };
if (gsti != nil) { if (gsti.kind == syntax.tykind.TY_STRUCT) {
let tf: *syntax.tfield = gsti.fields;
for (tf != nil) {
if (syntax.streq(tf.name, fld)) {
let foff: i32 = tf.offset: i32;
emitline("\tLEAQ\t");
emitsymname(c, basenm);
emitline("(SB), CX\n");
emitline("\tLEAQ\t");
emitdispreg(foff: i64, "CX");
emitline(", AX\n");
return;
};
tf = tf.tnext;
};
}; };
let isstr: bool = letvarisstr(c, basenm);
let issl: bool = letvarisslice(c, basenm);
if (isstr || issl) {
let gdelta: i32 = -1;
if (syntax.streq(fld, "ptr")) { gdelta = 0; };
if (syntax.streq(fld, "len")) { gdelta = 8; };
// str IS []u8: &str.cap is valid too, not slice-only
// — mirrors cstage (#1/Phase 3, #11).
if (syntax.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 == syntax.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: *syntax.node = fnretlookupmod(c, fld, usehint(c, basenm));
if (frt != nil) {
emitline("\tLEAQ\t");
emitfnname(c, fld, usehint(c, 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");
emitfnname(c, fld, usehint(c, 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 == syntax.nkind.N_INDEX) {
// &base[i] = base + i*esz, no dereference.
let base: *syntax.node = opnd.lhs;
let idx: *syntax.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 == syntax.nkind.N_IDENT) {
baselocal = localfindnode(c, base.str);
if (baselocal != nil) {
esz = elemsizeofc(c, baselocal.tnode);
let tn: *syntax.node = baselocal.tnode;
if (tn != nil) {
if (tn.kind == syntax.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: *syntax.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 == syntax.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: *syntax.tinfo = base.type_: *syntax.tinfo;
let basealias82: bool = false;
if (bt82 != nil) {
if (bt82.kind == syntax.tykind.TY_NAMED) { basealias82 = true; };
};
if (basealias82) {
let bu82: *syntax.tinfo = tichase(bt82);
if (bu82 != nil) {
if (baselocal != nil) {
isarr = bu82.kind == syntax.tykind.TY_ARRAY;
};
if (isglobalarr || isglobalptr) {
isglobalarr = bu82.kind == syntax.tykind.TY_ARRAY;
isglobalptr = !isglobalarr;
};
};
};
} else { if (base.kind == syntax.nkind.N_DOT
|| (base.kind == syntax.nkind.N_UN
&& base.op == syntax.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: *syntax.tinfo = opnd.type_: *syntax.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 == syntax.tkind.TK_MINUS) { emitline("\tNEGQ\tAX\n"); return; };
if (n.op == syntax.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 == syntax.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: *syntax.tinfo = n.type_: *syntax.tinfo;
rti = tichase(rti);
if (rti != nil && rti.kind == syntax.tykind.TY_FN) { return; };
if (rti != nil && rti.kind == syntax.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 == syntax.tykind.TY_TAGGED) {
if (rti.nullable == 0 && rti.size: i32 > 8) { return; };
};
// C1b: a whole str/slice loaded BY VALUE through *str /
// *[]T — AX (the operand value) IS the 24B {ptr,len,cap}
// header address. The scalar load below pulled ONLY word0
// (.ptr); .len/.cap were then stored from stale BX/CX, so
// len(*p) read garbage — byte-id-blind on both stages. Reuse
// the same 3-word header load as the slice-field / cgindex
// str-element arms.
if (rti != nil) {
if (rti.kind == syntax.tykind.TY_STR || rti.kind == syntax.tykind.TY_SLICE) {
cgslicehdr(c, "AX");
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 == syntax.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;
};
// cgstreqpush — push one str operand's (len, then ptr) header words for
// the rt_streq content-compare in cgbin. Mirrors cstage cgen.c:4568-4615:
// an ident loads its 2-word header (ptr+len, NOT cap) from name(SB) for a
// module-global str (#154 let_islet branch) or BP+off for a local; a
// non-ident evals via cgexpr (AX=ptr, BX=len) and pushes BX then AX. Push
// order is len then ptr so the matching POPQ pops ptr first.
fn cgstreqpush(c: *cgen, op: *syntax.node) void = {
if (op.kind == syntax.nkind.N_IDENT) {
let nm: str = op.str;
let lc: *local = localfindnode(c, nm);
if (lc == nil && isletvar(c, nm)) {
emitline("\tLEAQ\t");
emitfnname(c, nm, c.curmod);
emitline("(SB), BX\n");
emitline("\tMOVQ\t8(BX), AX\n");
emitline("\tPUSHQ\tAX\n");
emitline("\tMOVQ\t(BX), AX\n");
emitline("\tPUSHQ\tAX\n");
return;
};
if (lc == nil && deflookup(c, nm)) {
// A str def has no name(SB) header. Its ordinary expression
// load materialises the literal as AX=ptr, BX=len.
cgexpr(c, op);
emitline("\tPUSHQ\tBX\n");
emitline("\tPUSHQ\tAX\n");
return;
};
let off: i32 = 0;
if (lc != nil) { off = lc.off; };
emitline("\tMOVQ\t");
emitoff((off + 8): i64);
emitline("(BP), AX\n");
emitline("\tPUSHQ\tAX\n");
emitline("\tMOVQ\t");
emitoff(off: i64);
emitline("(BP), AX\n");
emitline("\tPUSHQ\tAX\n");
return;
};
cgexpr(c, op);
emitline("\tPUSHQ\tBX\n");
emitline("\tPUSHQ\tAX\n");
};
fn cgbin(c: *cgen, n: *syntax.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 == syntax.tkind.TK_AND || n.op == syntax.tkind.TK_OR) {
let prefix: str = "andend";
let jshrt: str = "JE";
if (n.op == syntax.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;
};
// #146 (#154 ww-twin): str ==/!= is a CONTENT compare via rt_streq,
// not a ptr compare. Must run before the generic eager-eval tail
// below collapses each str header to its ptr word (AX). Push rhs
// then lhs (len, ptr each); POPQ DI/SI/DX/CX lands a.ptr,a.len,
// b.ptr,b.len per rt/streq.s; CALL rt_streq -> AX in {0,1}; XOR 1
// for !=. The gate reads the checker stamp (typeisstr) exactly like
// cstage node_isstr. Byte-identical to cstage cbinop (cmd/w6c/
// cgen.c:4564-4623). cstage was fixed by #154; this is its mirror.
if ((n.op == syntax.tkind.TK_EQ || n.op == syntax.tkind.TK_NEQ)
&& n.lhs != nil && n.rhs != nil
&& syntax.typeisstr(n.lhs.type_: *syntax.tinfo)
&& syntax.typeisstr(n.rhs.type_: *syntax.tinfo)) {
cgstreqpush(c, n.rhs);
cgstreqpush(c, n.lhs);
emitline("\tPOPQ\tDI\n\tPOPQ\tSI\n\tPOPQ\tDX\n\tPOPQ\tCX\n");
emitline("\tCALL\trt_streq(SB)\n");
if (n.op == syntax.tkind.TK_NEQ) { emitline("\tXORQ\t$1, AX\n"); };
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: *syntax.tinfo = n.lhs.type_: *syntax.tinfo;
if (syntax.typeisf32(llt)) { lfk = 1; }
else { if (syntax.typeisfloat(llt)) { lfk = 2; }; };
};
let rfk: i32 = 0;
if (n.rhs != nil) {
let rrt: *syntax.tinfo = n.rhs.type_: *syntax.tinfo;
if (syntax.typeisf32(rrt)) { rfk = 1; }
else { if (syntax.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 == syntax.tkind.TK_PLUS ||
n.op == syntax.tkind.TK_MINUS ||
n.op == syntax.tkind.TK_STAR ||
n.op == syntax.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 == syntax.tkind.TK_MINUS) { op = "SUBSD"; };
if (n.op == syntax.tkind.TK_STAR) { op = "MULSD"; };
if (n.op == syntax.tkind.TK_SLASH) { op = "DIVSD"; };
if (fk == 1) {
if (n.op == syntax.tkind.TK_PLUS) { op = "ADDSS"; };
if (n.op == syntax.tkind.TK_MINUS) { op = "SUBSS"; };
if (n.op == syntax.tkind.TK_STAR) { op = "MULSS"; };
if (n.op == syntax.tkind.TK_SLASH) { op = "DIVSS"; };
};
emitline("\t"); emitline(op); emitline("\tX1, X0\n");
return;
};
let isfcmp: bool = false;
if (n.op == syntax.tkind.TK_EQ) { isfcmp = true; };
if (n.op == syntax.tkind.TK_NEQ) { isfcmp = true; };
if (n.op == syntax.tkind.TK_LT) { isfcmp = true; };
if (n.op == syntax.tkind.TK_LE) { isfcmp = true; };
if (n.op == syntax.tkind.TK_GT) { isfcmp = true; };
if (n.op == syntax.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 == syntax.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 == syntax.tkind.TK_EQ || n.op == syntax.tkind.TK_LT ||
n.op == syntax.tkind.TK_LE) {
// unordered -> false; otherwise the ordered Jcc decides.
let jcc: str = "JE";
if (n.op == syntax.tkind.TK_LT) { jcc = "JB"; };
if (n.op == syntax.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 == syntax.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 == syntax.tkind.TK_PLUS) { emitline("\tADDQ\tBX, AX\n"); return; };
if (n.op == syntax.tkind.TK_MINUS) { emitline("\tSUBQ\tBX, AX\n"); return; };
if (n.op == syntax.tkind.TK_STAR) { emitline("\tIMULQ\tBX, AX\n"); return; };
if (n.op == syntax.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 == syntax.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 == syntax.tkind.TK_AMP) { emitline("\tANDQ\tBX, AX\n"); return; };
if (n.op == syntax.tkind.TK_PIPE) { emitline("\tORQ\tBX, AX\n"); return; };
if (n.op == syntax.tkind.TK_CARET) { emitline("\tXORQ\tBX, AX\n"); return; };
if (n.op == syntax.tkind.TK_LSHIFT) {
emitline("\tMOVQ\tBX, CX\n");
emitline("\tSHLQ\tCX, AX\n");
return;
};
if (n.op == syntax.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 == syntax.tkind.TK_EQ) { iscmp = true; jcc = "JE"; };
if (n.op == syntax.tkind.TK_NEQ) { iscmp = true; jcc = "JNE"; };
if (n.op == syntax.tkind.TK_LT) { iscmp = true; if (unsignd) { jcc = "JB"; } else { jcc = "JL"; }; };
if (n.op == syntax.tkind.TK_LE) { iscmp = true; if (unsignd) { jcc = "JBE"; } else { jcc = "JLE"; }; };
if (n.op == syntax.tkind.TK_GT) { iscmp = true; if (unsignd) { jcc = "JA"; } else { jcc = "JG"; }; };
if (n.op == syntax.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: *syntax.node) void = {
let v: *syntax.node = n.list;
let sz: i32 = 8;
let si: *structinfo = nil;
if (v.kind == syntax.nkind.N_STRUCTLIT) {
// #26: chase the alias chain on the literal's type ref so an
// alias head (`type pt = point; alloc(pt{...})`) resolves to
// the underlying struct's layout — name-keyed structlookup on
// the syntactic head missed it (under-alloc $8 + zero field
// stores). Mirrors cstage cgen.c:8223-8240 (type_default chases
// named before sizing/walking fields); same alias-chase helper
// the #22 sites use.
si = structlookupchain(c, v.lhs);
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 == syntax.nkind.N_STRUCTLIT) {
if (si != nil) {
// C7c: route the heap field-fill through the shared
// structlit helper in mode 3 (DST_PTR_SP) — base reloaded
// from the just-pushed heap ptr at (SP). The prior inline
// loop had only float/str/scalar arms, so a nested
// struct/array/tuple field VALUE (an inner N_STRUCTLIT /
// N_ARRLIT) fell to the scalar tail and stored AX=0 over
// the whole inner slot, dropping its leaves. The helper
// recurses to arbitrary depth and reuses the
// tagged/call/str/slice/array/agg arms, closing the class
// symmetrically with cstage's DST_PTR_SP arm. Byte-id to
// the old inline loop for the float/str/scalar fields the
// corpus actually allocs (same (SP) reload, disp=0).
cgstructlitfill(c, si, v, 3, 0, "", 0);
};
} else {
// #57 (retained cs!=ww, deferred): scalar/ptr alloc keeps the
// default-8 size + MOVQ store; cstage sizes the scalar exactly
// ($1/MOVB for u8) and the latent alloc(str|slice) wants 24B not 8.
// Byte-id-visible, runtime-benign; not folded into the #26 arm.
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: *syntax.node) void = {
let sn: *syntax.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 == syntax.nkind.N_IDENT) {
snlocal = localfindnode(c, sn.str);
if (snlocal != nil) {
sndirect = true;
sn_off = snlocal.off;
};
};
let esz: i32 = 0;
let etnode: *syntax.node = nil;
let sti: *syntax.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: syntax.nkind = snlocal.tnode.kind;
if (stk == syntax.nkind.N_TSLICE) { etnode = snlocal.tnode.lhs; };
if (stk == syntax.nkind.N_TARRAY) { etnode = snlocal.tnode.lhs; };
if (stk == syntax.nkind.N_TPTR) { etnode = snlocal.tnode.lhs; };
sti = snlocal.tnode.type_: *syntax.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_: *syntax.tinfo;
let fsti: *syntax.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: *syntax.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: *syntax.tinfo = esubti;
esubti = tichase(esubti);
let elstr: bool = esubti != nil && esubti.kind == syntax.tykind.TY_STR;
let elslice: bool = esubti != nil && esubti.kind == syntax.tykind.TY_SLICE;
let eltagged: bool = esubti != nil && esubti.kind == syntax.tykind.TY_TAGGED;
let elstruct: bool = esubti != nil && esubti.kind == syntax.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: *syntax.node = sn.next;
for (vn != nil) {
if (vn.kind == syntax.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: *syntax.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: *syntax.tinfo = nil;
if (it != nil) { itu = it.type_: *syntax.tinfo; };
itu = tichase(itu);
if (itu == nil || (itu.kind != syntax.tykind.TY_SLICE
&& itu.kind != syntax.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 == syntax.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(syntax.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: *syntax.node = nil;
let aidx: *syntax.node = nil;
if (elstruct && vn.kind != syntax.nkind.N_STRUCTLIT && vn.kind != syntax.nkind.N_IDENT) {
let ch: *syntax.node = vn;
let aok: bool = true;
for (aok && ch.kind == syntax.nkind.N_DOT) {
let ab: *syntax.node = ch.lhs;
let af: *syntax.tfield = nil;
if (ab != nil) {
let abu: *syntax.tinfo = ab.type_: *syntax.tinfo;
abu = tichase(abu);
if (abu != nil && abu.kind == syntax.tykind.TY_STRUCT) {
let fl: *syntax.tfield = abu.fields;
for (fl != nil) {
if (syntax.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 == syntax.nkind.N_INDEX) {
let ab: *syntax.node = ch.lhs;
let aet: *syntax.tinfo = ch.type_: *syntax.tinfo;
aet = tichase(aet);
let abu: *syntax.tinfo = nil;
if (ab != nil) {
abu = ab.type_: *syntax.tinfo;
abu = tichase(abu);
};
if (ab == nil || abu == nil || aet == nil
|| (abu.kind != syntax.tykind.TY_SLICE && abu.kind != syntax.tykind.TY_ARRAY)) {
aok = false;
} else {
abaseslice = abu.kind == syntax.tykind.TY_SLICE;
aidxesz = aet.size: i32;
aidx = ch.rhs;
ch = ab;
};
};
if (aok) {
if (ch.kind == syntax.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: *syntax.node = defvartnode(c, ch.str);
if (dtn != nil) {
if (dtn.kind == syntax.nkind.N_TARRAY) { gok = true; };
};
};
if (!gok) { aok = false; };
};
} else {
if (ch.kind != syntax.nkind.N_UN || ch.op != syntax.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 == syntax.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 == syntax.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 == syntax.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 == syntax.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 == syntax.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: *syntax.node) void = {
let d: *syntax.node = n.list; // N_INDEX, checker-validated
let base: *syntax.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: *syntax.tinfo = base.type_: *syntax.tinfo;
sti = tichase(sti);
let esub: *syntax.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 == syntax.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 == syntax.nkind.N_UN) {
if (base.op == syntax.tkind.TK_STAR && base.lhs != nil) {
if (base.lhs.kind == syntax.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: *syntax.node) void = {
let d: *syntax.node = n.list; // N_SLICE, checker-validated
let base: *syntax.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: *syntax.tinfo = base.type_: *syntax.tinfo;
sti = tichase(sti);
let esub: *syntax.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 == syntax.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 == syntax.nkind.N_UN) {
if (base.op == syntax.tkind.TK_STAR && base.lhs != nil) {
if (base.lhs.kind == syntax.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 == syntax.nkind.N_INDEX) {
if (base.lhs != nil) {
if (base.lhs.kind == syntax.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: *syntax.node) void = {
let d: *syntax.node = n.list; // N_INDEX, checker-validated
let base: *syntax.node = d.lhs;
let v: *syntax.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: *syntax.tinfo = base.type_: *syntax.tinfo;
sti = tichase(sti);
let esub: *syntax.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 == syntax.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 == syntax.nkind.N_UN) {
if (base.op == syntax.tkind.TK_STAR && base.lhs != nil) {
if (base.lhs.kind == syntax.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: *syntax.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: *syntax.node = n.lhs;
if (callee != nil) {
if (callee.kind == syntax.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: *syntax.tinfo = callee.type_: *syntax.tinfo;
if (bti != nil && bti.kind == syntax.tykind.TY_ERR) {
if (syntax.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 (syntax.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: *syntax.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 (syntax.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 (syntax.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 (syntax.streq(callee.str, "len")) {
if (n.list != nil) {
let a: *syntax.node = n.list;
let at: *syntax.tinfo = a.type_: *syntax.tinfo;
let u: *syntax.tinfo = at;
u = tichase(u);
let hdrish: bool = false;
if (u != nil) {
if (u.kind == syntax.tykind.TY_SLICE
|| u.kind == syntax.tykind.TY_STR) {
hdrish = true;
};
};
if (hdrish && a.kind == syntax.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");
emitfnname(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 == syntax.nkind.N_DOT
&& a.lhs != nil
&& a.lhs.kind == syntax.nkind.N_IDENT) {
let lc: *local = localfindnode(c, a.lhs.str);
if (lc != nil) {
let tn: *syntax.node = lc.tnode;
for (tn != nil && tn.kind == syntax.nkind.N_TNAME) {
tn = aliaslookup(c, tn.str);
};
if (tn != nil) {
if (tn.kind == syntax.nkind.N_TTUPLE) {
let idx: i32 = fldnumidx(a.str);
if (idx >= 0) {
let tp: *syntax.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: *syntax.node = letvartnode(c, a.lhs.str);
for (gtn != nil && gtn.kind == syntax.nkind.N_TNAME) {
gtn = aliaslookup(c, gtn.str);
};
if (gtn != nil) {
if (gtn.kind == syntax.nkind.N_TTUPLE) {
let gidx: i32 = fldnumidx(a.str);
if (gidx >= 0) {
let gtp: *syntax.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 == syntax.nkind.N_INDEX) {
cgexpr(c, a);
emitline("\tMOVQ\tBX, AX\n");
return;
};
if (u != nil) {
if (u.kind == syntax.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 (syntax.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 (syntax.streq(callee.str, "delete")) {
if (n.list != nil) {
if (n.list.next == nil) {
if (n.list.kind == syntax.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 (syntax.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: *syntax.node = nil;
if (callee != nil) {
if (callee.kind == syntax.nkind.N_IDENT) {
calleeparams = fnparamslookup(c, callee.str);
} else { if (callee.kind == syntax.nkind.N_DOT) {
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee.lhs != nil) {
if (callee.lhs.kind == syntax.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: *syntax.node = callee_variadic_param(c, callee, &nfixed_v);
if (varp != nil) {
let nargs0: i32 = 0;
let aw: *syntax.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: *syntax.node = n.list;
let kk: i32 = 0;
for (kk < nfixed_v) {
aaf = aaf.next;
kk += 1;
};
if (aaf != nil) {
if (aaf.kind == syntax.nkind.N_SPREAD) {
forwarding = true;
};
};
};
if (forwarding) {
let prev: *syntax.node = nil;
let cur2: *syntax.node = n.list;
let kk2: i32 = 0;
for (kk2 < nfixed_v) {
prev = cur2;
cur2 = cur2.next;
kk2 += 1;
};
let inner: *syntax.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: *syntax.node = varp.lhs;
if (varp.lhs != nil
&& varp.lhs.kind == syntax.nkind.N_TSLICE) {
velem = varp.lhs.lhs;
};
let esz: i32 = 8;
if (velem != nil) {
if (velem.kind == syntax.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_: *syntax.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: *syntax.node = n.list;
let kk3: i32 = 0;
for (kk3 < nfixed_v) {
aa2 = aa2.next;
kk3 += 1;
};
let j: i32 = 0;
let prevarg: *syntax.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_: *syntax.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: *syntax.node = syntax.newnode(syntax.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; };
};
};
};
// C-variadic (bare `...`) detection: drives the SysV §3.5.7 AL=
// XMM-count emit (below, before CALL) and the f32→f64 variadic-
// tail promotion in pushargsrev (#14). cvarnfixed is the fixed-
// param count, or -1 when the callee is not C-variadic. Mirror of
// cstage's `cu && cu->kind == TY_FN && cu->variadic` gate.
let cvarnfixed: i32 = -1;
{
let nfx: i32 = 0;
if (calleecvariadic(c, callee, &nfx)) { cvarnfixed = nfx; };
};
// #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, 0, cvarnfixed);
let nargs: i32 = pushargsrev(c, n.list, calleeparams, false, 0, cvarnfixed);
// 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: *syntax.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: *syntax.node = n.list;
let dparam: *syntax.node = calleeparams;
let popped: i32 = 0;
let stackslots: i32 = 0;
let argidx: i32 = 0;
for (a != nil) {
// argidx is this arg's 0-based position; captured before any
// continue so the C-variadic f32-promotion check below tracks
// the push-side argidx for every arg shape (#14).
let curargidx: i32 = argidx;
argidx += 1;
// #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 == syntax.nkind.N_PARAM) {
if (dparam.op != syntax.tkind.TK_ELLIPSIS) {
if (dparam.lhs != nil) {
dmemsz = taggedmemargsize(dparam.lhs.type_: *syntax.tinfo);
};
};
}; };
if (dmemsz == 0) {
dmemsz = taggedmemargsize(a.type_: *syntax.tinfo);
};
if (dmemsz > 0) {
if (dparam != nil) { dparam = dparam.next; };
a = a.next;
continue;
};
// Widen-first pop (#30/#48): a concrete arg widened into a
// tagged-union param was pushed as slotsize/8 GP words (tag +
// payload). Drain those words into the INTEGER arg cursor
// BEFORE the float check below — else a widened f64-source box
// gets misclassified as a float arg (its tag word drained into
// X0, #48), and a float arg FOLLOWING a widened arg reads the
// widened box's leftover payload word (#30). Mirror of cstage's
// precomputed widen[i] branch (cmd/w6c/cgen.c cgcall, popped
// before node_isfloat). argtaggedwidensz is the shared SSoT with
// pushargsrev's push count.
let dwsz: i32 = argtaggedwidensz(c, a, dparam);
if (dwsz >= 16) {
let dwb: i32 = dwsz / 8;
let dwk: i32 = 0;
for (dwk < dwb) {
if (intidx < 6) {
emitline("\tPOPQ\t");
emitline(argregname(intidx));
emitline("\n");
intidx += 1;
} else {
stackslots += 1;
};
popped += 1;
dwk += 1;
};
if (dparam != nil) { dparam = dparam.next; };
a = a.next;
continue;
};
let fk: i32 = 0;
if (a != nil) {
let at: *syntax.tinfo = a.type_: *syntax.tinfo;
if (syntax.typeisf32(at)) { fk = 1; }
else { if (syntax.typeisfloat(at)) { fk = 2; }; };
};
if (fk != 0) {
let mov: str = "MOVSD";
if (fk == 1) { mov = "MOVSS"; };
// #14: a C-variadic-tail f32 was promoted to f64 at push
// (CVTSS2SD + MOVSD), so its slot reloads MOVSD. Mirror
// cstage cmd/w6c/cgen.c promote_f32 pop.
if (cvarnfixed >= 0 && curargidx >= cvarnfixed && fk == 1) {
mov = "MOVSD";
};
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: *syntax.node = nil;
if (a.kind == syntax.nkind.N_TUPLE) { if (dparam != nil) {
if (dparam.kind == syntax.nkind.N_PARAM && dparam.lhs != nil) {
let dtn2: *syntax.node = dparam.lhs;
for (dtn2 != nil && dtn2.kind == syntax.nkind.N_TNAME) {
dtn2 = aliaslookup(c, dtn2.str);
};
if (dtn2 != nil) { if (dtn2.kind == syntax.nkind.N_TTUPLE) { dptt = dtn2; }; };
};
}; };
let tuparg: *syntax.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 == syntax.nkind.N_TUPLE) { tuplit = true; }; };
let p: *syntax.node = tuparg.list;
for (p != nil) {
let et: *syntax.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 == syntax.nkind.N_IDENT) {
let lc: *local = localfindnode(c, a.str);
if (lc != nil) { stfc = structfloatclass(c, lc.tnode); }
else {
// #31 EXCEPTION (align-UP): a module-global float-bearing
// struct arg — lc is nil, so the local-only stfc stayed 0 and
// the drain fell to all-GP (X0 never loaded). Key the float-
// class off the global's declared tnode (letvartnode), the same
// SysV classification the local path uses. cstage keys
// structfloatclass off the operand TYPE, not a local slot.
let gtn: *syntax.node = letvartnode(c, a.str);
if (gtn != nil) { stfc = structfloatclass(c, gtn); };
};
};
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_: *syntax.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 == syntax.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 != syntax.nkind.N_IDENT
&& callee.kind != syntax.nkind.N_DOT) {
isfnptrcall = true;
};
if (callee.kind == syntax.nkind.N_DOT) {
let base: *syntax.node = callee.lhs;
let fld: str = callee.str;
if (base != nil) {
if (base.kind == syntax.nkind.N_IDENT) {
let bn: str = base.str;
let lc: *local = localfindnode(c, bn);
if (lc != nil) {
let tn: *syntax.node = lc.tnode;
if (tn != nil) {
let lkind: syntax.nkind = tn.kind;
let sname: str;
sname.ptr = nil; sname.len = 0;
if (lkind == syntax.nkind.N_TNAME) { sname = tn.str; };
if (lkind == syntax.nkind.N_TPTR) {
let inner: *syntax.node = tn.lhs;
if (inner != nil) {
if (inner.kind == syntax.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 (syntax.streq(fn_, fld)) {
let ft: *syntax.node = fi.tnode;
if (ft != nil) {
if (ft.kind == syntax.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");
};};
};
// SysV §3.5.7: a C-variadic call sets AL to the number of vector
// (XMM) regs used to pass the variable float args — the callee
// gates its xmm-save-area stores on `test %al,%al`, so a wrong AL
// makes va_arg(double) read garbage. fpidx is the XMM cursor
// (capped at 8 in the pop loop). Emitted after any sret-RDI LEAQ,
// right before CALL. Mirror cstage cmd/w6c/cgen.c (the imm→reg MOVQ
// idiom carries AL since MOVL-imm has no w6a encoding; AL = low byte,
// fpidx <= 8). Ref ref/qbe/amd64/sysv.c:384.
if (cvarnfixed >= 0) {
if (fpidx > 0) {
emitline("\tMOVQ\t$");
emitint(fpidx: i64);
emitline(", AX\n");
} else {
emitline("\tXORQ\tAX, AX\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 == syntax.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 == syntax.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 == syntax.nkind.N_IDENT) {
hint = usehint(c, 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: *syntax.node) void = {
let lhs: *syntax.node = n.lhs;
// #16: a single-dot aggregate-field unwrap store whose base is a
// module-GLOBAL value-struct (`g.f = mk()!`) or a CHAINED struct
// field (`o.m.f = mk()!`). The #12 single-dot arm covered only a
// LOCAL / via-ptr base (its enclosing block requires localfindnode
// != nil), so the global case fell to the generic single-word store
// (DROPPED w1/w2) and the chained case never reached any field arm
// (SILENT both-stage, byte-id blind). Route the dest ADDRESS through
// cgplaceaddr (global LEAQ root + chained deref+offset spine) and
// feed the SAME {AX,DX,CX} producer-shift materialise. cgplaceaddr
// clobbers AX/CX, so it runs BEFORE cgexpr(rhs) and the address is
// saved across the call. In-cap struct field only (#12 scope);
// float/over-cap loud-stop at the producer. Local/via-ptr single-dot
// stays on the #12 arm. Mirrors cstage cgen.c N_ASSIGN #16 arm.
if (lhs != nil) { if (lhs.kind == syntax.nkind.N_DOT
&& lhs.lhs != nil && n.op == syntax.tkind.TK_ASSIGN
&& n.rhs != nil
&& (n.rhs.kind == syntax.nkind.N_TRYUNW
|| n.rhs.kind == syntax.nkind.N_TRYPROP)) {
let db: *syntax.node = lhs.lhs;
let chained: bool = db.kind == syntax.nkind.N_DOT;
let globalbase: bool = false;
if (db.kind == syntax.nkind.N_IDENT) {
if (localfindnode(c, db.str) == nil) {
if (isletvar(c, db.str)) {
let dbu: *syntax.tinfo = tichase(db.type_: *syntax.tinfo);
if (dbu != nil) { if (dbu.kind == syntax.tykind.TY_STRUCT) {
globalbase = true;
}; };
};
};
};
if (chained || globalbase) {
let fu: *syntax.tinfo = tichase(lhs.type_: *syntax.tinfo);
if (fu != nil) { if (fu.kind == syntax.tykind.TY_STRUCT) {
let ssz: i32 = fu.size: i32;
if (ssz <= 24) {
// #14: choke-point now stores every in-cap tail; 3/5/6/7 no longer dropped to a lone narrow MOV.
if (!cgplaceaddr(c, lhs, "BX")) {
let m16: str = "#16: global/chained aggregate unwrap field dest unresolved (cgplaceaddr)\n";
os.write(2, m16.ptr, m16.len: u64);
os.exit(1);
};
emitline("\tPUSHQ\tBX\n");
cgexpr(c, n.rhs);
emitline("\tPOPQ\tBX\n");
cgaggregstore(c, "BX", 0, ssz, false);
return;
};
}; };
};
}; };
// #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 == syntax.nkind.N_SLICE
&& n.op == syntax.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 == syntax.nkind.N_DOT && lhs.lhs != nil) {
if (lhs.lhs.kind == syntax.nkind.N_INDEX) {
placedotidx = true;
};
};
if ((lhs.kind == syntax.nkind.N_INDEX
|| (lhs.kind == syntax.nkind.N_UN && lhs.op == syntax.tkind.TK_STAR)
|| placedotidx)
&& n.op == syntax.tkind.TK_ASSIGN && n.rhs != nil) {
if (n.rhs.kind == syntax.nkind.N_STRUCTLIT) {
let iet: *syntax.tinfo = lhs.type_: *syntax.tinfo;
iet = tichase(iet);
if (iet != nil) {
if (iet.kind == syntax.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 == syntax.nkind.N_UN && lhs.op == syntax.tkind.TK_STAR
&& n.op == syntax.tkind.TK_ASSIGN) {
let du: *syntax.tinfo = lhs.type_: *syntax.tinfo;
du = tichase(du);
if (du != nil) {
if (du.kind == syntax.tykind.TY_STRUCT
|| du.kind == syntax.tykind.TY_ARRAY
|| du.kind == syntax.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 == syntax.nkind.N_IDENT) {
if (lhs.str.len == 0) {
if (n.op == syntax.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 == syntax.tkind.TK_ASSIGN && lhs != nil && n.rhs != nil) {
if (n.rhs.kind == syntax.nkind.N_ARRLIT) {
let alt: *syntax.tinfo = lhs.type_: *syntax.tinfo;
alt = tichase(alt);
if (alt != nil) {
if (alt.kind == syntax.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);
};
};
};
};
// A plain aggregate field-to-field assignment is a memory copy, not
// a scalar expression/store. The direct-field arms below enumerate
// CALL, STRUCTLIT and local IDENT producers; an addressable DOT/INDEX/
// deref rhs fell through, so a 16-byte time.instant copied one word.
// Resolve both places through the existing address funnels and use the
// canonical tail-aware aggregate copier. Calls/literals/unwraps and
// tagged/str/slice fields remain on their specialized ABI paths.
if (lhs != nil && n.rhs != nil && n.op == syntax.tkind.TK_ASSIGN) {
if (lhs.kind == syntax.nkind.N_DOT && lhs.lhs != nil
&& (lhs.lhs.kind == syntax.nkind.N_IDENT
|| lhs.lhs.kind == syntax.nkind.N_DOT
|| (lhs.lhs.kind == syntax.nkind.N_UN
&& lhs.lhs.op == syntax.tkind.TK_STAR))) {
let au: *syntax.tinfo = lhs.type_: *syntax.tinfo;
au = tichase(au);
let memrhs: bool = n.rhs.kind == syntax.nkind.N_IDENT
|| n.rhs.kind == syntax.nkind.N_DOT
|| n.rhs.kind == syntax.nkind.N_INDEX
|| (n.rhs.kind == syntax.nkind.N_UN
&& n.rhs.op == syntax.tkind.TK_STAR);
if (au != nil && memrhs) {
if (au.kind == syntax.tykind.TY_STRUCT
|| au.kind == syntax.tykind.TY_ARRAY
|| au.kind == syntax.tykind.TY_TUPLE) {
if (!cgplaceaddr(c, lhs, "BX")) {
let md: str = "aggregate field destination unresolved\n";
os.write(2, md.ptr, md.len: u64);
os.exit(1);
};
emitline("\tPUSHQ\tBX\n");
if (!aggargsrcaddr(c, n.rhs, "SI")) {
let ms: str = "aggregate field source unresolved\n";
os.write(2, ms.ptr, ms.len: u64);
os.exit(1);
};
emitline("\tPOPQ\tBX\n");
aggcopy(c, au.size: i32);
return;
};
};
};
};
// #21: a COMPOUND op on a whole tagged-union IDENT (`g OP= v`
// with g:(int|bool)) is nonsense — the ident load-combine-store
// tail below reads and writes one word of the {payload,tag} box,
// corrupting the tag. Reject loud here, the ident twin of the #18
// deref / #133 index rejects; the byte-id twin of the cstage
// guard. Plain `=` (the tagged-ident reassign arm just below) is
// untouched.
if (lhs != nil) {
if (lhs.kind == syntax.nkind.N_IDENT && n.op != syntax.tkind.TK_ASSIGN) {
let itu: *syntax.tinfo = tichase(lhs.type_: *syntax.tinfo);
if (itu != nil) {
if (itu.kind == syntax.tykind.TY_TAGGED) {
let m21: str = "ident compound on tagged not wired (#21/rule-7)\n";
os.write(2, m21.ptr, m21.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 == syntax.nkind.N_IDENT) {
if (n.op == syntax.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 == syntax.nkind.N_CALL) {
asret = callsretsize(c, n.rhs);
};
};
if (asret > 0) {
let aru: *syntax.tinfo = n.rhs.type_: *syntax.tinfo;
aru = tichase(aru);
let alu: *syntax.tinfo = lc.tnode.type_: *syntax.tinfo;
alu = tichase(alu);
let aexact: bool = false;
if (aru != nil && aru == alu) { aexact = true; }
else {
if (syntax.typeeq(n.rhs.type_: *syntax.tinfo,
lc.tnode.type_: *syntax.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_: *syntax.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 == syntax.nkind.N_CALL) {
let gru: *syntax.tinfo = lhs.type_: *syntax.tinfo;
gru = tichase(gru);
if (gru != nil && gru.kind == syntax.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);
};
};
};
// #32 (#263 ww-runtime-correct): tagged-union GLOBAL reassign
// `g = expr`. No BP slot — LEAQ g(SB),BX then the shared widener
// stores tag+payload off BX (mirror the local arm above + the
// global-struct-field tagged arm at :10220). Pre-fix the generic
// scalar store below clobbered the tag word. cstage drops the
// store entirely — cs!=ww residual until the cstage half (#41).
if (lc == nil) {
let gtn: *syntax.node = letvartnode(c, lhs.str);
if (gtn != nil) {
if (istaggedtype(c, gtn)) {
let gsz: i32 = slotsize(c, gtn);
emitline("\tLEAQ\t");
emitsymname(c, lhs.str);
emitline("(SB), BX\n");
cgwidentaggedstore(c, gtn.type_: *syntax.tinfo, n.rhs, "BX", 0, gsz);
return;
};
};
};
};
};
};
// `*p = v` — deref-assign. Element width comes from the
// pointer's declared type. Mirrors C cgen: eval rhs (AX,
// and BX if str), push, eval pointer, pop value, store.
// We default to MOVQ (8B) since most fixtures use it; for
// `*bool` / `*u8` / `*i32` we narrow via the local's tnode.
// 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 == syntax.nkind.N_UN) {
if (lhs.op == syntax.tkind.TK_STAR) {
if (n.op == syntax.tkind.TK_ASSIGN && !placeslit
&& !derefagg) {
let inner: *syntax.node = lhs.lhs;
// #17: tagged-union pointee. The single-store
// tail below writes rhs into the tag word only,
// dropping the payload and corrupting the union.
// Materialise the widened value (tag + payload
// words, nullable fold, tag remap) into the shared
// @tagscr scratch via cgwidentaggedstore, then
// word-copy scratch -> *p. Mirror of the runtime-
// index tagged element arm (cgenexpr.ww:8768) and
// the cstage twin (cmd/w6c/cgen.c #17 deref arm).
let du: *syntax.tinfo = tichase(lhs.type_: *syntax.tinfo);
if (du != nil && du.kind == syntax.tykind.TY_TAGGED) {
let ssz: i32 = du.size: i32;
let scr: i32 = tagscradd(c, ssz);
emitline("\tXORQ\tAX, AX\n");
let zk: i32 = 0;
for (zk < ssz) {
emitline("\tMOVQ\tAX, ");
emitoff((scr + zk): i64);
emitline("(BP)\n");
zk += 8;
};
cgwidentaggedstore(c, du, n.rhs, "BP", scr, ssz);
cgexpr(c, inner);
emitline("\tMOVQ\tAX, BX\n");
let ck: i32 = 0;
for (ck < ssz) {
emitline("\tMOVQ\t");
emitoff((scr + ck): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff(ck: i64);
emitline("(BX)\n");
ck += 8;
};
return;
};
let elemstr: bool = false;
let elemfloat: bool = false;
let elemf32: bool = false;
let storeop: str = "MOVQ";
if (inner != nil) {
if (inner.kind == syntax.nkind.N_IDENT) {
let lc: *local = localfindnode(c, inner.str);
if (lc != nil) {
let tn: *syntax.node = lc.tnode;
if (tn != nil) {
if (tn.kind == syntax.nkind.N_TPTR) {
let pe: *syntax.node = tn.lhs;
if (pe != nil) {
if (pe.kind == syntax.nkind.N_TNAME) {
if (syntax.streq(pe.str, "str")) { elemstr = true; }
else { if (syntax.streq(pe.str, "f64")) { elemfloat = true; }
else { if (syntax.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 == syntax.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 == syntax.nkind.N_UN) {
if (lhs.op == syntax.tkind.TK_STAR) {
// Size gate (mirror cstage cgen.c handled=sz∈{1,2,4,8}):
// a tagged (or any non-scalar) pointee is not a
// meaningful compound target — skip this single-word
// store-and-return arm so `*p OP= v` on *tagged falls
// through to the assign-resolver's loud TY_TAGGED reject
// (#18). Without it the MOVQ default below clobbers the
// tag word and returns: a silent miscompile.
let psz: i32 = 8;
let lt: *syntax.tinfo = lhs.type_: *syntax.tinfo;
if (lt != nil) { psz = lt.size: i32; };
let scalarpointee: bool = (psz == 1 || psz == 2 || psz == 4 || psz == 8);
if (n.op != syntax.tkind.TK_ASSIGN && scalarpointee) {
let inner: *syntax.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: *syntax.node = nil;
if (inner != nil) {
if (inner.kind == syntax.nkind.N_IDENT) {
let lc: *local = localfindnode(c, inner.str);
if (lc != nil) {
let tn: *syntax.node = lc.tnode;
if (tn != nil) {
if (tn.kind == syntax.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 = syntax.typeisunsigned(pe.type_: *syntax.tinfo);
};
if (!unsignd) {
unsignd = nodeisunsigned(c, n.rhs);
};
if (n.op == syntax.tkind.TK_SLASHEQ || n.op == syntax.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 == syntax.tkind.TK_PERCENTEQ) {
emitline("\tMOVQ\tDX, AX\n");
};
emitline("\t");
emitline(storeop);
emitline("\tAX, (BX)\n");
return;
};
let combineop: str = "MOVQ";
if (n.op == syntax.tkind.TK_PLUSEQ) { combineop = "ADDQ"; }
else { if (n.op == syntax.tkind.TK_MINUSEQ) { combineop = "SUBQ"; }
else { if (n.op == syntax.tkind.TK_STAREQ) { combineop = "IMULQ"; }
else { if (n.op == syntax.tkind.TK_AMPEQ) { combineop = "ANDQ"; }
else { if (n.op == syntax.tkind.TK_PIPEEQ) { combineop = "ORQ"; }
else { if (n.op == syntax.tkind.TK_CARETEQ) { combineop = "XORQ"; }
else { if (n.op == syntax.tkind.TK_LSHIFTEQ) { combineop = "SHLQ"; }
else { if (n.op == syntax.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 == syntax.nkind.N_INDEX && !placeslit) {
if (n.op == syntax.tkind.TK_ASSIGN) {
let base: *syntax.node = lhs.lhs;
let idx: *syntax.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: *syntax.node = nil;
let basealias: bool = false;
if (base != nil) {
if (base.kind == syntax.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: *syntax.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 == syntax.nkind.N_TARRAY) {
isglobalarr = true;
} else {
isglobalptr = true;
};
};
};
} else { if (base.kind == syntax.nkind.N_DOT
|| (base.kind == syntax.nkind.N_UN
&& base.op == syntax.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: *syntax.tinfo = lhs.type_: *syntax.tinfo;
if (dt != nil) { esz = dt.size: i32; elemtn = lhs; };
} else { if (base.kind == syntax.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: *syntax.tinfo = lhs.type_: *syntax.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 == syntax.nkind.N_IDENT) {
let bt60: *syntax.tinfo = base.type_: *syntax.tinfo;
if (bt60 != nil) {
if (bt60.kind == syntax.tykind.TY_NAMED) { basealias = true; };
};
if (basealias) {
let et60: *syntax.tinfo = tichase(lhs.type_: *syntax.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: *syntax.tinfo = tichase(bt60);
if (bu60 != nil) {
isglobalarr = bu60.kind == syntax.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_: *syntax.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: *syntax.node = baselocal.tnode;
let isarr: bool = false;
if (tn != nil) {
if (tn.kind == syntax.nkind.N_TARRAY) {
isarr = true;
};
};
// #60: alias-NAMED base — chased kind
// (see cgindex twin).
if (basealias) {
let bu60: *syntax.tinfo = tichase(base.type_: *syntax.tinfo);
if (bu60 != nil) { isarr = bu60.kind == syntax.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 == syntax.nkind.N_CALL
&& callsretsize(c, n.rhs) > 0) {
let islocalarr: bool = false;
if (baselocal != nil) {
let btn: *syntax.node = baselocal.tnode;
if (btn != nil) {
if (btn.kind == syntax.nkind.N_TARRAY) { islocalarr = true; };
};
};
let constidx: bool = false;
let cidx: i32 = 0;
if (idx != nil) {
if (idx.kind == syntax.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;
};
// C2c / #31-G: an IN-CAP aggregate-returning CALL into
// an indexed element `a[i] = mk()`. The #234 arm above
// only fires for an OVER-cap (sret) return; the #270-1b
// arm below copies from a source ADDRESS (which a call
// result has none). An in-cap (<=24B) struct/array/tuple
// return leaves AX/DX/CX per the #4 cgreturn ABI but fell
// to the 1-word scalar store (AX only) — dropping DX/CX
// (the documented-but-unfixed #31-G). Materialise the
// return into a frame scratch (the AX/DX/CX receive shape,
// cstage cgen.c:3434), THEN compute &a[i] and word-copy
// scratch -> dest. Scratch-first (not a dest spill across
// the call) keeps the call at the frame's natural
// alignment. esz>8 non-str/non-slice IS struct/array/tuple
// here (tagged returned above). In-cap only
// (callsretsize==0). Mirror of cstage cgen.c C2c arm.
// #12: an unwrap `mk()!` / `r?` whose success variant
// is an in-cap struct/array rides the SAME {AX,DX,CX}
// payload shape as the call return (the producer shift
// at cgtrytaggedshift materialises it); admit it
// alongside N_CALL so the materialise + copy just works.
// callsretsize==0 holds (non-call → 0); the float/over-
// cap loud-stops live at the producer.
if ((n.rhs.kind == syntax.nkind.N_CALL
|| n.rhs.kind == syntax.nkind.N_TRYUNW
|| n.rhs.kind == syntax.nkind.N_TRYPROP) && esz > 8
&& !isstrtype(c, elemtn) && !isslicetype(c, elemtn)
&& callsretsize(c, n.rhs) == 0) {
let scrc2: i32 = tagscradd(c, esz);
cgexpr(c, n.rhs); // call -> AX/DX/CX
// AX/DX/CX -> scratch (mirror cstage cgen.c:3434).
cgaggregstore(c, "BP", scrc2, esz, true);
// dest &a[i] -> BX (#121 / #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: *syntax.node = baselocal.tnode;
let isarr2: bool = false;
if (tn2 != nil) { if (tn2.kind == syntax.nkind.N_TARRAY) { isarr2 = true; }; };
if (basealias) {
let bu60: *syntax.tinfo = tichase(base.type_: *syntax.tinfo);
if (bu60 != nil) { isarr2 = bu60.kind == syntax.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");
// word-copy scratch -> dest (tail-aware, #270-1b copy)
let kc: i32 = 0;
for (kc + 8 <= esz) {
emitline("\tMOVQ\t");
emitoff((scrc2 + kc): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff(kc: i64);
emitline("(BX)\n");
kc += 8;
};
if (kc + 4 <= esz) {
emitline("\tMOVL\t");
emitoff((scrc2 + kc): i64);
emitline("(BP), AX\n");
emitline("\tMOVL\tAX, ");
emitoff(kc: i64);
emitline("(BX)\n");
kc += 4;
};
if (kc + 2 <= esz) {
emitline("\tMOVW\t");
emitoff((scrc2 + kc): i64);
emitline("(BP), AX\n");
emitline("\tMOVW\tAX, ");
emitoff(kc: i64);
emitline("(BX)\n");
kc += 2;
};
if (kc + 1 <= esz) {
emitline("\tMOVB\t");
emitoff((scrc2 + kc): i64);
emitline("(BP), AX\n");
emitline("\tMOVB\tAX, ");
emitoff(kc: i64);
emitline("(BX)\n");
kc += 1;
};
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 == syntax.nkind.N_TUPLE && esz > 8
&& base != nil && base.kind == syntax.nkind.N_IDENT
&& elemtn != nil && elemtn.kind == syntax.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: *syntax.node = elemtn.list;
for (q121 != nil) {
let qt: *syntax.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: *syntax.node = baselocal.tnode;
let isarr2: bool = false;
if (tn2 != nil) { if (tn2.kind == syntax.nkind.N_TARRAY) { isarr2 = true; }; };
if (basealias) {
let bu60: *syntax.tinfo = tichase(base.type_: *syntax.tinfo);
if (bu60 != nil) { isarr2 = bu60.kind == syntax.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 == syntax.nkind.N_IDENT)
|| (n.rhs.kind == syntax.nkind.N_DOT)
|| (n.rhs.kind == syntax.nkind.N_UN
&& n.rhs.op == syntax.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: *syntax.node = baselocal.tnode;
let isarr2: bool = false;
if (tn2 != nil) { if (tn2.kind == syntax.nkind.N_TARRAY) { isarr2 = true; }; };
// #60: alias-NAMED base — chased kind (see cgindex twin).
if (basealias) {
let bu60: *syntax.tinfo = tichase(base.type_: *syntax.tinfo);
if (bu60 != nil) { isarr2 = bu60.kind == syntax.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 == syntax.nkind.N_UN
&& n.rhs.op == syntax.tkind.TK_STAR) {
cgexpr(c, n.rhs.lhs);
emitline("\tMOVQ\tAX, SI\n");
} else { if (n.rhs.kind == syntax.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: *syntax.node = baselocal.tnode;
let isarray: bool = false;
if (tn != nil) { if (tn.kind == syntax.nkind.N_TARRAY) { isarray = true; }; };
// #60: alias-NAMED base — chased kind (see cgindex twin).
if (basealias) {
let bu60: *syntax.tinfo = tichase(base.type_: *syntax.tinfo);
if (bu60 != nil) { isarray = bu60.kind == syntax.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 != syntax.tkind.TK_ASSIGN) {
let base: *syntax.node = lhs.lhs;
let idx: *syntax.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: *syntax.node = nil;
if (base != nil) {
if (base.kind == syntax.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: *syntax.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 == syntax.nkind.N_TARRAY) {
isglobalarr = true;
} else {
isglobalptr = true;
};
};
};
} else { if (base.kind == syntax.nkind.N_DOT
|| (base.kind == syntax.nkind.N_UN
&& base.op == syntax.tkind.TK_STAR)) {
// N_UN deref base (`(*p)[i] OP= v`, #61 C):
// same stamped source as the store arm.
let dt: *syntax.tinfo = lhs.type_: *syntax.tinfo;
if (dt != nil) { esz = dt.size: i32; elemtn = lhs; };
} else { if (base.kind == syntax.nkind.N_INDEX) {
let et: *syntax.tinfo = lhs.type_: *syntax.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 == syntax.nkind.N_IDENT) {
let bt60: *syntax.tinfo = base.type_: *syntax.tinfo;
if (bt60 != nil) {
if (bt60.kind == syntax.tykind.TY_NAMED) { basealias = true; };
};
if (basealias) {
let et60: *syntax.tinfo = tichase(lhs.type_: *syntax.tinfo);
if (et60 != nil) {
esz = et60.size: i32;
elemtn = lhs;
};
if (isglobalarr || isglobalptr) {
let bu60: *syntax.tinfo = tichase(bt60);
if (bu60 != nil) {
isglobalarr = bu60.kind == syntax.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: *syntax.node = baselocal.tnode;
let isarray: bool = false;
if (tn != nil) { if (tn.kind == syntax.nkind.N_TARRAY) { isarray = true; }; };
// #60: alias-NAMED base — chased kind (see cgindex twin).
if (basealias) {
let bu60: *syntax.tinfo = tichase(base.type_: *syntax.tinfo);
if (bu60 != nil) { isarray = bu60.kind == syntax.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 = syntax.typeisunsigned(elemtn.type_: *syntax.tinfo);
};
};
let wired: bool = false;
if (n.op == syntax.tkind.TK_PLUSEQ) { emitline("\tADDQ\tCX, AX\n"); wired = true; };
if (n.op == syntax.tkind.TK_MINUSEQ) { emitline("\tSUBQ\tCX, AX\n"); wired = true; };
if (n.op == syntax.tkind.TK_STAREQ) { emitline("\tIMULQ\tCX, AX\n"); wired = true; };
if (n.op == syntax.tkind.TK_AMPEQ) { emitline("\tANDQ\tCX, AX\n"); wired = true; };
if (n.op == syntax.tkind.TK_PIPEEQ) { emitline("\tORQ\tCX, AX\n"); wired = true; };
if (n.op == syntax.tkind.TK_CARETEQ) { emitline("\tXORQ\tCX, AX\n"); wired = true; };
if (n.op == syntax.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 == syntax.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 == syntax.tkind.TK_LSHIFTEQ) { emitline("\tSHLQ\tCX, AX\n"); wired = true; };
if (n.op == syntax.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 == syntax.nkind.N_DOT && lhs.lhs != nil
&& lhs.lhs.kind == syntax.nkind.N_INDEX && !placeslit) {
let idxbase: *syntax.node = lhs.lhs.lhs;
let idx: *syntax.node = lhs.lhs.rhs;
let fld2: str = lhs.str;
if (idxbase != nil) { if (idxbase.kind == syntax.nkind.N_IDENT) {
if (idx != nil) {
let lc: *local = localfindnode(c, idxbase.str);
if (lc != nil) { if (lc.tnode != nil) {
let tn: *syntax.node = lc.tnode;
// idxelemtn: `*[N]T` drills to the pointee
// array's element (#61).
let elemt: *syntax.node = idxelemtn(tn);
let baseisarray: bool = tn.kind == syntax.nkind.N_TARRAY;
let snode: *syntax.node = nil;
let viaptr: bool = false;
if (elemt != nil) {
if (elemt.kind == syntax.nkind.N_TPTR) {
let inner: *syntax.node = elemt.lhs;
if (inner != nil) { if (inner.kind == syntax.nkind.N_TNAME) {
snode = inner;
viaptr = true;
};};
} else { if (elemt.kind == syntax.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 (syntax.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 == syntax.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 (syntax.typeisfloat(n.rhs.type_: *syntax.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_: *syntax.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;
};
// #11: an in-cap aggregate-returning CALL into
// an AGGREGATE field of an indexed element
// `arr[i].f = mk()`. The scalar default below
// stores only AX (eb0), dropping DX/CX — a
// SILENT both-stage field-drop, the field-of-
// indexed twin of C2c's whole-element
// arr[i]=mk() arm (cgenexpr.ww :9100). Scratch-
// first materialise of the AX/DX/CX return (not
// a PUSHQ spill — keeps the CALL at the frame's
// 16B alignment and survives an idx that itself
// contains a call), reuse the scalar arm's
// &arr[i]->BX address computation verbatim, then
// word-copy scratch to fi.foff(BX). tsz from the
// type table (fieldsize, rule 13). In-cap only
// (callsretsize==0); over-cap sret-into-field
// LOUD-STOPS (#11c/#234, task #8) and a float-
// bearing aggregate LOUD-STOPS (#165/#171 — a
// pure-float return eightbyte rides X0/X1 which
// the GP AX/DX/CX cursor cannot read). Mirrors
// cstage cgen.c.
// #12: an unwrap `arr[i].f = mk()!` rides the same
// {AX,DX,CX} shape (producer shift) — admit it
// alongside N_CALL; the #11b non-call arm below
// excludes the unwrap kinds so this arm is the SOLE
// handler. callsretsize==0 for a non-call, so an
// over-cap unwrap stays loud at the producer.
if (n.op == syntax.tkind.TK_ASSIGN
&& n.rhs != nil
&& (n.rhs.kind == syntax.nkind.N_CALL
|| n.rhs.kind == syntax.nkind.N_TRYUNW
|| n.rhs.kind == syntax.nkind.N_TRYPROP)) {
if (callsretsize(c, n.rhs) > 0) {
let m11o: str = "#11c/#234: over-cap (sret) aggregate field receive arr[i].f=mk() unwired (cs!=ww; task #8)\n";
os.write(2, m11o.ptr, m11o.len: u64);
os.exit(1);
};
// structfloatclass mirrors the return-
// side SSE routing (cgenstmt.ww #171a);
// a field typed DIRECTLY as a tuple
// misses it (non-N_TNAME) yet the bare-
// tuple return routes floats to tupsse —
// guard it too so neither stage silently
// stores X0 garbage through the GP cursor.
let sse11: bool = structfloatclass(c, fi.tnode) != 0;
if (!sse11) {
let rt11: *syntax.node = resolvetype(c, fi.tnode);
if (rt11 != nil) { if (rt11.kind == syntax.nkind.N_TTUPLE) {
let q11: *syntax.node = rt11.list;
for (q11 != nil) {
if (isfloattype(c, q11.lhs)) { sse11 = true; };
q11 = q11.next;
};
};};
};
if (sse11) {
let m11f: str = "#11/#165: float-bearing aggregate field receive arr[i].f=mk() unwired (SSE return eightbyte; #171)\n";
os.write(2, m11f.ptr, m11f.len: u64);
os.exit(1);
};
// fi.fsz is the field's NATURAL size
// (tf.type_.size, cgenutil.ww :2736) —
// the byte-id twin of cstage's
// fsz=ft->size. fieldsize() returns the
// slot-PADDED size (16 for a 12B
// 3×i32), which both diverges from
// cstage AND a full-MOVQ tail on it
// would smash the next field (g at +12).
let tsz: i32 = fi.fsz;
if (tsz > 8) {
// The sub-8 tail materialise stores the FULL
// 8-byte register (MOVQ) into a ceil-8-padded
// scratch (tagscradd -> localadd rounds to 8):
// the over-stored high bytes land in the pad and
// the scratch->dest copy reads only tsz bytes, so
// every in-cap tail (incl. 3/5/6/7) is exact
// without an immediate-shift cascade (w6a has no
// SHRQ $imm). Shares the C2c whole-element
// materialise (:9100). #10; mirrors cstage cgen.c.
let scr11: i32 = tagscradd(c, tsz);
cgexpr(c, n.rhs);
// AX/DX/CX -> scratch: #14 folds this materialise into the choke-
// point (dest_padded=true; the scratch IS a ceil-8 slot so the
// tail eightbyte over-store lands in its pad — byte-identical to
// the prior inline form).
cgaggregstore(c, "BP", scr11, tsz, true);
// &arr[i] -> BX (verbatim scalar arm)
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"); };
// word-copy scratch -> fi.foff(BX),
// tail-aware (C2c copy); foff on every
// eightbyte, sub-8 tail stays sub-8.
let kc11: i32 = 0;
for (kc11 + 8 <= tsz) {
emitline("\tMOVQ\t");
emitoff((scr11 + kc11): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg((fi.foff + kc11): i64, "BX");
emitline("\n");
kc11 += 8;
};
if (kc11 + 4 <= tsz) {
emitline("\tMOVL\t");
emitoff((scr11 + kc11): i64);
emitline("(BP), AX\n");
emitline("\tMOVL\tAX, ");
emitdispreg((fi.foff + kc11): i64, "BX");
emitline("\n");
kc11 += 4;
};
if (kc11 + 2 <= tsz) {
emitline("\tMOVW\t");
emitoff((scr11 + kc11): i64);
emitline("(BP), AX\n");
emitline("\tMOVW\tAX, ");
emitdispreg((fi.foff + kc11): i64, "BX");
emitline("\n");
kc11 += 2;
};
if (kc11 + 1 <= tsz) {
emitline("\tMOVB\t");
emitoff((scr11 + kc11): i64);
emitline("(BP), AX\n");
emitline("\tMOVB\tAX, ");
emitdispreg((fi.foff + kc11): i64, "BX");
emitline("\n");
kc11 += 1;
};
return;
};
// tsz<=8 in-cap aggregate returns wholly
// in AX; the scalar default's MOVQ/MOVL AX
// store is the correct 1-word receive.
};
// #11b: a NON-call AGGREGATE source into an aggregate
// field of an indexed element `arr[i].f = src` (src an
// ident / .g / index). The scalar default below loads
// only the source's FIRST word into AX and stores ONE
// word — dropping the rest (a SILENT both-stage member
// drop, the non-call twin of the #11 in-cap CALL arm
// above; byte-id blind). Unlike #11's GP AX/DX/CX cursor
// the source is a MEMORY address, so the shared mem-to-
// mem aggcopy transports EVERY byte: a sub-8 tail (MOVL/
// MOVW/MOVB) and float bits copy verbatim, so NO tail/
// float/over-cap loud-stop is needed here (those #11
// stops were register-cursor artefacts). Reuse the
// block's own &arr[i] spine (proven for [N]S / *[N]S /
// []S by the sibling arms) -> BX + fi.foff, then funnel
// through aggargsrcaddr (src -> SI) + aggcopy — the ONE
// copy emitter the non-indexed bases use (DRY, rule 8).
// tsz natural (fi.fsz, type table). Mirrors cstage cgen.c.
// #12: exclude the unwrap kinds — the #11 arm above is
// their sole handler (they ride the {AX,DX,CX} register
// cursor, NOT a source address, so aggargsrcaddr can't
// reach them). Pre-#12 an N_TRYUNW matched != N_CALL and
// loud-stopped here; now it materialises in the #11 arm.
if (n.op == syntax.tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind != syntax.nkind.N_CALL
&& n.rhs.kind != syntax.nkind.N_TRYUNW
&& n.rhs.kind != syntax.nkind.N_TRYPROP) {
let fk11b: *syntax.tinfo = tichase(fi.tnode.type_: *syntax.tinfo);
let isagg11b: bool = false;
if (fk11b != nil) {
if (fk11b.kind == syntax.tykind.TY_STRUCT
|| fk11b.kind == syntax.tykind.TY_ARRAY
|| fk11b.kind == syntax.tykind.TY_TUPLE) {
isagg11b = true;
};
};
if (isagg11b) {
let tsz11b: i32 = fi.fsz;
if (tsz11b > 8) {
// &arr[i].f -> BX (verbatim sibling-arm spine)
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"); };
if (fi.foff != 0) {
emitline("\tADDQ\t$");
emitint(fi.foff: i64);
emitline(", BX\n");
};
// spill dest across the source-address resolution
// (the #270-1b order: aggargsrcaddr clobbers BX).
emitline("\tPUSHQ\tBX\n");
if (!aggargsrcaddr(c, n.rhs, "SI")) {
let m11b: str = "#11b: aggregate field receive arr[i].f=src - source shape unwired (rule-7)\n";
os.write(2, m11b.ptr, m11b.len: u64);
os.exit(1);
};
emitline("\tPOPQ\tBX\n");
aggcopy(c, tsz11b);
return;
};
// tsz<=8 aggregate: one word, the scalar default's
// single store is the correct copy.
};
};
// 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. #33/#263:
// all 10 integer ops wired (was 6 →
// SLASHEQ/PERCENTEQ/LSHIFTEQ/RSHIFTEQ
// silently no-op'd in BOTH stages);
// float/str/slice/tagged field hard-
// errors LOUD. Mirrors cstage cgen.c
// arr[i].field compound twin.
if (istaggedtype(c, fi.tnode)) {
let m: str = "arr[i].field compound on tagged field not wired (#33/rule-7)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
if (isstrtype(c, fi.tnode)) {
let m: str = "arr[i].field compound on str field not wired (#33/rule-7)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
if (isslicetype(c, fi.tnode)) {
let m: str = "arr[i].field compound on slice field not wired (#33/rule-7)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
if (isfloattype(c, fi.tnode)) {
let m: str = "arr[i].field compound on float field not wired (#33/rule-7)\n";
os.write(2, m.ptr, m.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("\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");
let unsignd_x: bool = false;
if (fi.tnode != nil) {
if (fi.tnode.type_ != nil) {
unsignd_x = syntax.typeisunsigned(fi.tnode.type_: *syntax.tinfo);
};
};
let wired_x: bool = false;
if (n.op == syntax.tkind.TK_PLUSEQ) { emitline("\tADDQ\tCX, AX\n"); wired_x = true; };
if (n.op == syntax.tkind.TK_MINUSEQ) { emitline("\tSUBQ\tCX, AX\n"); wired_x = true; };
if (n.op == syntax.tkind.TK_STAREQ) { emitline("\tIMULQ\tCX, AX\n"); wired_x = true; };
if (n.op == syntax.tkind.TK_AMPEQ) { emitline("\tANDQ\tCX, AX\n"); wired_x = true; };
if (n.op == syntax.tkind.TK_PIPEEQ) { emitline("\tORQ\tCX, AX\n"); wired_x = true; };
if (n.op == syntax.tkind.TK_CARETEQ) { emitline("\tXORQ\tCX, AX\n"); wired_x = true; };
if (n.op == syntax.tkind.TK_SLASHEQ) {
if (unsignd_x) { emitline("\tMOVQ\t$0, DX\n"); emitline("\tDIVQ\tCX\n"); }
else { emitline("\tCQO\n"); emitline("\tIDIVQ\tCX\n"); };
wired_x = true;
};
if (n.op == syntax.tkind.TK_PERCENTEQ) {
if (unsignd_x) { emitline("\tMOVQ\t$0, DX\n"); emitline("\tDIVQ\tCX\n"); }
else { emitline("\tCQO\n"); emitline("\tIDIVQ\tCX\n"); };
emitline("\tMOVQ\tDX, AX\n");
wired_x = true;
};
if (n.op == syntax.tkind.TK_LSHIFTEQ) { emitline("\tSHLQ\tCX, AX\n"); wired_x = true; };
if (n.op == syntax.tkind.TK_RSHIFTEQ) {
if (unsignd_x) { emitline("\tSHRQ\tCX, AX\n"); }
else { emitline("\tSARQ\tCX, AX\n"); };
wired_x = true;
};
if (!wired_x) {
let m: str = "arr[i].field compound: unknown op (#33/rule-7)\n";
os.write(2, m.ptr, m.len: u64);
os.exit(1);
};
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 == syntax.nkind.N_DOT) {
let base: *syntax.node = lhs.lhs;
let fld: str = lhs.str;
if (base != nil) {
if (base.kind == syntax.nkind.N_UN) {
if (base.op == syntax.tkind.TK_STAR) {
if (base.lhs != nil) {
if (base.lhs.kind == syntax.nkind.N_IDENT) {
base = base.lhs;
};
};
};
};
if (base.kind == syntax.nkind.N_IDENT) {
let bn: str = base.str;
let lc: *local = localfindnode(c, bn);
if (lc != nil) {
let tn: *syntax.node = lc.tnode;
let lkind: syntax.nkind = syntax.nkind.N_NONE;
if (tn != nil) { lkind = tn.kind; };
// Pointer-to-struct: deref then store.
if (lkind == syntax.nkind.N_TPTR) {
// #31: resolve the *struct field OFFSET + type off the
// checker-STAMPED receiver tinfo (tichase(base.type_)->.sub),
// NOT the name-keyed leaf lookup. A cross-module same-leaf
// collision makes the bare leaf mis-resolve to a FOREIGN
// same-leaf struct -> the field is STORED at the wrong
// offset/width (the inferred-local ptr-WRITE row). The
// stamped tinfo carries the right layout; mirror cstage
// type_chase_named(bu->sub)->fields. The in-loop #32 nested
// struct-receive/structlit/ident sub-arms close by
// construction (the tf walk has no name lookup). Non-struct
// pointees fall through to the str/slice arm.
let sti: *syntax.tinfo = tichase(base.type_: *syntax.tinfo);
if (sti != nil && sti.kind == syntax.tykind.TY_PTR) { sti = tichase(sti.sub); };
if (sti != nil) {
if (sti.kind == syntax.tykind.TY_STRUCT) {
let tf: *syntax.tfield = sti.fields;
for (tf != nil) {
if (syntax.streq(tf.name, fld)) {
let foff: i32 = tf.offset: i32;
let ftraw: *syntax.tinfo = tf.type_;
let fu: *syntax.tinfo = tichase(ftraw);
// 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 == syntax.tkind.TK_ASSIGN
&& syntax.typeistagged(ftraw)) {
let fsz: i32 = fu.size: i32;
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
cgwidentaggedstore(c, ftraw,
n.rhs, "BX", 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 == syntax.tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == syntax.nkind.N_CALL
&& sretretsizetn(c, ftraw) > 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). #12: an
// unwrap `p.f = mk()!` rides the same {AX,DX,CX}
// shape (producer shift) — admit it alongside
// N_CALL; float/over-cap loud-stop at the producer.
if (n.op == syntax.tkind.TK_ASSIGN
&& n.rhs != nil
&& (n.rhs.kind == syntax.nkind.N_CALL
|| n.rhs.kind == syntax.nkind.N_TRYUNW
|| n.rhs.kind == syntax.nkind.N_TRYPROP)) {
if (fu != nil && fu.kind == syntax.tykind.TY_STRUCT) {
let ssz: i32 = fu.size: i32;
if (ssz <= 24) {
// #14: choke-point now stores every in-cap tail; 3/5/6/7 no longer dropped to a lone narrow MOV.
cgexpr(c, n.rhs);
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
cgaggregstore(c, "BX", foff, ssz, false);
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 == syntax.tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == syntax.nkind.N_STRUCTLIT) {
if (fu != nil && fu.kind == syntax.tykind.TY_STRUCT) {
cgstructlitfilltn(c, fu, n.rhs, 1, lc.off, "",
foff);
return;
};
};
if (n.op == syntax.tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == syntax.nkind.N_IDENT) {
let srhs: *local = localfindnode(c, n.rhs.str);
if (fu != nil && fu.kind == syntax.tykind.TY_STRUCT) { if (srhs != nil) {
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
let ssz: i32 = copysrcnatsize(c, n.rhs); // #71: natural source size, not slot-padded 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((foff + k): i64, "BX");
emitline("\n");
k += 8;
};
if (k + 4 <= ssz) {
emitline("\tMOVL\t");
emitoff((srhs.off + k): i64);
emitline("(BP), AX\n");
emitline("\tMOVL\tAX, ");
emitdispreg((foff + k): i64, "BX");
emitline("\n");
k += 4;
};
if (k + 2 <= ssz) {
emitline("\tMOVW\t");
emitoff((srhs.off + k): i64);
emitline("(BP), AX\n");
emitline("\tMOVW\tAX, ");
emitdispreg((foff + k): i64, "BX");
emitline("\n");
k += 2;
};
if (k + 1 <= ssz) {
emitline("\tMOVB\t");
emitoff((srhs.off + k): i64);
emitline("(BP), AX\n");
emitline("\tMOVB\tAX, ");
emitdispreg((foff + k): i64, "BX");
emitline("\n");
k += 1;
};
return;
};};
};
if (n.op != syntax.tkind.TK_ASSIGN) {
// compound: load current value
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
let lop: str = loadopsz(syntax.typeissigned(ftraw), fu.size: i32);
emitline("\t");
emitline(lop);
emitline("\t");
emitdispreg(foff: i64, "BX");
emitline(", BX\n");
emitline("\tPUSHQ\tBX\n");
};
cgexpr(c, n.rhs);
if (n.op != syntax.tkind.TK_ASSIGN) {
emitline("\tPOPQ\tBX\n");
// PLUSEQ is commutative; MINUSEQ
// needs lhs - rhs (BX is old lhs,
// AX is rhs).
cgdotfieldhardstoptn(c, ftraw);
let uns34: bool = false;
uns34 = syntax.typeisunsigned(ftraw);
cgdotfieldcombine(c, n.op, uns34);
};
if (n.op == syntax.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 (syntax.typeisstr(ftraw) || syntax.typeisslice(ftraw)) {
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), DX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(foff: i64, "DX");
emitline("\n");
emitline("\tMOVQ\tBX, ");
emitdispreg((foff + 8): i64, "DX");
emitline("\n");
emitline("\tMOVQ\tCX, ");
emitdispreg((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 (syntax.typeisfloat(ftraw)) {
let mov: str = "MOVSD";
if (syntax.typeisf32(ftraw)) { mov = "MOVSS"; };
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitdispreg(foff: i64, "BX");
emitline("\n");
return;
};
};
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
let sop: str = storeopsz(fu.size: i32);
emitline("\t");
emitline(sop);
emitline("\tAX, ");
emitdispreg(foff: i64, "BX");
emitline("\n");
return;
};
tf = tf.tnext;
};
};
};
};
// Direct struct local: store at off+foff.
if (lkind == syntax.nkind.N_TNAME) {
// #31: resolve the value-struct field OFFSET + type off the
// checker-STAMPED receiver tinfo (tichase(base.type_)), NOT
// the name-keyed leaf lookup. A cross-module same-leaf
// collision makes the bare leaf mis-resolve to a FOREIGN
// same-leaf struct -> the field is STORED at the wrong
// offset/width (the inferred-local val-WRITE row, ww=107).
// The stamped tinfo carries the right layout; mirror cstage
// type_chase_named(base->type)->fields. The in-loop #32
// nested struct-receive/structlit/ident sub-arms close by
// construction (the tf walk has no name lookup).
let sti: *syntax.tinfo = tichase(base.type_: *syntax.tinfo);
if (sti != nil && sti.kind == syntax.tykind.TY_STRUCT) {
let tf: *syntax.tfield = sti.fields;
for (tf != nil) {
if (syntax.streq(tf.name, fld)) {
let foff: i32 = tf.offset: i32;
let ftraw: *syntax.tinfo = tf.type_;
let fu: *syntax.tinfo = tichase(ftraw);
// Tagged-union field in a direct struct local —
// full slot rewrite at (lc.off + foff)(BP)
// via cgwidentaggedstore basereg="BP". Pre-#26
// fell through and dropped tag + payload.
if (n.op == syntax.tkind.TK_ASSIGN
&& syntax.typeistagged(ftraw)) {
let fsz: i32 = fu.size: i32;
cgwidentaggedstore(c, ftraw,
n.rhs, "BP", lc.off + 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 + 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 == syntax.tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == syntax.nkind.N_CALL
&& sretretsizetn(c, ftraw) > 0) {
c.sretdestoff = lc.off + 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+foff)(BP).
// N_STRUCTLIT: field-walk; each inner
// field stored at +foff+inner_foff(BP).
// BP-rel direct, no addr scratch needed. #12: an
// unwrap `b.f = mk()!` rides the same {AX,DX,CX}
// shape (producer shift) — admit it alongside
// N_CALL; float/over-cap loud-stop at the producer.
if (n.op == syntax.tkind.TK_ASSIGN
&& n.rhs != nil
&& (n.rhs.kind == syntax.nkind.N_CALL
|| n.rhs.kind == syntax.nkind.N_TRYUNW
|| n.rhs.kind == syntax.nkind.N_TRYPROP)) {
if (fu != nil && fu.kind == syntax.tykind.TY_STRUCT) {
let ssz: i32 = fu.size: i32;
if (ssz <= 24) {
// #14: choke-point now stores every in-cap tail; 3/5/6/7 no longer dropped to a lone narrow MOV.
cgexpr(c, n.rhs);
cgaggregstore(c, "BP", lc.off + foff, ssz, false);
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 == syntax.tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == syntax.nkind.N_STRUCTLIT) {
if (fu != nil && fu.kind == syntax.tykind.TY_STRUCT) {
cgstructlitfilltn(c, fu, n.rhs, 0, 0, "",
lc.off + foff);
return;
};
};
if (n.op == syntax.tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == syntax.nkind.N_IDENT) {
let srhs: *local = localfindnode(c, n.rhs.str);
if (fu != nil && fu.kind == syntax.tykind.TY_STRUCT) { if (srhs != nil) {
let ssz: i32 = copysrcnatsize(c, n.rhs); // #71: natural source size, not slot-padded 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 + foff + k): i64);
emitline("(BP)\n");
k += 8;
};
if (k + 4 <= ssz) {
emitline("\tMOVL\t");
emitoff((srhs.off + k): i64);
emitline("(BP), AX\n");
emitline("\tMOVL\tAX, ");
emitoff((lc.off + foff + k): i64);
emitline("(BP)\n");
k += 4;
};
if (k + 2 <= ssz) {
emitline("\tMOVW\t");
emitoff((srhs.off + k): i64);
emitline("(BP), AX\n");
emitline("\tMOVW\tAX, ");
emitoff((lc.off + foff + k): i64);
emitline("(BP)\n");
k += 2;
};
if (k + 1 <= ssz) {
emitline("\tMOVB\t");
emitoff((srhs.off + k): i64);
emitline("(BP), AX\n");
emitline("\tMOVB\tAX, ");
emitoff((lc.off + foff + k): i64);
emitline("(BP)\n");
k += 1;
};
return;
};};
};
if (n.op != syntax.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 = loadopsz(syntax.typeissigned(ftraw), fu.size: i32);
emitline("\t");
emitline(lop);
emitline("\t");
emitoff((lc.off + foff): i64);
emitline("(BP), BX\n");
emitline("\tPUSHQ\tBX\n");
};
cgexpr(c, n.rhs);
if (n.op != syntax.tkind.TK_ASSIGN) {
emitline("\tPOPQ\tBX\n");
// PLUSEQ commutes; MINUSEQ needs
// lhs-rhs (BX old lhs, AX rhs).
cgdotfieldhardstoptn(c, ftraw);
let uns34: bool = false;
uns34 = syntax.typeisunsigned(ftraw);
cgdotfieldcombine(c, n.op, uns34);
};
// 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 (syntax.typeisstr(ftraw) || syntax.typeisslice(ftraw)) {
emitline("\tMOVQ\tAX, ");
emitoff((lc.off + foff): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tBX, ");
emitoff((lc.off + foff + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((lc.off + foff + 16): i64);
emitline("(BP)\n");
return;
};
// f64/f32 direct struct local store: route via X0.
if (syntax.typeisfloat(ftraw)) {
let mov: str = "MOVSD";
if (syntax.typeisf32(ftraw)) { mov = "MOVSS"; };
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff((lc.off + foff): i64);
emitline("(BP)\n");
return;
};
let sop: str = storeopsz(fu.size: i32);
emitline("\t");
emitline(sop);
emitline("\tAX, ");
emitoff((lc.off + foff): i64);
emitline("(BP)\n");
return;
};
tf = tf.tnext;
};
};
};
// str/slice pseudo-field assignment.
let delta: i32 = -1;
if (syntax.streq(fld, "ptr")) { delta = 0; };
if (syntax.streq(fld, "len")) { delta = 8; };
if (syntax.streq(fld, "cap")) { delta = 16; };
if (delta >= 0) {
if (lkind == syntax.nkind.N_TPTR) {
let inner: *syntax.node = tn.lhs;
let innerkind: syntax.nkind = syntax.nkind.N_NONE;
if (inner != nil) { innerkind = inner.kind; };
let innerstr: bool = false;
if (innerkind == syntax.nkind.N_TNAME) {
if (syntax.streq(inner.str, "str")) { innerstr = true; };
};
if (innerkind == syntax.nkind.N_TSLICE) { innerstr = true; };
if (innerstr) {
if (n.op != syntax.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.
cgdotfieldcombine(c, n.op, false);
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 != syntax.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");
cgdotfieldcombine(c, n.op, false);
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 == syntax.nkind.N_DOT) {
let base: *syntax.node = lhs.lhs;
let fld: str = lhs.str;
if (base != nil) {
if (base.kind == syntax.nkind.N_IDENT) {
let bn: str = base.str;
if (localfindnode(c, bn) == nil) {
// #31: resolve the global value-struct field OFFSET + type
// off the checker-STAMPED receiver tinfo (tichase(base.type_)),
// NOT the name-keyed global-struct leaf lookup. A global decl
// is qualified -> non-reddenable; converted for close-by-
// construction (byte-id). Mirror cstage type-keyed N_DOT global
// arm. The in-loop #32 nested struct-receive/structlit/ident
// sub-arms close by construction (the tf walk has no name
// lookup).
let sti: *syntax.tinfo = tichase(base.type_: *syntax.tinfo);
if (sti != nil && sti.kind == syntax.tykind.TY_STRUCT) {
let tf: *syntax.tfield = sti.fields;
for (tf != nil) {
if (syntax.streq(tf.name, fld)) {
let foff: i32 = tf.offset: i32;
let ftraw: *syntax.tinfo = tf.type_;
let fu: *syntax.tinfo = tichase(ftraw);
// #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 == syntax.tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == syntax.nkind.N_CALL
&& sretretsizetn(c, ftraw) > 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 == syntax.tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == syntax.nkind.N_CALL) {
if (fu != nil && fu.kind == syntax.tykind.TY_STRUCT) {
let ssz: i32 = fu.size: i32;
if (ssz <= 24) {
// #14: choke-point now stores every in-cap tail; 3/5/6/7 no longer dropped to a lone narrow MOV.
cgexpr(c, n.rhs);
emitline("\tLEAQ\t");
emitsymname(c, bn);
emitline("(SB), BX\n");
cgaggregstore(c, "BX", foff, ssz, false);
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 == syntax.tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == syntax.nkind.N_STRUCTLIT) {
if (fu != nil && fu.kind == syntax.tykind.TY_STRUCT) {
cgstructlitfilltn(c, fu, n.rhs, 2, 0, bn,
foff);
return;
};
};
if (n.op == syntax.tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == syntax.nkind.N_IDENT) {
let srhs: *local = localfindnode(c, n.rhs.str);
if (fu != nil && fu.kind == syntax.tykind.TY_STRUCT) { if (srhs != nil) {
emitline("\tLEAQ\t");
emitsymname(c, bn);
emitline("(SB), BX\n");
let ssz: i32 = copysrcnatsize(c, n.rhs); // #71: natural source size, not slot-padded 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((foff + k): i64, "BX");
emitline("\n");
k += 8;
};
if (k + 4 <= ssz) {
emitline("\tMOVL\t");
emitoff((srhs.off + k): i64);
emitline("(BP), AX\n");
emitline("\tMOVL\tAX, ");
emitdispreg((foff + k): i64, "BX");
emitline("\n");
k += 4;
};
if (k + 2 <= ssz) {
emitline("\tMOVW\t");
emitoff((srhs.off + k): i64);
emitline("(BP), AX\n");
emitline("\tMOVW\tAX, ");
emitdispreg((foff + k): i64, "BX");
emitline("\n");
k += 2;
};
if (k + 1 <= ssz) {
emitline("\tMOVB\t");
emitoff((srhs.off + k): i64);
emitline("(BP), AX\n");
emitline("\tMOVB\tAX, ");
emitdispreg((foff + k): i64, "BX");
emitline("\n");
k += 1;
};
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 == syntax.tkind.TK_ASSIGN
&& syntax.typeistagged(ftraw)) {
let fsz: i32 = fu.size: i32;
emitline("\tLEAQ\t");
emitsymname(c, bn);
emitline("(SB), BX\n");
cgwidentaggedstore(c,
ftraw,
n.rhs, "BX", foff, fsz);
return;
};
if (n.op == syntax.tkind.TK_ASSIGN) {
cgexpr(c, n.rhs);
if (syntax.typeisstr(ftraw) || syntax.typeisslice(ftraw)) {
// str OR slice field: str IS []u8, so
// both store the full {ptr,len,cap}
// header (cstage cgen.c:5055 gates
// TY_STR||TY_SLICE the same; without the
// slice arm this dropped to the 1-word
// scalar store below). 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(foff: i64, "DX");
emitline("\n");
emitline("\tMOVQ\tBX, ");
emitdispreg((foff + 8): i64, "DX");
emitline("\n");
emitline("\tMOVQ\tCX, ");
emitdispreg((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 (syntax.typeisfloat(ftraw)) {
let mov: str = "MOVSD";
if (syntax.typeisf32(ftraw)) { mov = "MOVSS"; };
emitline("\tLEAQ\t");
emitsymname(c, bn);
emitline("(SB), BX\n");
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitdispreg(foff: i64, "BX");
emitline("\n");
return;
};
let sop: str = storeopsz(fu.size: i32);
emitline("\tLEAQ\t");
emitsymname(c, bn);
emitline("(SB), BX\n");
emitline("\t");
emitline(sop);
emitline("\tAX, ");
emitdispreg(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 = loadopsz(syntax.typeissigned(ftraw), fu.size: i32);
emitline("\tLEAQ\t");
emitsymname(c, bn);
emitline("(SB), BX\n");
emitline("\t");
emitline(lop);
emitline("\t");
emitdispreg(foff: i64, "BX");
emitline(", BX\n");
emitline("\tPUSHQ\tBX\n");
cgexpr(c, n.rhs);
emitline("\tPOPQ\tBX\n");
cgdotfieldhardstoptn(c, ftraw);
let uns34: bool = false;
uns34 = syntax.typeisunsigned(ftraw);
cgdotfieldcombine(c, n.op, uns34);
let sop: str = storeopsz(fu.size: i32);
emitline("\tLEAQ\t");
emitsymname(c, bn);
emitline("(SB), BX\n");
emitline("\t");
emitline(sop);
emitline("\tAX, ");
emitdispreg(foff: i64, "BX");
emitline("\n");
return;
};
tf = tf.tnext;
};
};
};
};
};
};
};
// #8 (rule-10 byte-id): scalar field STORE through a module-GLOBAL
// `*struct` pointer base (`gp.f = v`; gp is isletvar — not a local,
// not a value-struct). The local-ptr arm (lc!=nil) and the value-
// struct global arm (letvarstructinfo, N_TNAME-only) both miss it, so
// it fell to the F6 cgplaceaddr route which folds the field offset via
// `ADDQ $foff, BX` + a plain `MOVQ AX,(BX)`; cstage's via_ptr global
// scalar arm (cgen.c #47, boff==0 && let_islet) emits the DISPLACEMENT
// store `<sop> AX, foff(BX)`. Align ww UP. Plain `=` scalar only —
// str/slice/tagged/float/struct fields keep their F6 route; compound
// (`OP=`) stays on F6 (the latent via_ptr global LOAD is #60/#61,
// deferred). The PUSHQ/POPQ AX bracketing the base load mirrors cstage
// byte-for-byte (rhs in AX survives the LEAQ/MOVQ, which touch only BX).
if (lhs != nil) {
if (lhs.kind == syntax.nkind.N_DOT && n.op == syntax.tkind.TK_ASSIGN) {
let base: *syntax.node = lhs.lhs;
let fld: str = lhs.str;
// `(*gp).f` parses as N_DOT over N_UN(STAR, IDENT); retarget
// to the inner IDENT so it fires identically to `gp.f`
// (mirror of the local-ptr arm + cstage retarget).
if (base != nil) {
if (base.kind == syntax.nkind.N_UN) {
if (base.op == syntax.tkind.TK_STAR) {
if (base.lhs != nil) {
if (base.lhs.kind == syntax.nkind.N_IDENT) {
base = base.lhs;
};
};
};
};
};
if (base != nil) {
if (base.kind == syntax.nkind.N_IDENT) {
let bn: str = base.str;
if (localfindnode(c, bn) == nil) {
let tn: *syntax.node = letvartnode(c, bn);
if (tn != nil) {
if (tn.kind == syntax.nkind.N_TPTR) {
// #31: scalar global *struct field store -- resolve offset +
// scalar-ness off the stamped *struct tinfo
// (tichase(base.type_)->.sub), NOT structlookupchain(inner).
// Non-scalar fields (str/slice/tagged/float/nested-struct)
// keep their F6 cgplaceaddr route (scalar=false -> fall
// through). Global *struct decls are qualified -> non-
// reddenable; convert for close-by-construction (byte-id).
let sti: *syntax.tinfo = tichase(base.type_: *syntax.tinfo);
if (sti != nil && sti.kind == syntax.tykind.TY_PTR) { sti = tichase(sti.sub); };
if (sti != nil) { if (sti.kind == syntax.tykind.TY_STRUCT) {
let tf: *syntax.tfield = sti.fields;
for (tf != nil) {
if (syntax.streq(tf.name, fld)) {
let foff: i32 = tf.offset: i32;
let ftraw: *syntax.tinfo = tf.type_;
// scalar only: str/slice/tagged/float/struct fields
// keep their F6 route (cstage type-keys the same).
let scalar: bool = true;
if (syntax.typeistagged(ftraw)) { scalar = false; };
if (syntax.typeisstr(ftraw) || syntax.typeisslice(ftraw)) { scalar = false; };
if (syntax.typeisfloat(ftraw)) { scalar = false; };
let ftc: *syntax.tinfo = tichase(ftraw);
if (ftc != nil) { if (ftc.kind == syntax.tykind.TY_STRUCT) { scalar = false; }; };
if (scalar) {
cgexpr(c, n.rhs);
emitline("\tPUSHQ\tAX\n");
emitline("\tLEAQ\t");
emitsymname(c, bn);
emitline("(SB), BX\n");
emitline("\tMOVQ\t");
emitdispreg(0i64, "BX");
emitline(", BX\n");
emitline("\tPOPQ\tAX\n");
let fsz: i32 = 0;
if (ftc != nil) { fsz = ftc.size: i32; };
let sop: str = "MOVQ";
if (fsz == 1) { sop = "MOVB"; }
else { if (fsz == 2) { sop = "MOVW"; }
else { if (fsz == 4) { sop = "MOVL"; }; }; };
emitline("\t");
emitline(sop);
emitline("\tAX, ");
emitdispreg(foff: i64, "BX");
emitline("\n");
return;
};
};
tf = tf.tnext;
};
}; };
};
};
};
};
};
};
};
// 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 == syntax.nkind.N_DOT) {
let base: *syntax.node = lhs.lhs;
let fld: str = lhs.str;
if (base != nil) {
if (base.kind == syntax.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: *syntax.node = base;
let allptr: bool = true;
for (croot != nil && croot.kind == syntax.nkind.N_DOT) {
let ct: *syntax.tinfo = croot.type_: *syntax.tinfo;
ct = tichase(ct);
let okp: bool = false;
if (ct != nil) { if (ct.kind == syntax.tykind.TY_PTR) {
let cs: *syntax.tinfo = ct.sub;
cs = tichase(cs);
if (cs != nil) { if (cs.kind == syntax.tykind.TY_STRUCT) { okp = true; }; };
}; };
if (!okp) { allptr = false; };
croot = croot.lhs;
};
let it: *syntax.tinfo = nil;
if (allptr && croot != nil && croot.kind == syntax.nkind.N_IDENT &&
localfindnode(c, croot.str) != nil) {
it = base.type_: *syntax.tinfo;
};
it = tichase(it);
if (it != nil) { if (it.kind == syntax.tykind.TY_PTR) {
let st: *syntax.tinfo = it.sub;
st = tichase(st);
if (st != nil) { if (st.kind == syntax.tykind.TY_STRUCT) {
let tf: *syntax.tfield = st.fields;
for (tf != nil) {
if (syntax.streq(tf.name, fld)) {
let ft: *syntax.tinfo = tf.type_;
if (n.op == syntax.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 (syntax.typeistagged(ft)) {
let flu: *syntax.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 (syntax.typeisstr(ft) || syntax.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 (syntax.typeisfloat(ft)) {
let mov: str = "MOVSD";
if (syntax.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 != syntax.tkind.TK_ASSIGN) {
if (syntax.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 (syntax.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 (syntax.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 (syntax.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 = syntax.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 == syntax.tkind.TK_PLUSEQ) { emitline("\tADDQ\tCX, AX\n"); wired_f = true; };
if (n.op == syntax.tkind.TK_MINUSEQ) { emitline("\tSUBQ\tCX, AX\n"); wired_f = true; };
if (n.op == syntax.tkind.TK_STAREQ) { emitline("\tIMULQ\tCX, AX\n"); wired_f = true; };
if (n.op == syntax.tkind.TK_AMPEQ) { emitline("\tANDQ\tCX, AX\n"); wired_f = true; };
if (n.op == syntax.tkind.TK_PIPEEQ) { emitline("\tORQ\tCX, AX\n"); wired_f = true; };
if (n.op == syntax.tkind.TK_CARETEQ) { emitline("\tXORQ\tCX, AX\n"); wired_f = true; };
if (n.op == syntax.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 == syntax.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 == syntax.tkind.TK_LSHIFTEQ) { emitline("\tSHLQ\tCX, AX\n"); wired_f = true; };
if (n.op == syntax.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 == syntax.nkind.N_DOT && lhs.lhs != nil
&& lhs.lhs.kind == syntax.nkind.N_DOT
&& n.op == syntax.tkind.TK_ASSIGN) {
let rootname: str = "";
let rootoff: i32 = 0;
let totaloff: i32 = 0;
let leaftype: *syntax.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);
// A global `*struct` root chained STORE: cstage stores
// via the address-compute spine (LEAQ name(SB),BX; MOVQ
// (BX),BX; ADDQ; MOVQ AX,(BX)), NOT the READ path's
// offset-fold — so the fold spine below would diverge.
// Decline and fall through to the address-spine mirror,
// exactly as the addr-of path declines a ptr-rooted chain
// (cgenexpr.ww:5114). A LOCAL `*T` root (ptrroot &&
// !isglobal) DOES fold in both stages and stays. (#16)
if (isglobal && ptrroot) { yok = false; };
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) {
emitchainbase(c, ptrroot, isglobal,
rootoff, rootname);
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 (syntax.typeistagged(leaftype)) {
let wlu: *syntax.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 (syntax.typeisstr(leaftype) || syntax.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: *syntax.tinfo = leaftype;
lp = tichase(lp);
if (lp != nil) {
if (lp.kind == syntax.tykind.TY_STRUCT) { leafstruct = true; };
};
if (leaftype.kind == syntax.tykind.TY_NAMED) {
leafname = leaftype.name;
};
};
if (n.rhs != nil
&& n.rhs.kind == syntax.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) {
// #14: choke-point now stores every in-cap tail; 3/5/6/7 no longer dropped to a lone narrow MOV.
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");
};
};
if (viacx) { cgaggregstore(c, "BX", totaloff, lsz, false); } else { cgaggregstore(c, "BP", rootoff + totaloff, lsz, false); };
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 == syntax.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 == syntax.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 = copysrcnatsize(c, n.rhs); // #71: natural source size, not slot-padded 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 + 4 <= ssz) {
emitline("\tMOVL\t");
emitoff((srhs.off + k): i64);
emitline("(BP), AX\n");
if (viacx) {
emitline("\tMOVL\tAX, ");
emitdispreg((totaloff + k): i64, "CX");
emitline("\n");
} else {
emitline("\tMOVL\tAX, ");
emitoff((rootoff + totaloff + k): i64);
emitline("(BP)\n");
};
k += 4;
};
if (k + 2 <= ssz) {
emitline("\tMOVW\t");
emitoff((srhs.off + k): i64);
emitline("(BP), AX\n");
if (viacx) {
emitline("\tMOVW\tAX, ");
emitdispreg((totaloff + k): i64, "CX");
emitline("\n");
} else {
emitline("\tMOVW\tAX, ");
emitoff((rootoff + totaloff + k): i64);
emitline("(BP)\n");
};
k += 2;
};
if (k + 1 <= ssz) {
emitline("\tMOVB\t");
emitoff((srhs.off + k): i64);
emitline("(BP), AX\n");
if (viacx) {
emitline("\tMOVB\tAX, ");
emitdispreg((totaloff + k): i64, "CX");
emitline("\n");
} else {
emitline("\tMOVB\tAX, ");
emitoff((rootoff + totaloff + k): i64);
emitline("(BP)\n");
};
k += 1;
};
return;
};};
};
if (syntax.typeisfloat(leaftype)) {
let mov: str = "MOVSD";
if (syntax.typeisf32(leaftype)) { mov = "MOVSS"; };
cgexpr(c, n.rhs);
if (viacx) {
emitchainbase(c, ptrroot, isglobal,
rootoff, rootname);
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 == syntax.nkind.N_DOT) {
let base: *syntax.node = lhs.lhs;
let fld: str = lhs.str;
if (base != nil) { if (base.kind == syntax.nkind.N_DOT) {
let inner: *syntax.node = base.lhs;
let innerfld: str = base.str;
if (inner != nil) { if (inner.kind == syntax.nkind.N_IDENT) {
let lc: *local = localfindnode(c, inner.str);
if (lc != nil) { if (lc.tnode != nil) {
let tn: *syntax.node = lc.tnode;
let lkind: syntax.nkind = tn.kind;
let outname: str;
outname.ptr = nil; outname.len = 0;
let isptr: bool = false;
if (lkind == syntax.nkind.N_TNAME) { outname = tn.str; };
if (lkind == syntax.nkind.N_TPTR) {
let pe: *syntax.node = tn.lhs;
if (pe != nil) { if (pe.kind == syntax.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 (syntax.streq(ofi.fname, innerfld)) {
let oft: *syntax.node = ofi.tnode;
if (oft != nil) { if (oft.kind == syntax.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 (syntax.streq(ffi.fname, fld)) {
if (n.op == syntax.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 == syntax.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: *syntax.node = nil;
for (lvf != nil) {
if (syntax.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 == syntax.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 == syntax.tkind.TK_PLUSEQ) { fop = addf; };
if (n.op == syntax.tkind.TK_MINUSEQ) { fop = subf; };
if (n.op == syntax.tkind.TK_STAREQ) { fop = mulf; };
if (n.op == syntax.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 == syntax.tkind.TK_ASSIGN && n.rhs != nil
&& n.rhs.kind == syntax.nkind.N_CALL && lvftn != nil) {
let gsz: i32 = 0;
if (lvftn.kind == syntax.nkind.N_TNAME) {
// #31: size off the stamped tinfo, not structlookup(lvftn.str).
// Non-reddenable (global decl qualified); convert for
// close-by-construction.
gsz = structabisizetn(lvftn.type_: *syntax.tinfo);
};
if (lvftn.kind == syntax.nkind.N_TARRAY) {
let gat: *syntax.tinfo = lvftn.type_: *syntax.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 == syntax.tkind.TK_ASSIGN && n.rhs != nil
&& n.rhs.kind == syntax.nkind.N_CALL && lvftn != nil
&& lvftn.kind == syntax.nkind.N_TARRAY) {
let aggsz: i32 = 0;
let aat: *syntax.tinfo = lvftn.type_: *syntax.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");
// #14: route through the choke-point so a 3/5/6/7 tail is no
// longer dropped to a lone MOVB (was silent both-stage). A global
// symbol is non-padded => dest_padded=false (its scratch detour
// copies exactly `tail` bytes).
cgaggregstore(c, "DI", 0, aggsz, false);
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 == syntax.tkind.TK_ASSIGN && lvftn != nil) {
let gau: *syntax.tinfo = lvftn.type_: *syntax.tinfo;
gau = tichase(gau);
if (gau != nil) {
if (gau.kind == syntax.tykind.TY_STRUCT
|| gau.kind == syntax.tykind.TY_ARRAY
|| gau.kind == syntax.tykind.TY_TUPLE) {
if (n.rhs != nil && n.rhs.kind == syntax.nkind.N_STRUCTLIT) {
// #31: fill off the stamped struct tinfo (gau =
// tichase(lvftn.type_)), NOT structlookup(lvftn.str). A
// global decl is qualified -> non-reddenable; converted
// for close-by-construction (byte-id). The W4b nested
// in-loop structlookup sub-arms close by construction
// (cgstructlitfilltn recurses on tichase(tf.type_)).
if (gau.kind != syntax.tykind.TY_STRUCT) {
// 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);
};
cgstructlitfilltn(c, gau, n.rhs, 2, 0, nm, 0);
return;
};
if (n.rhs != nil && n.rhs.kind == syntax.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 == syntax.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 (syntax.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 == syntax.tkind.TK_PLUSEQ) { emitline("\tADDQ\tAX, BX\n"); }
else { if (n.op == syntax.tkind.TK_MINUSEQ) { emitline("\tSUBQ\tAX, BX\n"); }
else { if (n.op == syntax.tkind.TK_STAREQ) { emitline("\tIMULQ\tAX, BX\n"); }
else { if (n.op == syntax.tkind.TK_AMPEQ) { emitline("\tANDQ\tAX, BX\n"); }
else { if (n.op == syntax.tkind.TK_PIPEEQ) { emitline("\tORQ\tAX, BX\n"); }
else { if (n.op == syntax.tkind.TK_CARETEQ) { emitline("\tXORQ\tAX, BX\n"); }
else { if (n.op == syntax.tkind.TK_LSHIFTEQ) {
emitline("\tMOVQ\tAX, CX\n");
emitline("\tSHLQ\tCX, BX\n");
}
else { if (n.op == syntax.tkind.TK_RSHIFTEQ) {
// #136: signed RSHIFTEQ → SARQ.
let unsignd_r: bool = false;
if (lvftn != nil) {
if (lvftn.type_ != nil) {
unsignd_r = syntax.typeisunsigned(lvftn.type_: *syntax.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 == syntax.tkind.TK_SLASHEQ || n.op == syntax.tkind.TK_PERCENTEQ) {
let unsignd: bool = false;
if (lvftn != nil) {
unsignd = syntax.typeisunsigned(lvftn.type_: *syntax.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 == syntax.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 == syntax.tkind.TK_ASSIGN && n.rhs != nil
&& n.rhs.kind == syntax.nkind.N_CALL) {
let rtup: *syntax.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: *syntax.node = lcn.tnode;
let lcsname: str;
lcsname.ptr = nil; lcsname.len = 0;
if (lctn != nil) {
if (lctn.kind == syntax.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 == syntax.tkind.TK_ASSIGN) {
if (n.rhs != nil
&& n.rhs.kind == syntax.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 == syntax.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) {
// #14: choke-point now stores every in-cap tail; 3/5/6/7 no longer dropped to a lone narrow MOV.
cgexpr(c, n.rhs);
cgaggregstore(c, "BP", off, lcsz, false);
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 == syntax.tkind.TK_ASSIGN
&& n.rhs != nil && n.rhs.kind == syntax.nkind.N_CALL) {
let acati: *syntax.tinfo = nil;
if (lcn.tnode != nil) { acati = lcn.tnode.type_: *syntax.tinfo; };
acati = tichase(acati);
if (acati != nil && acati.kind == syntax.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) {
// #14: choke-point now stores every in-cap tail; 3/5/6/7 no longer dropped to a lone narrow MOV.
cgexpr(c, n.rhs);
cgaggregstore(c, "BP", off, lcsz, false);
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 == syntax.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 == syntax.tkind.TK_PLUSEQ) { fop = addf; };
if (n.op == syntax.tkind.TK_MINUSEQ) { fop = subf; };
if (n.op == syntax.tkind.TK_STAREQ) { fop = mulf; };
if (n.op == syntax.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 == syntax.tkind.TK_ASSIGN) {
let agu: *syntax.tinfo = nil;
if (lcn != nil) {
if (lcn.tnode != nil) {
agu = lcn.tnode.type_: *syntax.tinfo;
};
};
agu = tichase(agu);
if (agu != nil) {
if (agu.kind == syntax.tykind.TY_STRUCT
|| agu.kind == syntax.tykind.TY_ARRAY
|| agu.kind == syntax.tykind.TY_TUPLE) {
if (n.rhs != nil && n.rhs.kind == syntax.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 == syntax.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 == syntax.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 (syntax.streq(llop, "MOVQ")) {
if (n.op == syntax.tkind.TK_PLUSEQ) {
emitline("\tADDQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
return;
};
if (n.op == syntax.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 == syntax.tkind.TK_PLUSEQ) { emitline("\tADDQ\tAX, BX\n"); };
if (n.op == syntax.tkind.TK_MINUSEQ) { emitline("\tSUBQ\tAX, BX\n"); };
if (n.op == syntax.tkind.TK_STAREQ) { emitline("\tIMULQ\tAX, BX\n"); };
if (n.op == syntax.tkind.TK_AMPEQ) { emitline("\tANDQ\tAX, BX\n"); };
if (n.op == syntax.tkind.TK_PIPEEQ) { emitline("\tORQ\tAX, BX\n"); };
if (n.op == syntax.tkind.TK_CARETEQ) { emitline("\tXORQ\tAX, BX\n"); };
if (n.op == syntax.tkind.TK_LSHIFTEQ) {
emitline("\tMOVQ\tAX, CX\n");
emitline("\tSHLQ\tCX, BX\n");
};
if (n.op == syntax.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 = syntax.typeisunsigned(lcn.tnode.type_: *syntax.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 == syntax.tkind.TK_SLASHEQ || n.op == syntax.tkind.TK_PERCENTEQ) {
let unsignd: bool = false;
if (lcn != nil) {
if (lcn.tnode != nil) {
unsignd = syntax.typeisunsigned(lcn.tnode.type_: *syntax.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 == syntax.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 == syntax.nkind.N_DOT || lhs.kind == syntax.nkind.N_INDEX
|| (lhs.kind == syntax.nkind.N_UN && lhs.op == syntax.tkind.TK_STAR)) {
let ft: *syntax.tinfo = lhs.type_: *syntax.tinfo;
let fu: *syntax.tinfo = ft;
fu = tichase(fu);
let fsz: i32 = 8;
if (ft != nil) { fsz = ft.size: i32; };
if (syntax.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 == syntax.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 == syntax.tykind.TY_STRUCT
|| fu.kind == syntax.tykind.TY_ARRAY
|| fu.kind == syntax.tykind.TY_TUPLE) {
if (n.op != syntax.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 == syntax.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 == syntax.nkind.N_STRUCTLIT
&& fu.kind == syntax.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 == syntax.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 == syntax.tykind.TY_STR
|| fu.kind == syntax.tykind.TY_SLICE) {
fstrsl = true;
};
};
if (fstrsl) {
if (n.op != syntax.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 == syntax.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(syntax.typeissigned(ft), fsz);
emitline("\t");
emitline(lop);
emitline("\t(BX), AX\n");
emitline("\tPOPQ\tCX\n");
let unsignd: bool = syntax.typeisunsigned(ft);
let wired: bool = false;
if (n.op == syntax.tkind.TK_PLUSEQ) { emitline("\tADDQ\tCX, AX\n"); wired = true; };
if (n.op == syntax.tkind.TK_MINUSEQ) { emitline("\tSUBQ\tCX, AX\n"); wired = true; };
if (n.op == syntax.tkind.TK_STAREQ) { emitline("\tIMULQ\tCX, AX\n"); wired = true; };
if (n.op == syntax.tkind.TK_AMPEQ) { emitline("\tANDQ\tCX, AX\n"); wired = true; };
if (n.op == syntax.tkind.TK_PIPEEQ) { emitline("\tORQ\tCX, AX\n"); wired = true; };
if (n.op == syntax.tkind.TK_CARETEQ) { emitline("\tXORQ\tCX, AX\n"); wired = true; };
if (n.op == syntax.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 == syntax.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 == syntax.tkind.TK_LSHIFTEQ) { emitline("\tSHLQ\tCX, AX\n"); wired = true; };
if (n.op == syntax.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;
};