Files
ww/selfhost/cmd/wcc/cgenexpr.ww
Hojun-Cho cbcc0167ae w6c+w6a+selfhost+lib: cgen+asm bugs surfaced by hash modules
Seven fixes across the toolchain, plus three new lib/hash modules
(adler32, crc16, crc32) that surfaced them.

  1. `~x` on u8/u16/u32 left the upper bits set: NOTQ inverts the
     whole 64-bit register and nothing trimmed it back to type
     width, so a returned `u16` would compare 64-bit against a
     typed literal and disagree. Both stages now mask after NOTQ
     for narrow unsigned: AND $0xFF/0xFFFF for u8/u16, MOVL r,r for
     u32 (ANDQ $0xFFFFFFFF sign-extends imm32 and is a no-op).
     Signed narrows stay sign-extended and need no fix-up. See
     cmd/w6c/cgen.c N_UN TK_TILDE and selfhost cgenexpr.ww cgun
     TK_TILDE with new nodeprimwidth helper.

  2. w6a had no D_CONST immediate path for ANDQ / ORQ. cgen would
     emit `ANDQ $65535, AX` and the rr encoder silently wrote
     `21 /r` with garbage reg fields — the mask never happened.
     Added `81 /4` (AND) and `81 /1` (OR) imm32 paths in both
     cstage and selfhost w6a. The ~width fix above depends on this.

  3. `s: []u8` cast as a direct fn argument produced a 0-length
     slice. cgexpr for N_CAST left (AX=ptr, BX=len) from the str
     source but never set CX (cap), and the arg-push fallback only
     pushed AX. cgcast now synthesises CX=BX when target is slice
     and source is str; node_isslice / arg-push recognise
     cast-to-slice and emit the full (cap, len, ptr) triple. Both
     stages.

  4. `*[N]T` element-store used 8-byte stride + MOVQ regardless of
     T's width. Indexing `buf: *[4]u16` would step 8 bytes and
     write 8 bytes per element. Added idx_eff (drills *[N]T → T)
     in cstage and the matching pointer-array drill in selfhost
     elemsizeof. Also added MOVW / MOVZWQ / MOVSWQ to w6c, w6a,
     and selfhost mirrors so 2-byte element stores/loads use the
     right opcode (was falling through to MOVQ and trailing 6 bytes
     into the next slot).

  5. Slicing a top-level fixed array (`g[0:n]` where `g: [N]T` is
     a global) computed the base from BP instead of the symbol —
     localfind returned 0 and the cgen treated it as a local at
     offset 0. Both N_SLICE-as-expression (cgslice) and N_SLICE-
     as-call-arg paths now check let_islet / letvartnode and emit
     LEAQ name(SB) when the base is a global array (or MOVQ
     name(SB) for a global slice/pointer base). Both stages.

  6. Top-level `let arr: [N]T = [v0, v1, ...]` link-failed on
     cstage — emit_lets bailed when it saw N_ARRLIT init on an
     array type, and the sz==8 scalar path then misemitted any
     8-byte-sized array (e.g. [4]u16, [8]u8) as a single quad.
     emit_lets now walks N_ARRLIT, evaluates each element as an
     int/rune/bool/nil literal, packs per-element bytes
     little-endian, and honours the trailing `...` repeat marker.
     Selfhost already handled the literal-init path; fixed the
     parallel sz==8 duplicate-DATAW emit on its side (the array
     and the scalar paths both fired, last write winning at link
     but the duplicate broke cross-stage byte-identicality on user
     code with this shape).

  7. w6a's per-line input buffer was a 1KB stack `char buf[1024]`.
     A `DATAW` for a [256]u16 emits ~2080 bytes on one line, which
     truncated mid-escape; the assembler then re-parsed the
     remaining tail as garbage opcodes ("unknown opcode"). Bumped
     cstage w6a to a 32K static buffer (selfhost w6a already
     allocated per-line via amalloc).

  lib: lib/hash/adler32, lib/hash/crc16, lib/hash/crc32 — pure
  buffer-subset shape (matching lib/hash/fnv), with per-module
  *_test.ww runnable via `ww test lib/hash/<name>`. Adler-32 plus
  CRC-16 (CCITT/CMDA2000/DECT/ANSI) and CRC-32 (IEEE/Castagnoli/
  Koopman) cover Hare's reference vectors bit-for-bit. Wired into
  test/wcc/900_stdlib.c. .gitignore: lib/**/*.s,*.o so `ww test`
  droppings stay untracked.

`make test` (26/26), `make bootstrap` (ww2≡ww3≡ww4), and per-module
`ww test` all pass. cgen output is byte-identical across cstage and
selfhost for every repro that previously diverged.
2026-05-13 14:26:18 +09:00

3703 lines
112 KiB
Plaintext

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