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

@@ -240,6 +240,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
$(BIN)/test_amp_dot $(BIN)/test_arr_elem_field \
$(BIN)/test_arr_elem_field_write \
$(BIN)/test_arr_enum_elem \
$(BIN)/test_arr_strslice_elem \
$(BIN)/test_dot_str_chained_arg \
$(BIN)/test_dot_slice_arg \
$(BIN)/test_dot_tagged_source \
@@ -554,6 +555,12 @@ $(BIN)/test_arr_enum_elem: test/wcc/682_arr_enum_elem.c $(BIN)/ww \
$(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
$(BIN)/test_arr_strslice_elem: test/wcc/683_arr_strslice_elem.c $(BIN)/ww \
$(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \
$(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
$(BIN)/test_dot_str_chained_arg: test/wcc/692_dot_str_chained_arg.c $(BIN)/ww \
$(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \

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));

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);

View File

@@ -0,0 +1,328 @@
/*
* 683_arr_strslice_elem — cstage and wwstage agree, byte-for-byte and
* at runtime, that an `[N][]u8` / `[N]str` array-LITERAL init copies the
* FULL 24B {ptr,len,cap} header of every element (task #20, the #270
* aggregate-element-store family's str/slice arm).
*
* The bug: cgen's N_LET / N_ARRLIT per-element store lowered each str/
* slice element's header into AX=ptr/BX=len/CX=cap (cgexpr) but stored
* only some words — a slice element fell through to the scalar 1-word
* MOVQ (dropping .len AND .cap), and a str element stored 2 words
* (dropping .cap, latent). Each element is 24B 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. struct/array/tuple elements already copied correctly via
* the #270-1c is_agg multi-word path; str/slice were the documented
* follow-up (cgen.c:9037-9042, cgenstmt.ww deferral comment).
*
* The fix (BOTH stages, converged byte-identical): cstage adds
* is_slice_el = type_isslice(esub) and stores 3 words (incl CX->base+16)
* for `is_str_el || is_slice_el`; wwstage adds isslicel (TY_SLICE ->
* esz = esubti.size, fixing the stride) and the matching 3-word store.
*
* Cap is validated via a WHOLE-ELEMENT COPY (`let q = t[i]; q.cap`), NOT
* a direct `t[i].cap` read: the `.cap` field-extract on an INDEXED slice/
* str element is a SEPARATE, still-open bug (returns .ptr on cstage,
* emits no read on wwstage — divergent), the "cgindex truncating-to-ptr"
* sibling cited at cgen.c:9039-9040, filed as task #13. A whole-element
* copy reads the stored cap through the (correct) ident-load path, so it
* exercises THIS fix's stored cap word without hitting #13.
*
* Mutation-sanity (the per-word coverage): the .len rows fail if the
* store drops the .len word (the pre-fix slice 1-word store), and the
* cap-via-copy rows fail if it drops the .cap word (the pre-fix str
* 2-word store) — so a regression to a 1-word or 2-word store is caught.
*
* row | shape | want
* -----------------+------------------------------------+--------------
* slice_len1 | [2][]u8, return t[1].len. Pre-fix | 2 + byte-id
* | slice 1-word store dropped .len. |
* slice_len0 | [2][]u8, return t[0].len. | 3 + byte-id
* slice_stride3 | [3][]u8, return t[2].len. Exercises| 7 + byte-id
* | the 24B per-element stride (the |
* | wwstage 8-sentinel frame-offset |
* | bug overran into the wrong slot). |
* slice_cap_copy | [2][]u8 w/ caps 7,6; let q=t[0]; | 7 + byte-id
* | return q.cap. Validates the stored |
* | .cap word (pre-fix dropped). |
* slice_ptr | [2][]u8 over a backing array; let | 4 + byte-id
* | q=t[1]; return q[0]. Pins .ptr |
* | stored correctly (deref the elem). |
* str_len1 | [2]str=[a,b], return t[1].len. | 2 + byte-id
* str_cap_copy | [2]str=["abcde","xy"]; let q=t[0]; | 5 + byte-id
* | return q.cap. The latent str cap- |
* | drop (pre-fix garbage); cap=len=5 |
* | for a static literal. |
* struct_elem | [2]Pt struct literal, return | 4 + byte-id
* | t[1].y. Regression pin: the is_agg |
* | multi-word path is untouched by |
* | the str/slice branch. |
*
* Exit-code rows confirm both stages run correctly. The asm-byte-id rows
* pin the symmetric 3-word store (cstage == wwstage); pre-fix wwstage
* mis-strided (slice esz=8) and under-copied, so the diff was non-empty.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
static int
runwait(const char *cmd)
{
int rc = system(cmd);
if (rc == -1) return -1;
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
return -1;
}
struct row { const char *label; const char *src; int want; };
static const struct row rows[] = {
{ "slice_len1",
"package main;\n"
"export fn main() i32 = {\n"
"\tlet hb: [8]u8;\n"
"\tlet a: []u8; a.ptr = &hb[0]; a.len = 3; a.cap = 8;\n"
"\tlet b: []u8; b.ptr = &hb[0]; b.len = 2; b.cap = 8;\n"
"\tlet t: [2][]u8 = [a, b];\n"
"\treturn t[1].len: i32;\n"
"};\n",
2 },
{ "slice_len0",
"package main;\n"
"export fn main() i32 = {\n"
"\tlet hb: [8]u8;\n"
"\tlet a: []u8; a.ptr = &hb[0]; a.len = 3; a.cap = 8;\n"
"\tlet b: []u8; b.ptr = &hb[0]; b.len = 2; b.cap = 8;\n"
"\tlet t: [2][]u8 = [a, b];\n"
"\treturn t[0].len: i32;\n"
"};\n",
3 },
{ "slice_stride3",
"package main;\n"
"export fn main() i32 = {\n"
"\tlet hb: [8]u8;\n"
"\tlet a: []u8; a.ptr = &hb[0]; a.len = 3; a.cap = 8;\n"
"\tlet b: []u8; b.ptr = &hb[0]; b.len = 5; b.cap = 8;\n"
"\tlet c: []u8; c.ptr = &hb[0]; c.len = 7; c.cap = 8;\n"
"\tlet t: [3][]u8 = [a, b, c];\n"
"\treturn t[2].len: i32;\n"
"};\n",
7 },
{ "slice_cap_copy",
"package main;\n"
"export fn main() i32 = {\n"
"\tlet hb: [8]u8;\n"
"\tlet a: []u8; a.ptr = &hb[0]; a.len = 3; a.cap = 7;\n"
"\tlet b: []u8; b.ptr = &hb[0]; b.len = 2; b.cap = 6;\n"
"\tlet t: [2][]u8 = [a, b];\n"
"\tlet q: []u8 = t[0];\n"
"\treturn q.cap: i32;\n"
"};\n",
7 },
{ "slice_ptr",
"package main;\n"
"export fn main() i32 = {\n"
"\tlet hb: [8]u8; hb[0] = 9u8; hb[1] = 4u8;\n"
"\tlet a: []u8; a.ptr = &hb[0]; a.len = 3; a.cap = 8;\n"
"\tlet b: []u8; b.ptr = &hb[1]; b.len = 2; b.cap = 8;\n"
"\tlet t: [2][]u8 = [a, b];\n"
"\tlet q: []u8 = t[1];\n"
"\treturn q[0]: i32;\n"
"};\n",
4 },
{ "str_len1",
"package main;\n"
"export fn main() i32 = {\n"
"\tlet a: str = \"abc\";\n"
"\tlet b: str = \"de\";\n"
"\tlet t: [2]str = [a, b];\n"
"\treturn t[1].len: i32;\n"
"};\n",
2 },
{ "str_cap_copy",
"package main;\n"
"export fn main() i32 = {\n"
"\tlet a: str = \"abcde\";\n"
"\tlet b: str = \"xy\";\n"
"\tlet t: [2]str = [a, b];\n"
"\tlet q: str = t[0];\n"
"\treturn q.cap: i32;\n"
"};\n",
5 },
/* Regression pin: a [N]struct element copies multi-word via the
* pre-existing #270-1c is_agg path, NOT the new str/slice 3-word
* header branch. Proves the str/slice esz/store change leaves the
* is_agg element path untouched (byte-id holds for it too). */
{ "struct_elem",
"package main;\n"
"type Pt = struct { x: i32, y: i32 };\n"
"export fn main() i32 = {\n"
"\tlet t: [2]Pt = [Pt { x = 1, y = 2 }, Pt { x = 3, y = 4 }];\n"
"\treturn t[1].y: i32;\n"
"};\n",
4 },
};
static int
run_driver(const char *driver, const struct row *r, int i)
{
char src[64], tmpdir[64], cmd[1024];
snprintf(src, sizeof src, "/tmp/asse_%d_%d.ww", getpid(), i);
snprintf(tmpdir, sizeof tmpdir, "/tmp/asse_%d_d_%d", getpid(), i);
FILE *f = fopen(src, "wb");
if (!f) return -1;
fputs(r->src, f);
fclose(f);
mkdir(tmpdir, 0755);
snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>/dev/null",
tmpdir, driver, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: build via %s failed\n",
r->label, driver);
unlink(src); rmdir(tmpdir);
return -1;
}
const char *base = strrchr(src, '/');
base = base ? base + 1 : src;
char outbin[128];
snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base);
char *dot = strrchr(outbin, '.');
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
int got = runwait(outbin);
unlink(src); unlink(outbin); rmdir(tmpdir);
return got;
}
/* asm_byte_identical — generate .s via cstage's w6c and wwstage's
* w6c_ww and diff. The regression-pinning row for #20: pre-fix wwstage
* mis-strided the slice element (esz=8) and under-copied, so the diff
* was non-empty; the converged 3-word store makes them identical. */
static int
asm_byte_identical(const char *bin, const struct row *r, int i)
{
char src[64], cs[64], ws[64], cmd[1024];
snprintf(src, sizeof src, "/tmp/asse_asm_%d_%d.ww", getpid(), i);
snprintf(cs, sizeof cs, "/tmp/asse_asm_%d_%d_c.s", getpid(), i);
snprintf(ws, sizeof ws, "/tmp/asse_asm_%d_%d_w.s", getpid(), i);
FILE *f = fopen(src, "wb");
if (!f) return -1;
fputs(r->src, f);
fclose(f);
snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s 2>/dev/null", bin, cs, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: w6c errored\n", r->label);
unlink(src);
return -1;
}
snprintf(cmd, sizeof cmd, "%s/w6c_ww -o %s %s 2>/dev/null",
bin, ws, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: w6c_ww errored\n", r->label);
unlink(src); unlink(cs);
return -1;
}
FILE *fc = fopen(cs, "rb");
FILE *fw = fopen(ws, "rb");
int rc = 0;
if (!fc || !fw) {
rc = -1;
} else {
for (;;) {
int a = fgetc(fc);
int b = fgetc(fw);
if (a != b) { rc = -1; break; }
if (a == EOF) break;
}
}
if (fc) fclose(fc);
if (fw) fclose(fw);
if (rc != 0)
fprintf(stderr, "row[%s]: cstage vs wwstage asm differs\n",
r->label);
unlink(src); unlink(cs); unlink(ws);
return rc;
}
int
main(void)
{
const char *bin = getenv("BIN");
if (!bin) bin = "out/bin";
char absbin[1024];
if (bin[0] != '/') {
char cwd[1024];
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
bin = absbin;
}
char cdrv[1024];
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
char wdrv[1024];
snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin);
struct { const char *name; const char *path; int gated_on_existence; }
drivers[] = {
{ "cstage", cdrv, 0 },
{ "wwstage", wdrv, 1 },
{ NULL, NULL, 0 },
};
int n = (int)(sizeof rows / sizeof rows[0]);
int total = 0, fail = 0;
for (int d = 0; drivers[d].name; d++) {
if (drivers[d].gated_on_existence
&& access(drivers[d].path, X_OK) != 0) {
fprintf(stderr, "arr_strslice_elem: skip %s (no %s)\n",
drivers[d].name, drivers[d].path);
continue;
}
for (int i = 0; i < n; i++) {
int got = run_driver(drivers[d].path, &rows[i], i);
total++;
if (got != rows[i].want) {
fprintf(stderr,
"arr_strslice_elem[%s][%s]: exit=%d want=%d\n",
drivers[d].name, rows[i].label,
got, rows[i].want);
fail++;
}
}
}
if (access(wdrv, X_OK) == 0) {
for (int i = 0; i < n; i++) {
total++;
if (asm_byte_identical(bin, &rows[i], i) != 0)
fail++;
}
}
if (fail) {
fprintf(stderr,
"arr_strslice_elem: %d/%d fixtures failed\n", fail, total);
return 1;
}
printf("arr_strslice_elem: %d/%d ok\n", total, total);
return 0;
}