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

@@ -9030,16 +9030,14 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
* `...` repeat marker (an N_FIELD with str=="...") fills the
* remaining slots with the last value.
*
* str element (16B = ptr+len) needs both halves stored: cgexpr
* leaves a str as (AX=ptr, BX=len), and a single MOVQ from AX
* would leave .len as whatever the stack held — silent
* miscompile. The per-element store branches on TY_STR before
* falling through to the scalar MOVB/MOVL/MOVQ path. Slice
* (24B) and struct/tuple/tagged element arrays land in the
* same multi-word-store gap; the read side (cgindex of an
* [N]slice) has its own truncating-to-ptr bug, so slice
* end-to-end repros surface sub-issues — both halves of the
* slice-element fix are tracked as a follow-up. */
* str/slice element (24B = ptr+len+cap, post-#1) needs all
* three words stored: cgexpr leaves 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 (#20/#270 str-
* slice arm). The per-element store branches on TY_STR/TY_SLICE
* before falling through to the scalar MOVB/MOVL/MOVQ path.
* [N]tagged element arrays still land in the multi-word gap
* (is_agg excludes TY_TAGGED) — tracked as task #12. */
if (n->rhs && n->rhs->kind == N_ARRLIT && lu
&& lu->kind == TY_ARRAY) {
Type *esub = lu->sub;
@@ -9054,6 +9052,11 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
|| esubu->kind == TY_ARRAY
|| esubu->kind == TY_TUPLE);
int is_str_el = type_isstr(esub);
/* #20/#270 str-slice arm: a slice element is a 24B
* {ptr,len,cap} header just like str; cgexpr lowers it
* into AX/BX/CX. Both must store all three words — the
* scalar 1-word MOVQ below drops .len and .cap. */
int is_slice_el = type_isslice(esub);
/* float element → store FROM X0; the AX path stores
* raw double low-bits, garbage for f32 (#122, twin of
* the arr[i]= store fix and the cgen.c:6423 read). */
@@ -9134,11 +9137,13 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
continue;
}
cgexpr(c, e, *locals);
if (is_str_el) {
if (is_str_el || is_slice_el) {
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, base));
ins2(c, A_MOVQ, areg(D_BX),
amem(D_BP, base + 8));
ins2(c, A_MOVQ, areg(D_CX),
amem(D_BP, base + 16));
} else if (is_float_el) {
ins2(c, fmov, areg(D_X0),
amem(D_BP, base));
@@ -9157,11 +9162,13 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
* AX (and BX for str). */
while (idx < (int)lu->alen) {
int base = off + idx * esz;
if (is_str_el) {
if (is_str_el || is_slice_el) {
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, base));
ins2(c, A_MOVQ, areg(D_BX),
amem(D_BP, base + 8));
ins2(c, A_MOVQ, areg(D_CX),
amem(D_BP, base + 16));
} else if (is_float_el) {
ins2(c, fmov, areg(D_X0),
amem(D_BP, base));