/* * 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 }, /* 8. Bare-let [N]str index as a call arg (task #34). Pre-#34 * wwstage emitted PUSHQ AX only for argv[i] — the str value's * BX=len half was dropped, and streq's `a.len` parameter read * stack residue. Symptom under getopttest: rc=11 pre-#21, then * 139 once #21 doubled the slot stride. Now byte-identical to * cstage at the call site. streq("files.txt",argv[2]) → 0 (eq) * → return 0. Pre-fix wwstage returned 11. */ { "barelet_index_call_arg", "fn streq(a: str, b: str) bool = {\n" " if (a.len != b.len) { return false; };\n" " let i: i32 = 0;\n" " for (i < a.len) {\n" " if (a[i] != b[i]) { return false; };\n" " i += 1;\n" " };\n" " return true;\n" "};\n" "fn main() i32 = {\n" " let argv: [3]str;\n" " argv[0] = \"ls\";\n" " argv[1] = \"-Fahs\";\n" " argv[2] = \"files.txt\";\n" " if (!streq(argv[2], \"files.txt\")) { return 11; };\n" " if (!streq(argv[1], \"-Fahs\")) { return 12; };\n" " if (!streq(argv[0], \"ls\")) { return 13; };\n" " return 0;\n" "};\n", 0 }, /* 9. Nested call: `f(g(argv[i]))` exercises the full call-arg * packer with an N_INDEX-of-str inside an N_CALL inside another * N_CALL. Pins that the inner N_INDEX recognition rides through * the same pushargsrev path that the simple row uses. dup1 here * is identity-on-str so the outer streq receives g(argv[2]), * which must be 9 bytes ("files.txt"). */ { "nested_call_index_arg", "fn dup1(s: str) str = { return s; };\n" "fn streq(a: str, b: str) bool = {\n" " if (a.len != b.len) { return false; };\n" " let i: i32 = 0;\n" " for (i < a.len) {\n" " if (a[i] != b[i]) { return false; };\n" " i += 1;\n" " };\n" " return true;\n" "};\n" "fn main() i32 = {\n" " let argv: [3]str;\n" " argv[0] = \"a\";\n" " argv[1] = \"bb\";\n" " argv[2] = \"files.txt\";\n" " if (!streq(dup1(argv[2]), \"files.txt\")) { return 11; };\n" " return 0;\n" "};\n", 0 }, /* 10. Index .len as a call arg via a one-arg consumer. Confirms * cgindex's (AX, BX) load still drives a single-i32 push when * the field selector picks the .len half off the str element. * Different code path from rows 8-9 (no str-arg push) but * uses the same N_INDEX base. 5 chars in "hello". */ { "barelet_index_len_arg", "fn ident(n: i32) i32 = { return n; };\n" "fn main() i32 = {\n" " let xs: [2]str;\n" " xs[0] = \"hi\";\n" " xs[1] = \"hello\";\n" " return ident(xs[1].len: i32);\n" "};\n", 5 }, }; 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; }