diff --git a/Makefile b/Makefile index 9e41f58e..515ee5f9 100644 --- a/Makefile +++ b/Makefile @@ -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 \ diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 82575e9a..b0b47473 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -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)); diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 41511ce4..84ff3f43 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -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); diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index 88d1e900..012d35c3 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -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); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 675a6596..d47ac05d 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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); diff --git a/test/wcc/683_arr_strslice_elem.c b/test/wcc/683_arr_strslice_elem.c new file mode 100644 index 00000000..f992c46e --- /dev/null +++ b/test/wcc/683_arr_strslice_elem.c @@ -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 +#include +#include +#include +#include +#include + +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; +}