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

@@ -185,7 +185,11 @@ fn cgtryprop(c: *cgen, n: *node) void = {
};
};
if (succisstr) {
// str IS []u8: success arrives DX=ptr, CX=len, R8=cap
// (slot 32B). Move len out before cap overwrites CX
// (#1/Phase 3).
emitline("\tMOVQ\tCX, BX\n");
emitline("\tMOVQ\tR8, CX\n");
};
emitline("\tMOVQ\tDX, AX\n");
return;
@@ -241,7 +245,11 @@ fn cgtryunw(c: *cgen, n: *node) void = {
};
};
if (succisstr) {
// str IS []u8: success arrives DX=ptr, CX=len, R8=cap
// (slot 32B). Move len out before cap overwrites CX
// (#1/Phase 3).
emitline("\tMOVQ\tCX, BX\n");
emitline("\tMOVQ\tR8, CX\n");
};
emitline("\tMOVQ\tDX, AX\n");
return;
@@ -514,8 +522,9 @@ fn cgcast(c: *cgen, n: *node) void = {
};
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.
// str IS []u8: the (ptr, len, cap) triple — ptr in AX, len in BX,
// cap in CX. A static literal has no spare storage, so cap = len
// (#1/Phase 3). Call sites that expect a str arg pick these up.
let nstr: str = n.str;
let lab: str = internstrlit(c, nstr);
emitline("\tLEAQ\t");
@@ -524,6 +533,9 @@ fn cgstrlit(c: *cgen, n: *node) void = {
emitline("\tMOVQ\t$");
emitint(nstr.len: i64);
emitline(", BX\n");
emitline("\tMOVQ\t$");
emitint(nstr.len: i64);
emitline(", CX\n");
return;
};
@@ -559,9 +571,14 @@ fn cgident(c: *cgen, n: *node) void = {
emitoff(off: i64);
emitline("(BP), AX\n");
if (isstr) {
// str IS []u8: load (ptr,len,cap) into AX/BX/CX,
// identical to the slice arm below (#1/Phase 3).
emitline("\tMOVQ\t");
emitoff((off + 8): i64);
emitline("(BP), BX\n");
emitline("\tMOVQ\t");
emitoff((off + 16): i64);
emitline("(BP), CX\n");
};
if (issl) {
emitline("\tMOVQ\t");
@@ -591,6 +608,11 @@ fn cgident(c: *cgen, n: *node) void = {
emitline("\tMOVQ\t$");
emitint(bytes.len: i64);
emitline(", BX\n");
// str IS []u8: cap = len for a static def literal
// (#1/Phase 3).
emitline("\tMOVQ\t$");
emitint(bytes.len: i64);
emitline(", CX\n");
return;
};
};
@@ -622,17 +644,16 @@ fn cgident(c: *cgen, n: *node) void = {
let isstr: bool = letvarisstr(c, nm);
let issl: bool = letvarisslice(c, nm);
if (isstr || issl) {
// str IS []u8: both str and slice carry a third 8B
// (cap); load it unconditionally. The address holder CX
// is overwritten by the cap as the last step, after
// ptr/len are already loaded (#1/Phase 3).
emitline("\tLEAQ\t");
emitsymname(c, nm);
emitline("(SB), CX\n");
emitline("\tMOVQ\t(CX), AX\n");
emitline("\tMOVQ\t8(CX), BX\n");
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");
};
emitline("\tMOVQ\t16(CX), CX\n");
return;
};
// Float global: same LEAQ-indirect shape, since MOVSS/
@@ -686,6 +707,12 @@ fn cgindex(c: *cgen, n: *node) void = {
let idx: *node = n.rhs;
let esz: i32 = 8;
let signed_elem: bool = false;
// #1/Phase 3: str=24B collides with slice=24B, so the str-element
// branches below MUST gate on kind (mirroring cstage's elem_is_str),
// not a bare `esz == primtypesize("str")` size check — otherwise a
// []u8 element (also 24B) misfires into the str 2-word load and
// diverges from cstage (#60 collision class; sentinel 754).
let elemisstr: 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)
@@ -727,7 +754,7 @@ fn cgindex(c: *cgen, n: *node) void = {
// cgen.c:3517-18). esz-only — N_DOT-base signedness
// stays unset, as before.
let dt: *tinfo = n.type_: *tinfo;
if (dt != nil) { esz = dt.size: i32; };
if (dt != nil) { esz = dt.size: i32; elemisstr = typeisstr(dt); };
} else { if (base.kind == nkind.N_INDEX) {
// #60: chained `names[i][k]` — n.type_ is the checker-
// stamped outer element tinfo (indexresult over the inner
@@ -776,6 +803,7 @@ fn cgindex(c: *cgen, n: *node) void = {
esz = elem_slot_sz;
};
};
elemisstr = isstrtype(c, etn);
};
};
cgexpr(c, idx);
@@ -811,7 +839,7 @@ fn cgindex(c: *cgen, n: *node) void = {
};
// #43: str element-stride routes through primtypesize so the
// 16-vs-24 dispatch tracks ty_str.size for #1.
if (esz == primtypesize("str"): i32) {
if (elemisstr) {
emitline("\tMOVQ\t8(BX), CX\n");
emitline("\tMOVQ\t(BX), AX\n");
emitline("\tMOVQ\tCX, BX\n");
@@ -853,7 +881,7 @@ fn cgindex(c: *cgen, n: *node) void = {
// str element (16B today): load (ptr, len) into (AX, BX) so
// the value flows through the str-rhs convention.
// #43: route via primtypesize so the stride tracks #1.
if (esz == primtypesize("str"): i32) {
if (elemisstr) {
emitline("\tMOVQ\t8(BX), CX\n");
emitline("\tMOVQ\t(BX), AX\n");
emitline("\tMOVQ\tCX, BX\n");
@@ -885,7 +913,7 @@ fn cgindex(c: *cgen, n: *node) void = {
};
// #43: str element-stride routes through primtypesize so the
// 16-vs-24 dispatch tracks ty_str.size for #1.
if (esz == primtypesize("str"): i32) {
if (elemisstr) {
emitline("\tMOVQ\t8(AX), BX\n");
emitline("\tMOVQ\t(AX), AX\n");
return;
@@ -1480,10 +1508,11 @@ fn cgdot(c: *cgen, n: *node) void = {
};
// 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.
// (matches the (scalar, str) init layout: scalar in an
// 8B slot, str in 24B — str IS []u8, #1/Phase 3). For a
// str element, load (ptr, len) into (AX, BX); the cap
// stays in the slot (the 2-word str-field read, like
// every other chained/dot str leaf read — task #14).
if (lkind == nkind.N_TTUPLE) {
let idx: i32 = fldnumidx(fld);
if (idx >= 0) {
@@ -1598,9 +1627,9 @@ fn cgdot(c: *cgen, n: *node) void = {
let delta: i32 = -1;
if (streq(fld, "ptr")) { delta = 0; };
if (streq(fld, "len")) { delta = 8; };
if (issl) {
if (streq(fld, "cap")) { delta = 16; };
};
// str IS []u8: .cap is valid on a str global too,
// not slice-only — mirrors cstage (#1/Phase 3, #11).
if (streq(fld, "cap")) { delta = 16; };
if (delta >= 0) {
emitline("\tLEAQ\t");
emitsymname(c, lhs.str);
@@ -2456,7 +2485,9 @@ fn cgun(c: *cgen, n: *node) void = {
let gdelta: i32 = -1;
if (streq(fld, "ptr")) { gdelta = 0; };
if (streq(fld, "len")) { gdelta = 8; };
if (issl) { if (streq(fld, "cap")) { gdelta = 16; }; };
// str IS []u8: &str.cap is valid too, not slice-only
// — mirrors cstage (#1/Phase 3, #11).
if (streq(fld, "cap")) { gdelta = 16; };
if (gdelta >= 0) {
emitline("\tLEAQ\t");
emitsymname(c, basenm);
@@ -2828,16 +2859,19 @@ fn cgalloc(c: *cgen, n: *node) void = {
emitline("\n");
fi = nil;
} else { if (isstrtype(c, fi.tnode)) {
// alloc(T{ fval = s }) for str field: cgexpr
// leaves (AX=ptr, BX=len). Use CX for the heap
// base so BX=len survives both stores. Mirrors
// cmd/w6c/cgen.c:4184-4190.
emitline("\tMOVQ\t(SP), CX\n");
// str IS []u8: cgexpr leaves (AX=ptr,
// BX=len, CX=cap). Route the heap base
// through DX so all three survive — CX
// holds cap, BX holds len (#1/Phase 3).
emitline("\tMOVQ\t(SP), DX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(fi.foff: i64, "CX");
emitdispreg(fi.foff: i64, "DX");
emitline("\n");
emitline("\tMOVQ\tBX, ");
emitdispreg((fi.foff + 8): i64, "CX");
emitdispreg((fi.foff + 8): i64, "DX");
emitline("\n");
emitline("\tMOVQ\tCX, ");
emitdispreg((fi.foff + 16): i64, "DX");
emitline("\n");
fi = nil;
} else {
@@ -3297,7 +3331,8 @@ fn cgcall(c: *cgen, n: *node) void = {
popped += 1;
} else {
let extra: i32 = 0;
if (nodeisstr(c, a)) { extra = 1; };
// str IS []u8: 3-word arg, same as slice (#1/Phase 3).
if (nodeisstr(c, a)) { extra = 2; };
if (nodeisslice(c, a)) { extra = 2; };
// #21: tagged-CALL arg was pushed AX/DX/CX/R8 high→low
// by pushargsrev; size the per-arg pop to match so the
@@ -3467,31 +3502,9 @@ fn cgcall(c: *cgen, n: *node) void = {
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.
// Route through fnretlookupmod: for N_DOT cross-module callees,
// the bare-leaf fnretlookup's same-module-first walk (#4e) would
// pick the caller-module's same-leaf fn — a str-returning
// caller-side `slice` over a []u8-returning `mod.slice` then
// emits a phantom MOVQ DX, BX after the cross-module CALL (#34).
if (calleename.len > 0) {
let cmod: str;
cmod.ptr = nil; cmod.len = 0;
if (callee != nil) {
if (callee.kind == nkind.N_IDENT) { cmod = c.curmod; };
if (callee.kind == nkind.N_DOT) {
if (callee.lhs != nil) {
if (callee.lhs.kind == nkind.N_IDENT) {
cmod = callee.lhs.str;
};
};
};
};
let rtyp: *node = fnretlookupmod(c, calleename, cmod);
if (isstrtype(c, rtyp)) {
emitline("\tMOVQ\tDX, BX\n");
};
};
// str IS []u8: a str-returning callee leaves AX=ptr, BX=len,
// CX=cap — same as a slice, so there is no receive-side shuffle
// (#1/Phase 3).
return;
};
@@ -3590,17 +3603,21 @@ fn cgassign(c: *cgen, n: *node) void = {
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).
// str IS []u8: PUSHQ AX (ptr) first, then
// PUSHQ BX (len) + PUSHQ CX (cap) across the
// pointer eval which clobbers BX/CX. Pop drains
// cap (top) → 16(BX), then len, then ptr → 0(BX)
// with len → 8(BX) (#1/Phase 3).
emitline("\tPUSHQ\tAX\n");
if (elemstr) { emitline("\tPUSHQ\tBX\n"); };
if (elemstr) {
emitline("\tPUSHQ\tBX\n");
emitline("\tPUSHQ\tCX\n");
};
cgexpr(c, inner);
emitline("\tMOVQ\tAX, BX\n");
if (elemstr) {
emitline("\tPOPQ\tCX\n");
emitline("\tMOVQ\tCX, 16(BX)\n");
emitline("\tPOPQ\tCX\n");
emitline("\tPOPQ\tAX\n");
emitline("\tMOVQ\tAX, (BX)\n");
@@ -3854,10 +3871,19 @@ fn cgassign(c: *cgen, n: *node) void = {
};
};
cgexpr(c, n.rhs); // value → AX
// #43: spill BX (str.len) before computing
// the index so the post-index store can pop
// it; the stride gate tracks ty_str.size.
if (esz == primtypesize("str"): i32) { emitline("\tPUSHQ\tBX\n"); };
// str IS []u8: spill cap (CX) + len (BX) before
// computing the index so the post-index store can
// pop all three. #1/Phase 3: str=24B collides with
// slice=24B, so this MUST gate on kind (cstage's
// elem_is_str, cmd/w6c/cgen.c:3576) — not a bare
// `esz == primtypesize("str")` — or a []u8 element
// (also 24B) misfires the str 3-word store and
// diverges from cstage. Write-side mirror of the
// cgindex read-path gate (#7/754).
if (isstrtype(c, elemtn)) {
emitline("\tPUSHQ\tCX\n");
emitline("\tPUSHQ\tBX\n");
};
emitline("\tPUSHQ\tAX\n");
cgexpr(c, idx); // idx → AX
if (esz > 1) {
@@ -3895,13 +3921,15 @@ fn cgassign(c: *cgen, n: *node) void = {
emitline("\tPOPQ\tAX\n"); // scaled idx
emitline("\tADDQ\tAX, BX\n");
emitline("\tPOPQ\tAX\n"); // value
// #43: str-element write — pop the saved
// .len and store both halves. Stride gate
// routes through primtypesize for #1.
if (esz == primtypesize("str"): i32) {
// str IS []u8: pop the saved len + cap and store
// all three words. Kind-gate, not size — see the
// spill site above (#1/Phase 3, #7/754).
if (isstrtype(c, elemtn)) {
emitline("\tMOVQ\tAX, (BX)\n");
emitline("\tPOPQ\tCX\n");
emitline("\tMOVQ\tCX, 8(BX)\n");
emitline("\tPOPQ\tCX\n");
emitline("\tMOVQ\tCX, 16(BX)\n");
return;
};
let isop: str = tnodestoreop(c, elemtn, esz);
@@ -4328,20 +4356,24 @@ fn cgassign(c: *cgen, n: *node) void = {
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.
// str IS []u8: rhs left (AX=ptr,
// BX=len, CX=cap). CX holds cap, so
// stage the struct addr in DX and
// store all three words — identical
// to the slice arm below (#1/Phase 3).
if (n.op == tkind.TK_ASSIGN) {
if (isstrtype(c, fi.tnode)) {
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), CX\n");
emitline("(BP), DX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(fi.foff: i64, "CX");
emitdispreg(fi.foff: i64, "DX");
emitline("\n");
emitline("\tMOVQ\tBX, ");
emitdispreg((fi.foff + 8): i64, "CX");
emitdispreg((fi.foff + 8): i64, "DX");
emitline("\n");
emitline("\tMOVQ\tCX, ");
emitdispreg((fi.foff + 16): i64, "DX");
emitline("\n");
return;
};
@@ -4532,11 +4564,11 @@ fn cgassign(c: *cgen, n: *node) void = {
};};
};
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.
// str IS []u8: cgexpr left (AX=ptr,
// BX=len, CX=cap); store all three at
// +0/+8/+16, identical to the slice
// arm below. BP base, no scratch
// reload needed (#1/Phase 3).
if (isstrtype(c, fi.tnode)) {
emitline("\tMOVQ\tAX, ");
emitoff((lc.off + fi.foff): i64);
@@ -4544,6 +4576,9 @@ fn cgassign(c: *cgen, n: *node) void = {
emitline("\tMOVQ\tBX, ");
emitoff((lc.off + fi.foff + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((lc.off + fi.foff + 16): i64);
emitline("(BP)\n");
return;
};
// slice field direct: cgexpr left
@@ -4791,14 +4826,22 @@ fn cgassign(c: *cgen, n: *node) void = {
if (n.op == tkind.TK_ASSIGN) {
cgexpr(c, n.rhs);
if (isstrtype(c, fi.tnode)) {
// str IS []u8: cgexpr left
// (AX=ptr, BX=len, CX=cap). CX
// holds cap, so stage the base
// addr in DX and store all three
// words (#1/Phase 3).
emitline("\tLEAQ\t");
emitsymname(c, bn);
emitline("(SB), CX\n");
emitline("(SB), DX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(fi.foff: i64, "CX");
emitdispreg(fi.foff: i64, "DX");
emitline("\n");
emitline("\tMOVQ\tBX, ");
emitdispreg((fi.foff + 8): i64, "CX");
emitdispreg((fi.foff + 8): i64, "DX");
emitline("\n");
emitline("\tMOVQ\tCX, ");
emitdispreg((fi.foff + 16): i64, "DX");
emitline("\n");
return;
};
@@ -5037,22 +5080,29 @@ fn cgassign(c: *cgen, n: *node) void = {
return;
};
if (typeisstr(leaftype)) {
// str IS []u8: store ptr/len/cap. cgexpr leaves
// CX=cap, so the viacx base goes in DX (not CX) to
// avoid clobbering it — same as the single-dot str
// field store (#1/Phase 3).
cgexpr(c, n.rhs);
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), CX\n");
emitline("(BP), DX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), CX\n");
emitline("(SB), DX\n");
};
emitline("\tMOVQ\tAX, ");
emitdispreg(totaloff: i64, "CX");
emitdispreg(totaloff: i64, "DX");
emitline("\n");
emitline("\tMOVQ\tBX, ");
emitdispreg((totaloff + 8): i64, "CX");
emitdispreg((totaloff + 8): i64, "DX");
emitline("\n");
emitline("\tMOVQ\tCX, ");
emitdispreg((totaloff + 16): i64, "DX");
emitline("\n");
} else {
emitline("\tMOVQ\tAX, ");
@@ -5061,6 +5111,9 @@ fn cgassign(c: *cgen, n: *node) void = {
emitline("\tMOVQ\tBX, ");
emitoff((rootoff + totaloff + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((rootoff + totaloff + 16): i64);
emitline("(BP)\n");
};
return;
};
@@ -5548,11 +5601,17 @@ fn cgassign(c: *cgen, n: *node) void = {
cgexpr(c, n.rhs);
if (n.op == tkind.TK_ASSIGN) {
if (letvarisstr(c, nm)) {
// str IS []u8: stash cap in DI before LEAQ
// overwrites CX, then store ptr/len/cap —
// identical to the slice arm below
// (#1/Phase 3).
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;
};
if (letvarisslice(c, nm)) {
@@ -5838,7 +5897,9 @@ fn cgassign(c: *cgen, n: *node) void = {
emitoff((off + 8): i64);
emitline("(BP)\n");
};
if (lcsl) {
// str IS []u8: store the cap word too, identical to
// the slice store (#1/Phase 3).
if (lcstr || lcsl) {
emitline("\tMOVQ\tCX, ");
emitoff((off + 16): i64);
emitline("(BP)\n");