wcc: str -> 24B {ptr,len,cap}, 3-reg ABI -- parity with []u8 (both stages)

A ww `str` becomes a 24-byte {ptr,len,cap} value, identical in layout to
[]u8 -- the enabling prerequisite for the Phase 2 `str == []u8` collapse.

Both stages, atomically:
- ty_str 16->24B; str value flows 3-reg AX/BX/CX (was 2-reg); str literals
  emit cap (=len).
- str in a tagged union grows to a 32B slot, using the AX/DX/CX/R8 4th-word
  path already used by 32B slice-variant unions -- str-variant is now
  structurally identical.
- tuple (scalar,str) return: 4-reg AX/DX/CX/R8 + 32B receive, extending the
  existing type-keyed return (no sret).
- str == []u8 for index and .ptr/.len/.cap, kind-gated where size-based
  dispatch collided at 24B; cstage and wwstage mirror exactly.
- table-driven runtime coverage: test/wcc/928_str_abi_run.c.

Cannot be split (rule 10/11): a 24B str and a 16B str cannot coexist across
the two compiler stages without breaking byte-identity, so the size change
and every dependent ABI/codegen site land in one atomic commit, both stages.

Known follow-ups (zero corpus impact, tracked): str-literal global .cap
static-init; >16B struct by-value (pre-existing); tagged-union
match-scrutinee stage divergence (pre-existing).
This commit is contained in:
2026-05-24 06:40:59 +09:00
parent d9345555c0
commit 1140a590bf
16 changed files with 1337 additions and 494 deletions

View File

@@ -1399,10 +1399,14 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src,
Type *fu = (ftype && ftype->kind == TY_NAMED)
? ftype->under : ftype;
if (fu && fu->kind == TY_STR) {
/* str IS []u8: 3-word field (ptr,len,cap)
* from cgexpr's AX/BX/CX (#1/Phase 3). */
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, write_off + 8 + (int)foff + 0));
ins2(c, A_MOVQ, areg(D_BX),
amem(D_BP, write_off + 8 + (int)foff + 8));
ins2(c, A_MOVQ, areg(D_CX),
amem(D_BP, write_off + 8 + (int)foff + 16));
continue;
}
int op = A_MOVQ;
@@ -1417,11 +1421,14 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src,
if (via_outer) goto copy_out;
return;
}
/* str payload: AX=ptr, BX=len from cgexpr. */
/* str IS []u8: AX=ptr, BX=len, CX=cap from cgexpr. Slot layout
* tag@+0, ptr@+8, len@+16, cap@+24 — same 32B shape as the slice
* payload below (#1/Phase 3). */
if (type_isstr(st) || (su && su->kind == TY_STR)) {
cgexpr(c, src, *locals_p);
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, write_off + 8));
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, write_off + 16));
ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, write_off + 24));
int tag = cg_tag_for_variant(du, st);
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag),
amem(D_BP, write_off + 0));
@@ -1511,8 +1518,11 @@ cg_widen_tagged_push(Cg *c, Local **locals_p, Type *dst, Node *src, int sz)
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). */
/* str IS []u8: slot 32 [+0]=tag, [+8]=ptr, [+16]=len,
* [+24]=cap — same shape as the slice arm below. Push
* cap, len, ptr, tag (high→low so pop drains tag first)
* (#1/Phase 3). */
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));
@@ -1731,6 +1741,35 @@ cg_structlit_fill(Cg *c, Local **locals_p, Type *lu, Node *lit,
}
continue;
}
/* str IS []u8: 3-word field (ptr,len,cap). cgexpr leaves
* AX/BX/CX; for non-BP modes the dst base goes in DX to dodge
* BX=len / CX=cap (the generic store below reloads BX, which
* would clobber len) (#1/Phase 3). */
if (fu && fu->kind == TY_STR) {
cgexpr(c, f->lhs, *locals_p);
if (mode == DST_BP) {
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, disp + (int)foff + 0));
ins2(c, A_MOVQ, areg(D_BX),
amem(D_BP, disp + (int)foff + 8));
ins2(c, A_MOVQ, areg(D_CX),
amem(D_BP, disp + (int)foff + 16));
} else {
if (mode == DST_PTR_LOCAL)
ins2(c, A_MOVQ, amem(D_BP, srcoff),
areg(D_DX));
else
ins2(c, A_LEAQ, masym(c, name),
areg(D_DX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_DX, disp + (int)foff + 0));
ins2(c, A_MOVQ, areg(D_BX),
amem(D_DX, disp + (int)foff + 8));
ins2(c, A_MOVQ, areg(D_CX),
amem(D_DX, disp + (int)foff + 16));
}
continue;
}
cgexpr(c, f->lhs, *locals_p);
/* For non-BP modes, cgexpr just clobbered BX; reload it
* before the store. */
@@ -1784,11 +1823,13 @@ cgexpr(Cg *c, Node *n, Local *locals)
break;
}
case N_STRLIT: {
/* result lives as the (ptr, len) pair: ptr in AX, len in BX.
* Call sites that pass a str arg pick these up directly. */
/* str IS []u8: the (ptr, len, cap) triple — ptr in AX, len in
* BX, cap in CX. A static literal has no spare storage, so
* cap = len (#1/Phase 3, task (b)). */
const char *lab = intern_strlit(c, n->str, n->strlen);
ins2(c, A_LEAQ, asym(lab), areg(D_AX));
ins2(c, A_MOVQ, aimm((long long)n->strlen), areg(D_BX));
ins2(c, A_MOVQ, aimm((long long)n->strlen), areg(D_CX));
break;
}
case N_TRUE: cgexpr_int(c, 1); break;
@@ -1802,11 +1843,11 @@ cgexpr(Cg *c, Node *n, Local *locals)
int op = op_for(n, A_MOVSD, A_MOVSS);
ins2(c, op, amem(D_BP, off), areg(D_X0));
} else if (node_isstr(n)) {
/* str values flow as (AX=ptr, BX=len) so they
* can be returned in AX:DX or pushed to the
* call-arg stack uniformly. */
ins2(c, A_MOVQ, amem(D_BP, off), areg(D_AX));
/* str IS []u8: flow as (AX=ptr, BX=len, CX=cap),
* mirroring the slice local load below (#1/Phase 3). */
ins2(c, A_MOVQ, amem(D_BP, off + 0), areg(D_AX));
ins2(c, A_MOVQ, amem(D_BP, off + 8), areg(D_BX));
ins2(c, A_MOVQ, amem(D_BP, off + 16), areg(D_CX));
} else if (node_isslice(n)) {
/* slice values flow as (AX=ptr, BX=len, CX=cap)
* — mirror the global-slice load so a slice
@@ -1864,23 +1905,26 @@ cgexpr(Cg *c, Node *n, Local *locals)
ins2(c, A_MOVQ,
aimm((long long)s->len),
areg(D_BX));
/* str IS []u8: cap = len for a static
* def literal (#1/Phase 3). */
ins2(c, A_MOVQ,
aimm((long long)s->len),
areg(D_CX));
goto ident_done;
}
}
if (let_islet(n->str)
&& (let_isstr(n->type) || let_isslice(n->type))) {
/* Top-level str/slice global: load each half
/* Top-level str/slice global: load each word
* via its address (the asm has no `name+8(SB)`
* operand form). Slice has a third 8B (cap)
* the address holder CX gets overwritten by
* the cap as the last step, after we no longer
* need it. */
int is_slice = let_isslice(n->type);
* operand form). str IS []u8 now — both carry a
* third 8B (cap); the address holder CX gets
* overwritten by the cap as the last step, after
* we no longer need it (#1/Phase 3). */
ins2(c, A_LEAQ, masym(c, n->str), areg(D_CX));
ins2(c, A_MOVQ, amem(D_CX, 0), areg(D_AX));
ins2(c, A_MOVQ, amem(D_CX, 8), areg(D_BX));
if (is_slice)
ins2(c, A_MOVQ, amem(D_CX, 16), areg(D_CX));
ins2(c, A_MOVQ, amem(D_CX, 16), areg(D_CX));
goto ident_done;
}
if (let_islet(n->str) && let_isfloat(n->type)) {
@@ -2539,27 +2583,31 @@ cgexpr(Cg *c, Node *n, Local *locals)
int is_global = (boff == 0 && !via_ptr
&& let_islet(base->str));
int foff = (int)f->offset;
/* str-typed field: rhs cgexpr leaves (AX=ptr, BX=len);
* store both halves at field+0 and field+8. The 8/16
* trailing-padding bytes are left untouched, which
* matches the let-init shape elsewhere in cgen. Only
* plain `=` is wired; compound on a str field is not
* meaningful. */
/* str IS []u8: rhs cgexpr leaves (AX=ptr, BX=len,
* CX=cap); store all three at field+0/+8/+16,
* mirroring the slice-field arm below. Address
* scratch must dodge CX (holds cap), so via_ptr/
* is_global stage the struct base in DX (#1/Phase 3).
* Only plain `=` is wired; compound on a str field is
* not meaningful. */
Type *str_fu = (f->type && f->type->kind == TY_NAMED)
? f->type->under : f->type;
if (n->op == TK_ASSIGN && str_fu && str_fu->kind == TY_STR) {
cgexpr(c, n->rhs, locals);
if (via_ptr) {
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_CX));
ins2(c, A_MOVQ, areg(D_AX), amem(D_CX, foff + 0));
ins2(c, A_MOVQ, areg(D_BX), amem(D_CX, foff + 8));
ins2(c, A_MOVQ, amem(D_BP, boff), areg(D_DX));
ins2(c, A_MOVQ, areg(D_AX), amem(D_DX, foff + 0));
ins2(c, A_MOVQ, areg(D_BX), amem(D_DX, foff + 8));
ins2(c, A_MOVQ, areg(D_CX), amem(D_DX, foff + 16));
} else if (is_global) {
ins2(c, A_LEAQ, masym(c, base->str), areg(D_CX));
ins2(c, A_MOVQ, areg(D_AX), amem(D_CX, foff + 0));
ins2(c, A_MOVQ, areg(D_BX), amem(D_CX, foff + 8));
ins2(c, A_LEAQ, masym(c, base->str), areg(D_DX));
ins2(c, A_MOVQ, areg(D_AX), amem(D_DX, foff + 0));
ins2(c, A_MOVQ, areg(D_BX), amem(D_DX, foff + 8));
ins2(c, A_MOVQ, areg(D_CX), amem(D_DX, foff + 16));
} else {
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, boff + foff + 0));
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, boff + foff + 8));
ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, boff + foff + 16));
}
break;
}
@@ -3259,25 +3307,34 @@ cgexpr(Cg *c, Node *n, Local *locals)
? leaf_type->size : 8);
int store_op = fldstoreop(leaf_type, fsz);
if (fu && fu->kind == TY_STR) {
/* str IS []u8: store ptr/len/cap. cgexpr
* leaves CX=cap, so the via_cx base goes in
* DX (not CX) to avoid clobbering it — same
* as the single-dot str field store
* (#1/Phase 3). */
cgexpr(c, n->rhs, locals);
if (via_cx) {
if (ptr_root)
ins2(c, A_MOVQ,
amem(D_BP, base_disp),
areg(D_CX));
areg(D_DX));
else
ins2(c, A_LEAQ,
masym(c, cur->str),
areg(D_CX));
areg(D_DX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_CX, total_off + 0));
amem(D_DX, total_off + 0));
ins2(c, A_MOVQ, areg(D_BX),
amem(D_CX, total_off + 8));
amem(D_DX, total_off + 8));
ins2(c, A_MOVQ, areg(D_CX),
amem(D_DX, total_off + 16));
} else {
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, base_disp + total_off + 0));
ins2(c, A_MOVQ, areg(D_BX),
amem(D_BP, base_disp + total_off + 8));
ins2(c, A_MOVQ, areg(D_CX),
amem(D_BP, base_disp + total_off + 16));
}
break;
}
@@ -3583,10 +3640,13 @@ cgexpr(Cg *c, Node *n, Local *locals)
break;
}
if (is_arr || is_sl || is_ptr) {
cgexpr(c, n->rhs, locals); /* AX (and BX if str) */
/* str element: also stash len so we can store both */
if (elem_is_str)
ins1(c, A_PUSHQ, areg(D_BX));
cgexpr(c, n->rhs, locals); /* AX=ptr (BX=len,CX=cap if str) */
/* str IS []u8: stash cap+len so all three store
* (#1/Phase 3). */
if (elem_is_str) {
ins1(c, A_PUSHQ, areg(D_CX)); /* cap */
ins1(c, A_PUSHQ, areg(D_BX)); /* len */
}
ins1(c, A_PUSHQ, areg(D_AX));
cgexpr(c, n->lhs->rhs, locals); /* idx → AX */
if (esz > 1) {
@@ -3626,9 +3686,12 @@ cgexpr(Cg *c, Node *n, Local *locals)
ins2(c, A_ADDQ, areg(D_AX), areg(D_BX));
ins1(c, A_POPQ, areg(D_AX)); /* value (ptr if str) */
if (elem_is_str) {
/* str IS []u8: store ptr/len/cap (#1/Phase 3). */
ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, 0));
ins1(c, A_POPQ, areg(D_CX));
ins2(c, A_MOVQ, areg(D_CX), amem(D_BX, 8));
ins1(c, A_POPQ, areg(D_CX));
ins2(c, A_MOVQ, areg(D_CX), amem(D_BX, 16));
break;
}
int store_op = fldstoreop(esub, esz);
@@ -3678,12 +3741,19 @@ cgexpr(Cg *c, Node *n, Local *locals)
ins2(c, mov, areg(D_X0), amem(D_BX, 0));
break;
}
cgexpr(c, n->rhs, locals); /* AX = value (BX too if str) */
cgexpr(c, n->rhs, locals); /* AX=ptr (BX=len, CX=cap if str) */
ins1(c, A_PUSHQ, areg(D_AX));
if (vt && vt->kind == TY_STR) ins1(c, A_PUSHQ, areg(D_BX));
if (vt && vt->kind == TY_STR) {
/* str IS []u8: also stash len + cap across the
* pointer eval, which clobbers BX/CX (#1/Phase 3). */
ins1(c, A_PUSHQ, areg(D_BX)); /* len */
ins1(c, A_PUSHQ, areg(D_CX)); /* cap */
}
cgexpr(c, n->lhs->lhs, locals); /* AX = pointer */
ins2(c, A_MOVQ, areg(D_AX), areg(D_BX));
if (vt && vt->kind == TY_STR) {
ins1(c, A_POPQ, areg(D_CX)); /* cap */
ins2(c, A_MOVQ, areg(D_CX), amem(D_BX, 16));
ins1(c, A_POPQ, areg(D_CX)); /* len */
ins1(c, A_POPQ, areg(D_AX)); /* ptr */
ins2(c, A_MOVQ, areg(D_AX), amem(D_BX, 0));
@@ -3752,11 +3822,11 @@ cgexpr(Cg *c, Node *n, Local *locals)
break;
}
}
/* Plain `name = strexpr;` for a str-typed local. cgexpr leaves
* (AX=ptr, BX=len); store both halves at off+0 and off+8.
* Mirrors the let-init shape so reassignment doesn't truncate.
* Top-level str globals follow the same shape but go through
* &name(SB) since the asm has no `name+8(SB)` operand form. */
/* Plain `name = strexpr;` for a str-typed local. str IS []u8:
* cgexpr leaves (AX=ptr, BX=len, CX=cap); store all three at
* off+0/+8/+16, identical to the slice arm below. Top-level
* str globals go through &name(SB) → DI scratch (CX holds cap)
* since the asm has no `name+8(SB)` operand form (#1/Phase 3). */
if (n->lhs && n->lhs->kind == N_IDENT && n->op == TK_ASSIGN
&& n->lhs->type) {
Type *lt = n->lhs->type;
@@ -3767,14 +3837,17 @@ cgexpr(Cg *c, Node *n, Local *locals)
cgexpr(c, n->rhs, locals);
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0));
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 8));
ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off + 16));
break;
}
if (let_islet(n->lhs->str)) {
cgexpr(c, n->rhs, locals);
ins2(c, A_MOVQ, areg(D_CX), areg(D_DI));
ins2(c, A_LEAQ, masym(c, n->lhs->str),
areg(D_CX));
ins2(c, A_MOVQ, areg(D_AX), amem(D_CX, 0));
ins2(c, A_MOVQ, areg(D_BX), amem(D_CX, 8));
ins2(c, A_MOVQ, areg(D_DI), amem(D_CX, 16));
break;
}
break;
@@ -4176,17 +4249,20 @@ cgexpr(Cg *c, Node *n, Local *locals)
amem(D_BX, (int)foff));
continue;
}
/* str-typed field: cgexpr leaves (AX=ptr, BX=len).
* Route the heap base through CX so both halves
* survive — using BX would clobber len. */
/* str IS []u8: cgexpr leaves (AX=ptr, BX=len,
* CX=cap). Route the heap base through DX so all
* three survive — CX now holds cap, BX holds len
* (#1/Phase 3). */
Type *fu = (ftype && ftype->kind == TY_NAMED)
? ftype->under : ftype;
if (fu && fu->kind == TY_STR) {
ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_CX));
ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_DX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_CX, (int)foff + 0));
amem(D_DX, (int)foff + 0));
ins2(c, A_MOVQ, areg(D_BX),
amem(D_CX, (int)foff + 8));
amem(D_DX, (int)foff + 8));
ins2(c, A_MOVQ, areg(D_CX),
amem(D_DX, (int)foff + 16));
continue;
}
ins2(c, A_MOVQ, amem(D_SP, 0), areg(D_BX));
@@ -4606,6 +4682,10 @@ cgexpr(Cg *c, Node *n, Local *locals)
ins2(c, A_SUBQ, aimm(8), areg(D_SP));
ins2(c, A_MOVSD, areg(D_X0), amem(D_SP, 0));
} else if (node_isstr(args[i])) {
/* str IS []u8: cgexpr left (AX=ptr, BX=len,
* CX=cap). Push the triple, same as slice
* (#1/Phase 3). */
ins1(c, A_PUSHQ, areg(D_CX)); /* cap */
ins1(c, A_PUSHQ, areg(D_BX)); /* len */
ins1(c, A_PUSHQ, areg(D_AX)); /* ptr — top */
} else if (node_isslice(args[i])) {
@@ -4711,7 +4791,8 @@ cgexpr(Cg *c, Node *n, Local *locals)
stackslots++; /* leave on stack */
}
} else if (node_isstr(args[i])) {
for (int k = 0; k < 2; k++) {
/* str IS []u8: 3-word arg, same as slice (#1/Phase 3). */
for (int k = 0; k < 3; k++) {
if (ii < 6)
ins1(c, A_POPQ, areg(sysv_argregs[ii++]));
else
@@ -4824,9 +4905,8 @@ cgexpr(Cg *c, Node *n, Local *locals)
/* SysV: caller cleans stack args. */
if (stackslots > 0)
ins2(c, A_ADDQ, aimm(stackslots * 8), areg(D_SP));
/* If callee returns a str (16B → AX:DX per SysV), shuffle
* len from DX into BX so str values stay in (AX, BX). */
if (node_isstr(n)) ins2(c, A_MOVQ, areg(D_DX), areg(D_BX));
/* str IS []u8: callee returns AX=ptr, BX=len, CX=cap —
* same as a slice, no receive-side shuffle (#1/Phase 3). */
break;
}
case N_MATCH: {
@@ -5089,8 +5169,13 @@ cgexpr(Cg *c, Node *n, Local *locals)
ins1(c, A_POPQ, areg(D_BP));
ins0(c, A_RET);
label(c, cont);
if (success_is_str)
if (success_is_str) {
/* str IS []u8: success value arrives in the tagged
* ABI as DX=ptr, CX=len, R8=cap (slot 32B). Move len
* out before cap overwrites CX (#1/Phase 3). */
ins2(c, A_MOVQ, areg(D_CX), areg(D_BX));
ins2(c, A_MOVQ, areg(D_R8), areg(D_CX));
}
ins2(c, A_MOVQ, areg(D_DX), areg(D_AX));
break;
}
@@ -5126,8 +5211,13 @@ cgexpr(Cg *c, Node *n, Local *locals)
ins2(c, A_MOVQ, aimm(60), areg(D_AX));
ins0(c, A_SYSCALL);
label(c, cont);
if (success_is_str)
if (success_is_str) {
/* str IS []u8: success arrives DX=ptr, CX=len, R8=cap
* (slot 32B). Move len out before cap clobbers CX
* (#1/Phase 3). */
ins2(c, A_MOVQ, areg(D_CX), areg(D_BX));
ins2(c, A_MOVQ, areg(D_R8), areg(D_CX));
}
ins2(c, A_MOVQ, areg(D_DX), areg(D_AX));
break;
}
@@ -6437,13 +6527,15 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
break;
}
}
/* str initialiser: cgexpr produces (AX=ptr, BX=len).
* #43: gate width via ty_str->size so a future str-layout
* bump (#1) propagates without touching this site. */
/* str IS []u8: cgexpr produces (AX=ptr, BX=len, CX=cap);
* store all three, same as the slice initialiser below.
* #43 gate via ty_str->size already tracks the 24B bump
* (#1/Phase 3). */
if (n->rhs && type_isstr(lt) && sz == (int)ty_str->size) {
cgexpr(c, n->rhs, *locals);
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0));
ins2(c, A_MOVQ, areg(D_BX), amem(D_BP, off + 8));
ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off + 16));
break;
}
/* 2-tuple initialiser from a function call: SysV returns
@@ -6455,13 +6547,14 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, off + 8));
break;
}
/* 24B tuple initialiser for `(scalar, str)` / `(str, scalar)`.
* Per the AX:DX:CX return convention: AX = scalar elem,
* DX = str.ptr, CX = str.len. The slot is laid out positionally
* (e0 at +0, e1 at +8 for scalars; str takes 16B starting at
* its position), so we route each register to the slot dictated
* by the element's type, not by AX/DX position. */
if (n->rhs && lu && lu->kind == TY_TUPLE && sz == 24) {
/* 32B tuple initialiser for `(scalar, str)` / `(str, scalar)`.
* Per the AX:DX:CX:R8 return convention: AX = scalar elem,
* DX = str.ptr, CX = str.len, R8 = str.cap. The slot is laid
* out positionally (str takes 24B at its position), so we route
* each register to the slot dictated by the element's type, not
* by AX/DX position. str IS []u8 (24B) → 32B tuple (#1/Phase 3,
* task #5). */
if (n->rhs && lu && lu->kind == TY_TUPLE && sz == 32) {
Tparam *p0 = lu->params;
Tparam *p1 = p0 ? p0->next : NULL;
Type *t0 = p0 ? p0->type : NULL;
@@ -6473,15 +6566,17 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
if (e0_str ^ e1_str) {
cgexpr(c, n->rhs, *locals);
if (e0_str) {
/* layout: str@+0 (16B), scalar@+16. */
/* layout: str@+0 (24B), scalar@+24. */
ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, off + 0));
ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off + 8));
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 16));
ins2(c, A_MOVQ, areg(D_R8), amem(D_BP, off + 16));
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 24));
} else {
/* layout: scalar@+0 (8B), str@+8 (16B). */
/* layout: scalar@+0 (8B), str@+8 (24B). */
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0));
ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, off + 8));
ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off + 16));
ins2(c, A_MOVQ, areg(D_R8), amem(D_BP, off + 24));
}
break;
}
@@ -6846,15 +6941,17 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
ins2(c, A_MOVQ, areg(D_AX),
areg(D_DX));
} else if (type_isstr(vt)) {
/* str IS []u8: cgexpr leaves
* (AX=ptr, BX=len, CX=cap). Same
* shuffle as the slice arm above —
* DX=ptr, CX=len, R8=cap
* (#1/Phase 3). */
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));
/* str fills DX,CX. Zero R8 if dst
* slot covers slot+24. */
if (rsz > 24)
ins2(c, A_MOVQ, aimm(0),
areg(D_R8));
} else {
ins2(c, A_MOVQ, areg(D_AX),
areg(D_DX));
@@ -7140,22 +7237,28 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
}
}
if (n->lhs && node_isstr(n->lhs)) {
cgexpr(c, n->lhs, *locals); /* AX=ptr, BX=len */
ins2(c, A_MOVQ, areg(D_BX), areg(D_DX));
/* str IS []u8: AX=ptr, BX=len, CX=cap from cgexpr —
* no AX:DX shuffle, same as a slice (#1/Phase 3). */
cgexpr(c, n->lhs, *locals);
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
ins1(c, A_POPQ, areg(D_BP));
ins0(c, A_RET);
break;
}
if (n->lhs && n->lhs->kind == N_TUPLE) {
/* 2-tuple ABI:
/* 2-tuple ABI, word-indexed AX→DX→CX→R8 (the SAME
* register sequence as the tagged-union return; the
* tuple just fills it positionally):
* (scalar, scalar) — AX = e0, DX = e1. (16B, fits SysV.)
* (scalar, str) — AX = scalar elem,
* DX = str.ptr, CX = str.len. (24B custom.)
* (scalar, str) — AX = scalar elem, DX = str.ptr,
* CX = str.len, R8 = str.cap. (32B.)
* (str, scalar) — same regs, type-keyed not position-keyed.
*
* The 24B convention mirrors the existing tagged-union return
* (AX:DX:CX); receive sites destructure off the same regs. */
* str IS []u8 (24B), so a (scalar, str) tuple is 32B and
* rides AX:DX:CX:R8 — the cap is the 4th word, matching the
* tagged-union return that already uses R8 for slot+24
* (#1/Phase 3, task #5). Receive sites destructure off the
* same regs. */
Node *e0 = n->lhs->list;
Node *e1 = e0 ? e0->next : NULL;
if (e1 && e1->next == NULL) {
@@ -7166,7 +7269,8 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
Node *scaln = e0_is_str ? e1 : e0;
cgexpr(c, scaln, *locals); /* AX = scalar */
ins1(c, A_PUSHQ, areg(D_AX));
cgexpr(c, strn, *locals); /* AX=ptr, BX=len */
cgexpr(c, strn, *locals); /* AX=ptr, BX=len, CX=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));
ins1(c, A_POPQ, areg(D_AX));
@@ -7346,11 +7450,12 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
case N_MLET: {
/* eval rhs; consume the per-type return-ABI registers.
* (scalar, scalar) — AX → l0, DX → l1.
* (scalar, str) — AX → scalar slot, (DX, CX) → str slot
* as (.ptr, .len). Position-agnostic.
* (scalar, str) — AX → scalar slot, (DX, CX, R8) → str slot
* as (.ptr, .len, .cap). Position-agnostic.
* Local sizing comes from each l->type so the str slot gets
* the full 16B; without this, only DX would land and the
* len half (CX) would have nowhere to go. */
* the full 24B; without this, only DX would land and the
* len/cap halves (CX/R8) would have nowhere to go.
* str IS []u8 (24B): the cap rides R8 (#1/Phase 3, task #5). */
cgexpr(c, n->rhs, *locals);
Node *l0 = n->list;
Node *l1 = l0 ? l0->next : NULL;
@@ -7368,15 +7473,17 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
int off0 = localoff(c, locals, l0->str, sz0, frame);
int off1 = localoff(c, locals, l1->str, sz1, frame);
if (s0_is_str) {
/* l0 is str: ptr=DX, len=CX. l1 is scalar: l1 = AX. */
/* l0 is str: ptr=DX, len=CX, cap=R8. l1 scalar = AX. */
ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, off0 + 0));
ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off0 + 8));
ins2(c, A_MOVQ, areg(D_R8), amem(D_BP, off0 + 16));
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off1));
} else {
/* l0 is scalar; l1 is str. */
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off0));
ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, off1 + 0));
ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off1 + 8));
ins2(c, A_MOVQ, areg(D_R8), amem(D_BP, off1 + 16));
}
break;
}
@@ -7554,10 +7661,11 @@ cgfn(Cg *c, FILE *out, Node *fn)
* offsets, no spill needed. */
int struct_eb = is_struct ? ((pu->size > 8) ? 2 : 1) : 0;
int tagged_eb = is_tagged ? (tagged_sz / 8) : 0;
int eightbytes = slice ? 3 :
(is_str ? 2 :
/* str IS []u8: 3 eightbytes (ptr,len,cap), same as a slice
* — the caller pushes the triple (#1/Phase 3). */
int eightbytes = (slice || is_str) ? 3 :
(is_struct ? struct_eb :
(is_tagged ? tagged_eb : 1)));
(is_tagged ? tagged_eb : 1));
int regs_left = isf ? (8 - fargi) : (6 - argi);
if (regs_left >= eightbytes) {
/* #60: route slice/str slot widths through Type.size SSoT

View File

@@ -60,8 +60,10 @@ typesinit(Arena *a)
ty_uintptr= prim(a, TY_UINTPTR,"uintptr", 8, 8);
ty_f32 = prim(a, TY_F32, "f32", 4, 4);
ty_f64 = prim(a, TY_F64, "f64", 8, 8);
/* str is { *u8, len } — 16 bytes on amd64. ABI: pointer + u64. */
ty_str = prim(a, TY_STR, "str", 16, 8); /* sizelint-ok: SSoT for ty_str (#64) */
/* str IS []u8: { *u8, len, cap } — 24 bytes, 3-reg ABI (#1/Phase 3).
* Size sourced from the slice SSoT (type_slice) so str and []u8 can
* never drift; no second hardcoded 24. */
ty_str = prim(a, TY_STR, "str", type_slice(a, ty_u8)->size, 8);
ty_err = prim(a, TY_ERR, "<err>", 0, 1);
ty_never = prim(a, TY_NEVER, "never", 0, 1);
/* #29: predeclared `type nomem = !void;`. NAMED so variant_match