w6c+cgen: full 24B header store for str/slice array-literal elements (fix #20, #270 str/slice arm)

A `let t: [N][]u8 = [a, b]` / `[N]str` literal init lowered each
element's {ptr,len,cap} header into AX/BX/CX (cgexpr) but stored only
some words: a slice element fell through to the scalar 1-word MOVQ
(dropping .len AND .cap), a str element stored 2 words (dropping .cap,
latent). Each element is 24B (post-#1) and must be copied whole.
wwstage was worse — a slice element matched no esz branch, so esz
stayed the 8 sentinel: the per-element stride collapsed (element i+1
overwrote element i's tail), the -96-vs-80 cs!=ww frame divergence.

This is the str/slice arm of the #270 aggregate-element-store family.
struct/array/tuple already copy correctly via the #270-1c is_agg
multi-word path; str/slice were the documented follow-up (cgen.c:9037,
cgenstmt.ww deferral). They can't join is_agg (that path word-copies
from a source slot and rejects non-ident/structlit elements, whereas
str/slice elements are commonly exprs cgexpr lowers into registers) —
the correct mechanism is the existing register header store, extended.

Fix (BOTH stages, converged byte-identical): cstage adds
is_slice_el = type_isslice(esub) and stores 3 words (incl CX->base+16,
the cap) for `is_str_el || is_slice_el`, in the main loop and the
repeat-fill. wwstage adds isslicel (esubti.kind == TY_SLICE -> esz =
esubti.size, fixing the stride) and the matching 3-word store. Closes
[N][]u8 (the bug) and the latent [N]str cap-drop in one branch.

The latent str cap-drop is now stored, but the indexed-element `.cap`
READ (`t[i].cap`) stays broken — a distinct cgindex/dot-selector bug,
cs!=ww divergent, filed as task #13. The new test validates the stored
cap via a whole-element copy (`let q = t[i]; q.cap`), which reads
through the correct ident-load path. [N]tagged literal init is the
remaining sibling (is_agg excludes TY_TAGGED), task #12.

Test 683_arr_strslice_elem: table-driven, dual-stage runtime + asm
byte-id; slice/str .len, 3-element stride-24, cap-via-copy, .ptr deref,
plus a [N]struct regression pin proving the is_agg path is untouched.
This commit is contained in:
2026-06-03 17:29:43 +09:00
parent a9228dabb3
commit b3d4d2df32
6 changed files with 438 additions and 48 deletions

View File

@@ -29518,16 +29518,17 @@ fn cglet(c: *cgen, n: *node) void = {
// after the last value (an nkind.N_FIELD with str=="...") fills the
// remaining slots up to the declared length with that value.
//
// str element (16B = ptr+len) needs both halves stored. cgstrlit
// / cgident leave a str as (AX=ptr, BX=len) and a single MOVQ
// from AX would leave .len as whatever the stack held — silent
// miscompile. Worse, primsize("str") returns 0 so esz would fall
// back to 8, also collapsing the per-element stride (element i+1
// would overwrite element i's would-be .len half). Detect the
// str-element case up front so both esz and the store path are
// right. (primsize's default-to-8-on-zero pattern is brittle for
// composites generally; same gap blocks slice / struct / tuple /
// tagged element arrays — tracked as a follow-up.)
// str/slice element (24B = ptr+len+cap, post-#1) needs all 3
// words stored. cgstrlit / cgident leave it as (AX=ptr, BX=len,
// CX=cap) and a single MOVQ from AX would leave .len/.cap as
// whatever the stack held — silent miscompile. Worse,
// primsize("str") returns 0 so esz would fall back to 8, also
// collapsing the per-element stride (element i+1 would overwrite
// element i's would-be .len half). Detect the str/slice element
// case up front so both esz and the store path are right.
// (primsize's default-to-8-on-zero pattern is brittle for
// composites generally. The str/slice element now stores all 3
// words; [N]tagged element arrays still hit the gap, task #12.)
if (rhs.kind == nkind.N_ARRLIT) {
let elemn: *node = n.lhs.lhs;
let esz: i32 = 8;
@@ -29559,6 +29560,15 @@ fn cglet(c: *cgen, n: *node) void = {
|| esubti.kind == tykind.TY_ARRAY
|| esubti.kind == tykind.TY_TUPLE);
if (isagg) { esz = esubti.size: i32; };
// #20/#270 str-slice arm: a slice element (N_TSLICE) is
// a 24B {ptr,len,cap} header — it matches no prim/str/agg
// branch above, so esz stayed the 8 sentinel (wrong stride,
// the -96-vs-80 cs!=ww frame divergence) and the scalar
// store dropped .len/.cap. Size it from the stamped tinfo
// and route it through the 3-word header store below.
let isslicel: bool = esubti != nil
&& esubti.kind == tykind.TY_SLICE;
if (isslicel) { esz = esubti.size: i32; };
// #8: a named-narrow element (`[N]tk`, tk = enum i32) is
// neither a builtin prim (primsize=0 above, so esz stayed
// the 8 sentinel) nor an aggregate, so the scalar store kept
@@ -29648,13 +29658,16 @@ fn cglet(c: *cgen, n: *node) void = {
}; };
} else {
cgexpr(c, e);
if (isstrel) {
if (isstrel || isslicel) {
emitline("\tMOVQ\tAX, ");
emitoff((off + idx * esz): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tBX, ");
emitoff((off + idx * esz + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off + idx * esz + 16): i64);
emitline("(BP)\n");
} else { if (isfloatel) {
emitline("\t");
emitline(fmov);
@@ -29692,13 +29705,16 @@ fn cglet(c: *cgen, n: *node) void = {
};
};
for (idx < total) {
if (isstrel) {
if (isstrel || isslicel) {
emitline("\tMOVQ\tAX, ");
emitoff((off + idx * esz): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tBX, ");
emitoff((off + idx * esz + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off + idx * esz + 16): i64);
emitline("(BP)\n");
} else { if (isfloatel) {
emitline("\t");
emitline(fmov);

View File

@@ -1732,16 +1732,17 @@ fn cglet(c: *cgen, n: *node) void = {
// after the last value (an nkind.N_FIELD with str=="...") fills the
// remaining slots up to the declared length with that value.
//
// str element (16B = ptr+len) needs both halves stored. cgstrlit
// / cgident leave a str as (AX=ptr, BX=len) and a single MOVQ
// from AX would leave .len as whatever the stack held — silent
// miscompile. Worse, primsize("str") returns 0 so esz would fall
// back to 8, also collapsing the per-element stride (element i+1
// would overwrite element i's would-be .len half). Detect the
// str-element case up front so both esz and the store path are
// right. (primsize's default-to-8-on-zero pattern is brittle for
// composites generally; same gap blocks slice / struct / tuple /
// tagged element arrays — tracked as a follow-up.)
// str/slice element (24B = ptr+len+cap, post-#1) needs all 3
// words stored. cgstrlit / cgident leave it as (AX=ptr, BX=len,
// CX=cap) and a single MOVQ from AX would leave .len/.cap as
// whatever the stack held — silent miscompile. Worse,
// primsize("str") returns 0 so esz would fall back to 8, also
// collapsing the per-element stride (element i+1 would overwrite
// element i's would-be .len half). Detect the str/slice element
// case up front so both esz and the store path are right.
// (primsize's default-to-8-on-zero pattern is brittle for
// composites generally. The str/slice element now stores all 3
// words; [N]tagged element arrays still hit the gap, task #12.)
if (rhs.kind == nkind.N_ARRLIT) {
let elemn: *node = n.lhs.lhs;
let esz: i32 = 8;
@@ -1773,6 +1774,15 @@ fn cglet(c: *cgen, n: *node) void = {
|| esubti.kind == tykind.TY_ARRAY
|| esubti.kind == tykind.TY_TUPLE);
if (isagg) { esz = esubti.size: i32; };
// #20/#270 str-slice arm: a slice element (N_TSLICE) is
// a 24B {ptr,len,cap} header — it matches no prim/str/agg
// branch above, so esz stayed the 8 sentinel (wrong stride,
// the -96-vs-80 cs!=ww frame divergence) and the scalar
// store dropped .len/.cap. Size it from the stamped tinfo
// and route it through the 3-word header store below.
let isslicel: bool = esubti != nil
&& esubti.kind == tykind.TY_SLICE;
if (isslicel) { esz = esubti.size: i32; };
// #8: a named-narrow element (`[N]tk`, tk = enum i32) is
// neither a builtin prim (primsize=0 above, so esz stayed
// the 8 sentinel) nor an aggregate, so the scalar store kept
@@ -1862,13 +1872,16 @@ fn cglet(c: *cgen, n: *node) void = {
}; };
} else {
cgexpr(c, e);
if (isstrel) {
if (isstrel || isslicel) {
emitline("\tMOVQ\tAX, ");
emitoff((off + idx * esz): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tBX, ");
emitoff((off + idx * esz + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off + idx * esz + 16): i64);
emitline("(BP)\n");
} else { if (isfloatel) {
emitline("\t");
emitline(fmov);
@@ -1906,13 +1919,16 @@ fn cglet(c: *cgen, n: *node) void = {
};
};
for (idx < total) {
if (isstrel) {
if (isstrel || isslicel) {
emitline("\tMOVQ\tAX, ");
emitoff((off + idx * esz): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tBX, ");
emitoff((off + idx * esz + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off + idx * esz + 16): i64);
emitline("(BP)\n");
} else { if (isfloatel) {
emitline("\t");
emitline(fmov);

View File

@@ -29518,16 +29518,17 @@ fn cglet(c: *cgen, n: *node) void = {
// after the last value (an nkind.N_FIELD with str=="...") fills the
// remaining slots up to the declared length with that value.
//
// str element (16B = ptr+len) needs both halves stored. cgstrlit
// / cgident leave a str as (AX=ptr, BX=len) and a single MOVQ
// from AX would leave .len as whatever the stack held — silent
// miscompile. Worse, primsize("str") returns 0 so esz would fall
// back to 8, also collapsing the per-element stride (element i+1
// would overwrite element i's would-be .len half). Detect the
// str-element case up front so both esz and the store path are
// right. (primsize's default-to-8-on-zero pattern is brittle for
// composites generally; same gap blocks slice / struct / tuple /
// tagged element arrays — tracked as a follow-up.)
// str/slice element (24B = ptr+len+cap, post-#1) needs all 3
// words stored. cgstrlit / cgident leave it as (AX=ptr, BX=len,
// CX=cap) and a single MOVQ from AX would leave .len/.cap as
// whatever the stack held — silent miscompile. Worse,
// primsize("str") returns 0 so esz would fall back to 8, also
// collapsing the per-element stride (element i+1 would overwrite
// element i's would-be .len half). Detect the str/slice element
// case up front so both esz and the store path are right.
// (primsize's default-to-8-on-zero pattern is brittle for
// composites generally. The str/slice element now stores all 3
// words; [N]tagged element arrays still hit the gap, task #12.)
if (rhs.kind == nkind.N_ARRLIT) {
let elemn: *node = n.lhs.lhs;
let esz: i32 = 8;
@@ -29559,6 +29560,15 @@ fn cglet(c: *cgen, n: *node) void = {
|| esubti.kind == tykind.TY_ARRAY
|| esubti.kind == tykind.TY_TUPLE);
if (isagg) { esz = esubti.size: i32; };
// #20/#270 str-slice arm: a slice element (N_TSLICE) is
// a 24B {ptr,len,cap} header — it matches no prim/str/agg
// branch above, so esz stayed the 8 sentinel (wrong stride,
// the -96-vs-80 cs!=ww frame divergence) and the scalar
// store dropped .len/.cap. Size it from the stamped tinfo
// and route it through the 3-word header store below.
let isslicel: bool = esubti != nil
&& esubti.kind == tykind.TY_SLICE;
if (isslicel) { esz = esubti.size: i32; };
// #8: a named-narrow element (`[N]tk`, tk = enum i32) is
// neither a builtin prim (primsize=0 above, so esz stayed
// the 8 sentinel) nor an aggregate, so the scalar store kept
@@ -29648,13 +29658,16 @@ fn cglet(c: *cgen, n: *node) void = {
}; };
} else {
cgexpr(c, e);
if (isstrel) {
if (isstrel || isslicel) {
emitline("\tMOVQ\tAX, ");
emitoff((off + idx * esz): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tBX, ");
emitoff((off + idx * esz + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off + idx * esz + 16): i64);
emitline("(BP)\n");
} else { if (isfloatel) {
emitline("\t");
emitline(fmov);
@@ -29692,13 +29705,16 @@ fn cglet(c: *cgen, n: *node) void = {
};
};
for (idx < total) {
if (isstrel) {
if (isstrel || isslicel) {
emitline("\tMOVQ\tAX, ");
emitoff((off + idx * esz): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tBX, ");
emitoff((off + idx * esz + 8): i64);
emitline("(BP)\n");
emitline("\tMOVQ\tCX, ");
emitoff((off + idx * esz + 16): i64);
emitline("(BP)\n");
} else { if (isfloatel) {
emitline("\t");
emitline(fmov);