// selfhost/cmd/wcc/cgenexpr.ww — split out of cgen.ww. // // cgexpr is a thin dispatcher over n.kind; each non-trivial branch // lives in a per-kind helper (cgstrlit, cgident, cgindex, cgmatch, // cgdot, cgun, cgbin, cgcall, cgassign). Trivial literal loads // (nkind.N_INTLIT, nkind.N_RUNELIT, nkind.N_TRUE/FALSE/NIL, nkind.N_CAST) stay inline. // // The remainder of cgen lives in cgen.ww (foundation: types, emit // primitives, the collect* tables, FFI/module maps) and cgenstmt.ww // (cgstmt). // // `use cgenexpr;` is unnecessary at consumer sites — cgen.ww imports // this file, so any caller of cgen transitively gets cgexpr. package wcc; import os; import ast; import tok; import typ; import sym; import strconv; // cgfloatbits — materialise a float constant in X0: MOVQ the IEEE bits // into AX, PUSH, MOVSD off the stack into X0. Shared by N_FLOATLIT (bits // already in n.uval from the lexer's bitcast) and the f64/f32-typed // N_INTLIT arm (#103 FACE X). fn cgfloatbits(c: *cgen, bits: u64) void = { emitline("\tMOVQ\t$"); emitint(bits: i64); emitline(", AX\n"); emitline("\tPUSHQ\tAX\n"); emitline("\tMOVSD\t(SP), X0\n"); emitline("\tADDQ\t$8, SP\n"); }; fn cgexpr(c: *cgen, n: *node) void = { if (n == nil) { return; }; let k: nkind = n.kind; if (k == 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; }; if (k == 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; }; if (k == nkind.N_RUNELIT) { emitline("\tMOVQ\t$"); emitint(n.uval: i64); emitline(", AX\n"); return; }; if (k == nkind.N_STRLIT) { cgstrlit(c, n); return; }; if (k == nkind.N_TRUE) { emitline("\tMOVQ\t$1, AX\n"); return; }; if (k == nkind.N_FALSE) { emitline("\tMOVQ\t$0, AX\n"); return; }; if (k == nkind.N_NIL) { emitline("\tMOVQ\t$0, AX\n"); return; }; if (k == nkind.N_VOIDLIT) { // void value: zero-size, but the consumer's ABI expects a // deterministic AX. Emit 0 like nil/false do. emitline("\tMOVQ\t$0, AX\n"); return; }; if (k == nkind.N_IDENT) { cgident(c, n); return; }; if (k == nkind.N_INDEX) { cgindex(c, n); return; }; if (k == nkind.N_SLICE) { cgslice(c, n); return; }; if (k == nkind.N_MATCH) { cgmatch(c, n); return; }; if (k == nkind.N_CAST) { cgcast(c, n); return; }; if (k == nkind.N_DOT) { cgdot(c, n); return; }; if (k == nkind.N_UN) { cgun(c, n); return; }; if (k == nkind.N_BIN) { cgbin(c, n); return; }; if (k == nkind.N_CALL) { cgcall(c, n); return; }; if (k == nkind.N_ASSIGN) { cgassign(c, n); return; }; if (k == nkind.N_TRYPROP) { cgtryprop(c, n); return; }; if (k == nkind.N_TRYUNW) { cgtryunw(c, n); return; }; if (k == nkind.N_TYPETEST) { cgtypetest(c, n); return; }; if (k == nkind.N_TYPEASSERT) { cgtypeassert(c, n); return; }; // Default fallback: produce a deterministic AX = 0. Mirrors // the C cgen's `default: cgexpr_int(c, 0)` branch, which is // what `return eof{};` (N_STRUCTLIT with an empty !void // variant) silently relies on — without this AX carries a // stale value into the tagged-union return shuffle. emitline("\tMOVQ\t$0, AX\n"); }; // cgtagvariantidx — find the 0-based variant index of `vt` inside the // tagged-union type expression `tagged`. -1 if `tagged` isn't an // nkind.N_TTAGGED or no variant matches. Mirrors the lookup that cgmatch // does inline; pulled out so `is` / `as` can reuse it. fn cgtagvariantidx(c: *cgen, tagged: *node, vt: *node) i32 = { if (tagged == nil) { return -1; }; if (vt == nil) { return -1; }; if (tagged.kind != nkind.N_TTAGGED) { return -1; }; // `is []T` / `as []T` — slice-shape lookup routes through the // element-aware helper, which carries the loose first-slice-shape // fallback (cstage type_assignable stand-in) that flatvariantidx's // strict typeeq below doesn't. Task #19; #66 refresh. if (vt.kind == nkind.N_TSLICE) { return flatslicevariantidx(c, tagged, vt.lhs); }; // #66 Phase-N step 3: match by typeeq on vt's stamped tinfo // (flatvariantidx), not vt's surface name. return flatvariantidx(c, tagged, vt); }; // cgtryprop — `e?` propagates the error variant up the stack. // Legacy semantics only (success tag = 0). No tag remap; the // selfhost code that uses ? today has the same variant order in // operand and enclosing fn. fn cgtryprop(c: *cgen, n: *node) void = { cgexpr(c, n.lhs); // AX = tag. If non-zero, this is an error; pop frame and RET. let cl: str = mklabel(c, "tryprop_ok"); emitline("\tCMPQ\t$0, AX\n"); emitline("\tJE\t"); emitline(cl); emitline("\n"); emitline("\tMOVQ\tBP, SP\n\tPOPQ\tBP\n\tRET\n"); emitlabel(cl); // Success: unwrap value. Tag-only result was AX; the rest of // the codegen expects the success value in AX (and BX for str). // AX=tag, DX=val0, CX=val1 from the call ABI. For str success, // shuffle (DX,CX) → (AX,BX); else move DX → AX. let succisstr: bool = false; if (n.lhs != nil) { if (n.lhs.kind == nkind.N_CALL) { let callee: *node = n.lhs.lhs; if (callee != nil) { let cname: str; cname.ptr = nil; cname.len = 0; let cmod: str; cmod.ptr = nil; cmod.len = 0; if (callee.kind == nkind.N_IDENT) { cname = callee.str; cmod = c.curmod; }; if (callee.kind == nkind.N_DOT) { cname = callee.str; if (callee.lhs != nil) { if (callee.lhs.kind == nkind.N_IDENT) { cmod = callee.lhs.str; }; }; }; if (cname.len > 0) { let rtyp: *node = fnretlookupmod(c, cname, cmod); if (rtyp != nil) { if (rtyp.kind == nkind.N_TTAGGED) { let first: *node = rtyp.list; if (first != nil) { if (isstrtype(c, first)) { succisstr = true; }; }; }; }; }; }; }; }; if (succisstr) { // str IS []u8: success arrives DX=ptr, CX=len, R8=cap // (slot 32B). Move len out before cap overwrites CX // (#1/Phase 3). emitline("\tMOVQ\tCX, BX\n"); emitline("\tMOVQ\tR8, CX\n"); }; emitline("\tMOVQ\tDX, AX\n"); return; }; // cgtryunw — `e!` aborts on the error variant via exit(1). Legacy // semantics (success tag = 0). fn cgtryunw(c: *cgen, n: *node) void = { cgexpr(c, n.lhs); let cl: str = mklabel(c, "tryunw_ok"); emitline("\tCMPQ\t$0, AX\n"); emitline("\tJE\t"); emitline(cl); emitline("\n"); emitline("\tMOVQ\t$1, DI\n\tMOVQ\t$60, AX\n\tSYSCALL\n"); emitlabel(cl); // Unwrap success value. (Same shuffle pattern as cgtryprop.) let succisstr: bool = false; if (n.lhs != nil) { if (n.lhs.kind == nkind.N_CALL) { let callee: *node = n.lhs.lhs; if (callee != nil) { let cname: str; cname.ptr = nil; cname.len = 0; let cmod: str; cmod.ptr = nil; cmod.len = 0; if (callee.kind == nkind.N_IDENT) { cname = callee.str; cmod = c.curmod; }; if (callee.kind == nkind.N_DOT) { cname = callee.str; if (callee.lhs != nil) { if (callee.lhs.kind == nkind.N_IDENT) { cmod = callee.lhs.str; }; }; }; if (cname.len > 0) { let rtyp: *node = fnretlookupmod(c, cname, cmod); if (rtyp != nil) { if (rtyp.kind == nkind.N_TTAGGED) { let first: *node = rtyp.list; if (first != nil) { if (isstrtype(c, first)) { succisstr = true; }; }; }; }; }; }; }; }; if (succisstr) { // str IS []u8: success arrives DX=ptr, CX=len, R8=cap // (slot 32B). Move len out before cap overwrites CX // (#1/Phase 3). emitline("\tMOVQ\tCX, BX\n"); emitline("\tMOVQ\tR8, CX\n"); }; emitline("\tMOVQ\tDX, AX\n"); return; }; fn cgtypetest(c: *cgen, n: *node) void = { // `e is T` — load the lhs's tag, compare against T's variant // index, set AX = (tag == idx). Result type is bool. // // Slot resolution is inlined (rather than factored into a helper // with output parameters): wwstage cgen has a trap with i32 // stored via *i32 in this context — direct assignment of the // local works, indirection through &scrutoff drops sign bits. let lhs: *node = n.lhs; let scrutoff: i32 = 0; let scrutt: *node = nil; if (lhs != nil) { if (lhs.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, lhs.str); if (lc != nil) { scrutoff = lc.off; scrutt = resolvetagged(c, lc.tnode); }; }; }; let want: i32 = cgtagvariantidx(c, scrutt, n.rhs); if (want < 0) { want = 0; }; emitline("\tMOVQ\t"); emitoff(scrutoff: i64); emitline("(BP), AX\n"); let nel: str = mklabel(c, "is_ne"); let dnl: str = mklabel(c, "is_done"); emitline("\tCMPQ\t$"); emitint(want: i64); emitline(", AX\n"); emitline("\tJNE\t"); emitline(nel); emitline("\n\tMOVQ\t$1, AX\n\tJMP\t"); emitline(dnl); emitline("\n"); emitlabel(nel); emitline("\tMOVQ\t$0, AX\n"); emitlabel(dnl); return; }; // isenumexpr — does this expression's static type resolve to an enum? // Recognises enum-member access (`Foo.MEMBER`), enum-typed local // idents, and nkind.N_BIN whose either operand is enum (so `R | W` flows // through the cast pass-through too). fn isenumexpr(c: *cgen, e: *node) bool = { if (e == nil) { return false; }; let k: nkind = e.kind; if (k == nkind.N_DOT) { if (e.lhs != nil) { if (e.lhs.kind == nkind.N_IDENT) { if (enumlookup(c, e.lhs.str) != nil) { return true; }; }; }; }; if (k == nkind.N_IDENT) { let lc: *local = localfindnode(c, e.str); if (lc != nil) { if (lc.tnode != nil) { if (lc.tnode.kind == nkind.N_TNAME) { if (enumlookup(c, lc.tnode.str) != nil) { return true; }; }; }; }; }; if (k == nkind.N_BIN) { if (isenumexpr(c, e.lhs)) { return true; }; if (isenumexpr(c, e.rhs)) { return true; }; }; if (k == nkind.N_UN) { if (isenumexpr(c, e.lhs)) { return true; }; }; return false; }; fn isenumtype(c: *cgen, t: *node) bool = { if (t == nil) { return false; }; if (t.kind == nkind.N_TENUM) { return true; }; if (t.kind == nkind.N_TNAME) { if (enumlookup(c, t.str) != nil) { return true; }; }; return false; }; fn cgtypeassert(c: *cgen, n: *node) void = { // Enum ↔ integer: reinterpret-only. The LHS value already // occupies AX (or AX:BX for str variants, irrelevant here); // no tag/unwrap. Matches cmd/w6c/cgen.c's same short-circuit. if (isenumexpr(c, n.lhs) || isenumtype(c, n.rhs)) { cgexpr(c, n.lhs); return; }; // `e as T` — load tag, abort (exit 1) if tag != T's variant // index, otherwise unwrap to T's ABI: scalar/ptr → AX, 16B // str → (AX, BX). Mirrors cgmatch's slot-based value load. // Slot resolution inlined; see cgtypetest comment. let lhs: *node = n.lhs; let scrutoff: i32 = 0; let scrutt: *node = nil; if (lhs != nil) { if (lhs.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, lhs.str); if (lc != nil) { scrutoff = lc.off; scrutt = resolvetagged(c, lc.tnode); }; }; }; let want: i32 = cgtagvariantidx(c, scrutt, n.rhs); if (want < 0) { want = 0; }; let okl: str = mklabel(c, "asrt_ok"); emitline("\tMOVQ\t"); emitoff(scrutoff: i64); emitline("(BP), AX\n"); emitline("\tCMPQ\t$"); emitint(want: i64); emitline(", AX\n"); emitline("\tJE\t"); emitline(okl); emitline("\n\tMOVQ\t$1, DI\n\tMOVQ\t$60, AX\n\tSYSCALL\n"); emitlabel(okl); emitline("\tMOVQ\t"); emitoff((scrutoff + 8): i64); emitline("(BP), AX\n"); if (isstrtype(c, n.rhs)) { emitline("\tMOVQ\t"); emitoff((scrutoff + 16): i64); emitline("(BP), BX\n"); }; return; }; fn cgcast(c: *cgen, n: *node) void = { let srcfk: i32 = 0; if (n.lhs != nil) { let st: *tinfo = n.lhs.type_: *tinfo; if (typeisf32(st)) { srcfk = 1; } else { if (typeisfloat(st)) { srcfk = 2; }; }; }; let dstf64: bool = isfloattype(c, n.rhs); let dstf32: bool = isf32type(c, n.rhs); let dstfk: i32 = 0; if (dstf32) { dstfk = 1; } else { if (dstf64) { dstfk = 2; }; }; cgexpr(c, n.lhs); // str → []T: cgexpr left (AX=ptr, BX=len). Slice register // convention is (AX=ptr, BX=len, CX=cap); synthesise cap = len // so downstream arg-push / let-init paths see the canonical // triple. Detect via dst-is-slice + src-ident's local-tnode // being str (the common shape; non-ident sources rare). if (isslicetype(c, n.rhs)) { let srcstr: bool = false; if (n.lhs != nil) { if (n.lhs.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, n.lhs.str); if (lc != nil) { if (isstrtype(c, lc.tnode)) { srcstr = true; }; }; }; }; if (srcstr) { emitline("\tMOVQ\tBX, CX\n"); }; }; // 0=int, 1=f32, 2=f64. CVT picks one direction per combo; // int↔int casts narrow via an explicit clamp before the early // return so `(big_u64): u32` doesn't leak the upper 32 bits. // Hare semantics: `expr: T` truncates to T's bit width (mod 2^n). // Mirrors cmd/w6c/cgen.c's N_CAST clamp. Unsigned narrow clears // the upper bits via MOVL/ANDQ; signed narrow sign-extends via // MOVSBQ/MOVSWQ/MOVSXD reg-reg so the sign bit propagates. // // Identity-width identity-sign cast is a no-op at the machine- // int level: src and dst share both width and signedness, so the // natural slot/load already carries the right canonical 64-bit // shape. Skip the clamp in that case. Symmetric with cstage's // principled gate (#33). Replaces the previous N_TENUM lacuna in // this walker (the alias-step missed `N_TENUM`, so any cast to // an enum dst landed on tn==nil and skipped the clamp by // accident — task #25 mirrored that into cstage as a single-site // gate, and #33 retires both). The walker now follows N_TENUM // too so a narrow-to-enum cast (u32→enum-u8, i64→enum-i32) // resolves to the underlying primitive and the clamp fires — // fixing a silent miscompile in the process. if (srcfk == 0 && dstfk == 0) { let sz: i32 = 0; let is_unsigned: bool = false; typenodeprimresolved(c, n.rhs, &sz, &is_unsigned); let src_sz: i32 = 0; let src_unsigned: bool = false; exprprimresolved(c, n.lhs, &src_sz, &src_unsigned); let identity: bool = false; if (sz > 0) { if (src_sz == sz) { if (src_unsigned == is_unsigned) { identity = true; }; }; }; // Detect bool dst by walking n.rhs to the leaf TNAME. bool // keeps its dedicated ANDQ $255 contract regardless of // upstream shape; it stays off the identity path. let leaf_tn: *node = n.rhs; for (leaf_tn != nil) { let lk: nkind = leaf_tn.kind; if (lk == nkind.N_TBANG) { leaf_tn = leaf_tn.lhs; } else { if (lk == nkind.N_TENUM) { leaf_tn = leaf_tn.lhs; } else { if (lk == nkind.N_TNAME) { let lnm: str = leaf_tn.str; if (primsize(lnm) > 0) { break; }; let lal: *node = aliaslookup(c, lnm); if (lal == nil) { leaf_tn = nil; } else { leaf_tn = lal; }; } else { leaf_tn = nil; }; }; }; }; let is_bool: bool = false; if (leaf_tn != nil) { if (leaf_tn.kind == nkind.N_TNAME) { is_bool = streq(leaf_tn.str, "bool"); }; }; // Symmetric narrow on signed vs unsigned (task #5): // unsigned (incl. rune) clears upper bits; signed // sign-extends. bool is size 1 but neither — falls // through to its dedicated ANDQ $255 below. if (sz > 0) { if (sz < 8) { if (!is_bool) { if (!identity) { if (is_unsigned) { if (sz == 4) { emitline("\tMOVL\tAX, AX\n"); } else { let mask: i64 = 0xFFi64; if (sz == 2) { mask = 0xFFFFi64; }; emitline("\tANDQ\t$"); emitint(mask); emitline(", AX\n"); }; } else { if (sz == 1) { emitline("\tMOVSBQ\tAX, AX\n"); } else { if (sz == 2) { emitline("\tMOVSWQ\tAX, AX\n"); } else { if (sz == 4) { emitline("\tMOVSXD\tAX, AX\n"); }; }; }; }; }; }; }; }; if (is_bool) { emitline("\tANDQ\t$255, AX\n"); }; return; }; if (srcfk == 0 && dstfk == 2) { emitline("\tCVTSI2SD\tAX, X0\n"); return; }; if (srcfk == 0 && dstfk == 1) { emitline("\tCVTSI2SS\tAX, X0\n"); return; }; if (srcfk == 2 && dstfk == 0) { emitline("\tCVTTSD2SI\tX0, AX\n"); return; }; if (srcfk == 1 && dstfk == 0) { emitline("\tCVTTSS2SI\tX0, AX\n"); return; }; if (srcfk == 2 && dstfk == 1) { emitline("\tCVTSD2SS\tX0, X0\n"); return; }; if (srcfk == 1 && dstfk == 2) { emitline("\tCVTSS2SD\tX0, X0\n"); return; }; // Same-kind float→float: nothing to emit. }; fn cgstrlit(c: *cgen, n: *node) void = { // str IS []u8: the (ptr, len, cap) triple — ptr in AX, len in BX, // cap in CX. A static literal has no spare storage, so cap = len // (#1/Phase 3). Call sites that expect a str arg pick these up. let nstr: str = n.str; let lab: str = internstrlit(c, nstr); emitline("\tLEAQ\t"); emitbytes( lab.ptr, lab.len: u64); emitline("(SB), AX\n"); emitline("\tMOVQ\t$"); emitint(nstr.len: i64); emitline(", BX\n"); emitline("\tMOVQ\t$"); emitint(nstr.len: i64); emitline(", CX\n"); return; }; fn cgident(c: *cgen, n: *node) void = { let nm: str = n.str; let lc: *local = localfindnode(c, nm); if (lc != nil) { let off: i32 = lc.off; // Float local: MOVSS / MOVSD into X0. Skips the AX shuffle // so consumers (cgbin, cgcast, return) pick up the SSE value // directly. if (isfloattype(c, lc.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, lc.tnode)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); emitoff(off: i64); emitline("(BP), X0\n"); return; }; // str / slice locals load (ptr[, len[, cap]]) through MOVQ // since the header is always 8B-clean. Scalar locals route // through localloadop so signed-narrow slots sign-extend // after a narrow deref-store. let isstr: bool = isstrtype(c, lc.tnode); let issl: bool = isslicetype(c, lc.tnode); let lop: str = "MOVQ"; if (!isstr) { if (!issl) { lop = localloadop(c, lc.tnode); }; }; emitline("\t"); emitline(lop); emitline("\t"); emitoff(off: i64); emitline("(BP), AX\n"); if (isstr) { // str IS []u8: load (ptr,len,cap) into AX/BX/CX, // identical to the slice arm below (#1/Phase 3). emitline("\tMOVQ\t"); emitoff((off + 8): i64); emitline("(BP), BX\n"); emitline("\tMOVQ\t"); emitoff((off + 16): i64); emitline("(BP), CX\n"); }; if (issl) { emitline("\tMOVQ\t"); emitoff((off + 8): i64); emitline("(BP), BX\n"); emitline("\tMOVQ\t"); emitoff((off + 16): i64); emitline("(BP), CX\n"); }; return; }; // Top-level `def` constant — load from its DATA symbol. // Str defs (rhs N_STRLIT) aren't laid out at a SB symbol; the // MOVQ symname(SB) fallback below would emit a bogus reference // (e.g. `alpha.MSG(SB)`, never DATAW-defined). Strlit-inline // the (LEAQ ptr, MOVQ $len) pair instead, mirroring cstage // Sdef walk #1 N_IDENT bare-load (cmd/w6c/cgen.c). Filed #12. if (deflookup(c, nm)) { let drhs: *node = deflookuprhs(c, nm); if (drhs != nil) { if (drhs.kind == nkind.N_STRLIT) { let bytes: str = drhs.str; let lab: str = internstrlit(c, bytes); emitline("\tLEAQ\t"); emitbytes( lab.ptr, lab.len: u64); emitline("(SB), AX\n"); emitline("\tMOVQ\t$"); emitint(bytes.len: i64); emitline(", BX\n"); // str IS []u8: cap = len for a static def literal // (#1/Phase 3). emitline("\tMOVQ\t$"); emitint(bytes.len: i64); emitline(", CX\n"); return; }; }; // Float def: load via LEAQ + MOVSS/MOVSD into X0, same shape // as the let-float arm below — MOVSS/MOVSD have no D_EXTERN // operand form. Pre-#129 fell through to the MOVQ-AX // integer-convention fallback, leaving X0 untouched (#129 // LOAD-side twin of the emitfloatlitdata DATA-side SSoT). if (isfloattype(c, n)) { let mov: str = "MOVSD"; if (isf32type(c, n)) { mov = "MOVSS"; }; emitline("\tLEAQ\t"); emitsymname(c, nm); emitline("(SB), CX\n"); emitline("\t"); emitline(mov); emitline("\t(CX), X0\n"); return; }; emitline("\tMOVQ\t"); emitsymname(c, nm); emitline("(SB), AX\n"); return; }; // Fn-name used as a value (e.g. `let f = some_fn;` or // `... = some_fn;`). LEAQ the symbol address into AX. The // emitfnname helper handles ffiresolve and module-mangling // in one go, so a body-less FFI binding emits the C symbol // it was declared with via @symbol(), not the ww-side ident. // Bare ident → same-module by ww's resolver, hint with c.curmod. let rtyp: *node = fnretlookup(c, nm); if (rtyp != nil) { emitline("\tLEAQ\t"); emitfnname(c, nm, c.curmod); emitline("(SB), AX\n"); return; }; // Top-level mutable `let` — RIP-relative load from its DATAW // slot. Mirrors C cgen's catch-all `MOVQ masym(s), AX` for // scalar lets, plus the (LEAQ, MOVQ, MOVQ[, MOVQ]) sequence // for str / slice globals so the ABI pair / triple lands in // (AX, BX[, CX]). Names that aren't lets either (typos, // never-defined) drop through to the silent return. if (isletvar(c, nm)) { let isstr: bool = letvarisstr(c, nm); let issl: bool = letvarisslice(c, nm); if (isstr || issl) { // str IS []u8: both str and slice carry a third 8B // (cap); load it unconditionally. The address holder CX // is overwritten by the cap as the last step, after // ptr/len are already loaded (#1/Phase 3). emitline("\tLEAQ\t"); emitsymname(c, nm); emitline("(SB), CX\n"); emitline("\tMOVQ\t(CX), AX\n"); emitline("\tMOVQ\t8(CX), BX\n"); emitline("\tMOVQ\t16(CX), CX\n"); return; }; // Float global: same LEAQ-indirect shape, since MOVSS/ // MOVSD have no D_EXTERN operand form in w6a. Signed-narrow // scalar globals route through the same LEAQ scratch since // MOVSXD/MOVSWQ/MOVSBQ also have no D_EXTERN form. let lvtnode: *node = nil; let lv: *letvar = c.lets; for (lv != nil) { if (streq(lv.name, nm)) { if (isfloattype(c, lv.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, lv.tnode)) { mov = "MOVSS"; }; emitline("\tLEAQ\t"); emitsymname(c, nm); emitline("(SB), CX\n"); emitline("\t"); emitline(mov); emitline("\t(CX), X0\n"); return; }; lvtnode = lv.tnode; lv = nil; } else { lv = lv.lvnext; }; }; let glop: str = localloadop(c, lvtnode); if (streq(glop, "MOVQ")) { emitline("\tMOVQ\t"); emitsymname(c, nm); emitline("(SB), AX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, nm); emitline("(SB), CX\n"); emitline("\t"); emitline(glop); emitline("\t(CX), AX\n"); }; return; }; return; }; // cgslicehdr — load the 24B slice/str header at base+0 into the // (AX=ptr, BX=len, CX=cap) triple. base holds the element address; // the load that targets base destroys it, so that word is emitted // LAST. Order otherwise mirrors the slice-field arm (len, cap, ptr). // Shared by the cgindex str-element arms (caller does the kind-gate) // and, later, the typeassert str-variant leaf (#9). c retained unused // for callsite symmetry with cstage cgslicehdr. fn cgslicehdr(c: *cgen, base: str) void = { if (!streq(base, "BX")) { emitmovqload(8i64, base, "BX"); }; if (!streq(base, "CX")) { emitmovqload(16i64, base, "CX"); }; if (!streq(base, "AX")) { emitmovqload(0i64, base, "AX"); }; if (streq(base, "BX")) { emitmovqload(8i64, base, "BX"); }; if (streq(base, "CX")) { emitmovqload(16i64, base, "CX"); }; if (streq(base, "AX")) { emitmovqload(0i64, base, "AX"); }; }; // dotbaseaddr — emit `&(inner.field)` into `dstreg` when `base` is an // N_DOT with N_IDENT inner. 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). Chained // N_DOT (`a.b.c.field[i]`) deferred — not in #135 scope. fn dotbaseaddr(c: *cgen, base: *node, dstreg: str) bool = { if (base == nil) { return false; }; if (base.kind != nkind.N_DOT) { return false; }; let inner: *node = base.lhs; if (inner == nil) { return false; }; if (inner.kind != nkind.N_IDENT) { 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). let lc: *local = localfindnode(c, inner.str); if (lc == nil) { let gt: *node = letvartnode(c, base.str); if (gt != nil && gt.kind == nkind.N_TARRAY) { emitline("\tLEAQ\t"); emitsymname(c, base.str); emitline("(SB), "); emitline(dstreg); emitline("\n"); return true; }; return false; }; let bu: *tinfo = inner.type_: *tinfo; for (bu != nil && bu.kind == tykind.TY_NAMED) { bu = bu.under; }; if (bu == nil) { return false; }; let viaptr: bool = false; let structt: *tinfo = nil; if (bu.kind == tykind.TY_PTR) { let st: *tinfo = bu.sub; for (st != nil && st.kind == tykind.TY_NAMED) { st = st.under; }; if (st != nil) { if (st.kind == tykind.TY_STRUCT) { structt = st; viaptr = true; }; }; } else { if (bu.kind == tykind.TY_STRUCT) { structt = bu; }; }; if (structt == nil) { return false; }; let f: *tfield = structt.fields; let foff: i64 = -1; let ft: *tinfo = nil; for (f != nil) { if (streq(f.name, base.str)) { foff = f.offset: i64; ft = f.type_; break; }; f = f.tnext; }; if (foff < 0) { return false; }; // Only fire on `[N]T` fields — for `*T` / `[]T` / `str` fields // the existing cgexpr(base) path correctly loads the pointer/ // header value; over-firing here would skip the deref. Cstage // twin gate at cg_dotbase_addr. for (ft != nil && ft.kind == tykind.TY_NAMED) { ft = ft.under; }; if (ft == nil) { return false; }; if (ft.kind != tykind.TY_ARRAY) { return false; }; let innoff: i64 = 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 { emitline("\tLEAQ\t"); emitoff(innoff + foff); emitline("(BP), "); emitline(dstreg); emitline("\n"); }; return true; }; fn cgindex(c: *cgen, n: *node) void = { // Element-size-aware load: u8 → MOVZBQ, i32 → MOVSXD, u32 → MOVL, // str → (ptr, len) into (AX, BX), everything else → MOVQ. Fast // path when the base is a bare ident (mem.ww shape). let base: *node = n.lhs; let idx: *node = n.rhs; let esz: i32 = 8; let signed_elem: bool = false; // #119: float element loads route to MOVSS/MOVSD into X0, not the // integer loadopsz into AX. float_elem/f32_elem are set per-branch // from the SAME tinfo esz reads — never a fresh node-stamp (#121). let float_elem: bool = false; let f32_elem: bool = false; // #156 (PREREQ-1 read-half): element is itself an array ([N][M]T → // element [M]T) → leave the sub-array's ADDRESS in the result reg // instead of dereferencing; the outer index adds its offset and the // final scalar element dereferences. Sister of #135. Mirrors cstage // esubu->kind == TY_ARRAY. Node-based (elemisarrayc) for ident bases, // n.type_ tinfo-based for N_DOT/N_INDEX bases — same source split as // esz above. let elem_isarray: bool = false; // #1/Phase 3: str and slice are both 24B (and a >16B struct is // 24B+ too), so the header branches below MUST gate on KIND // (elemisstr/elemisslice, mirroring cstage's elem_is_str|| // elem_is_slice), not a bare `esz == primtypesize("str")` size // check — a size gate would route a plain >16B struct into the // 3-word {ptr,len,cap} load and diverge from cstage (#60 collision // class; sentinel 754). let elemisstr: bool = false; let elemisslice: bool = false; let baselocal: *local = nil; // Global `[N]T` array or `*T` pointer used as an index base. // The local-ident lookup above misses it; we need LEAQ name(SB) // (array, the symbol IS the storage) or MOVQ name(SB) (pointer, // the symbol holds the address) to feed the addend. let isglobalarr: bool = false; let isglobalptr: bool = false; let globalname: str; globalname.ptr = nil; globalname.len = 0; if (base != nil) { if (base.kind == nkind.N_IDENT) { let bn: str = base.str; baselocal = localfindnode(c, bn); if (baselocal != nil) { esz = elemsizeofc(c, baselocal.tnode); signed_elem = elemissignedc(c, baselocal.tnode); float_elem = elemisfloatc(c, baselocal.tnode); f32_elem = elemisf32c(c, baselocal.tnode); elem_isarray = elemisarrayc(c, baselocal.tnode); } else { let tn: *node = letvartnode(c, bn); // #129 A.3: array-typed defs now have DATA storage; // resolve their base via the same N_TARRAY path as // lets. defvartnode is the def-side sister of // letvartnode (parallel to defvarstructinfo at the // A.2 cgdot widening site). if (tn == nil) { tn = defvartnode(c, bn); }; if (tn != nil) { if (tn.kind == nkind.N_TARRAY) { isglobalarr = true; globalname = bn; esz = elemsizeofc(c, tn); signed_elem = elemissignedc(c, tn); float_elem = elemisfloatc(c, tn); f32_elem = elemisf32c(c, tn); elem_isarray = elemisarrayc(c, tn); }; if (tn.kind == nkind.N_TPTR) { isglobalptr = true; 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); }; }; }; } else { if (base.kind == nkind.N_DOT) { // `s.ptr[i]` / struct-field index: stride is the // checker-stamped element tinfo's natural size, the // same idiom as the N_INDEX-base arm below (#60/#72). // cstage idx_eff(base->type)->sub->size (cmd/w6c/ // cgen.c:3517-18). esz-only — N_DOT-base signedness // stays unset, as before. let dt: *tinfo = n.type_: *tinfo; if (dt != nil) { esz = dt.size: i32; elemisstr = typeisstr(dt); elemisslice = typeisslice(dt); float_elem = typeisfloat(dt); f32_elem = typeisf32(dt); }; elem_isarray = tinfoisarray(dt); } else { if (base.kind == nkind.N_INDEX) { // #60: chained `names[i][k]` — n.type_ is the checker- // stamped outer element tinfo (indexresult over the inner // index's value type). cstage reads base->type->sub->size // for esz (cmd/w6c/cgen.c:2070-2071). Drops the // indexvaluetnode walk. let et: *tinfo = n.type_: *tinfo; if (et != nil) { esz = et.size: i32; signed_elem = typeissigned(et); float_elem = typeisfloat(et); f32_elem = typeisf32(et); elem_isarray = tinfoisarray(et); }; };};}; }; // Tagged-union element: load slot words into (AX=tag, DX=val0, // CX=val1) matching the tagged-return ABI so call-arg / let / // match consumers see the same shape as a tagged-returning fn. // Slot size = esz (8/16/24); nullable folded element is one // word, which the fallthrough below handles via MOVQ AX. let elem_tagged: bool = false; let elem_slot_sz: i32 = esz; if (base != nil) { if (base.kind == nkind.N_IDENT) { let bl: *local = baselocal; let etn: *node = nil; if (bl != nil) { let btn: *node = bl.tnode; if (btn != nil) { let bk: nkind = btn.kind; if (bk == nkind.N_TARRAY) { etn = btn.lhs; }; if (bk == nkind.N_TSLICE) { etn = btn.lhs; }; if (bk == nkind.N_TPTR) { etn = btn.lhs; }; }; } else { let tn: *node = letvartnode(c, base.str); if (tn != nil) { let bk: nkind = tn.kind; if (bk == nkind.N_TARRAY) { etn = tn.lhs; }; if (bk == nkind.N_TSLICE) { etn = tn.lhs; }; if (bk == nkind.N_TPTR) { etn = tn.lhs; }; }; }; if (istaggedtype(c, etn)) { if (!isnullabletype(etn)) { elem_tagged = true; elem_slot_sz = slotsize(c, etn); esz = elem_slot_sz; }; }; elemisstr = isstrtype(c, etn); elemisslice = isslicetype(c, etn); }; }; cgexpr(c, idx); if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; if (isglobalarr || isglobalptr) { if (isglobalarr) { emitline("\tLEAQ\t"); emitsymname(c, globalname); emitline("(SB), BX\n"); } else { emitline("\tMOVQ\t"); emitsymname(c, globalname); emitline("(SB), BX\n"); }; emitline("\tADDQ\tAX, BX\n"); // #156: array element ([N][M]T) → leave the sub-array ADDRESS // in AX (BX holds base+idx*esz); nested index dereferences. if (elem_isarray) { emitline("\tMOVQ\tBX, AX\n"); return; }; if (elem_tagged) { if (elem_slot_sz > 24) { emitline("\tMOVQ\t24(BX), R8\n"); }; if (elem_slot_sz > 16) { emitline("\tMOVQ\t16(BX), CX\n"); }; if (elem_slot_sz > 8) { emitline("\tMOVQ\t8(BX), DX\n"); }; emitline("\tMOVQ\t(BX), AX\n"); return; }; // str/slice element: load the full (ptr, len, cap) header into // (AX, BX, CX) — both are 24B since #1, so cap must survive. // Kind-gate on isstrtype||isslicetype, never size==24 (a >16B // struct is 24B+ too but takes the struct-copy path). Base is BX. if (elemisstr || elemisslice) { cgslicehdr(c, "BX"); return; }; // #119: float element → MOVSS/MOVSD into X0 (the consumer's // ADDSD/MOVSD spill machinery already expects X0); the integer // loadopsz below would leave it in AX and the SSE side reads // stale. Twin of cgen.c:2014's scalar-float global load. if (float_elem) { let fop1: str = "MOVSD"; if (f32_elem) { fop1 = "MOVSS"; }; emitline("\t"); emitline(fop1); emitline("\t(BX), X0\n"); return; }; let lop1: str = loadopsz(signed_elem, esz); emitline("\t"); emitline(lop1); emitline("\t(BX), AX\n"); return; }; if (baselocal != nil) { let tn: *node = baselocal.tnode; let isarray: bool = false; if (tn != nil) { if (tn.kind == nkind.N_TARRAY) { isarray = true; }; }; if (isarray) { emitline("\tLEAQ\t"); emitoff(baselocal.off: i64); emitline("(BP), BX\n"); } else { emitline("\tMOVQ\t"); emitoff(baselocal.off: i64); emitline("(BP), BX\n"); }; emitline("\tADDQ\tAX, BX\n"); // #156: array element ([N][M]T) → leave the sub-array ADDRESS // in AX (BX holds base+idx*esz); nested index dereferences. if (elem_isarray) { emitline("\tMOVQ\tBX, AX\n"); return; }; if (elem_tagged) { if (elem_slot_sz > 24) { emitline("\tMOVQ\t24(BX), R8\n"); }; if (elem_slot_sz > 16) { emitline("\tMOVQ\t16(BX), CX\n"); }; if (elem_slot_sz > 8) { emitline("\tMOVQ\t8(BX), DX\n"); }; emitline("\tMOVQ\t(BX), AX\n"); return; }; // str/slice element: full (ptr, len, cap) header into (AX, BX, CX); // cap must survive (#1). Kind-gate, never size==24. Base BX. if (elemisstr || elemisslice) { cgslicehdr(c, "BX"); return; }; // #119: float element → X0 (see the global arm above). if (float_elem) { let fop2: str = "MOVSD"; if (f32_elem) { fop2 = "MOVSS"; }; emitline("\t"); emitline(fop2); emitline("\t(BX), X0\n"); return; }; let lop2: str = loadopsz(signed_elem, esz); emitline("\t"); emitline(lop2); emitline("\t(BX), AX\n"); return; }; // Generic fallback when base isn't a plain ident. // #135: N_DOT base on `[N]T` field needs the field's ADDRESS, // not its value. cgexpr would auto-deref + load the 8-byte value // as if it were a pointer. dotbaseaddr emits the address inline. emitline("\tPUSHQ\tAX\n"); if (!dotbaseaddr(c, base, "AX")) { cgexpr(c, base); }; emitline("\tPOPQ\tBX\n"); emitline("\tADDQ\tBX, AX\n"); // #156: array element ([N][M]T) → AX already holds &elem // (base+idx*esz); a nested index dereferences. See ident arms. if (elem_isarray) { return; }; if (elem_tagged) { // AX holds the element address. Copy to BX (loading slot+0 // into AX clobbers it), then read slot words. emitline("\tMOVQ\tAX, BX\n"); if (elem_slot_sz > 16) { emitline("\tMOVQ\t16(BX), CX\n"); }; if (elem_slot_sz > 8) { emitline("\tMOVQ\t8(BX), DX\n"); }; emitline("\tMOVQ\t(BX), AX\n"); return; }; // str/slice element via fallback base: full (ptr, len, cap) header // into (AX, BX, CX); cap must survive (#1). Kind-gate, never // size==24. Base AX. if (elemisstr || elemisslice) { cgslicehdr(c, "AX"); return; }; // #119: float element → X0 (see the global arm above). The base // address is in AX; MOVSS/MOVSD reads the element into X0. if (float_elem) { let fop3: str = "MOVSD"; if (f32_elem) { fop3 = "MOVSS"; }; emitline("\t"); emitline(fop3); emitline("\t(AX), X0\n"); return; }; let lop3: str = loadopsz(signed_elem, esz); emitline("\t"); emitline(lop3); emitline("\t(AX), AX\n"); return; }; // cgbasecap — load the capacity of a sub-slice's UNDERLYING storage // into `dst` for the #20 cap = base_cap - lo formula (drew: harec // eval.c:1017 slice cap-=start / eval.c:1024 array cap=length-start; // ensure.ha:4-8 distinct capacity field). array [N]T -> N (literal); // slice/str -> the .capacity word in the header at +16 (mirrors the // hi-default +8 length dispatch, emitted unconditionally). Returns // false when base_cap isn't cleanly available so the caller keeps the // prior cap=len: a non-ident base (its header cap was discarded; // recomputing would re-evaluate a possibly side-effecting base -- #74, // which also owns the pre-existing defaulted-hi len gap there), or // a GLOBAL str base (no +16 load here, #73 -- matching the cstage // carve-out keeps both stages byte-identical). cstage twin: // cmd/w6c/cgen.c cg_base_cap. fn cgbasecap(c: *cgen, base: *node, dst: str) bool = { if (base == nil) { return false; }; if (base.kind != nkind.N_IDENT) { return false; }; let baselocal: *local = localfindnode(c, base.str); if (baselocal != nil) { let tn: *node = baselocal.tnode; if (tn == nil) { return false; }; if (tn.kind == nkind.N_TARRAY) { let lenn: *node = tn.rhs; if (lenn == nil) { return false; }; if (lenn.kind != nkind.N_INTLIT) { return false; }; emitline("\tMOVQ\t$"); emituint(lenn.uval); emitline(", "); emitline(dst); emitline("\n"); return true; }; if (tn.kind == nkind.N_TSLICE) { emitline("\tMOVQ\t"); emitoff((baselocal.off + 16): i64); emitline("(BP), "); emitline(dst); emitline("\n"); return true; }; if (tn.kind == nkind.N_TNAME) { if (streq(tn.str, "str")) { emitline("\tMOVQ\t"); emitoff((baselocal.off + 16): i64); emitline("(BP), "); emitline(dst); emitline("\n"); return true; }; }; return false; }; let gt: *node = letvartnode(c, base.str); if (gt == nil) { return false; }; if (gt.kind == nkind.N_TARRAY) { let lenn: *node = gt.rhs; if (lenn == nil) { return false; }; if (lenn.kind != nkind.N_INTLIT) { return false; }; emitline("\tMOVQ\t$"); emituint(lenn.uval); emitline(", "); emitline(dst); emitline("\n"); return true; }; if (gt.kind == nkind.N_TSLICE) { emitline("\tLEAQ\t"); emitsymname(c, base.str); emitline("(SB), "); emitline(dst); emitline("\n"); emitline("\tMOVQ\t16("); emitline(dst); emitline("), "); emitline(dst); emitline("\n"); return true; }; return false; }; // cgslice — `base[lo:hi]` as a slice value. Leaves (AX=base+lo*esz, // BX=hi-lo, CX=base_cap-lo) so callers can route to a slice slot, // return, or arg with the same triple ABI. cap is the storage // remaining to the base's end (#20, Go/Hare-identical) via cgbasecap. // ptr advances by BYTES (lo*esz, #76; ref/hare/rt/ensure.ha:30 // membsz-unit); esz from the type table, mirroring the cgindex idiom. fn cgslice(c: *cgen, n: *node) void = { let base: *node = n.lhs; let lo: *node = n.rhs; let hi: *node = n.cond; let baselocal: *local = nil; let globaltn: *node = nil; let globalname: str; globalname.ptr = nil; globalname.len = 0; if (base != nil) { if (base.kind == nkind.N_IDENT) { baselocal = localfindnode(c, base.str); if (baselocal == nil) { let gt: *node = letvartnode(c, base.str); if (gt != nil) { globaltn = gt; globalname = base.str; }; }; }; }; // esz from the type table for an N_IDENT base (#76; mirrors the // cgindex idiom). Non-ident base stays esz=1 -> ptr unscaled, // matching cstage's base->kind==N_IDENT gate. let esz: i32 = 1; if (baselocal != nil) { esz = elemsizeofc(c, baselocal.tnode); } else { if (globaltn != nil) { esz = elemsizeofc(c, globaltn); };}; // base address if (baselocal != nil) { let tn: *node = baselocal.tnode; let isarray: bool = false; if (tn != nil) { if (tn.kind == nkind.N_TARRAY) { isarray = true; }; }; if (isarray) { emitline("\tLEAQ\t"); emitoff(baselocal.off: i64); emitline("(BP), AX\n"); } else { emitline("\tMOVQ\t"); emitoff(baselocal.off: i64); emitline("(BP), AX\n"); }; } else { if (globaltn != nil) { // Top-level let: [N]T → LEAQ name(SB); pointer/slice/str // → MOVQ name(SB) (the symbol holds the {ptr,len,cap} or // {ptr,len} or pointer value). if (globaltn.kind == nkind.N_TARRAY) { emitline("\tLEAQ\t"); emitsymname(c, globalname); emitline("(SB), AX\n"); } else { emitline("\tMOVQ\t"); emitsymname(c, globalname); emitline("(SB), AX\n"); }; } else { if (base != nil) { cgexpr(c, base); };};}; emitline("\tPUSHQ\tAX\n"); // lo (default 0) if (lo != nil) { cgexpr(c, lo); } else { emitline("\tMOVQ\t$0, AX\n"); }; emitline("\tPUSHQ\tAX\n"); // hi (default base length) if (hi != nil) { cgexpr(c, hi); } else { if (baselocal != nil) { let tn: *node = baselocal.tnode; let handled: bool = false; if (tn != nil) { if (tn.kind == nkind.N_TARRAY) { let lenn: *node = tn.rhs; if (lenn != nil) { if (lenn.kind == nkind.N_INTLIT) { emitline("\tMOVQ\t$"); emituint(lenn.uval); emitline(", AX\n"); handled = true; }; }; } else { if (tn.kind == nkind.N_TSLICE) { emitline("\tMOVQ\t"); emitoff((baselocal.off + 8): i64); emitline("(BP), AX\n"); handled = true; } else { if (tn.kind == nkind.N_TNAME) { if (streq(tn.str, "str")) { emitline("\tMOVQ\t"); emitoff((baselocal.off + 8): i64); emitline("(BP), AX\n"); handled = true; }; };};}; }; if (!handled) { emitline("\tMOVQ\t$0, AX\n"); }; } else { if (globaltn != nil) { let handled: bool = false; if (globaltn.kind == nkind.N_TARRAY) { let lenn: *node = globaltn.rhs; if (lenn != nil) { if (lenn.kind == nkind.N_INTLIT) { emitline("\tMOVQ\t$"); emituint(lenn.uval); emitline(", AX\n"); handled = true; }; }; } else { if (globaltn.kind == nkind.N_TSLICE) { emitline("\tLEAQ\t"); emitsymname(c, globalname); emitline("(SB), CX\n"); emitline("\tMOVQ\t8(CX), AX\n"); handled = true; };}; if (!handled) { emitline("\tMOVQ\t$0, AX\n"); }; } else { emitline("\tMOVQ\t$0, AX\n"); };};}; emitline("\tMOVQ\tAX, BX\n"); emitline("\tPOPQ\tCX\n"); emitline("\tPOPQ\tAX\n"); // ptr = base + lo*esz (#76; ensure.ha:30 membsz-unit). // DX=lo*esz; CX=lo PRESERVED for len + cap (#20). if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", DX\n"); emitline("\tIMULQ\tCX, DX\n"); emitline("\tADDQ\tDX, AX\n"); } else { emitline("\tADDQ\tCX, AX\n"); }; emitline("\tSUBQ\tCX, BX\n"); // cap = base_cap - lo (#20); CX=lo, BX=len here. if (cgbasecap(c, base, "DX")) { emitline("\tSUBQ\tCX, DX\n"); emitline("\tMOVQ\tDX, CX\n"); } else { emitline("\tMOVQ\tBX, CX\n"); }; }; fn cgmatch(c: *cgen, n: *node) void = { // match (e) { case let v: T => stmt; ... } // // Read the tagged-union slot and dispatch by tag. Slot // layout: [+0]=tag, [+8]=value0, [+16]=value1. Bindings // (`case let v: T =>`) get a fresh local slot loaded from // slot+8 (and slot+16 for str-typed payload). let scrut: *node = n.lhs; let scrutoff: i32 = 0; let scrutt: *node = nil; if (scrut != nil) { if (scrut.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, scrut.str); if (lc != nil) { scrutoff = lc.off; scrutt = resolvetagged(c, lc.tnode); }; } else { // Non-ident scrutinee (call result, arr[i], p.field, // ?, etc.). Spill into an `@match_spill` scratch slot // and dispatch off it. Tagged returns (N_CALL) follow // the AX:DX:CX[:R8] convention; tagged-element loads // (N_INDEX) and tagged-field loads (N_DOT, fixed by // #28) produce the same triple. Nullable returns are // single-word (AX = ptr); only +0 is read. // Scrutinee type + spill size resolved through matchscrutt // / matchspillsz at first use (#15) — see cgenutil.ww // (task #9 align-down to cstage). scrutt = matchscrutt(c, scrut); let spillsz: i32 = matchspillsz(c, scrutt); scrutoff = localalloc(c, "@match_spill", spillsz, nil); cgexpr(c, scrut); emitline("\tMOVQ\tAX, "); emitoff(scrutoff: i64); emitline("(BP)\n"); if (!isnullabletype(scrutt)) { emitline("\tMOVQ\tDX, "); emitoff((scrutoff + 8): i64); emitline("(BP)\n"); // CX/R8 writes gated on spill size so 1-word- // payload variants (slot 16B) don't bump the // frame past the tag+word0 the receiver reads. // Mirrors cmd/w6c/cgen.c cgmatch's // `if (slot_size > 16)` / `> 24` guards. if (spillsz > 16) { emitline("\tMOVQ\tCX, "); emitoff((scrutoff + 16): i64); emitline("(BP)\n"); }; if (spillsz > 24) { emitline("\tMOVQ\tR8, "); emitoff((scrutoff + 24): i64); emitline("(BP)\n"); }; }; }; }; let endl: str = mklabel(c, "match_end"); // Push end label as the yield target for this match's arm bodies. if (c.yieldtop < LOOP_MAX) { c.yieldbuf[c.yieldtop] = endl; c.yieldtop += 1; }; let cs: *node = n.list; for (cs != nil) { let nxt: str = mklabel(c, "match_next"); let pat: *node = cs.lhs; let nullable: bool = isnullabletype(scrutt); // Per-arm scope: save c.locals before allocating the bind // and restore after the body runs, so the arm's bind (and // any nested lets) don't leak past the arm. Matches the // checker's newscope/restore around N_MCASE. Without this, // `let e: *T = ...; match (r) { case let e: str => ... }; // use e` would resolve `e` after the match to the inner // str slot instead of the outer ptr. let arm_locals_saved: *local = c.locals; // Compute the variant tag for this arm. Default arm // (no pattern) skips the tag check. if (pat != nil) { if (nullable) { // Discriminator = pointer-vs-null. // *T arm: skip if ptr == 0. // void arm: skip if ptr != 0. let ptr_tag: i32 = nullableptrtag(scrutt); let cur_tag: i32 = 0; if (pat.kind == nkind.N_TPTR) { cur_tag = ptr_tag; } else { if (ptr_tag == 0) { cur_tag = 1; }; }; emitline("\tMOVQ\t"); emitoff(scrutoff: i64); emitline("(BP), AX\n"); emitline("\tCMPQ\t$0, AX\n"); if (cur_tag == ptr_tag) { emitline("\tJE\t"); } else { emitline("\tJNE\t"); }; emitline(nxt); emitline("\n"); } else { let want: i32 = 0; if (scrutt != nil) { // #67: gate on the stamped tinfo, not the node kind // — matchscrutt now returns the scrutinee node itself // for an N_DOT field (its .type_ is the tagged tinfo) // rather than the resolved N_TTAGGED node. if (istaggedtype(c, scrutt)) { let r: i32 = -1; if (pat.kind == nkind.N_TNAME) { r = flatvariantidx(c, scrutt, pat); } else { if (pat.kind == nkind.N_TSLICE) { // `case let s: []T =>` — pat.str is empty // because the variant is a composite, so // route through the slice-shape helper. // Without this every (scalar | []T) match // arm collapses to tag 0 (task #19). r = flatslicevariantidx(c, scrutt, pat.lhs); }; }; if (r >= 0) { want = r; }; }; }; emitline("\tMOVQ\t"); emitoff(scrutoff: i64); emitline("(BP), AX\n"); emitline("\tCMPQ\t$"); emitint(want: i64); emitline(", AX\n"); emitline("\tJNE\t"); emitline(nxt); emitline("\n"); }; }; // Bind `let v: T` from the slot, if requested. let bn: str = cs.str; if (bn.len > 0) { if (pat != nil) { if (nullable) { // Bind the pointer (or skip for the // void arm, which has zero-size). The // value IS slot+0. if (pat.kind == nkind.N_TPTR) { let voff: i32 = localalloc(c, bn, 8, pat); emitline("\tMOVQ\t"); emitoff(scrutoff: i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitoff(voff: i64); emitline("(BP)\n"); }; } else { // Size the bind from the variant's declared // layout. slotsize covers str (16), []T (24), // N_TNAME named struct (si.totsize), aliases, // tuples, primitives (8). Hardcoding str/slice // + fall-through-8 dropped the high words of a // TY_STRUCT variant (e.g. only v.x reached the // bind for `case let v: pair`, project #31); // mirrors cstage's `bu->size` fallback in // cgen.c cgmatch. let bsz: i32 = slotsize(c, pat); if (bsz <= 0) { bsz = 8; }; // localalloc (not localadd): match-arm // binds don't dedup with same-named binds // in *other* matches, since C's cgexpr // allocates a fresh slot per match expr. let voff: i32 = localalloc(c, bn, bsz, pat); // Word-by-word copy. Round bsz up to 8 in case // a non-multiple-of-8 struct size leaked through // (registerstruct already pads totsize, but be // defensive — same shape as cstage's nwords = // (bsz + 7) / 8). let nwords: i32 = (bsz + 7) / 8; let bw: i32 = 0; for (bw < nwords) { emitline("\tMOVQ\t"); emitoff((scrutoff + 8 + 8 * bw): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((voff + 8 * bw): i64); emitline("(BP)\n"); bw += 1; }; }; }; }; // Body. Match arms are statements; we cgstmt them. if (cs.body != nil) { cgstmt(c, cs.body); }; // Restore the locals head — pop everything the arm pushed // so post-match code resolves names to their original (outer) // bindings. c.locals = arm_locals_saved; emitline("\tJMP\t"); emitline(endl); emitline("\n"); emitlabel(nxt); cs = cs.next; }; emitlabel(endl); if (c.yieldtop > 0) { c.yieldtop -= 1; }; return; }; fn cgdot(c: *cgen, n: *node) void = { let lhs: *node = n.lhs; let fld: str = n.str; // `(*p).f` read retarget: parser produces n.lhs = N_UN(STAR, // IDENT(p)). Substitute the inner IDENT as dotlhs so the // pointer-auto-deref branch (lhs.kind == N_IDENT && N_TPTR // tnode) fires the same as `p.f`. Mirror of the N_ASSIGN N_DOT // lhs retarget in cgassign. v1 scope: N_IDENT inner only; // (*expr).f follow-up task pending. Enum-leaf lookup above and // chained-N_DOT branches below keep checking raw lhs since // (*p) is neither shape. let dotlhs: *node = lhs; if (dotlhs != nil) { if (dotlhs.kind == nkind.N_UN) { if (dotlhs.op == tkind.TK_STAR) { if (dotlhs.lhs != nil) { if (dotlhs.lhs.kind == nkind.N_IDENT) { dotlhs = dotlhs.lhs; }; }; }; }; }; // Enum member access: `EnumName.MEMBER` or `pkg.EnumName.MEMBER` // → inline the pre-computed constant. `pkg.Enum.MEMBER` keeps // `pkg` so enumlookupmod can prefer the explicit module on a // leaf collision; bare `Enum.MEMBER` falls back to c.curmod via // enumlookup's same-module-first walk. if (lhs != nil) { let etname: str; let etmod: str; etname.ptr = nil; etname.len = 0; etmod.ptr = nil; etmod.len = 0; if (lhs.kind == nkind.N_IDENT) { etname = lhs.str; }; if (lhs.kind == nkind.N_DOT) { if (lhs.lhs != nil) { if (lhs.lhs.kind == nkind.N_IDENT) { etname = lhs.str; etmod = lhs.lhs.str; }; }; }; if (etname.len > 0) { let en: *enumtype = enumlookupmod(c, etname, etmod); if (en != nil) { let v: u64; if (enummemberval(en, fld, &v)) { emitline("\tMOVQ\t$"); emitint(v: i64); emitline(", AX\n"); return; }; }; }; }; if (dotlhs != nil) { if (dotlhs.kind == nkind.N_IDENT) { let nm: str = dotlhs.str; let lc: *local = localfindnode(c, nm); if (lc != nil) { let tn: *node = lc.tnode; let lkind: nkind = nkind.N_NONE; if (tn != nil) { lkind = tn.kind; }; // Pointer-to-struct: deref then field load. if (lkind == nkind.N_TPTR) { let inner: *node = tn.lhs; let sname: str; sname.ptr = nil; sname.len = 0; if (inner != nil) { if (inner.kind == nkind.N_TNAME) { sname = inner.str; }; }; if (sname.len > 0) { // structlookupchain walks the alias chain on // a miss so `*tokenizer` where tokenizer is // a transitively-aliased struct still // resolves to the underlying fieldinfo (#22). let si: *structinfo = structlookupchain(c, inner); if (si != nil) { let fi: *fieldinfo = si.fields; for (fi != nil) { let fn_: str = fi.fname; if (streq(fn_, fld)) { // tagged-union field via *struct: stage // the *struct in BX, then load the four // payload regs via cgloadtaggedfield. // BX isn't a target (AX/DX/CX/R8), so // load order doesn't matter. Mirrors // the direct-local branch above so the // match / let-init / call-arg consumer // shape is identical regardless of // pointer rooting. if (istaggedtype(c, fi.tnode)) { let tsz: i32 = slotsize(c, fi.tnode); emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); cgloadtaggedfield(c, "BX", fi.foff, tsz); return; }; // str IS []u8 — same 3-word {ptr,len,cap} // as a slice field via *struct: load // (ptr, len, cap) into (AX, BX, CX). BX // holds the *struct pointer, so load .len // LAST so the earlier reads still index // off the base. str folds onto the slice // arm (#1/Phase 3 collapse; cite cstage // cgen.c N_DOT *struct S2). emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); if (isstrtype(c, fi.tnode) || isslicetype(c, fi.tnode)) { emitline("\tMOVQ\t"); emitdispreg(fi.foff: i64, "BX"); emitline(", AX\n"); emitline("\tMOVQ\t"); emitdispreg((fi.foff + 16): i64, "BX"); emitline(", CX\n"); emitline("\tMOVQ\t"); emitdispreg((fi.foff + 8): i64, "BX"); emitline(", BX\n"); } else { if (isfloattype(c, fi.tnode)) { // f64/f32 via *struct: route through X0. // MOVQ into AX leaves the SSE reg stale // and any downstream consumer (arg // pass, return, arithmetic) reads // garbage. let mov: str = "MOVSD"; if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); emitdispreg(fi.foff: i64, "BX"); emitline(", X0\n"); } else { let op: str = fieldloadop(c, fi); emitline("\t"); emitline(op); emitline("\t"); emitdispreg(fi.foff: i64, "BX"); emitline(", AX\n"); }; }; return; }; fi = fi.finext; }; }; }; }; // Direct struct local: field load at off+foff. if (lkind == nkind.N_TNAME) { // structlookupchain walks the alias chain on // miss so a transitively-aliased struct (`type // b = a; a = struct`) still resolves to the // underlying fieldinfo (#22). let si: *structinfo = structlookupchain(c, tn); if (si != nil) { let fi: *fieldinfo = si.fields; for (fi != nil) { let fn_: str = fi.fname; if (streq(fn_, fld)) { // tagged-union field: emit the AX=tag, // DX=word0, CX=word1[, R8=word2] load // sequence so the match / let-init / // call-arg consumers see the same shape // as a tagged-returning fn. Pre-#28 fell // through to the scalar fieldloadop and // only AX (tag) was loaded — payload // words came from whatever the caller // left in DX/CX/R8. if (istaggedtype(c, fi.tnode)) { let tsz: i32 = slotsize(c, fi.tnode); cgloadtaggedfield(c, "BP", lc.off + fi.foff, tsz); return; }; // str IS []u8 — same 3-word {ptr,len,cap} // as a slice field: load (ptr, len, cap) // into (AX, BX, CX). Base is BP so no // aliasing — order doesn't matter. str // folds onto the slice arm (#1/Phase 3 // collapse; cite cstage cgen.c N_DOT S1). if (isstrtype(c, fi.tnode) || isslicetype(c, fi.tnode)) { emitline("\tMOVQ\t"); emitoff((lc.off + fi.foff): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\t"); emitoff((lc.off + fi.foff + 8): i64); emitline("(BP), BX\n"); emitline("\tMOVQ\t"); emitoff((lc.off + fi.foff + 16): i64); emitline("(BP), CX\n"); } else { if (isfloattype(c, fi.tnode)) { // f64/f32 field: route through X0. let mov: str = "MOVSD"; if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); emitoff((lc.off + fi.foff): i64); emitline("(BP), X0\n"); } else { let op: str = fieldloadop(c, fi); emitline("\t"); emitline(op); emitline("\t"); emitoff((lc.off + fi.foff): i64); emitline("(BP), AX\n"); }; }; return; }; fi = fi.finext; }; }; }; // Array pseudo-fields: `.ptr` is the array's // address (LEAQ); `.len` is the static element // count (immediate). if (lkind == nkind.N_TARRAY) { if (streq(fld, "ptr")) { emitline("\tLEAQ\t"); emitoff(lc.off: i64); emitline("(BP), AX\n"); return; }; if (streq(fld, "len")) { let lenn: *node = tn.rhs; let alen: i64 = 0i64; if (lenn != nil) { if (lenn.kind == nkind.N_INTLIT) { alen = lenn.uval: i64; }; }; emitline("\tMOVQ\t$"); emitint(alen); emitline(", AX\n"); return; }; }; // Hare-style tuple positional access: `t.0`, `t.1`. // Walk the tuple element type list summing slotsize // (matches the (scalar, str) init layout: scalar in an // 8B slot, str in 24B — str IS []u8, #1/Phase 3). For a // str element, load (ptr, len, cap) into (AX, BX, CX), // the canonical slice-header ABI. No slice-element // sibling here, so the triple is hand-authored; base is // BP (frame, not a target reg) so ptr/len/cap order has // no clobber risk. if (lkind == nkind.N_TTUPLE) { let idx: i32 = fldnumidx(fld); if (idx >= 0) { let tp: *node = tn.list; let foff: i32 = 0; let i: i32 = 0; for (i < idx) { if (tp == nil) { i = idx; } else { foff += slotsize(c, tp.lhs); tp = tp.next; i += 1; }; }; if (tp != nil) { let tpt: *node = tp.lhs; if (isstrtype(c, tpt)) { emitline("\tMOVQ\t"); emitoff((lc.off + foff + 0): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\t"); emitoff((lc.off + foff + 8): i64); emitline("(BP), BX\n"); emitline("\tMOVQ\t"); emitoff((lc.off + foff + 16): i64); emitline("(BP), CX\n"); return; }; // f64/f32 tuple field must ride X0 via // MOVSD/MOVSS; the integer load op left it // in AX (#103 FACE Z). Mirrors the float // local load above and cstage cgen.c:1462, // 1838 (the #96 pattern). if (isfloattype(c, tpt)) { let mov: str = "MOVSD"; if (isf32type(c, tpt)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); emitoff((lc.off + foff): i64); emitline("(BP), X0\n"); return; }; let sz: i32 = slotsize(c, tpt); let op: str = tnodeloadop(c, tpt, sz); emitline("\t"); emitline(op); emitline("\t"); emitoff((lc.off + foff): i64); emitline("(BP), AX\n"); return; }; }; }; // str/slice pseudo-fields .ptr/.len/.cap on a // direct local: load at slot+delta. let delta: i32 = -1; if (streq(fld, "ptr")) { delta = 0; }; if (streq(fld, "len")) { delta = 8; }; if (streq(fld, "cap")) { delta = 16; }; if (delta >= 0) { // Pointer to str/slice (`*[]u8`, `*str`): // deref, then load at delta within the // pointed-to header. C cgen does the same. if (lkind == nkind.N_TPTR) { let inner: *node = tn.lhs; let innerkind: nkind = nkind.N_NONE; if (inner != nil) { innerkind = inner.kind; }; let innerstr: bool = false; if (innerkind == nkind.N_TNAME) { if (streq(inner.str, "str")) { innerstr = true; }; }; if (innerkind == nkind.N_TSLICE) { innerstr = true; }; if (innerstr) { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); emitline("\tMOVQ\t"); emitdispreg(delta: i64, "BX"); emitline(", AX\n"); return; }; }; emitline("\tMOVQ\t"); emitoff((lc.off + delta): i64); emitline("(BP), AX\n"); return; }; }; }; }; // `def NAME: str = "..."` field access — inline the literal. // Sdef-backed strs aren't laid out in memory, so falling // through to the SB-load fallback below would mis-emit // `MOVQ (SB), AX` (looking up the field name as a // symbol). Mirrors cmd/w6c/cgen.c nkind.N_DOT off==0 / Sdef branch. if (lhs != nil) { if (lhs.kind == nkind.N_IDENT) { let drhs: *node = deflookuprhs(c, lhs.str); if (drhs != nil) { if (drhs.kind == nkind.N_STRLIT) { let bytes: str = drhs.str; if (streq(fld, "ptr")) { let lab: str = internstrlit(c, bytes); emitline("\tLEAQ\t"); emitbytes( lab.ptr, lab.len: u64); emitline("(SB), AX\n"); return; }; if (streq(fld, "len")) { emitline("\tMOVQ\t$"); emitint(bytes.len: i64); emitline(", AX\n"); return; }; }; }; }; }; // Top-level str/slice global field access — load .ptr / .len // (and .cap for slices) via &name(SB) into CX, then MOVQ // delta(CX), AX. Without this the module-qualified fallback // below would mis-emit `MOVQ (SB), AX`. if (lhs != nil) { if (lhs.kind == nkind.N_IDENT) { if (isletvar(c, lhs.str)) { let isstr: bool = letvarisstr(c, lhs.str); let issl: bool = letvarisslice(c, lhs.str); if (isstr || issl) { let delta: i32 = -1; if (streq(fld, "ptr")) { delta = 0; }; if (streq(fld, "len")) { delta = 8; }; // str IS []u8: .cap is valid on a str global too, // not slice-only — mirrors cstage (#1/Phase 3, #11). if (streq(fld, "cap")) { delta = 16; }; if (delta >= 0) { emitline("\tLEAQ\t"); emitsymname(c, lhs.str); emitline("(SB), CX\n"); emitline("\tMOVQ\t"); emitdispreg(delta: i64, "CX"); emitline(", AX\n"); return; }; }; }; }; }; // Top-level struct global field read — LEAQ name(SB), CX then // load at fi.foff(CX). Mirrors the local "Direct struct local" // branch above, swapping the BP frame slot for the global VA. // Field-width-aware op handles MOVQ / MOVL / MOVZBQ / MOVSXD. // #129 A.2: also handles struct-typed `def`s via defvarstructinfo; // emitstructdata gives them DATA storage at name(SB), and this // LEAQ-and-offset shape mirrors the let path. Pre-A.2 the def // fell through to the integer-let MOVQ catch-all (reading garbage // from the wrong offset). if (lhs != nil) { if (lhs.kind == nkind.N_IDENT) { let si: *structinfo = letvarstructinfo(c, lhs.str); if (si == nil) { si = defvarstructinfo(c, lhs.str); }; if (si != nil) { let fi: *fieldinfo = si.fields; for (fi != nil) { if (streq(fi.fname, fld)) { emitline("\tLEAQ\t"); emitsymname(c, lhs.str); emitline("(SB), CX\n"); // tagged-union field: load via the tagged- // return ABI off CX. cgloadtaggedfield orders // the loads so CX (word1 target) is written // LAST — otherwise the base address would be // trashed before the +24/R8 (slice variant) // read could index off it. Pre-#28 fell // through to fieldloadop and dropped payload. if (istaggedtype(c, fi.tnode)) { let tsz: i32 = slotsize(c, fi.tnode); cgloadtaggedfield(c, "CX", fi.foff, tsz); return; }; // str IS []u8 — 3-word {ptr,len,cap}, the local // slice-field arm (BP) retargeted to the CX global // base. cap→CX LAST: CX is the base, so .ptr/.len // must read first. cstage folds local+global in one // base_reg arm; ww splits them, so this global arm // carries its own lift (filed divergence task). if (isstrtype(c, fi.tnode)) { emitline("\tMOVQ\t"); emitdispreg(fi.foff: i64, "CX"); emitline(", AX\n"); emitline("\tMOVQ\t"); emitdispreg((fi.foff + 8): i64, "CX"); emitline(", BX\n"); emitline("\tMOVQ\t"); emitdispreg((fi.foff + 16): i64, "CX"); emitline(", CX\n"); } else { if (isfloattype(c, fi.tnode)) { // f64/f32 global field: route through X0. let mov: str = "MOVSD"; if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); emitdispreg(fi.foff: i64, "CX"); emitline(", X0\n"); } else { let op: str = fieldloadop(c, fi); emitline("\t"); emitline(op); emitline("\t"); emitdispreg(fi.foff: i64, "CX"); emitline(", AX\n"); }; }; return; }; fi = fi.finext; }; }; }; }; // `arr[i].field` — element-then-field through a `[N]*S` / `[N]S` // (and slice/`*[N]S`) base. Without this the cgen falls through // to the module-qualified SB fallback below and emits // `MOVQ (SB), AX` (linker: `undefined reference to `). // One branch covers both shapes: compute `&arr[i]` into BX, then // either deref (`*Struct` element) or move-to-AX (value `Struct` // element), so the leaf load is `(field.offset)(AX)` either way. // Bypasses cgindex deliberately — cgindex's final MOVQ would // truncate a value-struct element to 8 bytes. if (lhs != nil) { if (lhs.kind == nkind.N_INDEX) { let idxbase: *node = lhs.lhs; if (idxbase != nil) { if (idxbase.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, idxbase.str); if (lc != nil) { if (lc.tnode != nil) { let tn: *node = lc.tnode; let elemt: *node = nil; let baseisarray: bool = false; let tk: nkind = tn.kind; if (tk == nkind.N_TSLICE) { elemt = tn.lhs; }; if (tk == nkind.N_TARRAY) { elemt = tn.lhs; baseisarray = true; }; if (tk == nkind.N_TPTR) { elemt = tn.lhs; }; let sname: str; sname.ptr = nil; sname.len = 0; let viaptr: bool = false; if (elemt != nil) { if (elemt.kind == nkind.N_TPTR) { let inner: *node = elemt.lhs; if (inner != nil) { if (inner.kind == nkind.N_TNAME) { sname = inner.str; viaptr = true; };}; } else { if (elemt.kind == nkind.N_TNAME) { sname = elemt.str; };}; }; if (sname.len > 0) { let si: *structinfo = structlookup(c, sname); if (si != nil) { let fi: *fieldinfo = si.fields; for (fi != nil) { if (streq(fi.fname, fld)) { let esz: i32 = elemsizeofc(c, tn); cgexpr(c, lhs.rhs); // idx → AX if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; if (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"); }; if (isstrtype(c, fi.tnode) || isslicetype(c, fi.tnode)) { // str/slice: the 3-word {ptr,len,cap} // slice header (#1). AX holds the // element base, so load .ptr (which // targets AX) LAST. Matches the // caseB *struct slice arm and // cgslicehdr(D_AX). emitline("\tMOVQ\t"); emitdispreg((fi.foff + 8): i64, "AX"); emitline(", BX\n"); emitline("\tMOVQ\t"); emitdispreg((fi.foff + 16): i64, "AX"); emitline(", CX\n"); emitline("\tMOVQ\t"); emitdispreg(fi.foff: i64, "AX"); emitline(", AX\n"); return; }; if (isfloattype(c, fi.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); emitdispreg(fi.foff: i64, "AX"); emitline(", X0\n"); return; }; let lop: str = fieldloadop(c, fi); emitline("\t"); emitline(lop); emitline("\t"); emitdispreg(fi.foff: i64, "AX"); emitline(", AX\n"); return; }; fi = fi.finext; }; }; }; };}; };}; }; }; // Module-qualified value reference: `mod.name` where `mod` // is nkind.N_IDENT bound as skind.SK_USE and the leaf isn't a local. // Treat as a SB symbol — `MOVQ leaf(SB), AX` for the 8B case; // signed-narrow leaves route through LEAQ + localloadop so a // prior narrow deref-store doesn't leave stale upper bytes. Same // fallback the C cgen takes when bt is NULL/tyerr. if (lhs != nil) { if (lhs.kind == nkind.N_IDENT) { // `let p = mod.fn` — fn rvalue via N_DOT. Mirror of // cstage cgdot's TY_FN branch (mafn with module hint). // Without this the MOVQ leaf(SB) fallback below would // load 8 bytes of fn-prologue code into AX instead of // the fn address. // lhs.str is the explicit module hint so a same-leaf // def in another module (head of c.fnrets) can't shadow // the explicit qualifier (#17 N_DOT-arm omission audit). let frt: *node = fnretlookupmod(c, fld, lhs.str); if (frt != nil) { emitline("\tLEAQ\t"); emitfnname(c, fld, lhs.str); emitline("(SB), AX\n"); return; }; // `mod.MSG` where MSG is `def MSG: str = "..."` — // strlit-inline matches cstage Sdef walk #2 in // cmd/w6c/cgen.c N_DOT mod-qualified. Without this // the MOVQ leaf(SB) fallback emits a bogus ref // (`alpha.MSG(SB)`, never DATAW-defined). lhs.str is // the explicit module hint — a 3rd-module qualifier // `alpha.MSG` from gamma needs alpha (not c.curmod) // to beat a head-of-c.defs beta.MSG collision (#11). let drhs: *node = deflookuprhsmod(c, fld, lhs.str); if (drhs != nil) { if (drhs.kind == nkind.N_STRLIT) { let bytes: str = drhs.str; let lab: str = internstrlit(c, bytes); emitline("\tLEAQ\t"); emitbytes( lab.ptr, lab.len: u64); emitline("(SB), AX\n"); emitline("\tMOVQ\t$"); emitint(bytes.len: i64); emitline(", BX\n"); return; }; }; let mqop: str = localloadop(c, letvartnode(c, fld)); if (streq(mqop, "MOVQ")) { emitline("\tMOVQ\t"); emitsymname(c, fld); emitline("(SB), AX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, fld); emitline("(SB), CX\n"); emitline("\t"); emitline(mqop); emitline("\t(CX), AX\n"); }; return; }; }; // Chained N_DOT spine through value-struct fields (any depth). // Walks the spine to a root ident, summing field offsets, then // emits ONE load at base + total_off. Also handles a slice/str // pseudo-field leaf (`b.buf.len`): the walk lands on the slice/ // str header and slicedelta picks ptr/len/cap. Mirror of cstage // cgen.c's chained-DOT read branch. Without this, depth ≥ 3 // shapes (`v.a.a.a`) and `b.buf.len` fall through to the non- // ident-base pseudo branch below — which would cgexpr the inner // (loading only .ptr into AX) and shuffle stale BX into AX. // Placed BEFORE the .ptr/.len fast paths so the chain wins. if (lhs != nil) { if (lhs.kind == nkind.N_DOT) { let rootname: str = ""; let rootoff: i32 = 0; let totaloff: i32 = 0; let leaftype: *tinfo = nil; let slicedelta: i32 = -1; let isglobal: bool = false; let ptrroot: bool = false; let pok: bool = dotchainresolve(c, n, &rootname, &rootoff, &totaloff, &leaftype, &slicedelta, &isglobal, &ptrroot); if (pok) { // `*T` root: load the pointer slot once into CX, // then index every leaf at total_off off CX. Same // emit shape as the global path (LEAQ → CX) — only // the loader instruction differs. let viacx: bool = isglobal || ptrroot; if (slicedelta >= 0) { if (viacx) { if (ptrroot) { emitline("\tMOVQ\t"); emitoff(rootoff: i64); emitline("(BP), CX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, rootname); emitline("(SB), CX\n"); }; emitline("\tMOVQ\t"); emitdispreg((totaloff + slicedelta): i64, "CX"); emitline(", AX\n"); } else { emitline("\tMOVQ\t"); emitoff((rootoff + totaloff + slicedelta): i64); emitline("(BP), AX\n"); }; return; }; if (typeisstr(leaftype)) { if (viacx) { if (ptrroot) { emitline("\tMOVQ\t"); emitoff(rootoff: i64); emitline("(BP), CX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, rootname); emitline("(SB), CX\n"); }; emitline("\tMOVQ\t"); emitdispreg(totaloff: i64, "CX"); emitline(", AX\n"); emitline("\tMOVQ\t"); emitdispreg((totaloff + 8): i64, "CX"); emitline(", BX\n"); } else { emitline("\tMOVQ\t"); emitoff((rootoff + totaloff): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\t"); emitoff((rootoff + totaloff + 8): i64); emitline("(BP), BX\n"); }; return; }; if (typeisslice(leaftype)) { // Slice leaf: load all three header words into // (AX=ptr, BX=len, CX=cap). For the viacx path // (global or `*T` root) CX is the base; load // .cap LAST so the base survives the earlier // reads. For BP-rooted locals the registers // don't alias so order is free. if (viacx) { if (ptrroot) { emitline("\tMOVQ\t"); emitoff(rootoff: i64); emitline("(BP), CX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, rootname); emitline("(SB), CX\n"); }; emitline("\tMOVQ\t"); emitdispreg(totaloff: i64, "CX"); emitline(", AX\n"); emitline("\tMOVQ\t"); emitdispreg((totaloff + 8): i64, "CX"); emitline(", BX\n"); emitline("\tMOVQ\t"); emitdispreg((totaloff + 16): i64, "CX"); emitline(", CX\n"); } else { emitline("\tMOVQ\t"); emitoff((rootoff + totaloff): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\t"); emitoff((rootoff + totaloff + 8): i64); emitline("(BP), BX\n"); emitline("\tMOVQ\t"); emitoff((rootoff + totaloff + 16): i64); emitline("(BP), CX\n"); }; return; }; if (typeisfloat(leaftype)) { let mov: str = "MOVSD"; if (typeisf32(leaftype)) { mov = "MOVSS"; }; if (viacx) { if (ptrroot) { emitline("\tMOVQ\t"); emitoff(rootoff: i64); emitline("(BP), CX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, rootname); emitline("(SB), CX\n"); }; emitline("\t"); emitline(mov); emitline("\t"); emitdispreg(totaloff: i64, "CX"); emitline(", X0\n"); } else { emitline("\t"); emitline(mov); emitline("\t"); emitoff((rootoff + totaloff): i64); emitline("(BP), X0\n"); }; return; }; let lop: str = loadopsz(typeissigned(leaftype), leaftype.slotsize: i32); if (viacx) { if (ptrroot) { emitline("\tMOVQ\t"); emitoff(rootoff: i64); emitline("(BP), CX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, rootname); emitline("(SB), CX\n"); }; emitline("\t"); emitline(lop); emitline("\t"); emitdispreg(totaloff: i64, "CX"); emitline(", AX\n"); } else { emitline("\t"); emitline(lop); emitline("\t"); emitoff((rootoff + totaloff): i64); emitline("(BP), AX\n"); }; return; }; }; }; // Non-ident base pseudo-field: e.g. `"abc".ptr` / `"abc".len`. // Evaluate the str-producing expression — that leaves // (AX=ptr, BX=len). Then `.ptr` returns AX as is; `.len` // shuffles BX→AX. Mirrors what C cgen does (it just evaluates // the literal and picks the half it wants). if (streq(fld, "ptr")) { cgexpr(c, lhs); return; }; if (streq(fld, "len")) { cgexpr(c, lhs); emitline("\tMOVQ\tBX, AX\n"); return; }; // Chained struct-field-via-ptr-via-ptr access: // r.sym.val where r: *lrel, .sym: *lsym, .val: u64 // Inner DOT (`r.sym`) returns a *struct (a pointer-to-struct // field). Outer DOT dereferences and reads `val`. Without this // path the cgen falls through and AX retains whatever the // inner expression left there — typically the *struct pointer // itself, so reads silently get the pointer value instead of // the field. (Showed up porting w6l/pass.ww.) if (lhs != nil) { if (lhs.kind == nkind.N_DOT) { // #70 (#12): inner-struct layout via the stamped lhs.type_ // (peel *→struct) + tinfo.fields, replacing dotinnerstructptr's // structinfo walk. Gate is strict-equal to the deleted helper: // fire only when the chain root is a LOCAL ident AND every dot // in the chain resolves through a *struct (dotinnerstructptr // recursed per level on a *struct field and bailed on a by- // value-struct intermediate). Reproducing that exactly avoids // an untested widening past cstage; a deliberate widen, if ever // wanted, is a future task with its own probe. Global-root // chains stay in their pre-existing shared base-eval breakage // (filed #27), untouched here. let croot: *node = lhs; let allptr: bool = true; for (croot != nil && croot.kind == nkind.N_DOT) { let ct: *tinfo = croot.type_: *tinfo; for (ct != nil && ct.kind == tykind.TY_NAMED) { ct = ct.under; }; let okp: bool = false; if (ct != nil) { if (ct.kind == tykind.TY_PTR) { let cs: *tinfo = ct.sub; for (cs != nil && cs.kind == tykind.TY_NAMED) { cs = cs.under; }; if (cs != nil) { if (cs.kind == tykind.TY_STRUCT) { okp = true; }; }; }; }; if (!okp) { allptr = false; }; croot = croot.lhs; }; let it: *tinfo = nil; if (allptr) { if (croot != nil) { if (croot.kind == nkind.N_IDENT) { if (localfindnode(c, croot.str) != nil) { it = lhs.type_: *tinfo; }; }; }; }; for (it != nil && it.kind == tykind.TY_NAMED) { it = it.under; }; if (it != nil) { if (it.kind == tykind.TY_PTR) { let st: *tinfo = it.sub; for (st != nil && st.kind == tykind.TY_NAMED) { st = st.under; }; if (st != nil) { if (st.kind == tykind.TY_STRUCT) { let tf: *tfield = st.fields; for (tf != nil) { if (streq(tf.name, fld)) { let ft: *tinfo = tf.type_; cgexpr(c, lhs); // AX = ptr to inner struct // str IS []u8 — same 3-word {ptr,len,cap} // as a slice field: load (ptr, len, cap) // into (AX, BX, CX). AX is the *struct // base, so load .ptr (which targets // AX) LAST. str folds onto the slice // arm (#1/Phase 3 collapse; cite cstage // cgen.c N_DOT chained *struct caseB). if (typeisstr(ft) || typeisslice(ft)) { emitline("\tMOVQ\t"); emitdispreg((tf.offset + 8u64): i64, "AX"); emitline(", BX\n"); emitline("\tMOVQ\t"); emitdispreg((tf.offset + 16u64): i64, "AX"); emitline(", CX\n"); emitline("\tMOVQ\t"); emitdispreg(tf.offset: i64, "AX"); emitline(", AX\n"); return; }; // f64/f32 chained field: route through X0. if (typeisfloat(ft)) { let mov: str = "MOVSD"; if (typeisf32(ft)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); emitdispreg(tf.offset: i64, "AX"); emitline(", X0\n"); return; }; let lop: str = loadopsz(typeissigned(ft), ft.slotsize: i32); emitline("\t"); emitline(lop); emitline("\t"); emitdispreg(tf.offset: i64, "AX"); emitline(", AX\n"); return; }; tf = tf.tnext; }; }; }; }; }; }; }; // Chained `(ident).f1.f2` read where f1 is a struct-by-value // field. Mirror of the cgassign branch added for the same shape. // Without this, `L.cur.kind` (cur a by-value struct of *L) // falls into the SB-fallback and emits `MOVQ kind(SB), AX`. // Kept as a fallback below the generalized walker above (placed // earlier in cgdot) to preserve byte-identical output on shapes // it already handles. if (lhs != nil) { if (lhs.kind == nkind.N_DOT) { let inner: *node = lhs.lhs; let innerfld: str = lhs.str; if (inner != nil) { if (inner.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, inner.str); if (lc != nil) { if (lc.tnode != nil) { let tn: *node = lc.tnode; let lkind: nkind = tn.kind; let outname: str; outname.ptr = nil; outname.len = 0; let isptr: bool = false; if (lkind == nkind.N_TNAME) { outname = tn.str; }; if (lkind == nkind.N_TPTR) { let pe: *node = tn.lhs; if (pe != nil) { if (pe.kind == nkind.N_TNAME) { outname = pe.str; isptr = true; };}; }; if (outname.len > 0) { let osi: *structinfo = structlookup(c, outname); if (osi != nil) { let ofi: *fieldinfo = osi.fields; for (ofi != nil) { if (streq(ofi.fname, innerfld)) { let oft: *node = ofi.tnode; if (oft != nil) { if (oft.kind == nkind.N_TNAME) { if (primsize(oft.str) == 0) { let isi: *structinfo = structlookup(c, oft.str); if (isi != nil) { let ffi: *fieldinfo = isi.fields; for (ffi != nil) { if (streq(ffi.fname, fld)) { let totoff: i32 = ofi.foff + ffi.foff; if (isstrtype(c, ffi.tnode)) { if (isptr) { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), CX\n"); emitline("\tMOVQ\t"); emitdispreg((totoff + 8): i64, "CX"); emitline(", BX\n"); emitline("\tMOVQ\t"); emitdispreg(totoff: i64, "CX"); emitline(", AX\n"); } else { emitline("\tMOVQ\t"); emitoff((lc.off + totoff): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\t"); emitoff((lc.off + totoff + 8): i64); emitline("(BP), BX\n"); }; return; }; if (isfloattype(c, ffi.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, ffi.tnode)) { mov = "MOVSS"; }; if (isptr) { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); emitline("\t"); emitline(mov); emitline("\t"); emitdispreg(totoff: i64, "BX"); emitline(", X0\n"); } else { emitline("\t"); emitline(mov); emitline("\t"); emitoff((lc.off + totoff): i64); emitline("(BP), X0\n"); }; return; }; let lop: str = fieldloadop(c, ffi); if (isptr) { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); emitline("\t"); emitline(lop); emitline("\t"); emitdispreg(totoff: i64, "BX"); emitline(", AX\n"); } else { emitline("\t"); emitline(lop); emitline("\t"); emitoff((lc.off + totoff): i64); emitline("(BP), AX\n"); }; return; }; ffi = ffi.finext; }; }; }; };}; }; ofi = ofi.finext; }; }; }; };}; };}; }; }; // Nested module-qualified field where the chain didn't fold to a // known shape (raw w6c on a single file with `use mod;` but no // driver concatenation — the inner enum / struct hasn't been // seen). Emit `MOVQ (SB), AX` so the linker surfaces a // clean undefined-symbol error on the leaf. Mirror of // cmd/w6c/cgen.c N_DOT nested fallback. if (lhs != nil) { if (lhs.kind == nkind.N_DOT) { emitline("\tMOVQ\t"); emitsymname(c, fld); emitline("(SB), AX\n"); return; }; }; return; }; fn cgun(c: *cgen, n: *node) void = { // Match C cgen ordering: evaluate operand first (load into AX), // then apply the unary op. AMP / STAR override AX with the // address / deref. The wasted load before AMP keeps our asm // byte-identical to the C version. let fk: i32 = 0; if (n.lhs != nil) { let lt: *tinfo = n.lhs.type_: *tinfo; if (typeisf32(lt)) { fk = 1; } else { if (typeisfloat(lt)) { fk = 2; }; }; }; if (n.op == tkind.TK_MINUS && fk != 0) { // Float negate: X0 = 0 - X0. Stash orig, load 0.0, subtract. // Zero bit pattern equals 0.0 for both f32 and f64 so we // reuse the integer-zero materialisation. let mov: str = "MOVSD"; let sub: str = "SUBSD"; if (fk == 1) { mov = "MOVSS"; sub = "SUBSS"; }; cgexpr(c, n.lhs); emitline("\tSUBQ\t$8, SP\n"); emitline("\t"); emitline(mov); emitline("\tX0, (SP)\n"); emitline("\tMOVQ\t$0, AX\n"); emitline("\tPUSHQ\tAX\n"); emitline("\t"); emitline(mov); emitline("\t(SP), X0\n"); emitline("\tADDQ\t$8, SP\n"); emitline("\t"); emitline(mov); emitline("\t(SP), X1\n"); emitline("\tADDQ\t$8, SP\n"); emitline("\t"); emitline(sub); emitline("\tX1, X0\n"); return; }; // Address-of has its own evaluation strategy — we want the address // of the operand, not its value. Special-case here so `&arr[i]` // doesn't compile the value load and then discard it. if (n.op == tkind.TK_AMP) { let opnd: *node = n.lhs; if (opnd != nil) { if (opnd.kind == nkind.N_IDENT) { let nm: str = opnd.str; let off: i32 = localfind(c, nm); if (off != 0) { emitline("\tLEAQ\t"); emitoff(off: i64); emitline("(BP), AX\n"); return; }; // #180: address-of a top-level fn name. Twin of // the N_IDENT value-of-fn read-arm in cgident // (LEAQ + emitfnname(c, nm, c.curmod)). Previously // fell through silently — the AX-store at the // assign site picked up whatever AX held. if (fnretlookup(c, nm) != nil) { emitline("\tLEAQ\t"); emitfnname(c, nm, c.curmod); emitline("(SB), AX\n"); return; }; if (isletvar(c, nm)) { emitline("\tLEAQ\t"); emitsymname(c, nm); emitline("(SB), AX\n"); return; }; // #149/#147: address-of a top-level def with DATA // storage. emitdefs / emitstructdata / emitarraydata // all emit to emitsymname(name), so the address is // the same LEAQ name(SB) as a let. Address-of twin of // A.2/A.3's LOAD-side widening. if (defisaddressable(c, opnd)) { emitline("\tLEAQ\t"); emitsymname(c, nm); emitline("(SB), AX\n"); return; }; // rule-7: the name IS a def but has no DATA symbol // (str def inlined, or computed-rhs float like // `def NAN = 0.0/0.0`). Loud, not a wild deref. if (deflookup(c, nm)) { let m1: str = "ww: cannot take address of non-addressable def '"; os.write(2, m1.ptr, m1.len: u64); os.write(2, nm.ptr, nm.len: u64); let m2: str = "': no DATA symbol (str/computed-rhs def; #149/#147)\n"; os.write(2, m2.ptr, m2.len: u64); os.exit(1); }; return; }; // Address-of through a DOT chain. Mirror of cstage // cgen.c TK_AMP N_DOT branch. Three shapes converge // here, all returning an 8B address (no fldloadop — // just LEAQ / MOVQ+LEAQ). // // 1. Value-struct fields, any depth (`&o.f`, // `&o.i.a`, `&o.a.b.c`) and slice/str pseudo-field // tail (`&s.len`, `&b.buf.len`): the chained // (depth ≥ 2) case reuses dotchainresolve; the // single-DOT case is handled below by inspecting // the IDENT base's tnode. Byte-identical to the // cstage spine walker for both depths. // 2. Pointer-field (`&p.f` where p:*T): single-DOT // only; spine walker aborts on the *T base. Load // p into AX, then LEAQ field_off(AX), AX. Mirror // of the read at cgdot 1144. if (opnd.kind == nkind.N_DOT) { // Shape 1 chained: depth-≥2 via dotchainresolve. // `opnd.lhs.kind == N_DOT` gates the helper at // nsteps ≥ 2 (matches the read path's gate). if (opnd.lhs != nil) { if (opnd.lhs.kind == nkind.N_DOT) { let rootname: str = ""; let rootoff: i32 = 0; let totaloff: i32 = 0; let leaftype: *tinfo = nil; let slicedelta: i32 = -1; let isglobal: bool = false; let ptrroot: bool = false; let pok: bool = dotchainresolve(c, opnd, &rootname, &rootoff, &totaloff, &leaftype, &slicedelta, &isglobal, &ptrroot); // `&` through a `*T`-rooted chain is a // separate shape (would need MOVQ + LEAQ // disp(CX), AX). Not exercised by current // callers — skip and fall through. if (ptrroot) { pok = false; }; if (pok) { let extra: i32 = 0; if (slicedelta >= 0) { extra = slicedelta; }; if (isglobal) { emitline("\tLEAQ\t"); emitsymname(c, rootname); emitline("(SB), CX\n"); emitline("\tLEAQ\t"); emitdispreg((totaloff + extra): i64, "CX"); emitline(", AX\n"); } else { emitline("\tLEAQ\t"); emitoff((rootoff + totaloff + extra): i64); emitline("(BP), AX\n"); }; return; }; }; }; // Shape 1/2 single-DOT on an IDENT base. Inspect // the base's tnode to pick value-struct vs slice/ // str pseudo vs pointer-field. if (opnd.lhs != nil) { if (opnd.lhs.kind == nkind.N_IDENT) { let basenm: str = opnd.lhs.str; let fld: str = opnd.str; let lc: *local = localfindnode(c, basenm); if (lc != nil) { let tn: *node = lc.tnode; let lkind: nkind = nkind.N_NONE; if (tn != nil) { lkind = tn.kind; }; // Pointer-field: &p.f where p:*T. if (lkind == nkind.N_TPTR) { let inner: *node = tn.lhs; let sname: str; sname.ptr = nil; sname.len = 0; if (inner != nil) { if (inner.kind == nkind.N_TNAME) { sname = inner.str; }; }; if (sname.len > 0) { let si: *structinfo = structlookup(c, sname); if (si != nil) { let fi: *fieldinfo = si.fields; for (fi != nil) { if (streq(fi.fname, fld)) { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), AX\n"); emitline("\tLEAQ\t"); emitdispreg(fi.foff: i64, "AX"); emitline(", AX\n"); return; }; fi = fi.finext; }; }; }; }; // Value-struct local: &o.f. if (lkind == nkind.N_TNAME) { let sname: str = tn.str; let si: *structinfo = structlookup(c, sname); if (si != nil) { let fi: *fieldinfo = si.fields; for (fi != nil) { if (streq(fi.fname, fld)) { emitline("\tLEAQ\t"); emitoff((lc.off + fi.foff): i64); emitline("(BP), AX\n"); return; }; fi = fi.finext; }; }; }; // Slice/str pseudo-field on a local: // &s.ptr / &s.len / &s.cap. Delta is // 0/8/16 — matches the spine walker. let delta: i32 = -1; if (streq(fld, "ptr")) { delta = 0; }; if (streq(fld, "len")) { delta = 8; }; if (streq(fld, "cap")) { delta = 16; }; if (delta >= 0) { let isslor: bool = false; if (lkind == nkind.N_TSLICE) { isslor = true; }; if (lkind == nkind.N_TNAME) { if (streq(tn.str, "str")) { isslor = true; }; }; if (isslor) { emitline("\tLEAQ\t"); emitoff((lc.off + delta): i64); emitline("(BP), AX\n"); return; }; }; }; // Global root: top-level let, either a // struct or a slice/str. if (isletvar(c, basenm)) { let gsi: *structinfo = letvarstructinfo(c, basenm); if (gsi != nil) { let fi: *fieldinfo = gsi.fields; for (fi != nil) { if (streq(fi.fname, fld)) { emitline("\tLEAQ\t"); emitsymname(c, basenm); emitline("(SB), CX\n"); emitline("\tLEAQ\t"); emitdispreg(fi.foff: i64, "CX"); emitline(", AX\n"); return; }; fi = fi.finext; }; }; let isstr: bool = letvarisstr(c, basenm); let issl: bool = letvarisslice(c, basenm); if (isstr || issl) { let gdelta: i32 = -1; if (streq(fld, "ptr")) { gdelta = 0; }; if (streq(fld, "len")) { gdelta = 8; }; // str IS []u8: &str.cap is valid too, not slice-only // — mirrors cstage (#1/Phase 3, #11). if (streq(fld, "cap")) { gdelta = 16; }; if (gdelta >= 0) { emitline("\tLEAQ\t"); emitsymname(c, basenm); emitline("(SB), CX\n"); emitline("\tLEAQ\t"); emitdispreg(gdelta: i64, "CX"); emitline(", AX\n"); return; }; }; }; }; }; // #149 Shape 2: `&mod.G` module-qualified address-of // of an exported global (let or def). The base is an // N_IDENT that's neither a local nor a global let, so // it's an SK_USE module qualifier; LEAQ the leaf // symbol. Kind-agnostic (covers cross-module &let / // &def / &scalar) — the address-of twin of the value- // read mod-qual path (cgenexpr.ww). A fn leaf resolves // via emitfnname (fn address), mirroring that read // path's TY_FN branch. if (opnd.lhs != nil) { if (opnd.lhs.kind == nkind.N_IDENT) { let basenm: str = opnd.lhs.str; if (localfindnode(c, basenm) == nil) { // A def base (`&Pdef.field`) is NOT a module // qualifier: cstage's Shape-2 gate (base // type_ == NULL/ty_err) excludes it because // the checker types a def-struct/def-array // base, but the ww gate (not-local && not-let) // does not. Without this guard a def base would // mis-LEAQ the field leaf (e.g. `y(SB)`) while // cstage silent-drops, breaking cs==ww (rule // 10). Excluding defs restores byte-id; the // `&def.field` silent-drop itself is a separate // pre-#149 gap (file as #150-family). if (!isletvar(c, basenm) && !deflookup(c, basenm)) { let fld: str = opnd.str; let frt: *node = fnretlookupmod(c, fld, basenm); if (frt != nil) { emitline("\tLEAQ\t"); emitfnname(c, fld, basenm); emitline("(SB), AX\n"); return; }; emitline("\tLEAQ\t"); emitsymname(c, fld); emitline("(SB), AX\n"); return; }; }; }; }; // Fall through silently (mirrors cstage silent- // drop fallback at the end of the TK_AMP block). return; }; if (opnd.kind == nkind.N_INDEX) { // &base[i] = base + i*esz, no dereference. let base: *node = opnd.lhs; let idx: *node = opnd.rhs; let esz: i32 = 8; let isglobalarr: bool = false; let isglobalptr: bool = false; let globalname: str; globalname.ptr = nil; globalname.len = 0; let baselocal: *local = nil; let isarr: bool = false; if (base != nil) { if (base.kind == nkind.N_IDENT) { baselocal = localfindnode(c, base.str); if (baselocal != nil) { esz = elemsizeofc(c, baselocal.tnode); let tn: *node = baselocal.tnode; if (tn != nil) { if (tn.kind == nkind.N_TARRAY) { isarr = true; }; }; } else { let tn: *node = letvartnode(c, base.str); if (tn != nil) { if (tn.kind == nkind.N_TARRAY) { isglobalarr = true; globalname = base.str; esz = elemsizeofc(c, tn); }; if (tn.kind == nkind.N_TPTR) { isglobalptr = true; globalname = base.str; esz = elemsizeofc(c, tn); }; }; }; } else { if (base.kind == nkind.N_DOT) { // `&p.ptr[i]`: stride is the checker-stamped // element tinfo's natural size, mirroring // cgindex's N_DOT arm so &p.ptr[i] and // p.ptr[i] agree. cstage idx_eff(base->type) // ->sub->size (cmd/w6c/cgen.c:3517-18). #72. let dt: *tinfo = opnd.type_: *tinfo; if (dt != nil) { esz = dt.size: i32; }; };}; }; cgexpr(c, idx); if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; if (isglobalarr) { emitline("\tLEAQ\t"); emitsymname(c, globalname); emitline("(SB), BX\n"); } else { if (isglobalptr) { emitline("\tMOVQ\t"); emitsymname(c, globalname); emitline("(SB), BX\n"); } else { if (baselocal != nil) { if (isarr) { emitline("\tLEAQ\t"); emitoff(baselocal.off: i64); emitline("(BP), BX\n"); } else { emitline("\tMOVQ\t"); emitoff(baselocal.off: i64); emitline("(BP), BX\n"); }; } else { // Complex base: spill scaled idx, eval // base to AX, restore idx into BX. // Mirrors cstage's lean three-line shape // (cmd/w6c/cgen.c TK_AMP N_INDEX complex // base 2104-2107); the prior MOVQ AX, BX // + POPQ AX scratch shuffle was rule-10 // verbose-defensive on the wwstage side // with no semantic asymmetry (task #21). emitline("\tPUSHQ\tAX\n"); cgexpr(c, base); emitline("\tPOPQ\tBX\n"); };};}; emitline("\tADDQ\tBX, AX\n"); return; }; }; return; }; cgexpr(c, n.lhs); if (n.op == tkind.TK_MINUS) { emitline("\tNEGQ\tAX\n"); return; }; if (n.op == tkind.TK_TILDE) { emitline("\tNOTQ\tAX\n"); // NOTQ inverts the whole 64-bit register; clamp narrow // unsigned results to type width so subsequent 64-bit // compares against typed literals agree. u32 uses MOVL r,r // (zero-extends upper 32) because ANDQ $0xFFFFFFFF would // sign-extend imm32 to all-ones and act as a no-op. if (nodeisunsigned(c, n.lhs)) { let w: i32 = nodeprimwidth(c, n.lhs); if (w == 1) { emitline("\tANDQ\t$255, AX\n"); }; if (w == 2) { emitline("\tANDQ\t$65535, AX\n"); }; if (w == 4) { emitline("\tMOVL\tAX, AX\n"); }; }; return; }; if (n.op == tkind.TK_STAR) { // #185: deref of *fn — the pointer value IS the fn address. // cgexpr(n.lhs) left AX = fn-addr; a generic MOVQ (AX),AX // would load the first instruction word and a subsequent // CALL would segfault. Mirror ref/harec/src/check.c // expr_call's STORAGE_POINTER→STORAGE_FUNCTION skip. let rti: *tinfo = n.type_: *tinfo; for (rti != nil && rti.kind == tykind.TY_NAMED) { rti = rti.under; }; if (rti != nil && rti.kind == tykind.TY_FN) { return; }; // f64/f32 result rides X0 (SSE), not AX — an integer MOVQ // strands the value off the float ABI and the caller's // MOVSD X0 reads stale bits (#96). Mirrors the float // field/ident load idiom. if (isfloattype(c, n)) { let mov: str = "MOVSD"; if (isf32type(c, n)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t(AX), X0\n"); } else { // Load-twin of the landed signed-narrow-scalar-reads // sweep (selfhost/CLAUDE.md "Signed-narrow scalar // reads sign-extend honestly"); TK_STAR was the // omitted site, refiled as #116. A raw MOVQ pulls 8B // through a narrow `*iN` and overlaps the next element // — the `*p` value reads honest only when the caller's // sink truncates (i32 store, i32 return). Width- // preserving sinks (CMPQ, 64-bit arith) saw garbage in // the high bytes. localloadop keys MOVSXD/MOVSWQ/ // MOVSBQ + MOVL/MOVZWQ/MOVZBQ off n.type_; n is the // deref expression, n.type_ is the pointee tinfo // (check.ww unoptype TK_STAR L1871-1886 with // TY_NAMED/TY_ENUM peel pre-folded by // tinfofornode/typeissigned), the same shape the // float arm above feeds isfloattype. let lop: str = localloadop(c, n); emitline("\t"); emitline(lop); emitline("\t(AX), AX\n"); }; return; }; if (n.op == tkind.TK_NOT) { let t: str = mklabel(c, "tt"); let e: str = mklabel(c, "te"); emitline("\tCMPQ\t$0, AX\n"); emitline("\tJE\t"); emitline(t); emitline("\n"); emitline("\tMOVQ\t$0, AX\n"); emitline("\tJMP\t"); emitline(e); emitline("\n"); emitlabel(t); emitline("\tMOVQ\t$1, AX\n"); emitlabel(e); return; }; return; }; fn cgbin(c: *cgen, n: *node) void = { // Short-circuit `&&` / `||`. Operands are bool (0/1); the type // checker enforces it. Eval LHS into AX, branch over RHS on the // short-circuit polarity, otherwise eval RHS into AX. The // surviving AX is the result. Must precede any eager-eval path // below — `if (p != nil && p.x > 0)` would segfault on a nil // deref otherwise. Byte-identical to cmd/w6c/cgen.c N_BIN. if (n.op == tkind.TK_AND || n.op == tkind.TK_OR) { let prefix: str = "andend"; let jshrt: str = "JE"; if (n.op == tkind.TK_OR) { prefix = "orend"; jshrt = "JNE"; }; let end: str = mklabel(c, prefix); cgexpr(c, n.lhs); emitline("\tCMPQ\t$0, AX\n"); emitline("\t"); emitline(jshrt); emitline("\t"); emitline(end); emitline("\n"); cgexpr(c, n.rhs); emitlabel(end); return; }; let unsignd: bool = nodeisunsigned(c, n.lhs); if (!unsignd) { unsignd = nodeisunsigned(c, n.rhs); }; // Float arithmetic: both operands flow through X0. Spill rhs // across the stack (SUBQ/MOVSD/MOVSD/ADDQ) since there's no // general FP register saver. ADDSD/SUBSD/MULSD/DIVSD pick SS // variants for f32. Comparison uses UCOMISD + JCC and falls // out to the existing CMPQ-based path below. // Value-class read off the checker stamp (n.type_) — the SSoT // shared with cstage cgen.c node_isfloat / type_isf32. The armed // asserttyped bail (check.ww) guarantees every checked value-node // is stamped, so the read can't see a nil-typed float operand; // the sibling-evidence loud-aborts that used to pin that contract // are therefore dead and removed. let lfk: i32 = 0; if (n.lhs != nil) { let llt: *tinfo = n.lhs.type_: *tinfo; if (typeisf32(llt)) { lfk = 1; } else { if (typeisfloat(llt)) { lfk = 2; }; }; }; let rfk: i32 = 0; if (n.rhs != nil) { let rrt: *tinfo = n.rhs.type_: *tinfo; if (typeisf32(rrt)) { rfk = 1; } else { if (typeisfloat(rrt)) { rfk = 2; }; }; }; let fk: i32 = lfk; if (fk == 0) { fk = rfk; }; if (fk != 0) { let mov: str = "MOVSD"; if (fk == 1) { mov = "MOVSS"; }; if (n.op == tkind.TK_PLUS || n.op == tkind.TK_MINUS || n.op == tkind.TK_STAR || n.op == tkind.TK_SLASH) { cgexpr(c, n.rhs); emitline("\tSUBQ\t$8, SP\n"); emitline("\t"); emitline(mov); emitline("\tX0, (SP)\n"); cgexpr(c, n.lhs); emitline("\t"); emitline(mov); emitline("\t(SP), X1\n"); emitline("\tADDQ\t$8, SP\n"); let op: str = "ADDSD"; if (n.op == tkind.TK_MINUS) { op = "SUBSD"; }; if (n.op == tkind.TK_STAR) { op = "MULSD"; }; if (n.op == tkind.TK_SLASH) { op = "DIVSD"; }; if (fk == 1) { if (n.op == tkind.TK_PLUS) { op = "ADDSS"; }; if (n.op == tkind.TK_MINUS) { op = "SUBSS"; }; if (n.op == tkind.TK_STAR) { op = "MULSS"; }; if (n.op == tkind.TK_SLASH) { op = "DIVSS"; }; }; emitline("\t"); emitline(op); emitline("\tX1, X0\n"); return; }; let isfcmp: bool = false; if (n.op == tkind.TK_EQ) { isfcmp = true; }; if (n.op == tkind.TK_NEQ) { isfcmp = true; }; if (n.op == tkind.TK_LT) { isfcmp = true; }; if (n.op == tkind.TK_LE) { isfcmp = true; }; if (n.op == tkind.TK_GT) { isfcmp = true; }; if (n.op == tkind.TK_GE) { isfcmp = true; }; if (isfcmp) { cgexpr(c, n.rhs); emitline("\tSUBQ\t$8, SP\n"); emitline("\t"); emitline(mov); emitline("\tX0, (SP)\n"); cgexpr(c, n.lhs); emitline("\t"); emitline(mov); emitline("\t(SP), X1\n"); emitline("\tADDQ\t$8, SP\n"); let ucomi: str = "UCOMISD"; if (fk == 1) { ucomi = "UCOMISS"; }; emitline("\t"); emitline(ucomi); emitline("\tX1, X0\n"); // IEEE-754: UCOMISD/SS sets PF=ZF=CF=1 on unordered (a // NaN operand). Any relop with a NaN operand is // unordered -> `!=` true, the other five false. PF must // steer `!=`/`==`/`<`/`<=` (#97): JNE keys on ZF=0 so // `nan != nan` came out false; JE/JB/JBE fire on the // unordered ZF/CF. `>`/`>=` (JA/JAE) need CF=0, which // unordered never gives, so they are ALREADY NaN-correct // and stay byte-identical to the pre-#97 single-template // arm — no redundant PF guard. if (n.op == tkind.TK_NEQ) { // not-equal OR unordered -> true let t: str = mklabel(c, "ct"); let e: str = mklabel(c, "ce"); emitline("\tJNE\t"); emitline(t); emitline("\n"); emitline("\tJP\t"); emitline(t); emitline("\n"); emitline("\tMOVQ\t$0, AX\n"); emitline("\tJMP\t"); emitline(e); emitline("\n"); emitlabel(t); emitline("\tMOVQ\t$1, AX\n"); emitlabel(e); return; }; if (n.op == tkind.TK_EQ || n.op == tkind.TK_LT || n.op == tkind.TK_LE) { // unordered -> false; otherwise the ordered Jcc decides. let jcc: str = "JE"; if (n.op == tkind.TK_LT) { jcc = "JB"; }; if (n.op == tkind.TK_LE) { jcc = "JBE"; }; let fl: str = mklabel(c, "cf"); let t: str = mklabel(c, "ct"); let e: str = mklabel(c, "ce"); emitline("\tJP\t"); emitline(fl); emitline("\n"); emitline("\t"); emitline(jcc); emitline("\t"); emitline(t); emitline("\n"); emitlabel(fl); emitline("\tMOVQ\t$0, AX\n"); emitline("\tJMP\t"); emitline(e); emitline("\n"); emitlabel(t); emitline("\tMOVQ\t$1, AX\n"); emitlabel(e); return; }; // `>`/`>=`: JA/JAE already reject unordered (CF=1), so // keep the pre-#97 single-template shape verbatim. let jcc: str = "JA"; if (n.op == tkind.TK_GE) { jcc = "JAE"; }; let t: str = mklabel(c, "ct"); let e: str = mklabel(c, "ce"); emitline("\t"); emitline(jcc); emitline("\t"); emitline(t); emitline("\n"); emitline("\tMOVQ\t$0, AX\n"); emitline("\tJMP\t"); emitline(e); emitline("\n"); emitlabel(t); emitline("\tMOVQ\t$1, AX\n"); emitlabel(e); return; }; return; }; cgexpr(c, n.rhs); emitline("\tPUSHQ\tAX\n"); cgexpr(c, n.lhs); emitline("\tPOPQ\tBX\n"); if (n.op == tkind.TK_PLUS) { emitline("\tADDQ\tBX, AX\n"); return; }; if (n.op == tkind.TK_MINUS) { emitline("\tSUBQ\tBX, AX\n"); return; }; if (n.op == tkind.TK_STAR) { emitline("\tIMULQ\tBX, AX\n"); return; }; if (n.op == tkind.TK_SLASH) { // Signed IDIV reads dividend from RDX:RAX; CQO sign-extends // RAX. Zero-filling DX would treat a negative RAX as a huge // positive 128-bit value. Unsigned DIV needs RDX zero. if (unsignd) { emitline("\tMOVQ\t$0, DX\n"); emitline("\tDIVQ\tBX\n"); } else { emitline("\tCQO\n"); emitline("\tIDIVQ\tBX\n"); }; return; }; if (n.op == tkind.TK_PERCENT) { if (unsignd) { emitline("\tMOVQ\t$0, DX\n"); emitline("\tDIVQ\tBX\n"); } else { emitline("\tCQO\n"); emitline("\tIDIVQ\tBX\n"); }; emitline("\tMOVQ\tDX, AX\n"); return; }; if (n.op == tkind.TK_AMP) { emitline("\tANDQ\tBX, AX\n"); return; }; if (n.op == tkind.TK_PIPE) { emitline("\tORQ\tBX, AX\n"); return; }; if (n.op == tkind.TK_CARET) { emitline("\tXORQ\tBX, AX\n"); return; }; if (n.op == tkind.TK_LSHIFT) { emitline("\tMOVQ\tBX, CX\n"); emitline("\tSHLQ\tCX, AX\n"); return; }; if (n.op == tkind.TK_RSHIFT) { // #136: signed RSHIFT → SAR (arithmetic, sign-extends MSB); // unsigned → SHR (logical, zero-fill). `unsignd` derived above // at cgbin head from nodeisunsigned(lhs) || nodeisunsigned(rhs). emitline("\tMOVQ\tBX, CX\n"); if (unsignd) { emitline("\tSHRQ\tCX, AX\n"); } else { emitline("\tSARQ\tCX, AX\n"); }; return; }; // TK_AND / TK_OR handled with short-circuit codegen at the top of // cgbin — they never reach this eager-eval tail. // Comparison: emit CMPQ, jump on signed/unsigned variant, // materialise 0/1 in AX. Same shape as the C cgen. let iscmp: bool = false; let jcc: str = ""; if (n.op == tkind.TK_EQ) { iscmp = true; jcc = "JE"; }; if (n.op == tkind.TK_NEQ) { iscmp = true; jcc = "JNE"; }; if (n.op == tkind.TK_LT) { iscmp = true; if (unsignd) { jcc = "JB"; } else { jcc = "JL"; }; }; if (n.op == tkind.TK_LE) { iscmp = true; if (unsignd) { jcc = "JBE"; } else { jcc = "JLE"; }; }; if (n.op == tkind.TK_GT) { iscmp = true; if (unsignd) { jcc = "JA"; } else { jcc = "JG"; }; }; if (n.op == tkind.TK_GE) { iscmp = true; if (unsignd) { jcc = "JAE"; } else { jcc = "JGE"; }; }; if (iscmp) { let t: str = mklabel(c, "ct"); let e: str = mklabel(c, "ce"); emitline("\tCMPQ\tBX, AX\n"); emitline("\t"); emitline(jcc); emitline("\t"); emitline(t); emitline("\n"); emitline("\tMOVQ\t$0, AX\n"); emitline("\tJMP\t"); emitline(e); emitline("\n"); emitlabel(t); emitline("\tMOVQ\t$1, AX\n"); emitlabel(e); return; }; return; }; // cgalloc — `alloc(value)` builtin lowering. Allocate sizeof(value) // bytes via rt_malloc, then write the value's bytes into the new // region. For an N_STRUCTLIT arg, allocate the struct's totsize and // emit per-field stores at each field's offset. For a scalar/ptr, // allocate 8 bytes and store one word. Mirrors cmd/w6c/cgen.c's // alloc-special branch in N_CALL. // // Task #30: result is the graduated `(*T | nomem)` tagged-pointer // pair (AX=tag, DX=ptr). rt_malloc now returns 0 on OOM // (rt/alloc.s); branch on AX to emit the nomem variant (tag=1, // DX=0) or the success variant (tag=0, DX=ptr) after the // value-init stores complete. Callers wrap with `!` / `?` to // consume the union. fn cgalloc(c: *cgen, n: *node) void = { let v: *node = n.list; let sz: i32 = 8; let si: *structinfo = nil; if (v.kind == nkind.N_STRUCTLIT) { let trefn: *node = v.lhs; let sname: str; sname.ptr = nil; sname.len = 0; if (trefn != nil) { if (trefn.kind == nkind.N_IDENT) { sname = trefn.str; } else { if (trefn.kind == nkind.N_TNAME) { sname = trefn.str; }; }; }; si = structlookup(c, sname); if (si != nil) { sz = si.totsize; }; }; let okl: str = mklabel(c, "alloc_ok"); let donel: str = mklabel(c, "alloc_done"); emitline("\tMOVQ\t$"); emitint(sz: i64); emitline(", DI\n"); emitline("\tCALL\t"); emitline(ffiresolve(c, "malloc")); emitline("(SB)\n"); emitline("\tCMPQ\t$0, AX\n"); emitline("\tJNE\t"); emitline(okl); emitline("\n"); emitline("\tMOVQ\t$1, AX\n"); emitline("\tMOVQ\t$0, DX\n"); emitline("\tJMP\t"); emitline(donel); emitline("\n"); emitlabel(okl); emitline("\tPUSHQ\tAX\n"); if (v.kind == nkind.N_STRUCTLIT) { if (si != nil) { let f: *node = v.list; for (f != nil) { if (f.kind == nkind.N_FIELD) { let fname: str = f.str; let fi: *fieldinfo = si.fields; for (fi != nil) { let fn_: str = fi.fname; if (streq(fn_, fname)) { cgexpr(c, f.lhs); // alloc(T{ fval = v }) for f64/f32 field: cgexpr left // the value in X0, not AX — route the store via MOVSD/MOVSS. if (isfloattype(c, fi.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; emitline("\tMOVQ\t(SP), BX\n"); emitline("\t"); emitline(mov); emitline("\tX0, "); emitdispreg(fi.foff: i64, "BX"); emitline("\n"); fi = nil; } else { if (isstrtype(c, fi.tnode)) { // str IS []u8: cgexpr leaves (AX=ptr, // BX=len, CX=cap). Route the heap base // through DX so all three survive — CX // holds cap, BX holds len (#1/Phase 3). emitline("\tMOVQ\t(SP), DX\n"); emitline("\tMOVQ\tAX, "); emitdispreg(fi.foff: i64, "DX"); emitline("\n"); emitline("\tMOVQ\tBX, "); emitdispreg((fi.foff + 8): i64, "DX"); emitline("\n"); emitline("\tMOVQ\tCX, "); emitdispreg((fi.foff + 16): i64, "DX"); emitline("\n"); fi = nil; } else { emitline("\tMOVQ\t(SP), BX\n"); let sop: str = fieldstoreop(c, fi); emitline("\t"); emitline(sop); emitline("\tAX, "); emitdispreg(fi.foff: i64, "BX"); emitline("\n"); fi = nil; };}; } else { fi = fi.finext; }; }; }; f = f.next; }; }; } else { cgexpr(c, v); emitline("\tMOVQ\t(SP), BX\n"); let sop: str = "MOVQ"; if (sz == 1) { sop = "MOVB"; } else { if (sz == 4) { sop = "MOVL"; }; }; emitline("\t"); emitline(sop); emitline("\tAX, (BX)\n"); }; emitline("\tPOPQ\tDX\n"); emitline("\tMOVQ\t$0, AX\n"); emitlabel(donel); }; // cgappend — Hare-style `append(s, v)` / `append(s, items...)` lowering. // Mirrors cmd/w6c/cgen.c's N_CALL append branch (rt::ensure model). // Each value gets: // ; cgexpr → AX // ; PUSHQ AX // ; ADDQ $1, s.len(BP) // ; LEAQ s(BP), DI ; arg1 = &s // ; MOVQ esz, SI ; arg2 = membsz // ; CALL rt_ensure(SB) // ; MOVQ s.len(BP), CX ; CX = new len // ; SUBQ $1, CX ; CX = slot index // ; [IMULQ esz, CX] ; byte offset (esz>1) // ; MOVQ s.ptr(BP), BX // ; ADDQ CX, BX // ; POPQ AX // ; MOV* AX, (BX) ; store (MOVB / MOVQ) // nkind.N_SPREAD wraps the same body in a counted loop over items.len. fn cgappend(c: *cgen, n: *node) void = { let sn: *node = n.list; if (sn == nil) { return; }; if (sn.kind != nkind.N_IDENT) { return; }; let snlocal: *local = localfindnode(c, sn.str); if (snlocal == nil) { return; }; let sn_off: i32 = snlocal.off; let esz: i32 = elemsizeof(snlocal.tnode); let etnode: *node = nil; if (snlocal.tnode != nil) { let stk: nkind = snlocal.tnode.kind; if (stk == nkind.N_TSLICE) { etnode = snlocal.tnode.lhs; }; if (stk == nkind.N_TARRAY) { etnode = snlocal.tnode.lhs; }; if (stk == nkind.N_TPTR) { etnode = snlocal.tnode.lhs; }; }; let store_op: str = tnodestoreop(c, etnode, esz); let vn: *node = sn.next; for (vn != nil) { if (vn.kind == nkind.N_SPREAD) { let it: *node = vn.lhs; if (it == nil) { vn = vn.next; continue; }; if (it.kind != nkind.N_IDENT) { vn = vn.next; continue; }; let itlocal: *local = localfindnode(c, it.str); if (itlocal == nil) { vn = vn.next; continue; }; let it_off: i32 = itlocal.off; let load_op: str = tnodeloadop(c, etnode, esz); emitline("\tSUBQ\t$8, SP\n"); emitline("\tMOVQ\t$0, (SP)\n"); let ll: str = mklabel(c, "spr_l"); let le: str = mklabel(c, "spr_e"); emitlabel(ll); emitline("\tMOVQ\t(SP), CX\n"); emitline("\tMOVQ\t"); emitoff((it_off + 8): i64); emitline("(BP), DX\n"); emitline("\tCMPQ\tDX, CX\n"); emitline("\tJGE\t"); emitline(le); emitline("\n"); emitline("\tMOVQ\t"); emitoff(it_off: i64); emitline("(BP), BX\n"); if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", AX\n"); emitline("\tIMULQ\tAX, CX\n"); }; emitline("\tADDQ\tCX, BX\n"); emitline("\t"); emitline(load_op); emitline("\t(BX), AX\n"); emitline("\tPUSHQ\tAX\n"); emitline("\tADDQ\t$1, "); emitoff((sn_off + 8): i64); emitline("(BP)\n"); emitline("\tLEAQ\t"); emitoff(sn_off: i64); emitline("(BP), DI\n"); emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", SI\n"); emitline("\tCALL\trt_ensure(SB)\n"); emitline("\tMOVQ\t"); emitoff((sn_off + 8): i64); emitline("(BP), CX\n"); emitline("\tSUBQ\t$1, CX\n"); if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", AX\n"); emitline("\tIMULQ\tAX, CX\n"); }; emitline("\tMOVQ\t"); emitoff(sn_off: i64); emitline("(BP), BX\n"); emitline("\tADDQ\tCX, BX\n"); emitline("\tPOPQ\tAX\n"); emitline("\t"); emitline(store_op); emitline("\tAX, (BX)\n"); emitline("\tADDQ\t$1, (SP)\n"); emitline("\tJMP\t"); emitline(ll); emitline("\n"); emitlabel(le); emitline("\tADDQ\t$8, SP\n"); vn = vn.next; continue; }; cgexpr(c, vn); emitline("\tPUSHQ\tAX\n"); emitline("\tADDQ\t$1, "); emitoff((sn_off + 8): i64); emitline("(BP)\n"); emitline("\tLEAQ\t"); emitoff(sn_off: i64); emitline("(BP), DI\n"); emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", SI\n"); emitline("\tCALL\trt_ensure(SB)\n"); emitline("\tMOVQ\t"); emitoff((sn_off + 8): i64); emitline("(BP), CX\n"); emitline("\tSUBQ\t$1, CX\n"); if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", AX\n"); emitline("\tIMULQ\tAX, CX\n"); }; emitline("\tMOVQ\t"); emitoff(sn_off: i64); emitline("(BP), BX\n"); emitline("\tADDQ\tCX, BX\n"); emitline("\tPOPQ\tAX\n"); emitline("\t"); emitline(store_op); emitline("\tAX, (BX)\n"); vn = vn.next; }; return; }; fn cgcall(c: *cgen, n: *node) void = { // Hare-style `append(s, v)` / `append(s, items...)` builtin — // special-cased before pushargsrev so the spread variant can run // a counted loop over the items slice instead of a normal call. let callee: *node = n.lhs; if (callee != nil) { if (callee.kind == nkind.N_IDENT) { if (streq(callee.str, "append")) { if (n.list != nil) { if (n.list.next != nil) { cgappend(c, n); return; }; }; }; // `alloc(value)` builtin: heap-init a fresh *T with the // value's bytes. For struct literals, lower to rt_malloc // + per-field stores. Mirrors cmd/w6c/cgen.c's N_CALL // alloc path. // // Same-module-scope guard: skip the builtin when a fn // `alloc` is declared in the current module (lib/os and // rt/ensure both shadow it). Mirrors cstage check.c's // scope_lookup_prefer gating on the `abort` precedent; // without it, the bare same-module call lands in the // typed-builtin path and shadows the user decl. Task #23. if (streq(callee.str, "alloc")) { if (n.list != nil) { if (!samemodfn(c, "alloc")) { cgalloc(c, n); return; }; }; }; // `len(x)` Hare builtin — mirror cmd/w6c/cgen.c:4283-4297. // Required for byte-id when compiler-imported lib code uses // len(fixedarray) (e.g. lib/strconv/decimal.ha's `len(d.digits)` // over the [800]u8 field). Without this intercept wwstage falls // through to a regular CALL len(SB) while cstage folds to // `MOVQ $alen, AX` — rule-10 byte-id break (#131). // // Argument-type-driven branches: // TY_SLICE / TY_STR (+ N_IDENT operand) → load .len at BP+off+8. // TY_ARRAY → fold `MOVQ $alen, AX`. // else → evaluate operand (cstage's pseudo-.len fallback — // unlikely to fire on Hare-shaped sources). if (streq(callee.str, "len")) { if (n.list != nil) { let a: *node = n.list; let at: *tinfo = a.type_: *tinfo; let u: *tinfo = at; for (u != nil && u.kind == tykind.TY_NAMED) { u = u.under; }; if (u != nil) { if ((u.kind == tykind.TY_SLICE || u.kind == tykind.TY_STR) && a.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, a.str); if (lc != nil) { emitline("\tMOVQ\t"); emitoff((lc.off + 8): i64); emitline("(BP), AX\n"); return; }; }; if (u.kind == tykind.TY_ARRAY) { emitline("\tMOVQ\t$"); emitint(u.alen: i64); emitline(", AX\n"); return; }; }; // Fallback: evaluate the argument and let AX carry // whatever the value-load shape yields. Mirrors // cstage's `cgexpr(c, a, locals)` fallthrough. cgexpr(c, a); return; }; }; }; }; // Look up the callee's declared params for tagged-union widening. // fn-pointer calls (callee is a local) don't get widening — the // user must build the tagged value explicitly. // // N_DOT (`mod.fn(...)`) covers cross-module calls; pre-#28 wwstage // only handled N_IDENT, leaving N_DOT calls without widening // detection — pushargsrev then fell through to the N_IDENT-slice // fast path and dropped the variant tag word on widened slice args. // Cstage finds params via the checker-set `n->lhs->type`, sidestepping // the name-driven registry entirely (cmd/w6c/cgen.c:4161-4165). let calleeparams: *node = nil; if (callee != nil) { if (callee.kind == nkind.N_IDENT) { calleeparams = fnparamslookup(c, callee.str); } else { if (callee.kind == nkind.N_DOT) { let cmod: str; cmod.ptr = nil; cmod.len = 0; if (callee.lhs != nil) { if (callee.lhs.kind == nkind.N_IDENT) { cmod = callee.lhs.str; }; }; calleeparams = fnparamslookupmod(c, callee.str, cmod); }; }; }; // Hare-style variadic last param: gather N tail args into a // frame-resident [N]T (`@vararg_d_`) plus a 24B slice // descriptor (`@vararg_sl_`), 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. Per-call // seq comes from c.varargseq bumped at gather emit (mirrors // cstage's mklabel("vararg_d/sl") freshness). { let nfixed_v: i32 = 0; let varp: *node = callee_variadic_param(c, callee, &nfixed_v); if (varp != nil) { let nargs0: i32 = 0; let aw: *node = n.list; for (aw != nil) { nargs0 += 1; aw = aw.next; }; let nvar: i32 = nargs0 - nfixed_v; if (nvar < 0) { nvar = 0; }; let forwarding: bool = false; if (nvar == 1) { let aaf: *node = n.list; let kk: i32 = 0; for (kk < nfixed_v) { aaf = aaf.next; kk += 1; }; if (aaf != nil) { if (aaf.kind == nkind.N_SPREAD) { forwarding = true; }; }; }; if (forwarding) { let prev: *node = nil; let cur2: *node = n.list; let kk2: i32 = 0; for (kk2 < nfixed_v) { prev = cur2; cur2 = cur2.next; kk2 += 1; }; let inner: *node = cur2.lhs; if (inner != nil) { inner.next = nil; }; if (prev == nil) { n.list = inner; } else { prev.next = inner; }; } else { let seq: i32 = c.varargseq; c.varargseq += 1; let dname: str = mkvarargname(c, "@vararg_d_", seq); let sname: str = mkvarargname(c, "@vararg_sl_", seq); // Use raw element size, not stack-padded // slotsize. cstage cmd/w6c/cgen.c cgcall // gathers a `T...` slice at velem->size stride // (MOVL for u32, MOVB for u8); the callee // `arg[i]` reads at the same raw stride. wwstage // previously sized through slotsize which pads // scalars to 8, mismatching the stride at the // callee read site — runtime miscompile in // `(rune...)` callees per #36. // check.ww installparams promotes varp.lhs to // []T (mirrors cstage check.c:455 tp->type // wrap). Element predicates / esz read varp.lhs // .lhs; Ken's gate: only deref when the wrap // shape is confirmed N_TSLICE (mirrors cstage // cgen.c:4352 `vsu->kind == TY_SLICE` guard). let velem: *node = varp.lhs; if (varp.lhs != nil && varp.lhs.kind == nkind.N_TSLICE) { velem = varp.lhs.lhs; }; let esz: i32 = 8; if (velem != nil) { if (velem.kind == nkind.N_TNAME) { let ps: i32 = primsize(velem.str); if (ps > 0) { esz = ps; } else { esz = slotsize(c, velem); }; } else { esz = slotsize(c, velem); }; }; if (esz < 1) { esz = 1; }; let velemtagged: bool = istaggedtype(c, velem); let velemstr: bool = isstrtype(c, velem); let velemslice: bool = isslicetype(c, velem); let doff: i32 = 0; if (nvar > 0) { 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). let soff: i32 = localadd(c, sname, tyslicesize(): i32, varp.lhs); let aa2: *node = n.list; let kk3: i32 = 0; for (kk3 < nfixed_v) { aa2 = aa2.next; kk3 += 1; }; let j: i32 = 0; let prevarg: *node = n.list; if (nfixed_v == 0) { prevarg = nil; } else { let kk4: i32 = 0; for (kk4 < nfixed_v - 1) { prevarg = prevarg.next; kk4 += 1; }; }; for (aa2 != nil) { let slot: i32 = doff + j * esz; if (velemtagged) { // dst is the per-element tagged type; // pass velem (cstage cgen.c:4382 passes // velem, not the slice wrap vsu). cgwidentaggedstore(c, velem.type_: *tinfo, aa2, "BP", slot, esz); } else { if (velemstr) { cgexpr(c, aa2); emitline("\tMOVQ\tAX, "); emitoff(slot: i64); emitline("(BP)\n"); emitline("\tMOVQ\tBX, "); emitoff((slot + 8): i64); emitline("(BP)\n"); } else { if (velemslice) { cgexpr(c, aa2); emitline("\tMOVQ\tAX, "); emitoff(slot: i64); emitline("(BP)\n"); emitline("\tMOVQ\tBX, "); emitoff((slot + 8): i64); emitline("(BP)\n"); emitline("\tMOVQ\tCX, "); emitoff((slot + 16): i64); emitline("(BP)\n"); } else { cgexpr(c, aa2); let op: str = tnodestoreop(c, varp.lhs, esz); emitline("\t"); emitline(op); emitline("\tAX, "); emitoff(slot: i64); emitline("(BP)\n"); }; }; }; j += 1; aa2 = aa2.next; }; if (nvar > 0) { emitline("\tLEAQ\t"); emitoff(doff: i64); emitline("(BP), AX\n"); } else { emitline("\tXORQ\tAX, AX\n"); }; emitline("\tMOVQ\tAX, "); emitoff(soff: i64); emitline("(BP)\n"); emitline("\tMOVQ\t$"); emitint(nvar: i64); emitline(", AX\n"); emitline("\tMOVQ\tAX, "); emitoff((soff + 8): i64); emitline("(BP)\n"); emitline("\tMOVQ\tAX, "); emitoff((soff + 16): i64); emitline("(BP)\n"); let sn: *node = newnode(nkind.N_IDENT, "", 0, 0); sn.str = sname; // Synthesised after the checker has run, so the // asserttyped bail (check.ww) never stamps it. // Stamp the variadic param's []T slice tinfo // (resolvefnbody resolve-walks varp.lhs) so the // downstream value-class reads see a non-nil // stamp — the one cgen node the bail can't cover. sn.type_ = varp.lhs.type_; if (prevarg == nil) { n.list = sn; } else { prevarg.next = sn; }; }; }; }; let nargs: i32 = pushargsrev(c, n.list, calleeparams); // sret call (#23): callee returns plain TY_STRUCT > 24B. The // dest pointer lands in RDI; start intidx at 1 to skip RDI in // the user-arg pop loop and emit `LEAQ off(BP), DI` AFTER all // pops have finished (so they don't clobber RDI). The dest off // is either the receive site's slot (c.sretdestoff, propagated // from cglet / cgassign ident) or the per-fn @sretscr discard // slot, sized at first use per #15/#26c. let sretcs: i32 = callsretsize(c, n); let sretcalloff: i32 = 0; if (sretcs > 0) { if (c.sretdestoff != 0) { sretcalloff = c.sretdestoff; c.sretdestoff = 0; } else { sretcalloff = localadd(c, "@sretscr", sretcs, nil); }; }; // Pop forward. Float args were pushed as 8 bytes from X0 via // SUBQ+MOVSD; pop into the XMM stream (X0..X7). Everything else // pops into the int stream (DI..R9) per the SysV ABI. Walk the // args list alongside the pop counter so we know each arg's // register class. SysV has only 6 int arg regs (DI/SI/DX/CX/R8/R9); // the remaining slots stay on the stack and the callee reads them // via 16+8*k(BP). Caller-cleanup is emitted after the CALL. let intidx: i32 = 0; if (sretcs > 0) { intidx = 1; }; let fpidx: i32 = 0; let a: *node = n.list; let popped: i32 = 0; let stackslots: i32 = 0; for (a != nil) { let fk: i32 = 0; if (a != nil) { let at: *tinfo = a.type_: *tinfo; if (typeisf32(at)) { fk = 1; } else { if (typeisfloat(at)) { fk = 2; }; }; }; if (fk != 0) { let mov: str = "MOVSD"; if (fk == 1) { mov = "MOVSS"; }; if (fpidx < 8) { emitline("\t"); emitline(mov); emitline("\t(SP), "); emitline(fargregname(fpidx)); emitline("\n"); emitline("\tADDQ\t$8, SP\n"); fpidx += 1; } else { stackslots += 1; }; popped += 1; } else { let tuparg: *node = rettupleof(c, a); if (tuparg != nil) { // #163: drain the tuple's staged words (slot+0 pushed // first) into the SysV arg cursor by SysV class — a // float MOVSD/MOVSS off (SP) into the next XMM, else // POPQ into the next INTEGER arg reg; a slice/str its // 3 words. Reg overflow loud-stops (rule 7); the // partial-spill stitch is out of scope (twin of #164). let p: *node = tuparg.list; for (p != nil) { let et: *node = p.lhs; if (isfloattype(c, et)) { if (fpidx >= 8) { let msg: str = "tuple arg float element overflows SSE arg regs (X0..X7); stitch out of scope, see #163\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; let mov: str = "MOVSD"; if (isf32type(c, et)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t(SP), "); emitline(fargregname(fpidx)); emitline("\n"); emitline("\tADDQ\t$8, SP\n"); fpidx += 1; popped += 1; } else { let wide: bool = isstrtype(c, et) || isslicetype(c, et); let eb: i32 = tupebytes(wide); if (intidx + eb > 6) { let msg: str = "tuple arg element overflows integer arg regs (DI/SI/DX/CX/R8/R9); stitch out of scope, see #163\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; let k: i32 = 0; for (k < eb) { emitline("\tPOPQ\t"); emitline(argregname(intidx)); emitline("\n"); intidx += 1; popped += 1; k += 1; }; }; p = p.next; }; } else { let stfc: i32 = 0; if (a.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, a.str); if (lc != nil) { stfc = structfloatclass(c, lc.tnode); }; }; if (stfc != 0) { // #165: float-bearing struct arg — drain by SysV // eightbyte class: a lone-f64 eightbyte MOVSD off // (SP) into the next XMM (X0..X7), a pure-INT // eightbyte POPQ into the next INTEGER arg reg // (DI/SI/..). The struct-ident push staged raw slot // words (class-independent); only the drain differs. // Gated to qualifying floats; all-int + f32-packed // keep the generic pop below. Reg overflow loud- // stops (rule 7), the partial-spill stitch out of // scope (#163 twin). let nb: i32 = stfc & 15; let e: i32 = 0; for (e < nb) { let issse: bool = (stfc & (16 << e)) != 0; if (issse) { if (fpidx >= 8) { let msg: str = "float struct arg eightbyte overflows SSE arg regs (X0..X7); stitch out of scope, see #165\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; emitline("\tMOVSD\t(SP), "); emitline(fargregname(fpidx)); emitline("\n"); emitline("\tADDQ\t$8, SP\n"); fpidx += 1; } else { if (intidx >= 6) { let msg: str = "float struct arg eightbyte overflows integer arg regs (DI/SI/DX/CX/R8/R9); stitch out of scope, see #165\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; emitline("\tPOPQ\t"); emitline(argregname(intidx)); emitline("\n"); intidx += 1; }; popped += 1; e += 1; }; } else { let extra: i32 = 0; // str IS []u8: 3-word arg, same as slice (#1/Phase 3). if (nodeisstr(c, a)) { extra = 2; }; if (nodeisslice(c, a)) { extra = 2; }; // #21: tagged-CALL arg was pushed AX/DX/CX/R8 high→low // by pushargsrev; size the per-arg pop to match so the // next arg's POPQ doesn't land on residual tag/payload // words and shift intidx out of sync. let tcs: i32 = taggedcallslot(c, a); if (tcs > 0) { extra = tcs / 8 - 1; }; let words: i32 = 1 + extra; let w: i32 = 0; for (w < words) { if (intidx < 6) { emitline("\tPOPQ\t"); emitline(argregname(intidx)); emitline("\n"); intidx += 1; } else { stackslots += 1; }; popped += 1; w += 1; }; }; }; }; a = a.next; }; // Drain any remaining slots that the arg-walker didn't account // for (tagged-union arg sizes > 8B, struct-by-value, etc.). The // existing C cgen pops these into the int stream, so the worst // case here is identical pre-port behaviour. let i: i32 = popped; for (i < nargs) { if (intidx < 6) { emitline("\tPOPQ\t"); emitline(argregname(intidx)); emitline("\n"); intidx += 1; } else { stackslots += 1; }; i += 1; }; // `callee` is already in scope from line 2827; reuse it. Pre-#32 // silent-redecl masked the second `let callee` here as a no-op // (same value, same fn-body scope post-#27). let calleename: str; calleename.ptr = nil; calleename.len = 0; // Detect fn-pointer field call: `w.emit(args)` where `w` is // a struct local and `emit` is an nkind.N_TFN field. Load the // field value into AX and CALL through it. Also detect a // bare `fp(args)` where `fp` is a local holding a function // pointer — mirror C cgen's localfind dispatch (commit // 635818e). Without this the call emits `CALL fp(SB)` and // the linker rightly fails. let isfnptrcall: bool = false; if (callee != nil) { if (callee.kind == nkind.N_IDENT) { let cn: str = callee.str; if (localfindnode(c, cn) != nil) { isfnptrcall = true; }; }; if (callee.kind == nkind.N_DOT) { let base: *node = callee.lhs; let fld: str = callee.str; if (base != nil) { if (base.kind == nkind.N_IDENT) { let bn: str = base.str; let lc: *local = localfindnode(c, bn); if (lc != nil) { let tn: *node = lc.tnode; if (tn != nil) { let lkind: nkind = tn.kind; let sname: str; sname.ptr = nil; sname.len = 0; if (lkind == nkind.N_TNAME) { sname = tn.str; }; if (lkind == nkind.N_TPTR) { let inner: *node = tn.lhs; if (inner != nil) { if (inner.kind == nkind.N_TNAME) { sname = inner.str; }; }; }; if (sname.len > 0) { let si: *structinfo = structlookup(c, sname); if (si != nil) { let fi: *fieldinfo = si.fields; for (fi != nil) { let fn_: str = fi.fname; if (streq(fn_, fld)) { let ft: *node = fi.tnode; if (ft != nil) { if (ft.kind == nkind.N_TFN) { isfnptrcall = true; }; }; fi = nil; } else { fi = fi.finext; }; }; }; }; }; }; }; }; }; }; // sret hidden first-arg (#23): load &dest into RDI AFTER all // user-arg pops have finished — intidx started at 1 so RDI was // never written. The CALL emit follows immediately. // // Forwarding (task #9 follow-up): when outer's `return f();` // forwards through an sret callee, source RDI from outer's // saved @sretarg — inner writes directly into outer's caller- // prealloc dest. No temporary in outer's frame. The @sretscr // slot stays reserved for byte-id with cstage; it goes unused // on the forwarding branch. if (sretcs > 0) { if (c.sretforward != 0) { let sretargoff: i32 = localfind(c, "@sretarg"); emitline("\tMOVQ\t"); emitoff(sretargoff: i64); emitline("(BP), DI\n"); c.sretforward = 0; } else { emitline("\tLEAQ\t"); emitoff(sretcalloff: i64); emitline("(BP), DI\n"); }; }; if (isfnptrcall) { // Load fn-ptr field value into AX; CALL AX. We emit the // load AFTER the args have been popped (so AX/BX/etc // don't get clobbered by the field load before the pops). // `popped args` left DI/SI/etc set; AX is free. cgexpr(c, callee); emitline("\tCALL\tAX\n"); } else { emitline("\tCALL\t"); if (callee != nil) { if (callee.kind == nkind.N_IDENT) { // Bare `f()` — same-module by ww's resolver, // so c.curmod is the disambiguation hint. calleename = callee.str; emitfnname(c, calleename, c.curmod); } else { if (callee.kind == nkind.N_DOT) { // `m.f()` — pass the explicit module bareword // so cross-module same-leaf exports resolve. calleename = callee.str; let hint: str; hint.ptr = nil; hint.len = 0; if (callee.lhs != nil) { if (callee.lhs.kind == nkind.N_IDENT) { hint = callee.lhs.str; }; }; emitfnname(c, calleename, hint); };}; }; emitline("(SB)\n"); }; // Caller cleanup for stack-passed args (args 7+, or any // overflow past the int/float reg windows). Mirrors C cgen: // pushed 8 bytes each, ADDQ them off after the CALL. if (stackslots > 0) { emitline("\tADDQ\t$"); emitint((stackslots * 8): i64); emitline(", SP\n"); }; // str IS []u8: a str-returning callee leaves AX=ptr, BX=len, // CX=cap — same as a slice, so there is no receive-side shuffle // (#1/Phase 3). return; }; fn cgassign(c: *cgen, n: *node) void = { let lhs: *node = n.lhs; // Discard lvalue `_ = expr;` — evaluate rhs for side effects, // write nothing. Detected by lhs being an nkind.N_IDENT with empty str // (planted by parseprimary on the tkind.TK_UNDER token). if (lhs != nil) { if (lhs.kind == nkind.N_IDENT) { if (lhs.str.len == 0) { if (n.op == tkind.TK_ASSIGN) { cgexpr(c, n.rhs); return; }; }; }; }; // Tagged-union local reassignment: `r = expr;` where r has a // tagged-union type. Delegate to cgwidentaggedstore (same path // as cglet's tagged-init). Covers nullable fold, tagged source, // struct payload, str payload, scalar payload, with tag remap. if (lhs != nil) { if (lhs.kind == nkind.N_IDENT) { if (n.op == tkind.TK_ASSIGN) { let lc: *local = localfindnode(c, lhs.str); if (lc != nil) { if (istaggedtype(c, lc.tnode)) { let lsz: i32 = slotsize(c, lc.tnode); cgwidentaggedstore(c, lc.tnode.type_: *tinfo, n.rhs, "BP", lc.off, lsz); return; }; }; }; }; }; // `*p = v` — deref-assign. Element width comes from the // pointer's declared type. Mirrors C cgen: eval rhs (AX, // and BX if str), push, eval pointer, pop value, store. // We default to MOVQ (8B) since most fixtures use it; for // `*bool` / `*u8` / `*i32` we narrow via the local's tnode. if (lhs != nil) { if (lhs.kind == nkind.N_UN) { if (lhs.op == tkind.TK_STAR) { if (n.op == tkind.TK_ASSIGN) { let inner: *node = lhs.lhs; let elemstr: bool = false; let elemfloat: bool = false; let elemf32: bool = false; let storeop: str = "MOVQ"; if (inner != nil) { if (inner.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, inner.str); if (lc != nil) { let tn: *node = lc.tnode; if (tn != nil) { if (tn.kind == nkind.N_TPTR) { let pe: *node = tn.lhs; if (pe != nil) { if (pe.kind == nkind.N_TNAME) { if (streq(pe.str, "str")) { elemstr = true; } else { if (streq(pe.str, "f64")) { elemfloat = true; } else { if (streq(pe.str, "f32")) { elemfloat = true; elemf32 = true; } else { let ps: i32 = primsize(pe.str); if (ps == 1) { storeop = "MOVB"; } else { if (ps == 4) { storeop = "MOVL"; }; }; }; }; }; }; // A slice IS the same 3-word {ptr,len,cap} // header as str (ref/hare/rt/ensure.ha:4-8), // so `*p = sliceval` takes str's stash+store // path (#79; precedent cgenstmt.ww:1631, // cgenexpr.ww:1684). LIKE str this is // alias-BLIND: a slice-alias `*Foo` / non-ident // deref-store stays 1-word, the SAME divergence // str carries; resolved-vs-syntactic detection // is unified UP in #80, not patched here. if (pe.kind == nkind.N_TSLICE) { elemstr = true; }; }; }; }; }; }; }; cgexpr(c, n.rhs); // `*p = v` for *f64 / *f32: value sits in X0. Spill // to the stack, evaluate the pointer (clobbers AX), // then reload X0 and MOVSD/MOVSS through the pointer. if (elemfloat) { let mov: str = "MOVSD"; if (elemf32) { mov = "MOVSS"; }; emitline("\tSUBQ\t$8, SP\n"); emitline("\t"); emitline(mov); emitline("\tX0, (SP)\n"); cgexpr(c, inner); emitline("\tMOVQ\tAX, BX\n"); emitline("\t"); emitline(mov); emitline("\t(SP), X0\n"); emitline("\tADDQ\t$8, SP\n"); emitline("\t"); emitline(mov); emitline("\tX0, (BX)\n"); return; }; // str IS []u8: PUSHQ AX (ptr) first, then // PUSHQ BX (len) + PUSHQ CX (cap) across the // pointer eval which clobbers BX/CX. Pop drains // cap (top) → 16(BX), then len, then ptr → 0(BX) // with len → 8(BX) (#1/Phase 3). emitline("\tPUSHQ\tAX\n"); if (elemstr) { emitline("\tPUSHQ\tBX\n"); emitline("\tPUSHQ\tCX\n"); }; cgexpr(c, inner); emitline("\tMOVQ\tAX, BX\n"); if (elemstr) { emitline("\tPOPQ\tCX\n"); emitline("\tMOVQ\tCX, 16(BX)\n"); emitline("\tPOPQ\tCX\n"); emitline("\tPOPQ\tAX\n"); emitline("\tMOVQ\tAX, (BX)\n"); emitline("\tMOVQ\tCX, 8(BX)\n"); return; }; emitline("\tPOPQ\tAX\n"); emitline("\t"); emitline(storeop); emitline("\tAX, (BX)\n"); return; }; }; }; }; // `*p OP= v` — compound assign through a pointer deref. The // plain-assign branch above only fires for TK_ASSIGN; without // this, compound ops fall through and emit nothing (silent // no-op — exactly the trap that broke fmt.println). Mirror of // cmd/w6c/cgen.c's N_UN/TK_STAR compound branch. if (lhs != nil) { if (lhs.kind == nkind.N_UN) { if (lhs.op == tkind.TK_STAR) { if (n.op != tkind.TK_ASSIGN) { let inner: *node = lhs.lhs; let loadop: str = "MOVQ"; let storeop: str = "MOVQ"; // Pointee node for the lhs-sign side of the /= // and %= dispatch. Mirror of cstage's `vt` at // cmd/w6c/cgen.c's TK_STAR-compound branch. let pe: *node = nil; if (inner != nil) { if (inner.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, inner.str); if (lc != nil) { let tn: *node = lc.tnode; if (tn != nil) { if (tn.kind == nkind.N_TPTR) { pe = tn.lhs; if (pe != nil) { let ps: i32 = fieldsize(c, pe); if (ps == 1 || ps == 2 || ps == 4) { loadop = tnodeloadop(c, pe, ps); storeop = tnodestoreop(c, pe, ps); }; }; }; }; }; }; }; cgexpr(c, n.rhs); emitline("\tPUSHQ\tAX\n"); cgexpr(c, inner); emitline("\tMOVQ\tAX, BX\n"); emitline("\t"); emitline(loadop); emitline("\t(BX), AX\n"); emitline("\tPOPQ\tCX\n"); // Post-63332fe: /= and %= via CQO/IDIVQ on the // signed arm and MOVQ-zero/DIVQ on the unsigned // arm. Pre-fix the default branch silently stored // rhs into *p (combineop = MOVQ shape). // #136: lift unsignd above the SLASHEQ block so // RSHIFTEQ can route SHRQ vs SARQ on the same key. let unsignd: bool = false; if (pe != nil) { unsignd = typeisunsigned(pe.type_: *tinfo); }; if (!unsignd) { unsignd = nodeisunsigned(c, n.rhs); }; if (n.op == tkind.TK_SLASHEQ || n.op == tkind.TK_PERCENTEQ) { if (unsignd) { emitline("\tMOVQ\t$0, DX\n"); emitline("\tDIVQ\tCX\n"); } else { emitline("\tCQO\n"); emitline("\tIDIVQ\tCX\n"); }; if (n.op == tkind.TK_PERCENTEQ) { emitline("\tMOVQ\tDX, AX\n"); }; emitline("\t"); emitline(storeop); emitline("\tAX, (BX)\n"); return; }; let combineop: str = "MOVQ"; if (n.op == tkind.TK_PLUSEQ) { combineop = "ADDQ"; } else { if (n.op == tkind.TK_MINUSEQ) { combineop = "SUBQ"; } else { if (n.op == tkind.TK_STAREQ) { combineop = "IMULQ"; } else { if (n.op == tkind.TK_AMPEQ) { combineop = "ANDQ"; } else { if (n.op == tkind.TK_PIPEEQ) { combineop = "ORQ"; } else { if (n.op == tkind.TK_CARETEQ) { combineop = "XORQ"; } else { if (n.op == tkind.TK_LSHIFTEQ) { combineop = "SHLQ"; } else { if (n.op == tkind.TK_RSHIFTEQ) { // #136: signed RSHIFTEQ → SARQ. if (unsignd) { combineop = "SHRQ"; } else { combineop = "SARQ"; }; }; }; }; }; }; }; }; }; emitline("\t"); emitline(combineop); emitline("\tCX, AX\n"); emitline("\t"); emitline(storeop); emitline("\tAX, (BX)\n"); return; }; }; }; }; // Array/slice/ptr index store: `arr[i] = v;`. Element size // from base.tnode picks MOVB vs MOVQ. if (lhs != nil) { if (lhs.kind == nkind.N_INDEX) { if (n.op == tkind.TK_ASSIGN) { let base: *node = lhs.lhs; let idx: *node = lhs.rhs; let esz: i32 = 8; let baselocal: *local = nil; let isglobalarr: bool = false; let isglobalptr: bool = false; let globalname: str; globalname.ptr = nil; globalname.len = 0; let elemtn: *node = nil; if (base != nil) { if (base.kind == nkind.N_IDENT) { let bn: str = base.str; baselocal = localfindnode(c, bn); if (baselocal != nil) { esz = elemsizeofc(c, baselocal.tnode); let btn: *node = baselocal.tnode; if (btn != nil) { let bk: nkind = btn.kind; if (bk == nkind.N_TARRAY) { elemtn = btn.lhs; }; if (bk == nkind.N_TSLICE) { elemtn = btn.lhs; }; if (bk == nkind.N_TPTR) { elemtn = btn.lhs; }; }; } else { let tn: *node = letvartnode(c, bn); if (tn != nil) { if (tn.kind == nkind.N_TARRAY) { isglobalarr = true; globalname = bn; esz = elemsizeofc(c, tn); elemtn = tn.lhs; }; if (tn.kind == nkind.N_TPTR) { isglobalptr = true; globalname = bn; esz = elemsizeofc(c, tn); elemtn = tn.lhs; }; }; }; } else { if (base.kind == nkind.N_DOT) { // lhs.type_ is the checker-stamped element tinfo // of the N_INDEX: esz is its natural size and the // tagged-element gate (below) reads the same // .type_ — same idiom as cgindex's n.type_ read // (#60/#72). cstage idx_eff(base->type)->sub->size // (cmd/w6c/cgen.c:3517-18). let dt: *tinfo = lhs.type_: *tinfo; if (dt != nil) { esz = dt.size: i32; elemtn = lhs; }; } else { if (base.kind == nkind.N_INDEX) { // Chained-write write-side parallel of the // cgindex N_INDEX-base arm (#24): `names[i][k] // = v` (names: **u8) — outer element is u8 so // the store is MOVB, not MOVQ. lhs.type_ is the // checker-stamped outer element tinfo; esz is // its natural size and the gate reads it via // .type_. Drops the indexvaluetnode walk // (#69/#61d, mirror #60). cstage: esz = // idx_eff(base->type)->sub->size // (cmd/w6c/cgen.c:3517-3518). let et: *tinfo = lhs.type_: *tinfo; if (et != nil) { esz = et.size: i32; elemtn = lhs; }; };};}; }; // Tagged-union element: materialize source in a shared // scratch slot via cgwidentaggedstore (handles struct / // str / scalar / subset / nullable variants uniformly), // then compute &arr[i] and byte-copy. The scratch // (@tagscr) is reused across all tagged-arr stores in // the function; first-use sizes the slot (#15/#26c). if (elemtn != nil) { if (istaggedtype(c, elemtn)) { let slot_sz: i32 = slotsize(c, elemtn); let scroff: i32 = localadd(c, "@tagscr", slot_sz, nil); // Pre-zero scratch (matches push helper). emitline("\tXORQ\tAX, AX\n"); let zz: i32 = 0; for (zz < slot_sz) { emitline("\tMOVQ\tAX, "); emitoff((scroff + zz): i64); emitline("(BP)\n"); zz += 8; }; cgwidentaggedstore(c, elemtn.type_: *tinfo, n.rhs, "BP", scroff, slot_sz); cgexpr(c, idx); if (slot_sz > 1) { emitline("\tMOVQ\t$"); emitint(slot_sz: i64); emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; if (isglobalarr) { emitline("\tLEAQ\t"); emitsymname(c, globalname); emitline("(SB), BX\n"); } else { if (isglobalptr) { emitline("\tMOVQ\t"); emitsymname(c, globalname); emitline("(SB), BX\n"); } else { if (baselocal != nil) { let tn: *node = baselocal.tnode; let isarr: bool = false; if (tn != nil) { if (tn.kind == nkind.N_TARRAY) { isarr = true; }; }; if (isarr) { emitline("\tLEAQ\t"); emitoff(baselocal.off: i64); emitline("(BP), BX\n"); } else { emitline("\tMOVQ\t"); emitoff(baselocal.off: i64); emitline("(BP), BX\n"); }; } else { 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; }; }; cgexpr(c, n.rhs); // value → AX // str/slice: spill cap (CX) + len (BX) before // computing the index so the post-index store can // pop all three. str=24B (#1/Phase 3) collides with // slice=24B, so this MUST gate on kind (cstage's // elem_is_str||elem_is_slice, cmd/w6c/cgen.c:3581), // never a bare esz==24: a >16B struct is also >=24B // but takes the struct-copy path, not this 3-word // {ptr,len,cap} store. Write-side mirror of the // cgindex read-path gate (#7/754). if (isstrtype(c, elemtn) || isslicetype(c, elemtn)) { emitline("\tPUSHQ\tCX\n"); emitline("\tPUSHQ\tBX\n"); }; // Float element: spill X0 (not AX — AX is junk for // floats) across the idx/base eval. A call-index // (a[geti()]=v) clobbers X0 and would otherwise lose // the value. Mirrors the *p=v float deref store // twin in cgassign (#125). let spisfloat: bool = isfloattype(c, elemtn); let spmov: str = "MOVSD"; if (isf32type(c, elemtn)) { spmov = "MOVSS"; }; if (spisfloat) { emitline("\tSUBQ\t$8, SP\n"); emitline("\t"); emitline(spmov); emitline("\tX0, (SP)\n"); } else { emitline("\tPUSHQ\tAX\n"); }; cgexpr(c, idx); // idx → AX if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; emitline("\tPUSHQ\tAX\n"); // scaled idx if (isglobalarr) { emitline("\tLEAQ\t"); emitsymname(c, globalname); emitline("(SB), BX\n"); } else { if (isglobalptr) { emitline("\tMOVQ\t"); emitsymname(c, globalname); emitline("(SB), BX\n"); } else { if (baselocal != nil) { let tn: *node = baselocal.tnode; let isarray: bool = false; if (tn != nil) { if (tn.kind == nkind.N_TARRAY) { isarray = true; }; }; if (isarray) { emitline("\tLEAQ\t"); emitoff(baselocal.off: i64); emitline("(BP), BX\n"); } else { emitline("\tMOVQ\t"); emitoff(baselocal.off: i64); emitline("(BP), BX\n"); }; } else { if (dotbaseaddr(c, base, "BX")) { // #135: N_DOT base address-of-field inline. } else { cgexpr(c, base); emitline("\tMOVQ\tAX, BX\n"); };};};}; emitline("\tPOPQ\tAX\n"); // scaled idx emitline("\tADDQ\tAX, BX\n"); // Reload value: float reloads X0 from the spill slot; // non-float pops AX. Twin of the value-spill site // above (#125). if (spisfloat) { emitline("\t"); emitline(spmov); emitline("\t(SP), X0\n"); emitline("\tADDQ\t$8, SP\n"); } else { emitline("\tPOPQ\tAX\n"); // value }; // str/slice: pop the saved len + cap and store // all three words. Kind-gate, not size — see the // spill site above (#1/Phase 3, #7/754). if (isstrtype(c, elemtn) || isslicetype(c, elemtn)) { emitline("\tMOVQ\tAX, (BX)\n"); emitline("\tPOPQ\tCX\n"); emitline("\tMOVQ\tCX, 8(BX)\n"); emitline("\tPOPQ\tCX\n"); emitline("\tMOVQ\tCX, 16(BX)\n"); return; }; // float element → store FROM X0 (MOVSS/MOVSD): cgexpr // left the value in X0, and the value-spill pair // above keeps X0 live across the idx/base eval so // a call-index (a[geti()]=v) doesn't lose it (#125). // For f32 the #104 CVTSD2SS narrowing only touches X0, // so the AX store below would write raw double low- // bits, garbage for f32 (#122, mirrors cstage cgen.c // arr[i]= float store). if (isfloattype(c, elemtn)) { let fmov: str = "MOVSD"; if (isf32type(c, elemtn)) { fmov = "MOVSS"; }; emitline("\t"); emitline(fmov); emitline("\tX0, (BX)\n"); return; }; let isop: str = tnodestoreop(c, elemtn, esz); emitline("\t"); emitline(isop); emitline("\tAX, (BX)\n"); return; }; // Compound assign on an indexed scalar element // (`arr[i] OP= v`). Pre-#133 the outer `if (n.op == // TK_ASSIGN)` had no else and non-ASSIGN ops fell off // the cgassign function emitting NOTHING — silent // no-op. Mirror the chained-pointer-field compound // template at cmd/w6c/cgen.c:3281-3317: same address // computation as the ASSIGN arm above, then // tnodeloadop(BX)→AX, POP rhs→CX, combine, tnodestoreop. // Float / str / slice / tagged element compound stays // unwired — cstage's compound template never carried // those payload kinds. Same shape gate as the cstage // branch (cgen.c #133). if (n.op != tkind.TK_ASSIGN) { let base: *node = lhs.lhs; let idx: *node = lhs.rhs; let esz: i32 = 8; let baselocal: *local = nil; let isglobalarr: bool = false; let isglobalptr: bool = false; let globalname: str; globalname.ptr = nil; globalname.len = 0; let elemtn: *node = nil; if (base != nil) { if (base.kind == nkind.N_IDENT) { let bn: str = base.str; baselocal = localfindnode(c, bn); if (baselocal != nil) { esz = elemsizeofc(c, baselocal.tnode); let btn: *node = baselocal.tnode; if (btn != nil) { let bk: nkind = btn.kind; if (bk == nkind.N_TARRAY) { elemtn = btn.lhs; }; if (bk == nkind.N_TSLICE) { elemtn = btn.lhs; }; if (bk == nkind.N_TPTR) { elemtn = btn.lhs; }; }; } else { let tn: *node = letvartnode(c, bn); if (tn != nil) { if (tn.kind == nkind.N_TARRAY) { isglobalarr = true; globalname = bn; esz = elemsizeofc(c, tn); elemtn = tn.lhs; }; if (tn.kind == nkind.N_TPTR) { isglobalptr = true; globalname = bn; esz = elemsizeofc(c, tn); elemtn = tn.lhs; }; }; }; } else { if (base.kind == nkind.N_DOT) { let dt: *tinfo = lhs.type_: *tinfo; if (dt != nil) { esz = dt.size: i32; elemtn = lhs; }; } else { if (base.kind == nkind.N_INDEX) { let et: *tinfo = lhs.type_: *tinfo; if (et != nil) { esz = et.size: i32; elemtn = lhs; }; };};}; }; // #133-expanded: hard-error unwired payload kinds // LOUD (rule-7) — replaces prior silent skip. if (elemtn != nil) { if (istaggedtype(c, elemtn)) { let msg: str = "indexed-lvalue compound on tagged element not wired (#133/rule-7)\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; if (isstrtype(c, elemtn)) { let msg: str = "indexed-lvalue compound on str element not wired (#133/rule-7)\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; if (isslicetype(c, elemtn)) { let msg: str = "indexed-lvalue compound on slice element not wired (#133/rule-7)\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; if (isfloattype(c, elemtn)) { let msg: str = "indexed-lvalue compound on float element not wired (#133/rule-7)\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; }; cgexpr(c, n.rhs); emitline("\tPUSHQ\tAX\n"); cgexpr(c, idx); if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; emitline("\tPUSHQ\tAX\n"); if (isglobalarr) { emitline("\tLEAQ\t"); emitsymname(c, globalname); emitline("(SB), BX\n"); } else { if (isglobalptr) { emitline("\tMOVQ\t"); emitsymname(c, globalname); emitline("(SB), BX\n"); } else { if (baselocal != nil) { let tn: *node = baselocal.tnode; let isarray: bool = false; if (tn != nil) { if (tn.kind == nkind.N_TARRAY) { isarray = true; }; }; if (isarray) { emitline("\tLEAQ\t"); emitoff(baselocal.off: i64); emitline("(BP), BX\n"); } else { emitline("\tMOVQ\t"); emitoff(baselocal.off: i64); emitline("(BP), BX\n"); }; } else { if (dotbaseaddr(c, base, "BX")) { // #135: N_DOT base address-of-field inline. } else { cgexpr(c, base); emitline("\tMOVQ\tAX, BX\n"); };};};}; emitline("\tPOPQ\tAX\n"); emitline("\tADDQ\tAX, BX\n"); let lop: str = tnodeloadop(c, elemtn, esz); emitline("\t"); emitline(lop); emitline("\t(BX), AX\n"); emitline("\tPOPQ\tCX\n"); // #133-expanded: all 10 integer compound ops wired. // SLASHEQ/PERCENTEQ: CQO+IDIVQ (signed) or zero-DX+ // DIVQ (unsigned). LSHIFTEQ via SHLQ; RSHIFTEQ via // SARQ (signed) or SHRQ (unsigned) per #136. // Signedness from elemtn.type_. let unsignd_c: bool = false; if (elemtn != nil) { if (elemtn.type_ != nil) { unsignd_c = typeisunsigned(elemtn.type_: *tinfo); }; }; let wired: bool = false; if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tCX, AX\n"); wired = true; }; if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tCX, AX\n"); wired = true; }; if (n.op == tkind.TK_STAREQ) { emitline("\tIMULQ\tCX, AX\n"); wired = true; }; if (n.op == tkind.TK_AMPEQ) { emitline("\tANDQ\tCX, AX\n"); wired = true; }; if (n.op == tkind.TK_PIPEEQ) { emitline("\tORQ\tCX, AX\n"); wired = true; }; if (n.op == tkind.TK_CARETEQ) { emitline("\tXORQ\tCX, AX\n"); wired = true; }; if (n.op == tkind.TK_SLASHEQ) { if (unsignd_c) { emitline("\tMOVQ\t$0, DX\n"); emitline("\tDIVQ\tCX\n"); } else { emitline("\tCQO\n"); emitline("\tIDIVQ\tCX\n"); }; wired = true; }; if (n.op == tkind.TK_PERCENTEQ) { if (unsignd_c) { emitline("\tMOVQ\t$0, DX\n"); emitline("\tDIVQ\tCX\n"); } else { emitline("\tCQO\n"); emitline("\tIDIVQ\tCX\n"); }; emitline("\tMOVQ\tDX, AX\n"); wired = true; }; if (n.op == tkind.TK_LSHIFTEQ) { emitline("\tSHLQ\tCX, AX\n"); wired = true; }; if (n.op == tkind.TK_RSHIFTEQ) { if (unsignd_c) { emitline("\tSHRQ\tCX, AX\n"); } else { emitline("\tSARQ\tCX, AX\n"); }; wired = true; }; if (!wired) { let msg: str = "indexed-lvalue compound: unknown compound op (#133/rule-7)\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); }; let sop: str = tnodestoreop(c, elemtn, esz); emitline("\t"); emitline(sop); emitline("\tAX, (BX)\n"); return; }; }; }; // `arr[i].field = v`: N_DOT lhs whose lhs is N_INDEX. Symmetric // write-side of the cgdot N_INDEX-lhs branch added for task #8. // Compute &arr[i] inline (LEAQ for `[N]Struct`, MOVQ for // `[N]*Struct` / `[]Struct` / `*Struct`), deref once when the // element is `*Struct`, then store rhs at field.offset(addr). // Without this both shapes silently drop the store — there is no // existing wwstage branch for N_DOT(N_INDEX,...) lhs at all (the // N_INDEX-lhs branch above handles bare `arr[i] = v`, not the // field write). if (lhs != nil) { if (lhs.kind == nkind.N_DOT && lhs.lhs != nil && lhs.lhs.kind == nkind.N_INDEX) { let idxbase: *node = lhs.lhs.lhs; let idx: *node = lhs.lhs.rhs; let fld2: str = lhs.str; if (idxbase != nil) { if (idxbase.kind == nkind.N_IDENT) { if (idx != nil) { let lc: *local = localfindnode(c, idxbase.str); if (lc != nil) { if (lc.tnode != nil) { let tn: *node = lc.tnode; let elemt: *node = nil; let baseisarray: bool = false; let tk: nkind = tn.kind; if (tk == nkind.N_TSLICE) { elemt = tn.lhs; }; if (tk == nkind.N_TARRAY) { elemt = tn.lhs; baseisarray = true; }; if (tk == nkind.N_TPTR) { elemt = tn.lhs; }; let sname: str; sname.ptr = nil; sname.len = 0; let viaptr: bool = false; if (elemt != nil) { if (elemt.kind == nkind.N_TPTR) { let inner: *node = elemt.lhs; if (inner != nil) { if (inner.kind == nkind.N_TNAME) { sname = inner.str; viaptr = true; };}; } else { if (elemt.kind == nkind.N_TNAME) { sname = elemt.str; };}; }; if (sname.len > 0) { let si: *structinfo = structlookup(c, sname); if (si != nil) { let fi: *fieldinfo = si.fields; for (fi != nil) { if (streq(fi.fname, fld2)) { let esz: i32 = elemsizeofc(c, tn); // f64/f32: rhs in X0. Spill to stack, // compute &arr[i] in BX (deref if *T), // then reload X0 and MOVSD/MOVSS. if (n.op == tkind.TK_ASSIGN) { if (isfloattype(c, fi.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; cgexpr(c, n.rhs); emitline("\tSUBQ\t$8, SP\n"); emitline("\t"); emitline(mov); emitline("\tX0, (SP)\n"); cgexpr(c, idx); if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; if (baseisarray) { emitline("\tLEAQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); } else { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); }; emitline("\tADDQ\tAX, BX\n"); if (viaptr) { emitline("\tMOVQ\t(BX), BX\n"); }; emitline("\t"); emitline(mov); emitline("\t(SP), X0\n"); emitline("\tADDQ\t$8, SP\n"); emitline("\t"); emitline(mov); emitline("\tX0, "); emitdispreg(fi.foff: i64, "BX"); emitline("\n"); return; }; // str/slice: rhs leaves AX=ptr, // BX=len, CX=cap (#1/Phase 3). Spill // all three across the index/address // computation (IMULQ's CX scratch // clobbers cap), stage &arr[i] in DX // off the str AX/BX/CX convention // (mirrors s.f=v), then store the full // triple at foff+0/+8/+16. if (isstrtype(c, fi.tnode) || isslicetype(c, fi.tnode)) { cgexpr(c, n.rhs); emitline("\tPUSHQ\tCX\n"); emitline("\tPUSHQ\tBX\n"); emitline("\tPUSHQ\tAX\n"); cgexpr(c, idx); if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; if (baseisarray) { emitline("\tLEAQ\t"); emitoff(lc.off: i64); emitline("(BP), DX\n"); } else { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), DX\n"); }; emitline("\tADDQ\tAX, DX\n"); if (viaptr) { emitline("\tMOVQ\t(DX), DX\n"); }; emitline("\tPOPQ\tAX\n"); emitline("\tPOPQ\tBX\n"); emitline("\tPOPQ\tCX\n"); emitline("\tMOVQ\tAX, "); emitdispreg(fi.foff: i64, "DX"); emitline("\n"); emitline("\tMOVQ\tBX, "); emitdispreg((fi.foff + 8): i64, "DX"); emitline("\n"); emitline("\tMOVQ\tCX, "); emitdispreg((fi.foff + 16): i64, "DX"); emitline("\n"); return; }; // scalar plain `=` cgexpr(c, n.rhs); emitline("\tPUSHQ\tAX\n"); cgexpr(c, idx); if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; if (baseisarray) { emitline("\tLEAQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); } else { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); }; emitline("\tADDQ\tAX, BX\n"); if (viaptr) { emitline("\tMOVQ\t(BX), BX\n"); }; emitline("\tPOPQ\tAX\n"); let sop: str = fieldstoreop(c, fi); emitline("\t"); emitline(sop); emitline("\tAX, "); emitdispreg(fi.foff: i64, "BX"); emitline("\n"); return; }; // compound: rhs→push; compute struct // addr→BX (deref if *T); push addr; // load old field→AX; pop addr→BX, // rhs→CX; combine; store. Float/str // compound not wired. cgexpr(c, n.rhs); emitline("\tPUSHQ\tAX\n"); cgexpr(c, idx); if (esz > 1) { emitline("\tMOVQ\t$"); emitint(esz: i64); emitline(", CX\n"); emitline("\tIMULQ\tCX, AX\n"); }; if (baseisarray) { emitline("\tLEAQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); } else { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); }; emitline("\tADDQ\tAX, BX\n"); if (viaptr) { emitline("\tMOVQ\t(BX), BX\n"); }; emitline("\tPUSHQ\tBX\n"); let lop: str = fieldloadop(c, fi); emitline("\t"); emitline(lop); emitline("\t"); emitdispreg(fi.foff: i64, "BX"); emitline(", AX\n"); emitline("\tPOPQ\tBX\n"); emitline("\tPOPQ\tCX\n"); if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tCX, AX\n"); }; if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tCX, AX\n"); }; if (n.op == tkind.TK_STAREQ) { emitline("\tIMULQ\tCX, AX\n"); }; if (n.op == tkind.TK_AMPEQ) { emitline("\tANDQ\tCX, AX\n"); }; if (n.op == tkind.TK_PIPEEQ) { emitline("\tORQ\tCX, AX\n"); }; if (n.op == tkind.TK_CARETEQ) { emitline("\tXORQ\tCX, AX\n"); }; let sop2: str = fieldstoreop(c, fi); emitline("\t"); emitline(sop2); emitline("\tAX, "); emitdispreg(fi.foff: i64, "BX"); emitline("\n"); return; }; fi = fi.finext; }; }; }; };}; }; };}; }; }; // Struct/ptr-to-struct field assignment: `s.f = expr;` or // `p.f = expr;`. Only plain `=` is wired (compound on field // is rare and not yet needed by our fixtures). Base accepts the // explicit-deref form `(*p).f = ...` (parser N_UN(STAR, IDENT)) // by retargeting to the inner IDENT so the via_ptr branch fires // the same as auto-deref `p.f = v`. v1 scope: bare-IDENT inner. if (lhs != nil) { if (lhs.kind == nkind.N_DOT) { let base: *node = lhs.lhs; let fld: str = lhs.str; if (base != nil) { if (base.kind == nkind.N_UN) { if (base.op == tkind.TK_STAR) { if (base.lhs != nil) { if (base.lhs.kind == nkind.N_IDENT) { base = base.lhs; }; }; }; }; if (base.kind == nkind.N_IDENT) { let bn: str = base.str; let lc: *local = localfindnode(c, bn); if (lc != nil) { let tn: *node = lc.tnode; let lkind: nkind = nkind.N_NONE; if (tn != nil) { lkind = tn.kind; }; // Pointer-to-struct: deref then store. if (lkind == nkind.N_TPTR) { let inner: *node = tn.lhs; let sname: str; sname.ptr = nil; sname.len = 0; if (inner != nil) { if (inner.kind == nkind.N_TNAME) { sname = inner.str; }; }; if (sname.len > 0) { // structlookupchain (#22) handles the // alias-chain miss; same shape as the // cgdot pointer-to-struct read site. let si: *structinfo = structlookupchain(c, inner); if (si != nil) { let fi: *fieldinfo = si.fields; for (fi != nil) { let fn_: str = fi.fname; if (streq(fn_, fld)) { // Tagged-union field via *struct base — full slot // rewrite via cgwidentaggedstore basereg="BX". Pre-#26 // fell through to the scalar store and dropped tag // + payload. if (n.op == tkind.TK_ASSIGN && istaggedtype(c, fi.tnode)) { let fsz: i32 = slotsize(c, fi.tnode); emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); cgwidentaggedstore(c, fi.tnode.type_: *tinfo, n.rhs, "BX", fi.foff, fsz); return; }; // struct-typed field via *struct base — three // rhs shapes (call/structlit added with #5; // closes #27 marker here): // N_IDENT: word-copy from rhs slot. // N_CALL: cgexpr → AX/DX/CX per #4's cgreturn // ABI; load *struct ptr into BX after the // call, sized stores per the ABI size. // N_STRUCTLIT: field-walk; reload BX before // each store so cgexpr can clobber AX/BX. // register RECV reads AX/DX/CX at 8-byte // granularity — size via structabisize (cstage // SSoT lu->size, check.c:760; cgen.c:7720 // sz=lu->size at the receive twin). if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_CALL && fi.tnode != nil && fi.tnode.kind == nkind.N_TNAME && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); if (ssi != nil) { let ssz: i32 = structabisize(ssi); if (ssz <= 24) { let tlm: i32 = ssz - (ssz / 8) * 8; if (tlm == 0 || tlm == 1 || tlm == 2 || tlm == 4) { cgexpr(c, n.rhs); emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); let full: i32 = ssz / 8; let i: i32 = 0; for (i < full) { let reg: str = "AX"; if (i == 1) { reg = "DX"; }; if (i == 2) { reg = "CX"; }; emitline("\tMOVQ\t"); emitline(reg); emitline(", "); emitdispreg((fi.foff + i * 8): i64, "BX"); emitline("\n"); i += 1; }; if (tlm > 0) { let top: str = "MOVB"; if (tlm == 4) { top = "MOVL"; }; if (tlm == 2) { top = "MOVW"; }; let treg: str = "AX"; if (full == 1) { treg = "DX"; }; if (full == 2) { treg = "CX"; }; emitline("\t"); emitline(top); emitline("\t"); emitline(treg); emitline(", "); emitdispreg((fi.foff + full * 8): i64, "BX"); emitline("\n"); }; return; }; }; }; }; // #18: delegate to cgstructlitfill so a nested struct- // typed structlit value recurses instead of dropping // its trailing bytes. mode=1 (DST_PTR_LOCAL) reloads BX // from lc.off(BP) before zero-fill and before every // field store. if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT && fi.tnode != nil && fi.tnode.kind == nkind.N_TNAME && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); if (ssi != nil) { cgstructlitfill(c, ssi, n.rhs, 1, lc.off, "", fi.foff); return; }; }; if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_IDENT && fi.tnode != nil && fi.tnode.kind == nkind.N_TNAME && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); let srhs: *local = localfindnode(c, n.rhs.str); if (ssi != nil) { if (srhs != nil) { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); let ssz: i32 = ssi.totsize; let k: i32 = 0; for (k + 8 <= ssz) { emitline("\tMOVQ\t"); emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitdispreg((fi.foff + k): i64, "BX"); emitline("\n"); k += 8; }; if (k < ssz) { let tail: i32 = ssz - k; let lop: str = "MOVQ"; if (tail == 4) { lop = "MOVL"; } else { if (tail == 1) { lop = "MOVB"; }; }; emitline("\t"); emitline(lop); emitline("\t"); emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); emitline("\t"); emitline(lop); emitline("\tAX, "); emitdispreg((fi.foff + k): i64, "BX"); emitline("\n"); }; return; };}; }; if (n.op != tkind.TK_ASSIGN) { // compound: load current value emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); let lop: str = fieldloadop(c, fi); emitline("\t"); emitline(lop); emitline("\t"); emitdispreg(fi.foff: i64, "BX"); emitline(", BX\n"); emitline("\tPUSHQ\tBX\n"); }; cgexpr(c, n.rhs); if (n.op != tkind.TK_ASSIGN) { emitline("\tPOPQ\tBX\n"); // PLUSEQ is commutative; MINUSEQ // needs lhs - rhs (BX is old lhs, // AX is rhs). if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tBX, AX\n"); }; if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tAX, BX\n"); emitline("\tMOVQ\tBX, AX\n"); }; }; if (n.op == tkind.TK_ASSIGN) { // str/slice field via *struct: str IS []u8, so both // store the full 3-word {ptr,len,cap} from (AX,BX,CX). // CX holds cap, so stage the struct addr in DX and // store at foff/+8/+16 (#1/Phase 3). if (isstrtype(c, fi.tnode) || isslicetype(c, fi.tnode)) { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), DX\n"); emitline("\tMOVQ\tAX, "); emitdispreg(fi.foff: i64, "DX"); emitline("\n"); emitline("\tMOVQ\tBX, "); emitdispreg((fi.foff + 8): i64, "DX"); emitline("\n"); emitline("\tMOVQ\tCX, "); emitdispreg((fi.foff + 16): i64, "DX"); emitline("\n"); return; }; // f64/f32 plain `=` via *struct: cgexpr left the // value in X0. Reload struct ptr and MOVSD/MOVSS. if (isfloattype(c, fi.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); emitline("\t"); emitline(mov); emitline("\tX0, "); emitdispreg(fi.foff: i64, "BX"); emitline("\n"); return; }; }; emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); let sop: str = fieldstoreop(c, fi); emitline("\t"); emitline(sop); emitline("\tAX, "); emitdispreg(fi.foff: i64, "BX"); emitline("\n"); return; }; fi = fi.finext; }; }; }; }; // Direct struct local: store at off+foff. if (lkind == nkind.N_TNAME) { // structlookupchain (#22) — same shape // as the cgdot direct-local read site. let si: *structinfo = structlookupchain(c, tn); if (si != nil) { let fi: *fieldinfo = si.fields; for (fi != nil) { let fn_: str = fi.fname; if (streq(fn_, fld)) { // Tagged-union field in a direct struct local — // full slot rewrite at (lc.off + fi.foff)(BP) // via cgwidentaggedstore basereg="BP". Pre-#26 // fell through and dropped tag + payload. if (n.op == tkind.TK_ASSIGN && istaggedtype(c, fi.tnode)) { let fsz: i32 = slotsize(c, fi.tnode); cgwidentaggedstore(c, fi.tnode.type_: *tinfo, n.rhs, "BP", lc.off + fi.foff, fsz); return; }; // struct-typed field on a direct struct // local — three rhs shapes (call/structlit // added with #5; closes #27 marker here): // N_IDENT: word-copy from rhs slot. // N_CALL: cgexpr → AX/DX/CX; sized stores // directly at (lc.off+fi.foff)(BP). // N_STRUCTLIT: field-walk; each inner // field stored at +fi.foff+inner_foff(BP). // BP-rel direct, no addr scratch needed. if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_CALL && fi.tnode != nil && fi.tnode.kind == nkind.N_TNAME && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); if (ssi != nil) { let ssz: i32 = structabisize(ssi); if (ssz <= 24) { let tlm: i32 = ssz - (ssz / 8) * 8; if (tlm == 0 || tlm == 1 || tlm == 2 || tlm == 4) { cgexpr(c, n.rhs); let full: i32 = ssz / 8; let i: i32 = 0; for (i < full) { let reg: str = "AX"; if (i == 1) { reg = "DX"; }; if (i == 2) { reg = "CX"; }; emitline("\tMOVQ\t"); emitline(reg); emitline(", "); emitoff((lc.off + fi.foff + i * 8): i64); emitline("(BP)\n"); i += 1; }; if (tlm > 0) { let top: str = "MOVB"; if (tlm == 4) { top = "MOVL"; }; if (tlm == 2) { top = "MOVW"; }; let treg: str = "AX"; if (full == 1) { treg = "DX"; }; if (full == 2) { treg = "CX"; }; emitline("\t"); emitline(top); emitline("\t"); emitline(treg); emitline(", "); emitoff((lc.off + fi.foff + full * 8): i64); emitline("(BP)\n"); }; return; }; }; }; }; // #18: delegate to cgstructlitfill so a nested struct- // typed structlit value recurses instead of dropping // its trailing bytes. mode=0 (DST_BP) — direct BP-rel, // no BX reload. if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT && fi.tnode != nil && fi.tnode.kind == nkind.N_TNAME && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); if (ssi != nil) { cgstructlitfill(c, ssi, n.rhs, 0, 0, "", lc.off + fi.foff); return; }; }; if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_IDENT && fi.tnode != nil && fi.tnode.kind == nkind.N_TNAME && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); let srhs: *local = localfindnode(c, n.rhs.str); if (ssi != nil) { if (srhs != nil) { let ssz: i32 = ssi.totsize; let k: i32 = 0; for (k + 8 <= ssz) { emitline("\tMOVQ\t"); emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitoff((lc.off + fi.foff + k): i64); emitline("(BP)\n"); k += 8; }; if (k < ssz) { let tail: i32 = ssz - k; let lop: str = "MOVQ"; if (tail == 4) { lop = "MOVL"; } else { if (tail == 1) { lop = "MOVB"; }; }; emitline("\t"); emitline(lop); emitline("\t"); emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); emitline("\t"); emitline(lop); emitline("\tAX, "); emitoff((lc.off + fi.foff + k): i64); emitline("(BP)\n"); }; return; };}; }; cgexpr(c, n.rhs); // str/slice field direct: str IS []u8, so both store the // full 3-word {ptr,len,cap} from (AX,BX,CX) at +0/+8/+16. // BP base, no scratch reload needed; the generic fldstoreop // below would write only AX, dropping .len/.cap (#1/Phase 3). if (isstrtype(c, fi.tnode) || isslicetype(c, fi.tnode)) { emitline("\tMOVQ\tAX, "); emitoff((lc.off + fi.foff): i64); emitline("(BP)\n"); emitline("\tMOVQ\tBX, "); emitoff((lc.off + fi.foff + 8): i64); emitline("(BP)\n"); emitline("\tMOVQ\tCX, "); emitoff((lc.off + fi.foff + 16): i64); emitline("(BP)\n"); return; }; // f64/f32 direct struct local store: route via X0. if (isfloattype(c, fi.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\tX0, "); emitoff((lc.off + fi.foff): i64); emitline("(BP)\n"); return; }; let sop: str = fieldstoreop(c, fi); emitline("\t"); emitline(sop); emitline("\tAX, "); emitoff((lc.off + fi.foff): i64); emitline("(BP)\n"); return; }; fi = fi.finext; }; }; }; // str/slice pseudo-field assignment. let delta: i32 = -1; if (streq(fld, "ptr")) { delta = 0; }; if (streq(fld, "len")) { delta = 8; }; if (streq(fld, "cap")) { delta = 16; }; if (delta >= 0) { if (lkind == nkind.N_TPTR) { let inner: *node = tn.lhs; let innerkind: nkind = nkind.N_NONE; if (inner != nil) { innerkind = inner.kind; }; let innerstr: bool = false; if (innerkind == nkind.N_TNAME) { if (streq(inner.str, "str")) { innerstr = true; }; }; if (innerkind == nkind.N_TSLICE) { innerstr = true; }; if (innerstr) { if (n.op != tkind.TK_ASSIGN) { // Compound on `(*str|*slice).field`: load // current → push → eval rhs → combine → store. emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); emitline("\tMOVQ\t"); emitdispreg(delta: i64, "BX"); emitline(", BX\n"); emitline("\tPUSHQ\tBX\n"); cgexpr(c, n.rhs); emitline("\tPOPQ\tBX\n"); // PLUSEQ is commutative; MINUSEQ // needs lhs - rhs. if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tBX, AX\n"); }; if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tAX, BX\n"); emitline("\tMOVQ\tBX, AX\n"); }; emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); emitline("\tMOVQ\tAX, "); emitdispreg(delta: i64, "BX"); emitline("\n"); return; }; cgexpr(c, n.rhs); emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); emitline("\tMOVQ\tAX, "); emitdispreg(delta: i64, "BX"); emitline("\n"); return; }; }; cgexpr(c, n.rhs); emitline("\tMOVQ\tAX, "); emitoff((lc.off + delta): i64); emitline("(BP)\n"); return; }; }; }; }; }; }; // Top-level struct global field assignment: `g.f = expr;` and // `g.f += expr;` for a scalar/str field. Reached when the local // lookup miss but the IDENT base is a registered struct `let`. // LEAQ name(SB) into BX/CX takes the place of the frame slot // addressing the local branches use. Compound (PLUSEQ/MINUSEQ) // follows the same load → push → eval → combine → store shape // as the via-ptr local path. if (lhs != nil) { if (lhs.kind == nkind.N_DOT) { let base: *node = lhs.lhs; let fld: str = lhs.str; if (base != nil) { if (base.kind == nkind.N_IDENT) { let bn: str = base.str; if (localfindnode(c, bn) == nil) { let si: *structinfo = letvarstructinfo(c, bn); if (si != nil) { let fi: *fieldinfo = si.fields; for (fi != nil) { if (streq(fi.fname, fld)) { // struct-typed field on a global struct base — // three rhs shapes (call/structlit added with // #5; closes #27 marker here): // N_IDENT: word-copy from rhs slot. // N_CALL: cgexpr → AX/DX/CX; LEAQ base into BX // after call, sized stores per natural size. // N_STRUCTLIT: field-walk; reload BX per store. if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_CALL && fi.tnode != nil && fi.tnode.kind == nkind.N_TNAME && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); if (ssi != nil) { let ssz: i32 = structabisize(ssi); if (ssz <= 24) { let tlm: i32 = ssz - (ssz / 8) * 8; if (tlm == 0 || tlm == 1 || tlm == 2 || tlm == 4) { cgexpr(c, n.rhs); emitline("\tLEAQ\t"); emitsymname(c, bn); emitline("(SB), BX\n"); let full: i32 = ssz / 8; let i: i32 = 0; for (i < full) { let reg: str = "AX"; if (i == 1) { reg = "DX"; }; if (i == 2) { reg = "CX"; }; emitline("\tMOVQ\t"); emitline(reg); emitline(", "); emitdispreg((fi.foff + i * 8): i64, "BX"); emitline("\n"); i += 1; }; if (tlm > 0) { let top: str = "MOVB"; if (tlm == 4) { top = "MOVL"; }; if (tlm == 2) { top = "MOVW"; }; let treg: str = "AX"; if (full == 1) { treg = "DX"; }; if (full == 2) { treg = "CX"; }; emitline("\t"); emitline(top); emitline("\t"); emitline(treg); emitline(", "); emitdispreg((fi.foff + full * 8): i64, "BX"); emitline("\n"); }; return; }; }; }; }; // #18: delegate to cgstructlitfill so a nested struct- // typed structlit value recurses instead of dropping // its trailing bytes. mode=2 (DST_GLOBAL) reloads BX // via LEAQ bn(SB) before zero-fill and before every // field store. if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT && fi.tnode != nil && fi.tnode.kind == nkind.N_TNAME && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); if (ssi != nil) { cgstructlitfill(c, ssi, n.rhs, 2, 0, bn, fi.foff); return; }; }; if (n.op == tkind.TK_ASSIGN && n.rhs != nil && n.rhs.kind == nkind.N_IDENT && fi.tnode != nil && fi.tnode.kind == nkind.N_TNAME && primsize(fi.tnode.str) == 0) { let ssi: *structinfo = structlookup(c, fi.tnode.str); let srhs: *local = localfindnode(c, n.rhs.str); if (ssi != nil) { if (srhs != nil) { emitline("\tLEAQ\t"); emitsymname(c, bn); emitline("(SB), BX\n"); let ssz: i32 = ssi.totsize; let k: i32 = 0; for (k + 8 <= ssz) { emitline("\tMOVQ\t"); emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); emitline("\tMOVQ\tAX, "); emitdispreg((fi.foff + k): i64, "BX"); emitline("\n"); k += 8; }; if (k < ssz) { let tail: i32 = ssz - k; let lop: str = "MOVQ"; if (tail == 4) { lop = "MOVL"; } else { if (tail == 1) { lop = "MOVB"; }; }; emitline("\t"); emitline(lop); emitline("\t"); emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); emitline("\t"); emitline(lop); emitline("\tAX, "); emitdispreg((fi.foff + k): i64, "BX"); emitline("\n"); }; return; };}; }; if (n.op == tkind.TK_ASSIGN) { cgexpr(c, n.rhs); if (isstrtype(c, fi.tnode)) { // str IS []u8: cgexpr left // (AX=ptr, BX=len, CX=cap). CX // holds cap, so stage the base // addr in DX and store all three // words (#1/Phase 3). emitline("\tLEAQ\t"); emitsymname(c, bn); emitline("(SB), DX\n"); emitline("\tMOVQ\tAX, "); emitdispreg(fi.foff: i64, "DX"); emitline("\n"); emitline("\tMOVQ\tBX, "); emitdispreg((fi.foff + 8): i64, "DX"); emitline("\n"); emitline("\tMOVQ\tCX, "); emitdispreg((fi.foff + 16): i64, "DX"); emitline("\n"); return; }; // f64/f32 plain `=` on global struct field: value is // in X0; LEAQ the base into BX and MOVSD/MOVSS. if (isfloattype(c, fi.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, fi.tnode)) { mov = "MOVSS"; }; emitline("\tLEAQ\t"); emitsymname(c, bn); emitline("(SB), BX\n"); emitline("\t"); emitline(mov); emitline("\tX0, "); emitdispreg(fi.foff: i64, "BX"); emitline("\n"); return; }; let sop: str = fieldstoreop(c, fi); emitline("\tLEAQ\t"); emitsymname(c, bn); emitline("(SB), BX\n"); emitline("\t"); emitline(sop); emitline("\tAX, "); emitdispreg(fi.foff: i64, "BX"); emitline("\n"); return; }; // Compound on scalar field: load // → push → eval rhs → combine → // store. cgexpr clobbers BX, so // re-LEAQ for the store. let lop: str = fieldloadop(c, fi); emitline("\tLEAQ\t"); emitsymname(c, bn); emitline("(SB), BX\n"); emitline("\t"); emitline(lop); emitline("\t"); emitdispreg(fi.foff: i64, "BX"); emitline(", BX\n"); emitline("\tPUSHQ\tBX\n"); cgexpr(c, n.rhs); emitline("\tPOPQ\tBX\n"); if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tBX, AX\n"); }; if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tAX, BX\n"); emitline("\tMOVQ\tBX, AX\n"); }; let sop: str = fieldstoreop(c, fi); emitline("\tLEAQ\t"); emitsymname(c, bn); emitline("(SB), BX\n"); emitline("\t"); emitline(sop); emitline("\tAX, "); emitdispreg(fi.foff: i64, "BX"); emitline("\n"); return; }; fi = fi.finext; }; }; }; }; }; }; }; // Chained `.field = v` where `` itself is a chain // of dots resolving to a *struct. Mirrors the C cgen branch // added to close trap 1 (cmd/w6c/cgen.c). Without this, only // `local.field = v` and `local.fieldptr.field = v` get wired // (the latter through the IDENT-base branch above) — chains // like `s.last.snext = sy` (lib/ww/sym.ww) silently emit no // store. Only plain `=` is wired here; chained compound on a // pointer-field hasn't surfaced. if (lhs != nil) { if (lhs.kind == nkind.N_DOT) { let base: *node = lhs.lhs; let fld: str = lhs.str; if (base != nil) { if (base.kind == nkind.N_DOT) { // #70 (#12): inner-struct layout via the stamped // base.type_ (peel *→struct) + tinfo.fields, // replacing dotinnerstructptr's structinfo walk. // Gate is strict-equal to the deleted helper: fire // only when the chain root is a LOCAL ident AND every // dot resolves through a *struct (dotinnerstructptr // recursed per level on a *struct field, bailing on a // by-value-struct intermediate). Reproducing that // exactly avoids an untested widening past cstage. // Global-root chains stay in their pre-existing shared // base-eval breakage (filed #27). let croot: *node = base; let allptr: bool = true; for (croot != nil && croot.kind == nkind.N_DOT) { let ct: *tinfo = croot.type_: *tinfo; for (ct != nil && ct.kind == tykind.TY_NAMED) { ct = ct.under; }; let okp: bool = false; if (ct != nil) { if (ct.kind == tykind.TY_PTR) { let cs: *tinfo = ct.sub; for (cs != nil && cs.kind == tykind.TY_NAMED) { cs = cs.under; }; if (cs != nil) { if (cs.kind == tykind.TY_STRUCT) { okp = true; }; }; }; }; if (!okp) { allptr = false; }; croot = croot.lhs; }; let it: *tinfo = nil; if (allptr) { if (croot != nil) { if (croot.kind == nkind.N_IDENT) { if (localfindnode(c, croot.str) != nil) { it = base.type_: *tinfo; }; }; }; }; for (it != nil && it.kind == tykind.TY_NAMED) { it = it.under; }; if (it != nil) { if (it.kind == tykind.TY_PTR) { let st: *tinfo = it.sub; for (st != nil && st.kind == tykind.TY_NAMED) { st = st.under; }; if (st != nil) { if (st.kind == tykind.TY_STRUCT) { let tf: *tfield = st.fields; for (tf != nil) { if (streq(tf.name, fld)) { let ft: *tinfo = tf.type_; if (n.op == tkind.TK_ASSIGN) { if (typeisstr(ft) || typeisslice(ft)) { // str/slice: rhs leaves AX=ptr, // BX=len, CX=cap (#1/Phase 3). Spill // all three across the base-expr eval // (it may clobber any reg), stage the // *struct ptr in DX off the str // AX/BX/CX convention (mirrors s.f=v), // then store the full triple at // foff+0/+8/+16. cgexpr(c, n.rhs); emitline("\tPUSHQ\tCX\n"); emitline("\tPUSHQ\tBX\n"); emitline("\tPUSHQ\tAX\n"); cgexpr(c, base); emitline("\tMOVQ\tAX, DX\n"); emitline("\tPOPQ\tAX\n"); emitline("\tPOPQ\tBX\n"); emitline("\tPOPQ\tCX\n"); emitline("\tMOVQ\tAX, "); emitdispreg(tf.offset: i64, "DX"); emitline("\n"); emitline("\tMOVQ\tBX, "); emitdispreg((tf.offset + 8u64): i64, "DX"); emitline("\n"); emitline("\tMOVQ\tCX, "); emitdispreg((tf.offset + 16u64): i64, "DX"); emitline("\n"); return; }; // f64/f32 chained plain `=`: cgexpr rhs left value in // X0. Spill to stack so cgexpr(base) can use AX, then // reload and MOVSD/MOVSS into the slot. if (typeisfloat(ft)) { let mov: str = "MOVSD"; if (typeisf32(ft)) { mov = "MOVSS"; }; cgexpr(c, n.rhs); emitline("\tSUBQ\t$8, SP\n"); emitline("\t"); emitline(mov); emitline("\tX0, (SP)\n"); cgexpr(c, base); emitline("\tMOVQ\tAX, BX\n"); emitline("\t"); emitline(mov); emitline("\t(SP), X0\n"); emitline("\tADDQ\t$8, SP\n"); emitline("\t"); emitline(mov); emitline("\tX0, "); emitdispreg(tf.offset: i64, "BX"); emitline("\n"); return; }; cgexpr(c, n.rhs); emitline("\tPUSHQ\tAX\n"); cgexpr(c, base); emitline("\tMOVQ\tAX, BX\n"); emitline("\tPOPQ\tAX\n"); let sop: str = tnodestoreop(c, n.rhs, ft.slotsize: i32); emitline("\t"); emitline(sop); emitline("\tAX, "); emitdispreg(tf.offset: i64, "BX"); emitline("\n"); return; }; // #133-expanded site 3: chained-pointer- // field compound. Pre-#133-expanded the // wwstage chained-DOT-spine branch only // handled TK_ASSIGN; compound ops on a // chained-*struct.field shape (e.g. // `d.i.v += 7`) silently emitted nothing. // cstage cgen.c:3281-3317 handles this // (now-expanded for the same 10 ops + // hard-errors); this is its rule-10 twin. // All 10 integer compound ops wired; // float/str/slice/tagged field-type // hard-errors LOUD. Signed RSHIFTEQ uses // SARQ (signed) or SHRQ (unsigned) per #136. if (n.op != tkind.TK_ASSIGN) { if (typeisstr(ft)) { let m: str = "chained-ptr-field compound on str element not wired (#133/rule-7)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; if (typeisslice(ft)) { let m: str = "chained-ptr-field compound on slice element not wired (#133/rule-7)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; if (typeisfloat(ft)) { let m: str = "chained-ptr-field compound on float element not wired (#133/rule-7)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; if (typeistagged(ft)) { let m: str = "chained-ptr-field compound on tagged element not wired (#133/rule-7)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; cgexpr(c, n.rhs); emitline("\tPUSHQ\tAX\n"); cgexpr(c, base); emitline("\tPUSHQ\tAX\n"); let fsz: i32 = ft.slotsize: i32; let unsignd_f: bool = typeisunsigned(ft); let lopf: str = loadopsz(!unsignd_f, fsz); emitline("\t"); emitline(lopf); emitline("\t"); emitdispreg(tf.offset: i64, "AX"); emitline(", AX\n"); emitline("\tPOPQ\tBX\n"); emitline("\tPOPQ\tCX\n"); let wired_f: bool = false; if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tCX, AX\n"); wired_f = true; }; if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tCX, AX\n"); wired_f = true; }; if (n.op == tkind.TK_STAREQ) { emitline("\tIMULQ\tCX, AX\n"); wired_f = true; }; if (n.op == tkind.TK_AMPEQ) { emitline("\tANDQ\tCX, AX\n"); wired_f = true; }; if (n.op == tkind.TK_PIPEEQ) { emitline("\tORQ\tCX, AX\n"); wired_f = true; }; if (n.op == tkind.TK_CARETEQ) { emitline("\tXORQ\tCX, AX\n"); wired_f = true; }; if (n.op == tkind.TK_SLASHEQ) { if (unsignd_f) { emitline("\tMOVQ\t$0, DX\n"); emitline("\tDIVQ\tCX\n"); } else { emitline("\tCQO\n"); emitline("\tIDIVQ\tCX\n"); }; wired_f = true; }; if (n.op == tkind.TK_PERCENTEQ) { if (unsignd_f) { emitline("\tMOVQ\t$0, DX\n"); emitline("\tDIVQ\tCX\n"); } else { emitline("\tCQO\n"); emitline("\tIDIVQ\tCX\n"); }; emitline("\tMOVQ\tDX, AX\n"); wired_f = true; }; if (n.op == tkind.TK_LSHIFTEQ) { emitline("\tSHLQ\tCX, AX\n"); wired_f = true; }; if (n.op == tkind.TK_RSHIFTEQ) { if (unsignd_f) { emitline("\tSHRQ\tCX, AX\n"); } else { emitline("\tSARQ\tCX, AX\n"); }; wired_f = true; }; if (!wired_f) { let m: str = "chained-ptr-field compound: unknown op (#133/rule-7)\n"; os.write(2, m.ptr, m.len: u64); os.exit(1); }; let sopf: str = tnodestoreop(c, n.rhs, fsz); emitline("\t"); emitline(sopf); emitline("\tAX, "); emitdispreg(tf.offset: i64, "BX"); emitline("\n"); return; }; }; tf = tf.tnext; }; }; }; }; }; }; }; }; }; // Chained N_DOT spine write through value-struct fields (any // depth) — `o.i.a = 10`, `v.a.b.c = …`. Also handles a slice/str // pseudo-field leaf (`b.buf.len = 5`). Mirror of cstage cgen.c's // chained-DOT write branch. Without this, depth ≥ 3 writes and // the slice/str pseudo-field write through a value-struct chain // silently emit no store. Only plain `=` is wired. if (lhs != nil) { if (lhs.kind == nkind.N_DOT && lhs.lhs != nil && lhs.lhs.kind == nkind.N_DOT && n.op == tkind.TK_ASSIGN) { let rootname: str = ""; let rootoff: i32 = 0; let totaloff: i32 = 0; let leaftype: *tinfo = nil; let slicedelta: i32 = -1; let isglobal: bool = false; let ptrroot: bool = false; let yok: bool = dotchainresolve(c, lhs, &rootname, &rootoff, &totaloff, &leaftype, &slicedelta, &isglobal, &ptrroot); if (yok) { // `*T` root and global share the CX-based emit: // loader runs AFTER cgexpr(rhs) so AX/BX/X0 stay // intact, then stores at total_off off CX. let viacx: bool = isglobal || ptrroot; if (slicedelta >= 0) { cgexpr(c, n.rhs); if (viacx) { if (ptrroot) { emitline("\tMOVQ\t"); emitoff(rootoff: i64); emitline("(BP), CX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, rootname); emitline("(SB), CX\n"); }; emitline("\tMOVQ\tAX, "); emitdispreg((totaloff + slicedelta): i64, "CX"); emitline("\n"); } else { emitline("\tMOVQ\tAX, "); emitoff((rootoff + totaloff + slicedelta): i64); emitline("(BP)\n"); }; return; }; if (typeisstr(leaftype) || typeisslice(leaftype)) { // str/slice: store ptr/len/cap. cgexpr leaves // CX=cap, so the viacx base goes in DX (not CX) to // avoid clobbering it — same as the single-dot str // field store (#1/Phase 3). cgexpr(c, n.rhs); if (viacx) { if (ptrroot) { emitline("\tMOVQ\t"); emitoff(rootoff: i64); emitline("(BP), DX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, rootname); emitline("(SB), DX\n"); }; emitline("\tMOVQ\tAX, "); emitdispreg(totaloff: i64, "DX"); emitline("\n"); emitline("\tMOVQ\tBX, "); emitdispreg((totaloff + 8): i64, "DX"); emitline("\n"); emitline("\tMOVQ\tCX, "); emitdispreg((totaloff + 16): i64, "DX"); emitline("\n"); } else { emitline("\tMOVQ\tAX, "); emitoff((rootoff + totaloff): i64); emitline("(BP)\n"); emitline("\tMOVQ\tBX, "); emitoff((rootoff + totaloff + 8): i64); emitline("(BP)\n"); emitline("\tMOVQ\tCX, "); emitoff((rootoff + totaloff + 16): i64); emitline("(BP)\n"); }; return; }; // TY_STRUCT terminal: three rhs shapes: // - N_IDENT: word-copy from the rhs local slot // (cgexpr is skipped — no whole-struct register // convention for an arbitrary local). // - N_CALL (added with #5): cgexpr leaves the // value in AX/DX/CX per #4's cgreturn ABI; sized // stores write only the declared field size. // cgreturn touches only AX/DX/CX so for // ptrroot/global we load the dst addr into BX // (not CX) after the call to keep CX as the // third value word. // - N_STRUCTLIT (added with #5): field-by-field // store; for ptrroot/global the dst addr is // reloaded into BX before each store so cgexpr // can clobber AX/BX between fields. // #71: the struct-terminal cases below still drive the // structinfo machinery (structnaturalsize / // cgstructlitfill), so recover the struct NAME from the // leaf tinfo's TY_NAMED wrapper. Peeled-TY_STRUCT + // structlookup!=nil is byte-equal to the old `N_TNAME && // primsize==0 && structlookup` guard: a named non-struct // (tagged/alias) peels to a non-STRUCT kind, and // structlookup decides struct-ness off the same declared // name either way. let leafstruct: bool = false; let leafname: str = ""; if (leaftype != nil) { let lp: *tinfo = leaftype; for (lp != nil && lp.kind == tykind.TY_NAMED) { lp = lp.under; }; if (lp != nil) { if (lp.kind == tykind.TY_STRUCT) { leafstruct = true; }; }; if (leaftype.kind == tykind.TY_NAMED) { leafname = leaftype.name; }; }; if (n.rhs != nil && n.rhs.kind == nkind.N_CALL && leafstruct) { let lsi: *structinfo = structlookup(c, leafname); if (lsi != nil) { // register RECV reads AX/DX/CX at 8-byte // granularity — size via structabisize // (cstage SSoT lu->size, check.c:760). let lsz: i32 = structabisize(lsi); if (lsz <= 24) { let tlm: i32 = lsz - (lsz / 8) * 8; if (tlm == 0 || tlm == 1 || tlm == 2 || tlm == 4) { cgexpr(c, n.rhs); if (viacx) { if (ptrroot) { emitline("\tMOVQ\t"); emitoff(rootoff: i64); emitline("(BP), BX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, rootname); emitline("(SB), BX\n"); }; }; let full: i32 = lsz / 8; let i: i32 = 0; for (i < full) { let reg: str = "AX"; if (i == 1) { reg = "DX"; }; if (i == 2) { reg = "CX"; }; if (viacx) { emitline("\tMOVQ\t"); emitline(reg); emitline(", "); emitdispreg((totaloff + i * 8): i64, "BX"); emitline("\n"); } else { emitline("\tMOVQ\t"); emitline(reg); emitline(", "); emitoff((rootoff + totaloff + i * 8): i64); emitline("(BP)\n"); }; i += 1; }; if (tlm > 0) { let top: str = "MOVB"; if (tlm == 4) { top = "MOVL"; }; if (tlm == 2) { top = "MOVW"; }; let treg: str = "AX"; if (full == 1) { treg = "DX"; }; if (full == 2) { treg = "CX"; }; if (viacx) { emitline("\t"); emitline(top); emitline("\t"); emitline(treg); emitline(", "); emitdispreg((totaloff + full * 8): i64, "BX"); emitline("\n"); } else { emitline("\t"); emitline(top); emitline("\t"); emitline(treg); emitline(", "); emitoff((rootoff + totaloff + full * 8): i64); emitline("(BP)\n"); }; }; return; }; }; }; }; // #18: delegate to cgstructlitfill so a nested struct- // typed structlit value recurses instead of dropping // its trailing bytes. mode picks the dst flavor: // ptrroot → mode=1 (DST_PTR_LOCAL), reload BX from // rootoff(BP). // isglobal → mode=2 (DST_GLOBAL), reload BX via // LEAQ rootname(SB). // else → mode=0 (DST_BP), direct BP-rel, no reload. if (n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT && leafstruct) { let lsi: *structinfo = structlookup(c, leafname); if (lsi != nil) { let dmode: i32 = 0; let ddisp: i32 = rootoff + totaloff; if (ptrroot) { dmode = 1; ddisp = totaloff; }; if (isglobal) { dmode = 2; ddisp = totaloff; }; cgstructlitfill(c, lsi, n.rhs, dmode, rootoff, rootname, ddisp); return; }; }; if (n.rhs != nil && n.rhs.kind == nkind.N_IDENT && leafstruct) { let ssi: *structinfo = structlookup(c, leafname); let srhs: *local = localfindnode(c, n.rhs.str); if (ssi != nil) { if (srhs != nil) { if (viacx) { if (ptrroot) { emitline("\tMOVQ\t"); emitoff(rootoff: i64); emitline("(BP), CX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, rootname); emitline("(SB), CX\n"); }; }; let ssz: i32 = ssi.totsize; let k: i32 = 0; for (k + 8 <= ssz) { emitline("\tMOVQ\t"); emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); if (viacx) { emitline("\tMOVQ\tAX, "); emitdispreg((totaloff + k): i64, "CX"); emitline("\n"); } else { emitline("\tMOVQ\tAX, "); emitoff((rootoff + totaloff + k): i64); emitline("(BP)\n"); }; k += 8; }; if (k < ssz) { let tail: i32 = ssz - k; let lop: str = "MOVQ"; if (tail == 4) { lop = "MOVL"; } else { if (tail == 1) { lop = "MOVB"; }; }; emitline("\t"); emitline(lop); emitline("\t"); emitoff((srhs.off + k): i64); emitline("(BP), AX\n"); if (viacx) { emitline("\t"); emitline(lop); emitline("\tAX, "); emitdispreg((totaloff + k): i64, "CX"); emitline("\n"); } else { emitline("\t"); emitline(lop); emitline("\tAX, "); emitoff((rootoff + totaloff + k): i64); emitline("(BP)\n"); }; }; return; };}; }; if (typeisfloat(leaftype)) { let mov: str = "MOVSD"; if (typeisf32(leaftype)) { mov = "MOVSS"; }; cgexpr(c, n.rhs); if (viacx) { if (ptrroot) { emitline("\tMOVQ\t"); emitoff(rootoff: i64); emitline("(BP), CX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, rootname); emitline("(SB), CX\n"); }; emitline("\t"); emitline(mov); emitline("\tX0, "); emitdispreg(totaloff: i64, "CX"); emitline("\n"); } else { emitline("\t"); emitline(mov); emitline("\tX0, "); emitoff((rootoff + totaloff): i64); emitline("(BP)\n"); }; return; }; // Scalar leaf store-op by size — the same size→op // dispatch fieldstoreop used on the leaf fieldinfo, now // keyed on the leaf tinfo's slot width (#71). let sop: str = "MOVQ"; if (leaftype != nil) { let ssz: i32 = leaftype.slotsize: i32; if (ssz == 1) { sop = "MOVB"; } else { if (ssz == 2) { sop = "MOVW"; } else { if (ssz == 4) { sop = "MOVL"; }; }; }; }; cgexpr(c, n.rhs); if (viacx) { if (ptrroot) { emitline("\tMOVQ\t"); emitoff(rootoff: i64); emitline("(BP), CX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, rootname); emitline("(SB), CX\n"); }; emitline("\t"); emitline(sop); emitline("\tAX, "); emitdispreg(totaloff: i64, "CX"); emitline("\n"); } else { emitline("\t"); emitline(sop); emitline("\tAX, "); emitoff((rootoff + totaloff): i64); emitline("(BP)\n"); }; return; }; }; }; // Chained `(ident).f1.f2 = v` where f1 is a struct-by-value // field. The earlier chained-DOT branch handles f1: *T (deref // then store). This handles f1: T (in-place sub-struct), which // would otherwise silently emit no store — lispcore's lexer had // to flatten `cur.kind`/`cur.ival`/... into top-level fields to // work around it. Only plain `=` is wired; compound on a by- // value sub-field hasn't surfaced. // Kept as fallback below the generalized walker for any shape // the walker doesn't recognize. if (lhs != nil) { if (lhs.kind == nkind.N_DOT) { let base: *node = lhs.lhs; let fld: str = lhs.str; if (base != nil) { if (base.kind == nkind.N_DOT) { let inner: *node = base.lhs; let innerfld: str = base.str; if (inner != nil) { if (inner.kind == nkind.N_IDENT) { let lc: *local = localfindnode(c, inner.str); if (lc != nil) { if (lc.tnode != nil) { let tn: *node = lc.tnode; let lkind: nkind = tn.kind; let outname: str; outname.ptr = nil; outname.len = 0; let isptr: bool = false; if (lkind == nkind.N_TNAME) { outname = tn.str; }; if (lkind == nkind.N_TPTR) { let pe: *node = tn.lhs; if (pe != nil) { if (pe.kind == nkind.N_TNAME) { outname = pe.str; isptr = true; };}; }; if (outname.len > 0) { let osi: *structinfo = structlookup(c, outname); if (osi != nil) { let ofi: *fieldinfo = osi.fields; for (ofi != nil) { if (streq(ofi.fname, innerfld)) { let oft: *node = ofi.tnode; if (oft != nil) { if (oft.kind == nkind.N_TNAME) { if (primsize(oft.str) == 0) { let isi: *structinfo = structlookup(c, oft.str); if (isi != nil) { let ffi: *fieldinfo = isi.fields; for (ffi != nil) { if (streq(ffi.fname, fld)) { if (n.op == tkind.TK_ASSIGN) { let totoff: i32 = ofi.foff + ffi.foff; cgexpr(c, n.rhs); if (isstrtype(c, ffi.tnode)) { if (isptr) { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), CX\n"); emitline("\tMOVQ\tAX, "); emitdispreg(totoff: i64, "CX"); emitline("\n"); emitline("\tMOVQ\tBX, "); emitdispreg((totoff + 8): i64, "CX"); emitline("\n"); } else { emitline("\tMOVQ\tAX, "); emitoff((lc.off + totoff): i64); emitline("(BP)\n"); emitline("\tMOVQ\tBX, "); emitoff((lc.off + totoff + 8): i64); emitline("(BP)\n"); }; return; }; if (isfloattype(c, ffi.tnode)) { let mov: str = "MOVSD"; if (isf32type(c, ffi.tnode)) { mov = "MOVSS"; }; if (isptr) { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); emitline("\t"); emitline(mov); emitline("\tX0, "); emitdispreg(totoff: i64, "BX"); emitline("\n"); } else { emitline("\t"); emitline(mov); emitline("\tX0, "); emitoff((lc.off + totoff): i64); emitline("(BP)\n"); }; return; }; let sop: str = fieldstoreop(c, ffi); if (isptr) { emitline("\tMOVQ\t"); emitoff(lc.off: i64); emitline("(BP), BX\n"); emitline("\t"); emitline(sop); emitline("\tAX, "); emitdispreg(totoff: i64, "BX"); emitline("\n"); } else { emitline("\t"); emitline(sop); emitline("\tAX, "); emitoff((lc.off + totoff): i64); emitline("(BP)\n"); }; return; }; }; ffi = ffi.finext; }; }; }; };}; }; ofi = ofi.finext; }; }; }; };}; };}; };}; }; }; // Local-ident target — plain `=` and the simple compound // forms (+= -= *= /=); other compounds fall back to // "evaluate rhs, replace". Mirrors C cgen's IDENT-assign path. if (lhs != nil) { if (lhs.kind == nkind.N_IDENT) { let nm: str = lhs.str; let off: i32 = localfind(c, nm); if (off == 0) { // Top-level let target: RIP-relative store // for `=`, or load→combine→store for the // compound forms. For a str/slice global, // take its address into CX and store both // halves (plus cap for slice — stashed via // DI since LEAQ overwrites CX); the asm has // no `name+8(SB)` operand form. if (!isletvar(c, nm)) { return; }; // Float global: rhs lands in X0; store via // LEAQ+indirect since MOVSS/MOVSD have no // D_EXTERN operand form. let lvf: *letvar = c.lets; let isfg: bool = false; let isf32g: bool = false; let lvftn: *node = nil; for (lvf != nil) { if (streq(lvf.name, nm)) { isfg = isfloattype(c, lvf.tnode); isf32g = isf32type(c, lvf.tnode); lvftn = lvf.tnode; lvf = nil; } else { lvf = lvf.lvnext; }; }; if (isfg) { cgexpr(c, n.rhs); let mov: str = "MOVSD"; let addf: str = "ADDSD"; let subf: str = "SUBSD"; let mulf: str = "MULSD"; let divf: str = "DIVSD"; if (isf32g) { mov = "MOVSS"; addf = "ADDSS"; subf = "SUBSS"; mulf = "MULSS"; divf = "DIVSS"; }; emitline("\tLEAQ\t"); emitsymname(c, nm); emitline("(SB), CX\n"); if (n.op == tkind.TK_ASSIGN) { emitline("\t"); emitline(mov); emitline("\tX0, (CX)\n"); return; }; // Compound: X1 = load; X1 OP= X0; store X1. // ADDSD/SUBSD/MULSD/DIVSD are register-register // only, so we can't combine direct to memory. let fop: str; fop.ptr = nil; fop.len = 0; if (n.op == tkind.TK_PLUSEQ) { fop = addf; }; if (n.op == tkind.TK_MINUSEQ) { fop = subf; }; if (n.op == tkind.TK_STAREQ) { fop = mulf; }; if (n.op == tkind.TK_SLASHEQ) { fop = divf; }; if (fop.len == 0) { // Unsupported (e.g., %= on float): // fall back to plain store of rhs. emitline("\t"); emitline(mov); emitline("\tX0, (CX)\n"); return; }; emitline("\t"); emitline(mov); emitline("\t(CX), X1\n"); emitline("\t"); emitline(fop); emitline("\tX0, X1\n"); emitline("\t"); emitline(mov); emitline("\tX1, (CX)\n"); return; }; cgexpr(c, n.rhs); if (n.op == tkind.TK_ASSIGN) { // str/slice top-level let: str IS []u8, so both store the // full 3-word {ptr,len,cap}. Stash cap in DI before LEAQ // overwrites CX, then store ptr/len/cap via &name(SB) // (#1/Phase 3). if (letvarisstr(c, nm) || letvarisslice(c, nm)) { emitline("\tMOVQ\tCX, DI\n"); emitline("\tLEAQ\t"); emitsymname(c, nm); emitline("(SB), CX\n"); emitline("\tMOVQ\tAX, (CX)\n"); emitline("\tMOVQ\tBX, 8(CX)\n"); emitline("\tMOVQ\tDI, 16(CX)\n"); return; }; emitline("\tMOVQ\tAX, "); emitsymname(c, nm); emitline("(SB)\n"); return; }; // Compound RMW for a top-level let: load through // LEAQ + localloadop when the slot is narrow so // a prior `*(&letname): *iN` deref-store doesn't // leave stale upper bytes feeding the combine. let glop: str = localloadop(c, lvftn); if (streq(glop, "MOVQ")) { emitline("\tMOVQ\t"); emitsymname(c, nm); emitline("(SB), BX\n"); } else { emitline("\tLEAQ\t"); emitsymname(c, nm); emitline("(SB), CX\n"); emitline("\t"); emitline(glop); emitline("\t(CX), BX\n"); }; let didcompound: bool = true; if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tAX, BX\n"); } else { if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tAX, BX\n"); } else { if (n.op == tkind.TK_STAREQ) { emitline("\tIMULQ\tAX, BX\n"); } else { if (n.op == tkind.TK_AMPEQ) { emitline("\tANDQ\tAX, BX\n"); } else { if (n.op == tkind.TK_PIPEEQ) { emitline("\tORQ\tAX, BX\n"); } else { if (n.op == tkind.TK_CARETEQ) { emitline("\tXORQ\tAX, BX\n"); } else { if (n.op == tkind.TK_LSHIFTEQ) { emitline("\tMOVQ\tAX, CX\n"); emitline("\tSHLQ\tCX, BX\n"); } else { if (n.op == tkind.TK_RSHIFTEQ) { // #136: signed RSHIFTEQ → SARQ. let unsignd_r: bool = false; if (lvftn != nil) { if (lvftn.type_ != nil) { unsignd_r = typeisunsigned(lvftn.type_: *tinfo); }; }; if (!unsignd_r) { unsignd_r = nodeisunsigned(c, n.rhs); }; emitline("\tMOVQ\tAX, CX\n"); if (unsignd_r) { emitline("\tSHRQ\tCX, BX\n"); } else { emitline("\tSARQ\tCX, BX\n"); }; } // Post-63332fe: /= and %= for a top-level // let. Same shape as the IDENT-local path: // park rhs in CX, slot value (BX) into AX, // CQO (or zero DX), IDIVQ (or DIVQ) CX, // ferry AX or DX back to BX for the shared // store-BX tail below. else { if (n.op == tkind.TK_SLASHEQ || n.op == tkind.TK_PERCENTEQ) { let unsignd: bool = false; if (lvftn != nil) { unsignd = typeisunsigned(lvftn.type_: *tinfo); }; if (!unsignd) { unsignd = nodeisunsigned(c, n.rhs); }; emitline("\tMOVQ\tAX, CX\n"); emitline("\tMOVQ\tBX, AX\n"); if (unsignd) { emitline("\tMOVQ\t$0, DX\n"); emitline("\tDIVQ\tCX\n"); } else { emitline("\tCQO\n"); emitline("\tIDIVQ\tCX\n"); }; if (n.op == tkind.TK_SLASHEQ) { emitline("\tMOVQ\tAX, BX\n"); } else { emitline("\tMOVQ\tDX, BX\n"); }; } else { // Unsupported compound: store rhs // directly. Mirrors the local path's // legacy fallback for unknown ops. didcompound = false; emitline("\tMOVQ\tAX, "); emitsymname(c, nm); emitline("(SB)\n"); };};};};};};};};}; if (didcompound) { emitline("\tMOVQ\tBX, "); emitsymname(c, nm); emitline("(SB)\n"); }; return; }; // Detect str/slice-typed local — assignment must store // both halves (AX=ptr at +0, BX=len at +8) for str, // plus the cap (CX at +16) for slice. let lcstr: bool = false; let lcsl: bool = false; let lcn: *local = localfindnode(c, nm); if (lcn != nil) { lcstr = isstrtype(c, lcn.tnode); lcsl = isslicetype(c, lcn.tnode); }; let lcf: bool = false; let lcf32: bool = false; if (lcn != nil) { lcf = isfloattype(c, lcn.tnode); lcf32 = isf32type(c, lcn.tnode); }; // Struct-typed local reassignment: `s = expr;` where s // is a TY_STRUCT local of size <=24B. Two rhs shapes // (mirrors cglet's N_STRUCTLIT and the call-result // receive branch): // - N_STRUCTLIT: walk fields, store at off+foff // directly. ASYMMETRY-safe (no register copy from // the caller; values come from cgexpr). // - N_CALL: cgexpr → AX/DX/CX, sized stores per the // declared struct size — MOVQ for full 8B chunks // plus MOVL/MOVW/MOVB tail. See cglet receive // site for the ASYMMETRY rationale. // Struct-IDENT word-copy rhs (s = p) is left unwired; // #5 is scoped to receive-side of #4 (calls + literals). // fsz dispatch uses the explicit {1→MOVB, 4→MOVL, else // MOVQ} pattern (not fieldstoreop) to match cstage // cgen.c N_ASSIGN byte-identically — wwstage's // fieldstoreop returns MOVW for fsz==2 which cstage // doesn't emit (tracked separately as the cstage/ // wwstage MOVW divergence task). if (lcn != nil) { let lctn: *node = lcn.tnode; let lcsname: str; lcsname.ptr = nil; lcsname.len = 0; if (lctn != nil) { if (lctn.kind == nkind.N_TNAME) { lcsname = lctn.str; }; }; if (lcsname.len > 0) { let lcsi: *structinfo = structlookup(c, lcsname); if (lcsi != nil) { // register RECV reads AX/DX/CX at 8-byte // granularity — size via structabisize (cstage // N_ASSIGN-IDENT branch sets sz = lu->size, // cgen.c:4704; check.c:760 SSoT). The pre- // #169 structnaturalsize shorts struct{i64,i32} // (natural 12, ABI 16) to MOVQ+MOVL where // cstage writes MOVQ+MOVQ. let lcnsz: i32 = structabisize(lcsi); if (n.op == tkind.TK_ASSIGN) { if (n.rhs != nil && n.rhs.kind == nkind.N_STRUCTLIT) { // Delegate to the shared BP-relative // structlit fill helper. Handles // TK_ELLIPSIS autofill + per-field // walk; nested struct-typed values // recurse via the helper (#17 fix). // Helper uses the explicit {1→MOVB, // 4→MOVL, else MOVQ} sized-store // dispatch (NOT fieldstoreop) to stay // byte-identical with cstage pending // #13 (fsz==2 MOVW divergence). See // cgstructlitfillbp docstring. cgstructlitfillbp(c, lcsi, n.rhs, off); return; }; if (n.rhs != nil && n.rhs.kind == nkind.N_CALL) { // sret receive (#23): plain // TY_STRUCT > 24B from a CALL. // `s` is the prealloc dest; the // callee writes through hidden RDI // directly into off(BP). Mirror of // cglet's sret branch. if (lcnsz > 24) { let rscs: i32 = callsretsize(c, n.rhs); if (rscs > 0) { c.sretdestoff = off; cgexpr(c, n.rhs); c.sretdestoff = 0; return; }; }; let lcsz: i32 = lcnsz; if (lcsz <= 24) { let tlm: i32 = lcsz - (lcsz / 8) * 8; if (tlm == 0 || tlm == 1 || tlm == 2 || tlm == 4) { cgexpr(c, n.rhs); let full: i32 = lcsz / 8; let i: i32 = 0; for (i < full) { let reg: str = "AX"; if (i == 1) { reg = "DX"; }; if (i == 2) { reg = "CX"; }; emitline("\tMOVQ\t"); emitline(reg); emitline(", "); emitoff((off + i * 8): i64); emitline("(BP)\n"); i += 1; }; if (tlm > 0) { let top: str = "MOVB"; if (tlm == 4) { top = "MOVL"; }; if (tlm == 2) { top = "MOVW"; }; let treg: str = "AX"; if (full == 1) { treg = "DX"; }; if (full == 2) { treg = "CX"; }; emitline("\t"); emitline(top); emitline("\t"); emitline(treg); emitline(", "); emitoff((off + full * 8): i64); emitline("(BP)\n"); }; return; }; }; }; }; }; }; }; // Float-typed local: rhs lands in X0; store via MOVSD/ // MOVSS, no AX shuffle. Compound (+= -= *= /=) loads // slot into X1, combines into X1, stores X1 back — // ADDSD/SUBSD/MULSD/DIVSD are register-register only. if (lcf) { cgexpr(c, n.rhs); let mov: str = "MOVSD"; let addf: str = "ADDSD"; let subf: str = "SUBSD"; let mulf: str = "MULSD"; let divf: str = "DIVSD"; if (lcf32) { mov = "MOVSS"; addf = "ADDSS"; subf = "SUBSS"; mulf = "MULSS"; divf = "DIVSS"; }; if (n.op == tkind.TK_ASSIGN) { emitline("\t"); emitline(mov); emitline("\tX0, "); emitoff(off: i64); emitline("(BP)\n"); return; }; let fop: str; fop.ptr = nil; fop.len = 0; if (n.op == tkind.TK_PLUSEQ) { fop = addf; }; if (n.op == tkind.TK_MINUSEQ) { fop = subf; }; if (n.op == tkind.TK_STAREQ) { fop = mulf; }; if (n.op == tkind.TK_SLASHEQ) { fop = divf; }; if (fop.len == 0) { emitline("\t"); emitline(mov); emitline("\tX0, "); emitoff(off: i64); emitline("(BP)\n"); return; }; emitline("\t"); emitline(mov); emitline("\t"); emitoff(off: i64); emitline("(BP), X1\n"); emitline("\t"); emitline(fop); emitline("\tX0, X1\n"); emitline("\t"); emitline(mov); emitline("\tX1, "); emitoff(off: i64); emitline("(BP)\n"); return; }; cgexpr(c, n.rhs); if (n.op == tkind.TK_ASSIGN) { emitline("\tMOVQ\tAX, "); emitoff(off: i64); emitline("(BP)\n"); if (lcstr || lcsl) { emitline("\tMOVQ\tBX, "); emitoff((off + 8): i64); emitline("(BP)\n"); }; // str IS []u8: store the cap word too, identical to // the slice store (#1/Phase 3). if (lcstr || lcsl) { emitline("\tMOVQ\tCX, "); emitoff((off + 16): i64); emitline("(BP)\n"); }; return; }; // Pick the load width for compound RMW. Signed-narrow // locals must sign-extend the slot before the combine // — ADDQ/SUBQ on amem reads 8B raw, which is wrong // after a 4B deref-store leaves the upper bytes stale. let llop: str = "MOVQ"; if (lcn != nil) { llop = localloadop(c, lcn.tnode); }; if (streq(llop, "MOVQ")) { if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tAX, "); emitoff(off: i64); emitline("(BP)\n"); return; }; if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tAX, "); emitoff(off: i64); emitline("(BP)\n"); return; }; }; // Generic compound: load → combine in BX → store. emitline("\t"); emitline(llop); emitline("\t"); emitoff(off: i64); emitline("(BP), BX\n"); if (n.op == tkind.TK_PLUSEQ) { emitline("\tADDQ\tAX, BX\n"); }; if (n.op == tkind.TK_MINUSEQ) { emitline("\tSUBQ\tAX, BX\n"); }; if (n.op == tkind.TK_STAREQ) { emitline("\tIMULQ\tAX, BX\n"); }; if (n.op == tkind.TK_AMPEQ) { emitline("\tANDQ\tAX, BX\n"); }; if (n.op == tkind.TK_PIPEEQ) { emitline("\tORQ\tAX, BX\n"); }; if (n.op == tkind.TK_CARETEQ) { emitline("\tXORQ\tAX, BX\n"); }; if (n.op == tkind.TK_LSHIFTEQ) { emitline("\tMOVQ\tAX, CX\n"); emitline("\tSHLQ\tCX, BX\n"); }; if (n.op == tkind.TK_RSHIFTEQ) { // #136: signed RSHIFTEQ → SARQ. let unsignd_r: bool = false; if (lcn != nil) { if (lcn.tnode != nil) { if (lcn.tnode.type_ != nil) { unsignd_r = typeisunsigned(lcn.tnode.type_: *tinfo); }; }; }; if (!unsignd_r) { unsignd_r = nodeisunsigned(c, n.rhs); }; emitline("\tMOVQ\tAX, CX\n"); if (unsignd_r) { emitline("\tSHRQ\tCX, BX\n"); } else { emitline("\tSARQ\tCX, BX\n"); }; }; // Post-63332fe: /= and %= for an IDENT local. Pre-fix // fell through with no case, so BX (still holding the // freshly loaded slot value) was stored back unchanged // — a silent no-op rather than the natural rhs-only // shape the global/deref siblings took. Park rhs in // CX, slot value (BX) into AX, CQO/IDIVQ, ferry AX // (quotient) or DX (remainder) back to BX. if (n.op == tkind.TK_SLASHEQ || n.op == tkind.TK_PERCENTEQ) { let unsignd: bool = false; if (lcn != nil) { if (lcn.tnode != nil) { unsignd = typeisunsigned(lcn.tnode.type_: *tinfo); }; }; if (!unsignd) { unsignd = nodeisunsigned(c, n.rhs); }; emitline("\tMOVQ\tAX, CX\n"); emitline("\tMOVQ\tBX, AX\n"); if (unsignd) { emitline("\tMOVQ\t$0, DX\n"); emitline("\tDIVQ\tCX\n"); } else { emitline("\tCQO\n"); emitline("\tIDIVQ\tCX\n"); }; if (n.op == tkind.TK_SLASHEQ) { emitline("\tMOVQ\tAX, BX\n"); } else { emitline("\tMOVQ\tDX, BX\n"); }; }; emitline("\tMOVQ\tBX, "); emitoff(off: i64); emitline("(BP)\n"); return; }; }; return; };