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

@@ -249,8 +249,11 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = {
emitline(", AX\n");
emitline("\tPUSHQ\tAX\n");
} else { if (nodeisstr(c, arg)) {
// slot 24: [+0]=tag,[+8]=ptr,[+16]=len. Push high→low
// so pop drains tag first into arg-reg[0].
// str IS []u8: slot 32 [+0]=tag,[+8]=ptr,[+16]=len,
// [+24]=cap — same shape as the slice arm above. Push
// cap, len, ptr, tag high→low so pop drains tag first
// into arg-reg[0] (#1/Phase 3).
emitline("\tPUSHQ\tCX\n");
emitline("\tPUSHQ\tBX\n");
emitline("\tPUSHQ\tAX\n");
emitline("\tMOVQ\t$");
@@ -468,9 +471,12 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = {
return rest + 3;
};
if (nodeisstr(c, arg)) {
// str IS []u8: cgexpr left (AX=ptr, BX=len, CX=cap). Push
// the triple, same as the slice arm above (#1/Phase 3).
emitline("\tPUSHQ\tCX\n");
emitline("\tPUSHQ\tBX\n");
emitline("\tPUSHQ\tAX\n");
return rest + 2;
return rest + 3;
};
// #21: CALL returning a tagged-union — the aistagged guard
// above kept us out of the widening path. Push the tagged-
@@ -2320,7 +2326,7 @@ fn cgloadtaggedfield(c: *cgen, basereg: str, foff: i32, slot_sz: i32) void = {
// would need a reversed direction we don't currently emit).
// - struct src (literal or ident): zero slot, write fields at +8+foff,
// tag last.
// - str src: tag@+0, ptr@+8, len@+16.
// - str src: tag@+0, ptr@+8, len@+16, cap@+24 (str IS []u8, #1/Phase 3).
// - scalar src: tag@+0, value@+8.
fn cgwidentaggedstore(c: *cgen, dst: *tinfo, src: *node,
basereg: str, slot_off: i32, slot_sz: i32) void = {
@@ -2538,12 +2544,18 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s
emitoff((slot_off + 8 + fi.foff): i64);
emitline("(BP)\n");
} else { if (isstrtype(c, fi.tnode)) {
// str IS []u8: 3-word field
// (ptr,len,cap) from cgexpr's
// AX/BX/CX (#1/Phase 3).
emitline("\tMOVQ\tAX, ");
emitoff((slot_off + 8 + fi.foff): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tBX, ");
emitoff((slot_off + 8 + fi.foff + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((slot_off + 8 + fi.foff + 16): i64);
emitline("(BP)\n");
} else {
let sop: str = fieldstoreop(c, fi);
emitline("\t");
@@ -2601,7 +2613,9 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s
return;
};
};
// Str payload.
// 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 (nodeisstr(c, src)) {
cgexpr(c, src);
emitline("\tMOVQ\tAX, ");
@@ -2610,6 +2624,9 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s
emitline("\tMOVQ\tBX, ");
emitoff((slot_off + 16): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((slot_off + 24): i64);
emitline("(BP)\n");
let tag: i32 = taggedvariantindext(c, dt, src);
if (tag < 0) { tag = 0; };
emitline("\tMOVQ\t$");
@@ -2837,11 +2854,15 @@ export fn dotchainresolve(c: *cgen, n: *node,
for (ft != nil && ft.kind == tykind.TY_NAMED) { ft = ft.under; };
if (ft == nil) { return false; };
if (ft.kind == tykind.TY_STR) {
// str IS []u8: .cap is the third header word, same as
// the TY_SLICE leaf below — cstage treats str≡slice for
// .ptr/.len/.cap (cmd/w6c/cgen.c:2478) (#1/Phase 3, #11).
if (i != 1) { return false; };
let pseudo: str = stk[0].str;
let delta: i32 = -1;
if (streq(pseudo, "ptr")) { delta = 0; }
else { if (streq(pseudo, "len")) { delta = 8; }; };
else { if (streq(pseudo, "len")) { delta = 8; }
else { if (streq(pseudo, "cap")) { delta = 16; }; }; };
if (delta < 0) { return false; };
*outtotaloff = *outtotaloff + foff;
*outslicedelta = delta;
@@ -3154,6 +3175,44 @@ fn cgstructlitfill(c: *cgen, si: *structinfo, lit: *node,
fi = nil;
} else if (callwhole) {
fi = nil;
} else if (isstrtype(c, fi.tnode)) {
// 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 reloads BX, which
// would clobber len) (#1/Phase 3).
cgexpr(c, fieldnode.lhs);
if (mode == 0) {
emitline("\tMOVQ\tAX, ");
emitoff((disp + fi.foff): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tBX, ");
emitoff((disp + fi.foff + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((disp + fi.foff + 16): i64);
emitline("(BP)\n");
} else {
if (mode == 1) {
emitline("\tMOVQ\t");
emitoff(srcoff: i64);
emitline("(BP), DX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, srcname);
emitline("(SB), DX\n");
};
emitline("\tMOVQ\tAX, ");
emitdispreg((disp + fi.foff): i64, "DX");
emitline("\n");
emitline("\tMOVQ\tBX, ");
emitdispreg((disp + fi.foff + 8): i64, "DX");
emitline("\n");
emitline("\tMOVQ\tCX, ");
emitdispreg((disp + fi.foff + 16): i64, "DX");
emitline("\n");
};
fi = nil;
} else {
cgexpr(c, fieldnode.lhs);
// For non-BP modes, cgexpr just clobbered