From cbb9fbbb6558709f973438666168f0118e2ac117 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sat, 16 May 2026 10:38:23 +0900 Subject: [PATCH] cstage+selfhost+test: full-element store for [N]str array literals (#21) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [N]str array literals wrote only the .ptr half of each element. cstage used esz=16 from `lu->sub->size` and a single per-element MOVQ → .len trailed uninitialized stack residue. Wwstage was worse: primsize("str")=0 fell through to esz=8, so element i+1's ptr-MOVQ clobbered element i's .len slot, scrambling everything. Worker-18 sidestepped during #18 by rewriting array primer rows to [N]i64. cstage cgen.c N_ARRLIT TY_STR branch: emit AX → base+i*16 then BX → base+i*16+8. Repeat-`...` path mirrored. type_isstr handles TY_UNTYPED_STR + TY_NAMED-aliased-str. Wwstage cgenstmt.ww: isstrel flag conditionally drives the two-MOVQ store in both the per-element walk and the repeat fill. The dispatch loop was refactored to unify FIELD/ellipsis branches via isellip, cleaning up the duplicated arms. Wwstage cgenutil.ww slotsize/letslotsize: TNAME-"str" element gets esz=16, replacing the primsize=0 → 8B fallback. Without this the frame collapsed to 24B for [3]str. Slice (24B), struct, tuple, tagged element arrays have the same root cause but distinct width/layout concerns — deferred to #35 per rob. Test 711 (arrlit_str_full): 7 rows × 2 stages = 14 fixtures — str_lens_3el, str_ptrs_3el, str_repeat_5el (TK_ELLIPSIS), bool_3el, rune_3el, i32_3el, i64_3el. Rune relies on the pre-existing esz==4 → MOVL path (incidental correctness); sibling slot types pinned as regression nets. Followups filed: #34 (wwstage cgindex truncate on [N]str bare-let read side, surfaced by this fix), #35 (composite element types), #36 (primsize-returns-0-default-to-8 cleanup). --- Makefile | 7 + cmd/w6c/cgen.c | 59 +++++-- selfhost/cmd/w6c/main.combined.ww | 91 ++++++++--- selfhost/cmd/wcc/cgenstmt.ww | 70 +++++--- selfhost/cmd/wcc/cgenutil.ww | 21 ++- selfhost/cmd/wwdump/main.combined.ww | 91 ++++++++--- test/wcc/711_arrlit_str_full.c | 229 +++++++++++++++++++++++++++ 7 files changed, 483 insertions(+), 85 deletions(-) create mode 100644 test/wcc/711_arrlit_str_full.c diff --git a/Makefile b/Makefile index bf2ffb77..630cba7d 100644 --- a/Makefile +++ b/Makefile @@ -234,6 +234,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_nested_call_rhs \ $(BIN)/test_fnlabel_mangle \ $(BIN)/test_cgreturn_variant_zero \ + $(BIN)/test_arrlit_str_full \ $(BIN)/test_param_shadow_mod \ $(BIN)/test_localoff_scope \ $(BIN)/test_cast_enum_movl \ @@ -450,6 +451,12 @@ $(BIN)/test_cast_enum_movl: test/wcc/710_cast_enum_movl.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_arrlit_str_full: test/wcc/711_arrlit_str_full.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_use_promote_alias: test/wcc/699_use_promote_alias.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 155fcfee..7e6be785 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -5974,17 +5974,33 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) * Walk elements in declaration order, store each at off + i*esz * using the right width for the element type. The trailing * `...` repeat marker (an N_FIELD with str=="...") fills the - * remaining slots with the last value. */ + * 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. */ if (n->rhs && n->rhs->kind == N_ARRLIT && lu && lu->kind == TY_ARRAY) { - int esz = lu->sub ? (int)lu->sub->size : 1; + Type *esub = lu->sub; + int esz = esub ? (int)esub->size : 1; + int is_str_el = type_isstr(esub); int op = A_MOVQ; - if (esz == 1) op = A_MOVB; - else if (esz == 4) op = A_MOVL; - /* esz == 2 (i16/u16) falls through to MOVQ — over-writes - * by 6B; the next element store rewrites the high half. - * For the last element this trails 6 bytes into the next - * stack slot. Add MOVW to w6a if real i16 arrays land. */ + if (!is_str_el) { + if (esz == 1) op = A_MOVB; + else if (esz == 4) op = A_MOVL; + /* esz == 2 (i16/u16) falls through to MOVQ — + * over-writes by 6B; the next element store + * rewrites the high half. For the last element + * this trails 6 bytes into the next stack slot. + * Add MOVW to w6a if real i16 arrays land. */ + } int idx = 0; Node *last = NULL; int repeat = 0; @@ -5995,16 +6011,33 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) break; } cgexpr(c, e, *locals); - ins2(c, op, areg(D_AX), - amem(D_BP, off + idx * esz)); + int base = off + idx * esz; + if (is_str_el) { + ins2(c, A_MOVQ, areg(D_AX), + amem(D_BP, base)); + ins2(c, A_MOVQ, areg(D_BX), + amem(D_BP, base + 8)); + } else { + ins2(c, op, areg(D_AX), + amem(D_BP, base)); + } last = e; idx++; } if (repeat && last) { - /* fill remaining slots with the value already in AX. */ + /* fill remaining slots with the value still in + * AX (and BX for str). */ while (idx < (int)lu->alen) { - ins2(c, op, areg(D_AX), - amem(D_BP, off + idx * esz)); + int base = off + idx * esz; + if (is_str_el) { + ins2(c, A_MOVQ, areg(D_AX), + amem(D_BP, base)); + ins2(c, A_MOVQ, areg(D_BX), + amem(D_BP, base + 8)); + } else { + ins2(c, op, areg(D_AX), + amem(D_BP, base)); + } idx++; } } diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index bfc367b3..9592734c 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -7556,8 +7556,15 @@ export fn letslotsize(c: *cgen, n: *node) i32 = { let esz: i32 = 8; if (elemn != nil) { if (elemn.kind == nkind.N_TNAME) { - let ps: i32 = primsize(elemn.str); - if (ps > 0) { esz = ps; }; + // Composite primitive: `str` is 16B + // (ptr+len) — primsize returns 0 for + // it, so it'd slot 8B without this. + if (streq(elemn.str, "str")) { + esz = 16; + } else { + let ps: i32 = primsize(elemn.str); + if (ps > 0) { esz = ps; }; + }; }; }; let cnt: i32 = 0; @@ -7665,8 +7672,14 @@ fn slotsize(c: *cgen, typn: *node) i32 = { if (elemn != nil) { if (elemn.kind == nkind.N_TNAME) { let en: str = elemn.str; + // `str` is a composite primitive (ptr+len, 16B); + // primsize returns 0 for it, so without this + // explicit case a `[N]str` would slot 8B/elem, + // collapsing the per-element stride and losing + // every .len half. + if (streq(en, "str")) { esz = 16; }; let ps: i32 = primsize(en); - if (ps > 0) { esz = ps; } + if (esz == 8) { if (ps > 0) { esz = ps; } else { // Named struct / aliased type: size off // the structinfo if present, else follow @@ -7683,7 +7696,7 @@ fn slotsize(c: *cgen, typn: *node) i32 = { esz = slotsize(c, al); }; }; }; - }; + }; }; } else { if (elemn.kind == nkind.N_TTAGGED) { // Tagged-union element: full slot (8 tag + // padded max payload). Matches C cgen's @@ -15274,13 +15287,30 @@ fn cglet(c: *cgen, n: *node) void = { // using the right width for the element type. Trailing `...` // 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.) if (rhs.kind == nkind.N_ARRLIT) { let elemn: *node = n.lhs.lhs; let esz: i32 = 8; + let isstrel: bool = false; if (elemn != nil) { if (elemn.kind == nkind.N_TNAME) { - let ps: i32 = primsize(elemn.str); - if (ps > 0) { esz = ps; }; + if (streq(elemn.str, "str")) { + esz = 16; + isstrel = true; + } else { + let ps: i32 = primsize(elemn.str); + if (ps > 0) { esz = ps; }; + }; }; }; let mop: str = tnodestoreop(c, elemn, esz); @@ -15288,33 +15318,37 @@ fn cglet(c: *cgen, n: *node) void = { let repeat: bool = false; let e: *node = rhs.list; for (e != nil) { + let isellip: bool = false; if (e.kind == nkind.N_FIELD) { if (streq(e.str, "...")) { repeat = true; - e = nil; + isellip = true; + }; + }; + if (isellip) { + e = nil; + } else { + cgexpr(c, e); + if (isstrel) { + emitline("\tMOVQ\tAX, "); + emitoff((off + idx * esz): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tBX, "); + emitoff((off + idx * esz + 8): i64); + emitline("(BP)\n"); } else { - cgexpr(c, e); emitline("\t"); emitline(mop); emitline("\tAX, "); emitoff((off + idx * esz): i64); emitline("(BP)\n"); - idx += 1; - e = e.next; }; - } else { - cgexpr(c, e); - emitline("\t"); - emitline(mop); - emitline("\tAX, "); - emitoff((off + idx * esz): i64); - emitline("(BP)\n"); idx += 1; e = e.next; }; }; - // AX still holds the last stored value; fill remaining - // slots up to the declared length with it. + // AX (and BX for str) still holds the last stored value; + // fill remaining slots up to the declared length with it. if (repeat) { let total: i32 = idx; if (n.lhs != nil) { @@ -15327,11 +15361,20 @@ fn cglet(c: *cgen, n: *node) void = { }; }; for (idx < total) { - emitline("\t"); - emitline(mop); - emitline("\tAX, "); - emitoff((off + idx * esz): i64); - emitline("(BP)\n"); + if (isstrel) { + emitline("\tMOVQ\tAX, "); + emitoff((off + idx * esz): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tBX, "); + emitoff((off + idx * esz + 8): i64); + emitline("(BP)\n"); + } else { + emitline("\t"); + emitline(mop); + emitline("\tAX, "); + emitoff((off + idx * esz): i64); + emitline("(BP)\n"); + }; idx += 1; }; }; diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index eaa0a968..9d04f6fd 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -503,13 +503,30 @@ fn cglet(c: *cgen, n: *node) void = { // using the right width for the element type. Trailing `...` // 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.) if (rhs.kind == nkind.N_ARRLIT) { let elemn: *node = n.lhs.lhs; let esz: i32 = 8; + let isstrel: bool = false; if (elemn != nil) { if (elemn.kind == nkind.N_TNAME) { - let ps: i32 = primsize(elemn.str); - if (ps > 0) { esz = ps; }; + if (streq(elemn.str, "str")) { + esz = 16; + isstrel = true; + } else { + let ps: i32 = primsize(elemn.str); + if (ps > 0) { esz = ps; }; + }; }; }; let mop: str = tnodestoreop(c, elemn, esz); @@ -517,33 +534,37 @@ fn cglet(c: *cgen, n: *node) void = { let repeat: bool = false; let e: *node = rhs.list; for (e != nil) { + let isellip: bool = false; if (e.kind == nkind.N_FIELD) { if (streq(e.str, "...")) { repeat = true; - e = nil; + isellip = true; + }; + }; + if (isellip) { + e = nil; + } else { + cgexpr(c, e); + if (isstrel) { + emitline("\tMOVQ\tAX, "); + emitoff((off + idx * esz): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tBX, "); + emitoff((off + idx * esz + 8): i64); + emitline("(BP)\n"); } else { - cgexpr(c, e); emitline("\t"); emitline(mop); emitline("\tAX, "); emitoff((off + idx * esz): i64); emitline("(BP)\n"); - idx += 1; - e = e.next; }; - } else { - cgexpr(c, e); - emitline("\t"); - emitline(mop); - emitline("\tAX, "); - emitoff((off + idx * esz): i64); - emitline("(BP)\n"); idx += 1; e = e.next; }; }; - // AX still holds the last stored value; fill remaining - // slots up to the declared length with it. + // AX (and BX for str) still holds the last stored value; + // fill remaining slots up to the declared length with it. if (repeat) { let total: i32 = idx; if (n.lhs != nil) { @@ -556,11 +577,20 @@ fn cglet(c: *cgen, n: *node) void = { }; }; for (idx < total) { - emitline("\t"); - emitline(mop); - emitline("\tAX, "); - emitoff((off + idx * esz): i64); - emitline("(BP)\n"); + if (isstrel) { + emitline("\tMOVQ\tAX, "); + emitoff((off + idx * esz): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tBX, "); + emitoff((off + idx * esz + 8): i64); + emitline("(BP)\n"); + } else { + emitline("\t"); + emitline(mop); + emitline("\tAX, "); + emitoff((off + idx * esz): i64); + emitline("(BP)\n"); + }; idx += 1; }; }; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index fb103a93..37c67564 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -1346,8 +1346,15 @@ export fn letslotsize(c: *cgen, n: *node) i32 = { let esz: i32 = 8; if (elemn != nil) { if (elemn.kind == nkind.N_TNAME) { - let ps: i32 = primsize(elemn.str); - if (ps > 0) { esz = ps; }; + // Composite primitive: `str` is 16B + // (ptr+len) — primsize returns 0 for + // it, so it'd slot 8B without this. + if (streq(elemn.str, "str")) { + esz = 16; + } else { + let ps: i32 = primsize(elemn.str); + if (ps > 0) { esz = ps; }; + }; }; }; let cnt: i32 = 0; @@ -1455,8 +1462,14 @@ fn slotsize(c: *cgen, typn: *node) i32 = { if (elemn != nil) { if (elemn.kind == nkind.N_TNAME) { let en: str = elemn.str; + // `str` is a composite primitive (ptr+len, 16B); + // primsize returns 0 for it, so without this + // explicit case a `[N]str` would slot 8B/elem, + // collapsing the per-element stride and losing + // every .len half. + if (streq(en, "str")) { esz = 16; }; let ps: i32 = primsize(en); - if (ps > 0) { esz = ps; } + if (esz == 8) { if (ps > 0) { esz = ps; } else { // Named struct / aliased type: size off // the structinfo if present, else follow @@ -1473,7 +1486,7 @@ fn slotsize(c: *cgen, typn: *node) i32 = { esz = slotsize(c, al); }; }; }; - }; + }; }; } else { if (elemn.kind == nkind.N_TTAGGED) { // Tagged-union element: full slot (8 tag + // padded max payload). Matches C cgen's diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index e4e73824..f32c9311 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -7556,8 +7556,15 @@ export fn letslotsize(c: *cgen, n: *node) i32 = { let esz: i32 = 8; if (elemn != nil) { if (elemn.kind == nkind.N_TNAME) { - let ps: i32 = primsize(elemn.str); - if (ps > 0) { esz = ps; }; + // Composite primitive: `str` is 16B + // (ptr+len) — primsize returns 0 for + // it, so it'd slot 8B without this. + if (streq(elemn.str, "str")) { + esz = 16; + } else { + let ps: i32 = primsize(elemn.str); + if (ps > 0) { esz = ps; }; + }; }; }; let cnt: i32 = 0; @@ -7665,8 +7672,14 @@ fn slotsize(c: *cgen, typn: *node) i32 = { if (elemn != nil) { if (elemn.kind == nkind.N_TNAME) { let en: str = elemn.str; + // `str` is a composite primitive (ptr+len, 16B); + // primsize returns 0 for it, so without this + // explicit case a `[N]str` would slot 8B/elem, + // collapsing the per-element stride and losing + // every .len half. + if (streq(en, "str")) { esz = 16; }; let ps: i32 = primsize(en); - if (ps > 0) { esz = ps; } + if (esz == 8) { if (ps > 0) { esz = ps; } else { // Named struct / aliased type: size off // the structinfo if present, else follow @@ -7683,7 +7696,7 @@ fn slotsize(c: *cgen, typn: *node) i32 = { esz = slotsize(c, al); }; }; }; - }; + }; }; } else { if (elemn.kind == nkind.N_TTAGGED) { // Tagged-union element: full slot (8 tag + // padded max payload). Matches C cgen's @@ -15274,13 +15287,30 @@ fn cglet(c: *cgen, n: *node) void = { // using the right width for the element type. Trailing `...` // 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.) if (rhs.kind == nkind.N_ARRLIT) { let elemn: *node = n.lhs.lhs; let esz: i32 = 8; + let isstrel: bool = false; if (elemn != nil) { if (elemn.kind == nkind.N_TNAME) { - let ps: i32 = primsize(elemn.str); - if (ps > 0) { esz = ps; }; + if (streq(elemn.str, "str")) { + esz = 16; + isstrel = true; + } else { + let ps: i32 = primsize(elemn.str); + if (ps > 0) { esz = ps; }; + }; }; }; let mop: str = tnodestoreop(c, elemn, esz); @@ -15288,33 +15318,37 @@ fn cglet(c: *cgen, n: *node) void = { let repeat: bool = false; let e: *node = rhs.list; for (e != nil) { + let isellip: bool = false; if (e.kind == nkind.N_FIELD) { if (streq(e.str, "...")) { repeat = true; - e = nil; + isellip = true; + }; + }; + if (isellip) { + e = nil; + } else { + cgexpr(c, e); + if (isstrel) { + emitline("\tMOVQ\tAX, "); + emitoff((off + idx * esz): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tBX, "); + emitoff((off + idx * esz + 8): i64); + emitline("(BP)\n"); } else { - cgexpr(c, e); emitline("\t"); emitline(mop); emitline("\tAX, "); emitoff((off + idx * esz): i64); emitline("(BP)\n"); - idx += 1; - e = e.next; }; - } else { - cgexpr(c, e); - emitline("\t"); - emitline(mop); - emitline("\tAX, "); - emitoff((off + idx * esz): i64); - emitline("(BP)\n"); idx += 1; e = e.next; }; }; - // AX still holds the last stored value; fill remaining - // slots up to the declared length with it. + // AX (and BX for str) still holds the last stored value; + // fill remaining slots up to the declared length with it. if (repeat) { let total: i32 = idx; if (n.lhs != nil) { @@ -15327,11 +15361,20 @@ fn cglet(c: *cgen, n: *node) void = { }; }; for (idx < total) { - emitline("\t"); - emitline(mop); - emitline("\tAX, "); - emitoff((off + idx * esz): i64); - emitline("(BP)\n"); + if (isstrel) { + emitline("\tMOVQ\tAX, "); + emitoff((off + idx * esz): i64); + emitline("(BP)\n"); + emitline("\tMOVQ\tBX, "); + emitoff((off + idx * esz + 8): i64); + emitline("(BP)\n"); + } else { + emitline("\t"); + emitline(mop); + emitline("\tAX, "); + emitoff((off + idx * esz): i64); + emitline("(BP)\n"); + }; idx += 1; }; }; diff --git a/test/wcc/711_arrlit_str_full.c b/test/wcc/711_arrlit_str_full.c new file mode 100644 index 00000000..a90b63a2 --- /dev/null +++ b/test/wcc/711_arrlit_str_full.c @@ -0,0 +1,229 @@ +/* + * 711_arrlit_str_full — `let xs: [N]str = [...]` writes BOTH halves + * of every element (task #21). + * + * Pre-fix: cgen's N_LET / N_ARRLIT branch dispatched its per-element + * store off a single esz / mop pair derived from `lu->sub->size` + * (cstage) or `primsize(elem.str)` (wwstage). Both fell through to a + * single MOVQ AX, off+i*esz(BP) for a str element — leaving the .len + * half (off+i*esz+8) as whatever stack residue the prologue's SUBQ + * happened to land on. Wwstage was worse: primsize("str") returns 0, + * so esz collapsed to 8, and the per-element stride was wrong too — + * element i+1's MOVQ overwrote what should have been element i's + * .len half. + * + * Fix: detect str-element arrays at letslotsize / slotsize / cgen + * dispatch and emit both halves per element — MOVQ AX, off+i*16(BP) + * for .ptr, MOVQ BX, off+i*16+8(BP) for .len. Symmetric across cstage + * cgen.c (N_LET / N_ARRLIT, type_isstr dispatch) and wwstage + * cgenstmt.ww + cgenutil.ww (slotsize / letslotsize TNAME-"str" + * special-case, cgenstmt isstrel branch). bool / rune / iN element + * arrays were never broken — their primitive store widths covered + * the full element — but they're pinned here as regressions so a + * future esz refactor can't quietly redo the slot-rounding gap. + * + * What this test pins (runtime only): + * - [3]str literal: every element's .len reads back correctly + * (pre-fix: zero or stack residue). + * - [3]str literal: every element's .ptr → first byte reads back + * correctly (the ptr half was always right; sanity). + * - [3]bool literal: regression-pin true / false / true. + * - [3]rune literal: regression-pin primitive size 4 element. + * - [3]i32 / [3]i64 literal: regression-pin primitive widths. + * - [5]str = ["x", ...] repeat marker: all 5 slots get full 16B + * stores (pre-fix: trailing slots' .len = zero). + * + * Slice / struct / tuple / tagged element arrays are NOT covered: + * the read side (cgindex of an [N]slice) has its own truncating-to- + * ptr bug, so an end-to-end repro surfaces sub-issues. Tracked as a + * follow-up. + * + * Asm byte-identity across stages is NOT diffed here: 995_self_rebuild + * covers the broader cross-stage drift surface, and the str / repeat + * rows produce identical MOVQ pairs across stages by construction. + */ +#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[] = { + /* 1. [3]str — every .len reads back correctly. Exit code is + * 100*a[0].len + 10*a[1].len + a[2].len = 100*2 + 10*6 + 1 = 261 + * mod 256 = 5. Pre-fix: zero (all .len halves uninit). */ + { "str_lens_3el", + "fn main() i32 = {\n" + " let a: [3]str = [\"hi\", \"byebye\", \"x\"];\n" + " let p: i32 = a[0].len: i32;\n" + " let q: i32 = a[1].len: i32;\n" + " let r: i32 = a[2].len: i32;\n" + " return p * 100i32 + q * 10i32 + r;\n" + "};\n", + 5 }, + /* 2. [3]str — every .ptr is reachable. Reads element 0's first + * byte through `.ptr` to confirm the ptr half wasn't broken by + * the .len-half fix. 'h' (104) - 100 = 4. */ + { "str_ptrs_3el", + "fn main() i32 = {\n" + " let a: [3]str = [\"hi\", \"by\", \"x\"];\n" + " let p: *u8 = a[0].ptr;\n" + " let c: u8 = *p;\n" + " return (c: i32) - 100i32;\n" + "};\n", + 4 }, + /* 3. [5]str = ["x", ...] — repeat marker fills all 5 slots with + * a full 16B store, not just .ptr. Sum each .len: 5*1 = 5. + * Pre-fix: a[0].len = 1 from explicit init, a[1..4].len = stack + * residue (often 0, but unspecified). */ + { "str_repeat_5el", + "fn main() i32 = {\n" + " let a: [5]str = [\"x\"...];\n" + " let s: i32 = 0i32;\n" + " let i: i32 = 0;\n" + " for (i < 5) {\n" + " s += a[i].len: i32;\n" + " i += 1;\n" + " };\n" + " return s;\n" + "};\n", + 5 }, + /* 4. [3]bool — regression-pin. Pre-fix and post-fix: true/false/ + * true with element width 1. Exit = 4*b[0] + 2*b[1] + b[2] = + * 4 + 0 + 1 = 5. */ + { "bool_3el_regression", + "fn main() i32 = {\n" + " let b: [3]bool = [true, false, true];\n" + " let s: i32 = 0i32;\n" + " if (b[0]) { s += 4i32; };\n" + " if (b[1]) { s += 2i32; };\n" + " if (b[2]) { s += 1i32; };\n" + " return s;\n" + "};\n", + 5 }, + /* 5. [3]rune — regression-pin primitive size 4. 'b' - 'a' = 1. */ + { "rune_3el_regression", + "fn main() i32 = {\n" + " let r: [3]rune = ['a', 'b', 'c'];\n" + " let v: rune = r[1];\n" + " return (v: i32) - 97i32;\n" + "};\n", + 1 }, + /* 6. [3]i32 — regression-pin primitive size 4. */ + { "i32_3el_regression", + "fn main() i32 = {\n" + " let a: [3]i32 = [10i32, 20i32, 30i32];\n" + " return a[0] + a[1] - a[2];\n" + "};\n", + 0 }, + /* 7. [3]i64 — regression-pin primitive size 8. Sum 1+2+3 = 6. */ + { "i64_3el_regression", + "fn main() i32 = {\n" + " let a: [3]i64 = [1i64, 2i64, 3i64];\n" + " return (a[0] + a[1] + a[2]): i32;\n" + "};\n", + 6 }, +}; + +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/wcrarrs_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/wcrarrs_%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", + 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; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[512]; + if (bin[0] != '/') { + char cwd[256]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char cdrv[640]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + char wdrv[640]; + 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, "arrlit_str_full: 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, + "arrlit_str_full[%s][%s]: exit=%d want=%d\n", + drivers[d].name, rows[i].label, + got, rows[i].want); + fail++; + } + } + } + + if (fail) { + fprintf(stderr, + "arrlit_str_full: %d/%d fixtures failed\n", fail, total); + return 1; + } + printf("arrlit_str_full: %d/%d ok\n", total, total); + return 0; +}