w6c+selfhost+lib: cgen quality batch + lib Hare-shape graduation

Six fixes across the toolchain, surfaced by lib/lisp porting work.

  1. f64 compound assigns (`acc += d`, `-=`, `*=`, `/=`). Both stages
     load slot → X1, OP X0 into X1, store back (ADDSD/SUBSD/MULSD/
     DIVSD are reg-reg only). Previous MOVSD-overwrite dropped the
     OP. Locals and top-level lets.

  2. Top-level `[N]u8` arrays + `&arr[i]`. let_emit_size grows a
     TY_ARRAY branch so zero-init DATAW lands; cgindex / N_INDEX
     store / `&base[i]` all detect a global array base and use
     LEAQ name(SB) instead of LEAQ (BP). TK_AMP no longer pre-
     evaluates the operand as a value-load — `&base[i]` computes
     base + i*esz directly. Unblocks Hare's static-buffer pattern:
     strconv.{u64,i64,f64}tos graduate to module-level `*_buf`
     arrays and return owned views.

  3. Cross-module `pkg.Enum.MEMBER`. Nested N_DOT chains that
     don't fold to a known shape now emit `MOVQ <leaf>(SB), AX`
     (mirrors the bare-IDENT unresolved fallback), so isolation
     probes — and the test 990 cgen-match floor — stay consistent
     across stages. strconv exposes `base` as a real `enum i32`;
     callers updated. The `main` exemption (linker entry-point
     keeps bare name even when not exported) mirrors C-side
     collectmods into selfhost cgendecl.

  4. Sum-typed parameter ABI. lib/bytes.{index,rindex} take
     `(u8 | []u8)` needle; lib/strings.byteindex / rbyteindex take
     `(str | rune)` needle (Hare-shaped; the byte-wise misnomer
     `index` is dropped). tagged_arg_size cap bumps to 48 (6 int
     regs), with a new partial-fit branch on the callee: when an
     N-word tagged arg overflows remaining regs, fill what fits and
     stitch the rest from positive BP offsets. scanlocals MCASE
     handles slice binds (24B) and walks each arm with a saved /
     restored seenmark set so two arms naming the same local each
     get their own slot — matches cstage's per-arm scope reset.

  5. 4-reg tagged-return ABI (AX=tag, DX=word0, CX=word1, R8=word2),
     up from 3 regs. Slice-payload variants (`([]T | E)`, slot 32B)
     round-trip ptr/len/cap end-to-end. Every receive site updates:
     let-init via cgwidentaggedstore, match scrutinee spill, cgindex
     tagged-element load (both N_IDENT and fallback bases),
     pushargsrev tagged-ident arg (reads word count from slot size),
     cgreturn slice variant in the shuffle path.

  6. `expr: TaggedAlias` is a widening, not a re-interpret. C cgen +
     selfhost cgwidentaggedstore peel an N_CAST whose destination IS
     the union — so cgexpr's natural shape (str: AX=ptr, BX=len;
     slice: AX=ptr, BX=len, CX=cap) is consumed by the matching
     concrete-variant branch instead of being misread as a tagged
     AX/DX/CX triple. Inner casts to a concrete variant (`7: i32`)
     keep their type for proper tag lookup. `[N]Alias` arrays
     resolve element size via slotsize + aliaslookup, and aliaslookup
     strips a `pkg.` prefix so cross-module references work.

lib/fmt grows `formattable = (i64 | str | bool | rune)` plus
`printv` / `printlnv` taking an explicit `[]formattable` slice (the
receive side of Hare's `args: formattable...`). Call-site variadic
gather isn't wired — callers either hand-build the slice or compose
strconv.i64tos + strings.concat.

700_e2e: 114 → 123 rows (f64 compound, top-level u8 arrays + `&buf[i]`,
pkg.Enum.MEMBER, sum-typed (str|rune) and (u8|[]u8) params, 4-reg
slice-return ABI, formattable array). 26/26 tests, bootstrap stable
through ww4.
This commit is contained in:
2026-05-13 08:05:01 +09:00
parent 6fd0160c0f
commit 46edb8db4a
23 changed files with 2665 additions and 915 deletions

View File

@@ -152,7 +152,11 @@ tagged_arg_size(Type *t)
if (t == NULL) return 0;
if (t->kind == TY_NAMED) t = t->under;
if (t == NULL || t->kind != TY_TAGGED) return 0;
if (t->size > 24) return 0;
/* Param/let/struct contexts have 6 int regs (DI..R9) so a 48B
* tagged union (6 words) still fits in registers. Return values
* are stricter (AX:DX:CX, max 24B) — gated separately in
* cgreturn. */
if (t->size > 48) return 0;
return (int)t->size;
}
@@ -444,6 +448,11 @@ let_emit_size(Type *t)
case TY_STRUCT:
return (int)u->size; /* zero-init only; field reads/
* scalar-field writes only. */
case TY_ARRAY:
return (int)u->size; /* zero-init only; element
* loads/stores via cgindex. Mirror
* of selfhost letemitsize's
* N_TARRAY branch. */
default:
return 0;
}
@@ -481,6 +490,17 @@ let_isstruct(Type *t)
return u && u->kind == TY_STRUCT;
}
/* Is the unwrapped type a fixed-length array? Array globals are
* zero-init DATAW slots; cgindex addresses them as LEAQ name(SB)
* and lets the element load/store run as usual. */
static int
let_isarray(Type *t)
{
if (t == NULL) return 0;
Type *u = (t->kind == TY_NAMED) ? t->under : t;
return u && u->kind == TY_ARRAY;
}
/* Is the unwrapped type a float (f32 or f64)? Float globals flow
* through X0 — load/store goes LEAQ name(SB),CX → MOVSS/MOVSD via the
* indirect, since the asm has no D_EXTERN form for SSE moves yet. */
@@ -848,6 +868,27 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src,
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, slot_off + 0));
return;
}
/* `expr: TaggedAlias` where the cast's destination IS the union
* itself is a widening, not a re-interpret. cgexpr on the cast
* leaves the inner expression's register shape (str: AX=ptr,
* BX=len), not the tagged AX/DX/CX triple — so route through the
* concrete-variant branches below by peeling the cast. Casts to
* a concrete variant (`7: i32`) keep their type for proper tag
* lookup and fall through to the matching branch. */
if (src && src->kind == N_CAST && src->lhs) {
Type *castt = src->type;
Type *castu = (castt && castt->kind == TY_NAMED)
? castt->under : castt;
Type *innert = src->lhs->type;
Type *innu = (innert && innert->kind == TY_NAMED)
? innert->under : innert;
int cast_is_widen = (castu == du) ||
(castu && castu->kind == TY_TAGGED && type_eq(castt, dst));
int inner_is_tagged = innu && innu->kind == TY_TAGGED;
if (cast_is_widen && !inner_is_tagged) {
src = src->lhs;
}
}
Type *st = src ? src->type : NULL;
Type *su = (st && st->kind == TY_NAMED) ? st->under : st;
/* Tagged → tagged subset: copy slot words then tag-remap. */
@@ -871,6 +912,9 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src,
if (ssz > 16)
ins2(c, A_MOVQ, areg(D_CX),
amem(D_BP, slot_off + 16));
if (ssz > 24)
ins2(c, A_MOVQ, areg(D_R8),
amem(D_BP, slot_off + 24));
}
if (ssz < sz) {
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
@@ -965,6 +1009,19 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src,
amem(D_BP, slot_off + 0));
return;
}
/* Slice payload: cgexpr leaves (AX=ptr, BX=len, CX=cap). The
* slot layout is tag@+0, ptr@+8, len@+16, cap@+24 — requires the
* destination tagged-union slot be at least 32B. */
if (type_isslice(st) || (su && su->kind == TY_SLICE)) {
cgexpr(c, src, *locals_p);
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, slot_off + 8));
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, slot_off + 16));
ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, slot_off + 24));
int tag = cg_tag_for_variant(du, st);
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag),
amem(D_BP, slot_off + 0));
return;
}
/* Scalar / pointer / etc. The high slot word (when sz > 16) is
* left untouched here — match dispatches on the tag word first
* and only the str branch reads slot+16, so leaving the pad
@@ -977,9 +1034,12 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src,
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag), amem(D_BP, slot_off + 0));
}
/* cg_widen_tagged_push — call-site widening. Materialise the tagged
* value in a stack scratch slot then push slot words high→low so the
* arg-register pop drain sees tag first, then payload words. */
/* cg_widen_tagged_push — call-site widening. For shapes where cgexpr
* leaves the value directly in registers (str: AX=ptr, BX=len; slice:
* AX=ptr, BX=len, CX=cap; scalar: AX), push from registers without a
* scratch slot. Struct payload and tagged-subset re-layout still
* route through a scratch slot. The direct-push form keeps wwstage's
* asm byte-identical to cstage on the byteindex / index family. */
static void
cg_widen_tagged_push(Cg *c, Local **locals_p, Type *dst, Node *src, int sz)
{
@@ -990,12 +1050,51 @@ cg_widen_tagged_push(Cg *c, Local **locals_p, Type *dst, Node *src, int sz)
ins1(c, A_PUSHQ, areg(D_AX));
return;
}
Type *st = src ? src->type : NULL;
Type *su = (st && st->kind == TY_NAMED) ? st->under : st;
int src_is_struct = su && su->kind == TY_STRUCT;
int src_is_tagged = su && su->kind == TY_TAGGED;
if (!src_is_struct && !src_is_tagged) {
/* Direct-push fast path: str / slice / scalar / pointer. */
cgexpr(c, src, *locals_p);
int tag = cg_tag_for_variant(du, st);
if (tag < 0) tag = 0;
if (type_isstr(st) || (su && su->kind == TY_STR)) {
/* slot 24: [+0]=tag, [+8]=ptr, [+16]=len. Push len,
* ptr, tag (high→low so pop drains tag first). */
ins1(c, A_PUSHQ, areg(D_BX)); /* len */
ins1(c, A_PUSHQ, areg(D_AX)); /* ptr */
ins2(c, A_MOVQ, aimm(tag), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX)); /* tag */
return;
}
if (type_isslice(st) || (su && su->kind == TY_SLICE)) {
/* slot 32: [+0]=tag, [+8]=ptr, [+16]=len, [+24]=cap. */
ins1(c, A_PUSHQ, areg(D_CX)); /* cap */
ins1(c, A_PUSHQ, areg(D_BX)); /* len */
ins1(c, A_PUSHQ, areg(D_AX)); /* ptr */
ins2(c, A_MOVQ, aimm(tag), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX)); /* tag */
return;
}
/* Scalar / pointer variant. Pad with zero high words when
* the slot has room for a wider variant. */
int nwords = sz / 8;
for (int k = nwords - 1; k >= 2; k--) {
ins2(c, A_XORQ, areg(D_DX), areg(D_DX));
ins1(c, A_PUSHQ, areg(D_DX));
}
ins1(c, A_PUSHQ, areg(D_AX)); /* value at +8 */
ins2(c, A_MOVQ, aimm(tag), areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX)); /* tag at +0 */
return;
}
const char *scr_name = mklabel(c, "argscr");
int scr = local_alloc(c, locals_p, scr_name, sz, cg_frame);
/* Zero the scratch slot first so any pad word the store path
* leaves untouched (scalar variant in a >16B slot, struct payload
* shorter than the slot's value area) reads as 0 on the callee.
* The store path then writes the variant bytes over the zeros. */
* leaves untouched (struct payload shorter than the slot's value
* area) reads as 0 on the callee. The store path then writes the
* variant bytes over the zeros. */
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
for (int k = 0; k < sz; k += 8)
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, scr + k));
@@ -1120,6 +1219,84 @@ cgexpr(Cg *c, Node *n, Local *locals)
break;
}
case N_UN:
/* Address-of has its own evaluation strategy — we want the
* address of the operand, not its value. Special-case before
* the cgexpr pre-eval below so `&arr[i]` doesn't compile the
* value load and then discard it. */
if (n->op == TK_AMP) {
Node *opnd = n->lhs;
if (opnd && opnd->kind == N_IDENT) {
int off = localfind(locals, opnd->str);
if (off != 0) {
ins2(c, A_LEAQ, amem(D_BP, off), areg(D_AX));
} else if (let_islet(opnd->str)) {
ins2(c, A_LEAQ, masym(c, opnd->str),
areg(D_AX));
}
break;
}
if (opnd && opnd->kind == N_INDEX) {
/* &base[i] = base + i*esz, no dereference. */
Node *base = opnd->lhs;
Node *idx = opnd->rhs;
Type *bt = base ? base->type : NULL;
Type *bu = (bt && bt->kind == TY_NAMED)
? bt->under : bt;
int esz = (bu && bu->sub)
? (int)bu->sub->size : 1;
if (bu && bu->kind == TY_STR) esz = 1;
cgexpr(c, idx, locals); /* idx → AX */
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz),
areg(D_CX));
ins2(c, A_IMULQ, areg(D_CX),
areg(D_AX));
}
if (base && base->kind == N_IDENT) {
int boff = localfind(locals,
base->str);
int is_arr = bu &&
bu->kind == TY_ARRAY;
if (boff != 0) {
if (is_arr) {
ins2(c, A_LEAQ,
amem(D_BP, boff),
areg(D_BX));
} else {
ins2(c, A_MOVQ,
amem(D_BP, boff),
areg(D_BX));
}
} else if (let_islet(base->str)) {
if (is_arr) {
ins2(c, A_LEAQ,
masym(c, base->str),
areg(D_BX));
} else {
ins2(c, A_MOVQ,
masym(c, base->str),
areg(D_BX));
}
} else {
ins2(c, A_XORQ, areg(D_BX),
areg(D_BX));
}
ins2(c, A_ADDQ, areg(D_BX),
areg(D_AX));
break;
}
/* Complex base: eval to AX, swap into BX,
* then add the saved scaled idx. */
ins1(c, A_PUSHQ, areg(D_AX));
cgexpr(c, base, locals);
ins1(c, A_POPQ, areg(D_BX));
ins2(c, A_ADDQ, areg(D_BX), areg(D_AX));
break;
}
/* Other shapes (& on a complex expr): silent drop,
* mirrors the pre-existing fallback. */
break;
}
cgexpr(c, n->lhs, locals);
switch (n->op) {
case TK_MINUS:
@@ -1158,21 +1335,9 @@ cgexpr(Cg *c, Node *n, Local *locals)
label(c, e);
break;
}
case TK_AMP: {
/* address-of for an N_IDENT: local frame slot first,
* else a top-level mutable let (RIP-relative LEAQ).
* Anything else (e.g. & on an undefined name) silently
* drops, matching the pre-existing behaviour. */
if (n->lhs->kind == N_IDENT) {
int off = localfind(locals, n->lhs->str);
if (off != 0) {
ins2(c, A_LEAQ, amem(D_BP, off), areg(D_AX));
} else if (let_islet(n->lhs->str)) {
ins2(c, A_LEAQ, masym(c, n->lhs->str), areg(D_AX));
}
}
case TK_AMP:
/* Handled in the pre-cgexpr early-exit above. */
break;
}
case TK_STAR: /* deref */
ins2(c, A_MOVQ, amem(D_AX, 0), areg(D_AX));
break;
@@ -1734,16 +1899,62 @@ cgexpr(Cg *c, Node *n, Local *locals)
}
}
/* float assignment to a local or top-level global. Globals
* route through LEAQ+indirect (no D_EXTERN SSE in w6a). */
* route through LEAQ+indirect (no D_EXTERN SSE in w6a).
* Compound (`acc += d` etc.) loads slot into X1, combines
* into X1 (Plan 9 syntax: OP src, dst), stores X1 back —
* w6a's ADDSD/SUBSD/MULSD/DIVSD are register-register only,
* so we can't use a direct mem-form like the integer ADDQ. */
if (n->lhs && n->lhs->kind == N_IDENT && node_isfloat(n)) {
cgexpr(c, n->rhs, locals); /* X0 */
int op = op_for(n, A_MOVSD, A_MOVSS);
int mvop = op_for(n, A_MOVSD, A_MOVSS);
int addop = op_for(n, A_ADDSD, A_ADDSS);
int subop = op_for(n, A_SUBSD, A_SUBSS);
int mulop = op_for(n, A_MULSD, A_MULSS);
int divop = op_for(n, A_DIVSD, A_DIVSS);
int off = localfind(locals, n->lhs->str);
int isglobal = (off == 0) && let_islet(n->lhs->str);
if (off == 0 && !isglobal) break;
if (n->op == TK_ASSIGN) {
if (off != 0) {
ins2(c, mvop, areg(D_X0), amem(D_BP, off));
} else {
ins2(c, A_LEAQ, masym(c, n->lhs->str),
areg(D_CX));
ins2(c, mvop, areg(D_X0), amem(D_CX, 0));
}
break;
}
/* Compound: X1 = load; X1 OP= X0; store X1. */
int fop = -1;
switch (n->op) {
case TK_PLUSEQ: fop = addop; break;
case TK_MINUSEQ: fop = subop; break;
case TK_STAREQ: fop = mulop; break;
case TK_SLASHEQ: fop = divop; break;
default: break;
}
if (off != 0) {
ins2(c, op, areg(D_X0), amem(D_BP, off));
} else if (let_islet(n->lhs->str)) {
ins2(c, A_LEAQ, masym(c, n->lhs->str), areg(D_CX));
ins2(c, op, areg(D_X0), amem(D_CX, 0));
if (fop < 0) {
/* Unsupported compound (e.g., %= on float):
* fall back to plain store of rhs. */
ins2(c, mvop, areg(D_X0),
amem(D_BP, off));
break;
}
ins2(c, mvop, amem(D_BP, off), areg(D_X1));
ins2(c, fop, areg(D_X0), areg(D_X1));
ins2(c, mvop, areg(D_X1), amem(D_BP, off));
} else {
ins2(c, A_LEAQ, masym(c, n->lhs->str),
areg(D_CX));
if (fop < 0) {
ins2(c, mvop, areg(D_X0),
amem(D_CX, 0));
break;
}
ins2(c, mvop, amem(D_CX, 0), areg(D_X1));
ins2(c, fop, areg(D_X0), areg(D_X1));
ins2(c, mvop, areg(D_X1), amem(D_CX, 0));
}
break;
}
@@ -1824,13 +2035,30 @@ cgexpr(Cg *c, Node *n, Local *locals)
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
}
ins1(c, A_PUSHQ, areg(D_AX)); /* scaled idx */
/* base address → BX */
if (base->kind == N_IDENT && is_arr) {
/* base address → BX. Top-level array → LEAQ
* name(SB); top-level ptr → MOVQ name(SB); locals
* route off BP. */
if (base->kind == N_IDENT) {
int off = localfind(locals, base->str);
ins2(c, A_LEAQ, amem(D_BP, off), areg(D_BX));
} else if (base->kind == N_IDENT) {
int off = localfind(locals, base->str);
ins2(c, A_MOVQ, amem(D_BP, off), areg(D_BX));
int isglobal = (off == 0) &&
let_islet(base->str);
if (isglobal && is_arr) {
ins2(c, A_LEAQ,
masym(c, base->str),
areg(D_BX));
} else if (isglobal) {
ins2(c, A_MOVQ,
masym(c, base->str),
areg(D_BX));
} else if (is_arr) {
ins2(c, A_LEAQ,
amem(D_BP, off),
areg(D_BX));
} else {
ins2(c, A_MOVQ,
amem(D_BP, off),
areg(D_BX));
}
} else {
cgexpr(c, base, locals);
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
@@ -2377,7 +2605,7 @@ cgexpr(Cg *c, Node *n, Local *locals)
ins1(c, A_PUSHQ, areg(D_AX));
continue;
}
if (args[i]->kind == N_SLICE) {
if (!widen[i] && args[i]->kind == N_SLICE) {
Node *base = args[i]->lhs;
Node *lo = args[i]->rhs;
Node *hi = args[i]->cond;
@@ -2482,12 +2710,15 @@ cgexpr(Cg *c, Node *n, Local *locals)
ins1(c, A_PUSHQ, areg(D_BX)); /* len */
ins1(c, A_PUSHQ, areg(D_AX)); /* ptr — top */
} else if (node_istaggedarg(args[i])) {
/* Tagged-return ABI: AX=tag, DX=val0[, CX=val1].
* Push high-to-low so pop drains tag first (into
* arg-reg[0]), then values into arg-reg[1..].
* Nullable (sz=8): AX holds the pointer, no
* value-word registers — push just AX. */
/* Tagged-return ABI: AX=tag, DX=val0,
* CX=val1, R8=val2. Push high-to-low so pop
* drains tag first (into arg-reg[0]), then
* values into arg-reg[1..]. Nullable (sz=8):
* AX holds the pointer, no value-word
* registers — push just AX. */
int sz = tagged_arg_size(args[i]->type);
if (sz > 24)
ins1(c, A_PUSHQ, areg(D_R8));
if (sz > 16)
ins1(c, A_PUSHQ, areg(D_CX));
if (sz > 8)
@@ -2667,9 +2898,9 @@ cgexpr(Cg *c, Node *n, Local *locals)
/* Spill non-ident scrutinees (e.g. `match (foo()?)`) into
* a scratch slot so we can index out the tag/value. The
* call ABI for tagged returns is AX=tag, DX=value0,
* CX=value1 — copy each word into the slot. Nullable
* returns are single-word: AX is the pointer; spill
* only that. */
* CX=value1, R8=value2 — copy each word into the slot.
* Nullable returns are single-word: AX is the pointer;
* spill only that. */
sl_off = localoff(c, &locals, "@match_spill", slot_size,
cg_frame);
cgexpr(c, s, locals);
@@ -2680,6 +2911,9 @@ cgexpr(Cg *c, Node *n, Local *locals)
if (slot_size > 16)
ins2(c, A_MOVQ, areg(D_CX),
amem(D_BP, sl_off + 16));
if (slot_size > 24)
ins2(c, A_MOVQ, areg(D_R8),
amem(D_BP, sl_off + 24));
}
}
char *end = mklabel(c, "match_end");
@@ -2758,8 +2992,11 @@ cgexpr(Cg *c, Node *n, Local *locals)
amem(D_BP, voff));
}
} else {
int bsz = (bu && bu->kind == TY_STR)
? 16 : 8;
int bsz = 8;
if (bu && bu->kind == TY_STR) bsz = 16;
else if (bu && bu->kind == TY_SLICE) bsz = 24;
else if (bu) bsz = (int)bu->size;
if (bsz <= 0) bsz = 8;
/* local_alloc to dodge name-collision
* dedup — a 16B str bind shadowing an
* 8B outer would otherwise overflow
@@ -3352,6 +3589,18 @@ cgexpr(Cg *c, Node *n, Local *locals)
}
}
}
/* Nested module-qualified field where the chain didn't fold to
* a known shape (typical when w6c runs on a single file with
* `use mod;` but no driver concatenation — the body's enum /
* struct hasn't been seen). Emit `MOVQ <leaf>(SB), AX` so the
* linker surfaces a clean undefined-symbol error on the leaf
* — mirrors the bare-N_IDENT unresolved fallback used by
* single-segment N_DOTs. Keeps cstage / wwstage byte-aligned
* on the cgen-match isolation probes. */
if (n->lhs && n->lhs->kind == N_DOT && n->str) {
ins2(c, A_MOVQ, masym(c, n->str), areg(D_AX));
break;
}
/* fall through to base evaluation; result placeholder */
cgexpr(c, n->lhs, locals);
dot_done:
@@ -3372,13 +3621,22 @@ cgexpr(Cg *c, Node *n, Local *locals)
if (n->lhs->kind == N_IDENT && u) {
int off = localfind(locals, n->lhs->str);
int isglobal = (off == 0) && let_islet(n->lhs->str);
cgexpr(c, n->rhs, locals); /* idx → AX */
if (esz > 1) {
ins2(c, A_MOVQ, aimm(esz), areg(D_CX));
ins2(c, A_IMULQ, areg(D_CX), areg(D_AX));
}
/* base address into BX */
if (u->kind == TY_ARRAY) {
/* base address into BX. Top-level array → LEAQ
* name(SB); top-level ptr → MOVQ name(SB) (the symbol
* holds the pointer); locals route off BP. */
if (isglobal && u->kind == TY_ARRAY) {
ins2(c, A_LEAQ, masym(c, n->lhs->str),
areg(D_BX));
} else if (isglobal) {
ins2(c, A_MOVQ, masym(c, n->lhs->str),
areg(D_BX));
} else if (u->kind == TY_ARRAY) {
ins2(c, A_LEAQ, amem(D_BP, off), areg(D_BX));
} else {
/* slice/str/ptr: ptr field is at off+0 */
@@ -3394,12 +3652,16 @@ cgexpr(Cg *c, Node *n, Local *locals)
break;
}
/* tagged element: load slot words into (AX=tag,
* DX=val0, CX=val1) — matches the tagged-return ABI
* so let-init / match / call-arg paths consume it
* without spilling. Nullable folded element is one
* word in AX (caller treats it as a pointer). */
* DX=val0, CX=val1, R8=val2) — matches the
* tagged-return ABI so let-init / match / call-arg
* paths consume it without spilling. Nullable folded
* element is one word in AX (caller treats it as a
* pointer). */
if (elem_tagged) {
int ssz = (int)esubu->size;
if (ssz > 24)
ins2(c, A_MOVQ, amem(D_BX, 24),
areg(D_R8));
if (ssz > 16)
ins2(c, A_MOVQ, amem(D_BX, 16),
areg(D_CX));
@@ -3446,6 +3708,8 @@ cgexpr(Cg *c, Node *n, Local *locals)
if (elem_tagged) {
int ssz = (int)esubu->size;
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
if (ssz > 24)
ins2(c, A_MOVQ, amem(D_BX, 24), areg(D_R8));
if (ssz > 16)
ins2(c, A_MOVQ, amem(D_BX, 16), areg(D_CX));
if (ssz > 8)
@@ -3861,13 +4125,27 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
/* same tagged type: forward AX/DX/CX. */
cgexpr(c, n->lhs, *locals);
} else if (!istagged && !isstruct) {
/* str / scalar variant: synthesise the
* tag in AX and shuffle the value into
* DX[/CX]. Direct register path keeps
* the asm short — no scratch slot. */
/* str / slice / scalar variant: synthesise
* the tag in AX and shuffle the value into
* DX[/CX[/R8]]. Direct register path keeps
* the asm short — no scratch slot.
* Tagged-return ABI: AX=tag, DX=word0,
* CX=word1, R8=word2. Slice payload uses
* all four; str uses three; scalar uses
* two. */
int tag = cg_tag_for_variant(rt, vt);
cgexpr(c, n->lhs, *locals);
if (type_isstr(vt)) {
if (type_isslice(vt)) {
/* cgexpr leaves (AX=ptr, BX=len,
* CX=cap). Move into the return
* shuffle: DX=ptr, CX=len, R8=cap. */
ins2(c, A_MOVQ, areg(D_CX),
areg(D_R8));
ins2(c, A_MOVQ, areg(D_BX),
areg(D_CX));
ins2(c, A_MOVQ, areg(D_AX),
areg(D_DX));
} else if (type_isstr(vt)) {
ins2(c, A_MOVQ, areg(D_BX),
areg(D_CX));
ins2(c, A_MOVQ, areg(D_AX),
@@ -3881,10 +4159,12 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
} else {
/* Struct variant or tagged-subset:
* materialise the widened value in a
* scratch slot, then load AX/DX/CX
* scratch slot, then load AX/DX/CX/R8
* from the slot. Struct literal: field
* stores; struct ident: word copy;
* tagged subset: copy + tag remap. */
* tagged subset: copy + tag remap.
* 4th word in R8 covers slice payload
* variants (slot >= 32B). */
int sz = (int)rt->size;
const char *scrn = mklabel(c, "retscr");
int scr = local_alloc(c, locals, scrn,
@@ -3905,6 +4185,10 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
ins2(c, A_MOVQ,
amem(D_BP, scr + 16),
areg(D_CX));
if (sz > 24)
ins2(c, A_MOVQ,
amem(D_BP, scr + 24),
areg(D_R8));
}
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
ins1(c, A_POPQ, areg(D_BP));
@@ -4337,6 +4621,33 @@ cgfn(Cg *c, FILE *out, Node *fn)
amem(D_BP, off));
argi++;
}
} else if (eightbytes > 1 && regs_left > 0 &&
(slice || is_str || is_struct || is_tagged)) {
/* Multi-word arg that partially fits in regs: caller
* filled (regs_left) registers greedily, the rest spilled
* to stack at positive BP offsets. Stitch a single local
* slot from both sources so the body sees a contiguous
* value. Mirrors the SysV greedy reg fill the caller
* does. */
int sz = slice ? 24 : (is_str ? 16 :
(is_struct ? (int)pu->size :
(is_tagged ? tagged_sz : 8)));
int off = localoff(c, &locals, p->str, sz, &frame);
extern int cg_stack_arg_cursor;
int k = 0;
for (; k < regs_left; k++, argi++)
ins2(c, A_MOVQ,
areg(sysv_argregs[argi]),
amem(D_BP, off + k * 8));
for (; k < eightbytes; k++) {
int stack_off = 16 +
cg_stack_arg_cursor * 8;
cg_stack_arg_cursor++;
ins2(c, A_MOVQ, amem(D_BP, stack_off),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, off + k * 8));
}
} else {
/* stack-spilled. Access in place via positive BP offset. */
static int stack_arg_off;
@@ -4501,11 +4812,14 @@ emit_lets(Cg *c, FILE *out, Node *file)
continue;
}
/* Otherwise: zero-init. str accepts nil / ""; struct
* accepts no rhs at all; slice accepts nil. */
* accepts no rhs at all; slice accepts nil; array accepts
* no rhs (literal-array init isn't wired). */
if (r != NULL) {
int is_struct = let_isstruct(d->type);
int is_array = let_isarray(d->type);
int empty_str = (r->kind == N_STRLIT && r->strlen == 0);
if (is_struct) continue;
if (is_array) continue;
if (r->kind != N_NIL && !empty_str) continue;
}
emit_data_row_zero(out, "DATAW", mod_mangle(c, d->str), sz);