cgen: str N_INDEX read -> 3-word {ptr,len,cap} -- Phase 2 F2 (both stages)

str element value read at N_INDEX dropped the cap word (2-word ptr,len load); str is 24B {ptr,len,cap} since 1140a59. A new named helper cgslicehdr (both stages) loads the full 3-word header and is called by the N_INDEX str-element sites, kind-gated type_isstr/elemisstr -- never size==24, since str and slice collide at 24B. The base-targeting word loads last (clobber-safe). cstage==wwstage byte-identical. The #9 typeassert leaf is split out to F2b (it needs a wwstage spill twin first).

test/wcc/932: table-driven runtime .cap-survives probe over both N_INDEX base forms and both drivers; verified fail-before/pass-after. NNN<950 mirrors the 928 precedent -- the fixtures are self-contained (/tmp, no imports), so rule-14's selfhost-sibling race does not apply.

main.combined.ww regenerated via the canonical make path (md5-stable) and committed alongside source, per the 1140a59 precedent.
This commit is contained in:
2026-05-24 12:27:15 +09:00
parent fb4c567e0d
commit 97707155ae
7 changed files with 318 additions and 53 deletions

View File

@@ -14527,6 +14527,22 @@ fn cgident(c: *cgen, n: *node) void = {
return;
};
// cgslicehdr — load the 24B slice/str header at base+0 into the
// (AX=ptr, BX=len, CX=cap) triple. base holds the element address;
// the load that targets base destroys it, so that word is emitted
// LAST. Order otherwise mirrors the slice-field arm (len, cap, ptr).
// Shared by the cgindex str-element arms (caller does the kind-gate)
// and, later, the typeassert str-variant leaf (#9). c retained unused
// for callsite symmetry with cstage cgslicehdr.
fn cgslicehdr(c: *cgen, base: str) void = {
if (!streq(base, "BX")) { emitmovqload(8i64, base, "BX"); };
if (!streq(base, "CX")) { emitmovqload(16i64, base, "CX"); };
if (!streq(base, "AX")) { emitmovqload(0i64, base, "AX"); };
if (streq(base, "BX")) { emitmovqload(8i64, base, "BX"); };
if (streq(base, "CX")) { emitmovqload(16i64, base, "CX"); };
if (streq(base, "AX")) { emitmovqload(0i64, base, "AX"); };
};
fn cgindex(c: *cgen, n: *node) void = {
// Element-size-aware load: u8 → MOVZBQ, i32 → MOVSXD, u32 → MOVL,
// str → (ptr, len) into (AX, BX), everything else → MOVQ. Fast
@@ -14665,12 +14681,11 @@ fn cgindex(c: *cgen, n: *node) void = {
emitline("\tMOVQ\t(BX), AX\n");
return;
};
// #43: str element-stride routes through primtypesize so the
// 16-vs-24 dispatch tracks ty_str.size for #1.
// str element: load the full (ptr, len, cap) header into
// (AX, BX, CX) — str is 24B since #1, so cap must survive.
// Kind-gate on isstrtype, never size==24. Base is BX.
if (elemisstr) {
emitline("\tMOVQ\t8(BX), CX\n");
emitline("\tMOVQ\t(BX), AX\n");
emitline("\tMOVQ\tCX, BX\n");
cgslicehdr(c, "BX");
return;
};
let lop1: str = loadopsz(signed_elem, esz);
@@ -14706,13 +14721,10 @@ fn cgindex(c: *cgen, n: *node) void = {
emitline("\tMOVQ\t(BX), AX\n");
return;
};
// 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.
// str element: full (ptr, len, cap) header into (AX, BX, CX);
// cap must survive (#1). Kind-gate, never size==24. Base BX.
if (elemisstr) {
emitline("\tMOVQ\t8(BX), CX\n");
emitline("\tMOVQ\t(BX), AX\n");
emitline("\tMOVQ\tCX, BX\n");
cgslicehdr(c, "BX");
return;
};
let lop2: str = loadopsz(signed_elem, esz);
@@ -14739,11 +14751,11 @@ fn cgindex(c: *cgen, n: *node) void = {
emitline("\tMOVQ\t(BX), AX\n");
return;
};
// #43: str element-stride routes through primtypesize so the
// 16-vs-24 dispatch tracks ty_str.size for #1.
// str element via fallback base: full (ptr, len, cap) header
// into (AX, BX, CX); cap must survive (#1). Kind-gate, never
// size==24. Base AX.
if (elemisstr) {
emitline("\tMOVQ\t8(AX), BX\n");
emitline("\tMOVQ\t(AX), AX\n");
cgslicehdr(c, "AX");
return;
};
let lop3: str = loadopsz(signed_elem, esz);
@@ -22725,6 +22737,16 @@ fn emitdispreg(off: i64, reg: str) void = {
emitline(")");
};
// emitmovqload — `MOVQ off(base), dst`, the per-word unit of a
// 3-word slice/str header load (cgslicehdr).
fn emitmovqload(off: i64, base: str, dst: str) void = {
emitline("\tMOVQ\t");
emitdispreg(off, base);
emitline(", ");
emitline(dst);
emitline("\n");
};
// emitoff — print an integer offset, suppressing it entirely when 0.
// Use before any emitline("(BP)...") or emitline("(SB)...") sequence.
// Plan 9 cc convention: "(BP)" not "0(BP)".

View File

@@ -747,6 +747,16 @@ fn emitdispreg(off: i64, reg: str) void = {
emitline(")");
};
// emitmovqload — `MOVQ off(base), dst`, the per-word unit of a
// 3-word slice/str header load (cgslicehdr).
fn emitmovqload(off: i64, base: str, dst: str) void = {
emitline("\tMOVQ\t");
emitdispreg(off, base);
emitline(", ");
emitline(dst);
emitline("\n");
};
// emitoff — print an integer offset, suppressing it entirely when 0.
// Use before any emitline("(BP)...") or emitline("(SB)...") sequence.
// Plan 9 cc convention: "(BP)" not "0(BP)".

View File

@@ -699,6 +699,22 @@ fn cgident(c: *cgen, n: *node) void = {
return;
};
// cgslicehdr — load the 24B slice/str header at base+0 into the
// (AX=ptr, BX=len, CX=cap) triple. base holds the element address;
// the load that targets base destroys it, so that word is emitted
// LAST. Order otherwise mirrors the slice-field arm (len, cap, ptr).
// Shared by the cgindex str-element arms (caller does the kind-gate)
// and, later, the typeassert str-variant leaf (#9). c retained unused
// for callsite symmetry with cstage cgslicehdr.
fn cgslicehdr(c: *cgen, base: str) void = {
if (!streq(base, "BX")) { emitmovqload(8i64, base, "BX"); };
if (!streq(base, "CX")) { emitmovqload(16i64, base, "CX"); };
if (!streq(base, "AX")) { emitmovqload(0i64, base, "AX"); };
if (streq(base, "BX")) { emitmovqload(8i64, base, "BX"); };
if (streq(base, "CX")) { emitmovqload(16i64, base, "CX"); };
if (streq(base, "AX")) { emitmovqload(0i64, base, "AX"); };
};
fn cgindex(c: *cgen, n: *node) void = {
// Element-size-aware load: u8 → MOVZBQ, i32 → MOVSXD, u32 → MOVL,
// str → (ptr, len) into (AX, BX), everything else → MOVQ. Fast
@@ -837,12 +853,11 @@ fn cgindex(c: *cgen, n: *node) void = {
emitline("\tMOVQ\t(BX), AX\n");
return;
};
// #43: str element-stride routes through primtypesize so the
// 16-vs-24 dispatch tracks ty_str.size for #1.
// str element: load the full (ptr, len, cap) header into
// (AX, BX, CX) — str is 24B since #1, so cap must survive.
// Kind-gate on isstrtype, never size==24. Base is BX.
if (elemisstr) {
emitline("\tMOVQ\t8(BX), CX\n");
emitline("\tMOVQ\t(BX), AX\n");
emitline("\tMOVQ\tCX, BX\n");
cgslicehdr(c, "BX");
return;
};
let lop1: str = loadopsz(signed_elem, esz);
@@ -878,13 +893,10 @@ fn cgindex(c: *cgen, n: *node) void = {
emitline("\tMOVQ\t(BX), AX\n");
return;
};
// 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.
// str element: full (ptr, len, cap) header into (AX, BX, CX);
// cap must survive (#1). Kind-gate, never size==24. Base BX.
if (elemisstr) {
emitline("\tMOVQ\t8(BX), CX\n");
emitline("\tMOVQ\t(BX), AX\n");
emitline("\tMOVQ\tCX, BX\n");
cgslicehdr(c, "BX");
return;
};
let lop2: str = loadopsz(signed_elem, esz);
@@ -911,11 +923,11 @@ fn cgindex(c: *cgen, n: *node) void = {
emitline("\tMOVQ\t(BX), AX\n");
return;
};
// #43: str element-stride routes through primtypesize so the
// 16-vs-24 dispatch tracks ty_str.size for #1.
// str element via fallback base: full (ptr, len, cap) header
// into (AX, BX, CX); cap must survive (#1). Kind-gate, never
// size==24. Base AX.
if (elemisstr) {
emitline("\tMOVQ\t8(AX), BX\n");
emitline("\tMOVQ\t(AX), AX\n");
cgslicehdr(c, "AX");
return;
};
let lop3: str = loadopsz(signed_elem, esz);

View File

@@ -14527,6 +14527,22 @@ fn cgident(c: *cgen, n: *node) void = {
return;
};
// cgslicehdr — load the 24B slice/str header at base+0 into the
// (AX=ptr, BX=len, CX=cap) triple. base holds the element address;
// the load that targets base destroys it, so that word is emitted
// LAST. Order otherwise mirrors the slice-field arm (len, cap, ptr).
// Shared by the cgindex str-element arms (caller does the kind-gate)
// and, later, the typeassert str-variant leaf (#9). c retained unused
// for callsite symmetry with cstage cgslicehdr.
fn cgslicehdr(c: *cgen, base: str) void = {
if (!streq(base, "BX")) { emitmovqload(8i64, base, "BX"); };
if (!streq(base, "CX")) { emitmovqload(16i64, base, "CX"); };
if (!streq(base, "AX")) { emitmovqload(0i64, base, "AX"); };
if (streq(base, "BX")) { emitmovqload(8i64, base, "BX"); };
if (streq(base, "CX")) { emitmovqload(16i64, base, "CX"); };
if (streq(base, "AX")) { emitmovqload(0i64, base, "AX"); };
};
fn cgindex(c: *cgen, n: *node) void = {
// Element-size-aware load: u8 → MOVZBQ, i32 → MOVSXD, u32 → MOVL,
// str → (ptr, len) into (AX, BX), everything else → MOVQ. Fast
@@ -14665,12 +14681,11 @@ fn cgindex(c: *cgen, n: *node) void = {
emitline("\tMOVQ\t(BX), AX\n");
return;
};
// #43: str element-stride routes through primtypesize so the
// 16-vs-24 dispatch tracks ty_str.size for #1.
// str element: load the full (ptr, len, cap) header into
// (AX, BX, CX) — str is 24B since #1, so cap must survive.
// Kind-gate on isstrtype, never size==24. Base is BX.
if (elemisstr) {
emitline("\tMOVQ\t8(BX), CX\n");
emitline("\tMOVQ\t(BX), AX\n");
emitline("\tMOVQ\tCX, BX\n");
cgslicehdr(c, "BX");
return;
};
let lop1: str = loadopsz(signed_elem, esz);
@@ -14706,13 +14721,10 @@ fn cgindex(c: *cgen, n: *node) void = {
emitline("\tMOVQ\t(BX), AX\n");
return;
};
// 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.
// str element: full (ptr, len, cap) header into (AX, BX, CX);
// cap must survive (#1). Kind-gate, never size==24. Base BX.
if (elemisstr) {
emitline("\tMOVQ\t8(BX), CX\n");
emitline("\tMOVQ\t(BX), AX\n");
emitline("\tMOVQ\tCX, BX\n");
cgslicehdr(c, "BX");
return;
};
let lop2: str = loadopsz(signed_elem, esz);
@@ -14739,11 +14751,11 @@ fn cgindex(c: *cgen, n: *node) void = {
emitline("\tMOVQ\t(BX), AX\n");
return;
};
// #43: str element-stride routes through primtypesize so the
// 16-vs-24 dispatch tracks ty_str.size for #1.
// str element via fallback base: full (ptr, len, cap) header
// into (AX, BX, CX); cap must survive (#1). Kind-gate, never
// size==24. Base AX.
if (elemisstr) {
emitline("\tMOVQ\t8(AX), BX\n");
emitline("\tMOVQ\t(AX), AX\n");
cgslicehdr(c, "AX");
return;
};
let lop3: str = loadopsz(signed_elem, esz);
@@ -22725,6 +22737,16 @@ fn emitdispreg(off: i64, reg: str) void = {
emitline(")");
};
// emitmovqload — `MOVQ off(base), dst`, the per-word unit of a
// 3-word slice/str header load (cgslicehdr).
fn emitmovqload(off: i64, base: str, dst: str) void = {
emitline("\tMOVQ\t");
emitdispreg(off, base);
emitline(", ");
emitline(dst);
emitline("\n");
};
// emitoff — print an integer offset, suppressing it entirely when 0.
// Use before any emitline("(BP)...") or emitline("(SB)...") sequence.
// Plan 9 cc convention: "(BP)" not "0(BP)".