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

@@ -111,12 +111,16 @@ fn cgreturn(c: *cgen, n: *node) void = {
rundefers(c);
let rhs: *node = n.lhs;
if (rhs != nil) {
// Tuple return `return a, b;`:
// Tuple return `return a, b;`, word-indexed AX→DX→CX→R8 (the
// SAME register sequence as the tagged-union return below; the
// tuple just fills it positionally):
// (scalar, scalar) — AX = v0, DX = v1.
// (scalar, str) / (str, scalar) — AX = scalar elem,
// DX = str.ptr, CX = str.len.
// 24B convention mirrors the tagged-union return below; receive
// sites destructure off the same regs regardless of position.
// DX = str.ptr, CX = str.len, R8 = str.cap.
// str IS []u8 (24B) → 32B tuple; cap rides R8, matching the
// tagged-union return that already uses R8 for slot+24
// (#1/Phase 3, task #5). Receive sites destructure off the
// same regs regardless of position.
if (rhs.kind == nkind.N_TUPLE) {
let v: *node = rhs.list;
if (v != nil) {
@@ -131,6 +135,7 @@ fn cgreturn(c: *cgen, n: *node) void = {
cgexpr(c, scaln);
emitline("\tPUSHQ\tAX\n");
cgexpr(c, strn);
emitline("\tMOVQ\tCX, R8\n");
emitline("\tMOVQ\tBX, CX\n");
emitline("\tMOVQ\tAX, DX\n");
emitline("\tPOPQ\tAX\n");
@@ -283,12 +288,12 @@ fn cgreturn(c: *cgen, n: *node) void = {
emitline("\tMOVQ\tBX, CX\n");
emitline("\tMOVQ\tAX, DX\n");
} else { if (nodeisstr(c, rhs)) {
// 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).
emitline("\tMOVQ\tCX, R8\n");
emitline("\tMOVQ\tBX, CX\n");
emitline("\tMOVQ\tAX, DX\n");
// str fills DX,CX. Zero R8 if dst covers slot+24.
if (rsz > 24) {
emitline("\tMOVQ\t$0, R8\n");
};
} else {
emitline("\tMOVQ\tAX, DX\n");
// scalar fills DX only. Zero CX / R8 if dst
@@ -544,11 +549,8 @@ fn cgreturn(c: *cgen, n: *node) void = {
};
emitline("\tMOVQ\t$0, AX\n");
};
// SysV: 16-byte aggregates (str, 2-tuple) return in (AX, DX).
// cgexpr leaves str in (AX, BX); shuffle BX→DX.
if (isstrtype(c, c.fnret)) {
emitline("\tMOVQ\tBX, DX\n");
};
// str IS []u8: cgexpr leaves AX=ptr, BX=len, CX=cap — str now
// returns exactly like a slice, no AX:DX shuffle (#1/Phase 3).
emitline("\tMOVQ\tBP, SP\n");
emitline("\tPOPQ\tBP\n");
emitline("\tRET\n");
@@ -719,11 +721,13 @@ fn cglet(c: *cgen, n: *node) void = {
c.lastwasreturn = 0;
return;
};
// 24B tuple init for `let t: (scalar, str) = call()` /
// `let t: (str, scalar) = call()`. Per the AX:DX:CX return
// convention: AX = scalar elem, DX = str.ptr, CX = str.len.
// Layout is positional, so we route each register to the
// slot dictated by element type, not by AX/DX position.
// 32B tuple init for `let t: (scalar, str) = call()` /
// `let t: (str, scalar) = call()`. Per the AX:DX:CX:R8 return
// convention: AX = scalar elem, DX = str.ptr, CX = str.len,
// R8 = str.cap. Layout is positional (str takes 24B at its
// position), so we route each register to the slot dictated by
// element type, not by AX/DX position. str IS []u8 (24B) → 32B
// tuple (#1/Phase 3, task #5).
if (n.lhs != nil) {
if (n.lhs.kind == nkind.N_TTUPLE) {
let p0: *node = n.lhs.list;
@@ -746,9 +750,12 @@ fn cglet(c: *cgen, n: *node) void = {
emitline("\tMOVQ\tCX, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tAX, ");
emitline("\tMOVQ\tR8, ");
emitoff((off + 16): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tAX, ");
emitoff((off + 24): i64);
emitline("(BP)\n");
} else {
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
@@ -759,6 +766,9 @@ fn cglet(c: *cgen, n: *node) void = {
emitline("\tMOVQ\tCX, ");
emitoff((off + 16): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tR8, ");
emitoff((off + 24): i64);
emitline("(BP)\n");
};
c.lastwasreturn = 0;
return;
@@ -1049,15 +1059,19 @@ fn cglet(c: *cgen, n: *node) void = {
emitline("\tMOVQ\tAX, ");
emitoff(off: i64);
emitline("(BP)\n");
// str init: cgexpr also leaves len in BX; store both.
// str IS []u8: cgexpr leaves (ptr,len,cap) in AX/BX/CX; store
// all three, same as the slice arm below (#1/Phase 3).
// #60: gate by kind too — under #1's str=24 bump, sizeof(str)
// and sizeof(slice) collide, so a bare `sz ==` check fires
// both branches for one let. Mirrors cstage cgen.c:6439's
// both branches for one let. Mirrors cstage cgen.c's
// `type_isstr(lt) && sz == ty_str->size` shape.
if (isstrtype(c, tn) && sz == primtypesize("str"): i32) {
emitline("\tMOVQ\tBX, ");
emitoff((off + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off + 16): i64);
emitline("(BP)\n");
};
// slice init: ptr/len/cap in AX/BX/CX. Same kind+size gate as
// the str arm — without the kind check this fires on a str let
@@ -1218,11 +1232,12 @@ fn cgmassign(c: *cgen, n: *node) void = {
// type is taken from its explicit annotation (l.lhs) when present
// or inferred from the called fn's return-type tuple element.
//
// Per the AX:DX:CX return convention (mirrors C cgen nkind.N_MLET):
// Per the AX:DX:CX:R8 return convention (mirrors C cgen nkind.N_MLET):
// (scalar, scalar) — AX → l0, DX → l1.
// (scalar, str) — AX → scalar slot, (DX, CX) → str slot
// as (.ptr, .len). Position-agnostic — the
// (scalar, str) — AX → scalar slot, (DX, CX, R8) → str slot
// as (.ptr, .len, .cap). Position-agnostic — the
// regs are routed by element type, not by AX/DX.
// str IS []u8 (24B): cap rides R8 (#1/Phase 3, task #5).
fn cgmlet(c: *cgen, n: *node) void = {
let rhs: *node = n.rhs;
if (rhs == nil) { return; };
@@ -1291,16 +1306,21 @@ fn cgmlet(c: *cgen, n: *node) void = {
let off0: i32 = localadd(c, l0.str, sz0, t0);
let off1: i32 = localadd(c, l1.str, sz1, t1);
if (s0_is_str) {
// l0 str: ptr=DX, len=CX, cap=R8. l1 scalar = AX.
emitline("\tMOVQ\tDX, ");
emitoff(off0: i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off0 + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tR8, ");
emitoff((off0 + 16): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tAX, ");
emitoff(off1: i64);
emitline("(BP)\n");
} else {
// l0 scalar; l1 str: ptr=DX, len=CX, cap=R8.
emitline("\tMOVQ\tAX, ");
emitoff(off0: i64);
emitline("(BP)\n");
@@ -1310,6 +1330,9 @@ fn cgmlet(c: *cgen, n: *node) void = {
emitline("\tMOVQ\tCX, ");
emitoff((off1 + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tR8, ");
emitoff((off1 + 16): i64);
emitline("(BP)\n");
};
c.lastwasreturn = 0;
return;