/* * 800_append_wide_elem — cstage and wwstage agree, byte-for-byte and at * runtime, that `append(s, v)` stores the FULL element width for every * element kind (task #34; the regex fold-2a blocker). * * The bug: both stages lowered the append element store as ONE sized * mov from AX (`MOV* AX, (BX)`) — correct only for scalars <= 8B. A * str/slice element (24B {ptr,len,cap}, cgexpr -> AX/BX/CX) kept only * .ptr (byte-id-BLIND: both stages identical and identically wrong); a * tagged element got the raw payload written into the tag slot (no * boxing — the #12 pathology); a struct element kept only its first * qword. wwstage ADDITIONALLY fed rt_ensure membsz from bare * elemsizeof, whose 8-sentinel under-allocated AND mis-strided named * tagged/struct elements (the #8 family; cs!=ww on the `MOVQ $N, SI` * line and the stride IMUL). * * The fix (BOTH stages, converged byte-identical), keyed on the * DECLARED slice local's element type (cstage su->sub; wwstage stamped * tinfo via elemsizeofc — NEVER the value node, the #25/#31 esz=0 * trap): * - scalar 1/2/4/8: UNTOUCHED (the u8 row's asm is unchanged vs * pre-fix master — regression-pinned by byte-id + runtime). * - str/slice: push AX/BX/CX across rt_ensure, dst in DX (BX holds * the element .len after the pops — the #24 register discipline), * 3-word store at (DX)/8(DX)/16(DX). * - tagged: grow FIRST, dst -> BX, then the #12 widen choke-point * (cg_widen_tagged_store / cgwidentaggedstore) boxes {tag,payload} * through @tagbase/@tagscr (via_outer mode). * - struct: grow first; literal -> dst spilled to @appendscr + * structlit fill (DST_PTR_LOCAL); local ident -> word-copy; ANY * other source shape is a rule-7 loud-stop (build fails), never a * silent scalar fall-through. * - spread `append(s, items...)`: the source element is already a * fully-formed T (tag included), so the wide arm grows first and * whole-width word-copies &items[i] -> dst, recomputing both * addresses from the slice headers after the (possibly * reallocating) rt_ensure. * * row | shape | want * ---------------------+--------------------------------------+------ * u8_baseline | []u8, two appends, sum | 8 * enum_alias_esz | []ek (enum i32 alias) — pins the | 5 * | elemsizeofc swap: wwstage fed SI=$8 | * | where cstage fed $4 (cs!=ww growth). | * tagged_box | [](i64|bool), append 100i64, match | 100 * | readback. Pre-fix: raw 100 landed in | * | the tag slot, no arm matched. | * tagged_two_append | two appends — element 0 survives the | 42 * | realloc full-width. | * str_len_bytes | []str, append, len + byte readback | 11 * str_two_append | two str appends, len0*10 + len1 | 32 * slice_elem | [][]u8, append, len + byte readback | 12 * struct_first8 | []pt (16B), x+y (first qword — ken's | 8 * | characterization row) | * struct_tail_word | []pt, z at +8 (the word the 1-word | 8 * | store dropped) | * struct_lit_two | two struct-LITERAL appends (pins the | 7 * | @appendscr per-fn dedup: frame + | * | byte-id diverge if cstage allocs two | * | scratch slots where wwstage dedups) | * spread_str | append(ys, xs...) of []str + single | 7 * spread_tagged | append(ys, xs...) of [](i64|bool) | 42 * tagged_ident_src | append of ALREADY-TAGGED locals | 42 * | (i64 + bool members) — the widener's | * | already-tagged source path. | * struct_eight_appends | 8 struct-lit appends of the getopt | 15 * | option shape ({rune,str} = 32B), | * | full-width readback of element 7 — | * | the >6-element OOB regression pin | * | (getopt's deleted helper fed a stale | * | hardcoded membsz=24: 8-slot grow was | * | 192B while writes strode 32; the | * | builtin derives 32 from the type | * | table, MOVQ $32, SI). | * struct_call_loudstop | append(xs, f()) struct-from-call is | BUILD_FAIL * | the deferred source shape — must | * | fail LOUD on both stages (rule-7), | * | never silently store one word. | * spread_call_loudstop | append(ys, f()...) non-ident spread | BUILD_FAIL * | source — pre-review this fell PAST | * | the spread arm (cstage garbage store | * | vs wwstage silent skip, divergent); | * | now a rule-7 loud-stop (task #37). | * * Every non-BUILD_FAIL row also asserts cstage/wwstage asm byte-id, * which subsumes the frame-size canary (TEXT main,$N) — the @tagbase/ * @tagscr/@appendscr scratch allocations must land in the same order * and size on both stages (#25/#31 lesson). */ #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; } /* want == BUILD_FAIL: the row must FAIL to build on both stages (the * rule-7 loud-stop for a deferred source shape). run_driver returns -1 * exactly when the build fails, so `got == -1` is the pass. */ #define BUILD_FAIL (-2147483647 - 1) struct row { const char *label; const char *src; int want; }; static const struct row rows[] = { { "u8_baseline", "package main;\n" "export fn main() i32 = {\n" "\tlet xs: []u8 = [];\n" "\tappend(xs, 5u8);\n" "\tappend(xs, 3u8);\n" "\treturn (xs[0] + xs[1]): i32;\n" "};\n", 8 }, { "enum_alias_esz", "package main;\n" "type ek = enum { A = 5, B = 9 };\n" "export fn main() i32 = {\n" "\tlet xs: []ek = [];\n" "\tappend(xs, ek.A);\n" "\treturn xs[0]: i32;\n" "};\n", 5 }, { "tagged_box", "package main;\n" "type cell = (i64 | bool);\n" "export fn main() i32 = {\n" "\tlet xs: []cell = [];\n" "\tappend(xs, 100i64);\n" "\tlet r: i32 = 1;\n" "\tmatch (xs[0]) {\n" "\tcase let v: i64 => r = v: i32;\n" "\tcase bool => r = 2;\n" "\t};\n" "\treturn r;\n" "};\n", 100 }, { "tagged_two_append", "package main;\n" "type cell = (i64 | bool);\n" "export fn main() i32 = {\n" "\tlet xs: []cell = [];\n" "\tappend(xs, 40i64);\n" "\tappend(xs, 2i64);\n" "\tlet r: i32 = 0;\n" "\tmatch (xs[0]) {\n" "\tcase let v: i64 => r += v: i32;\n" "\tcase bool => r = 99;\n" "\t};\n" "\tmatch (xs[1]) {\n" "\tcase let v: i64 => r += v: i32;\n" "\tcase bool => r = 98;\n" "\t};\n" "\treturn r;\n" "};\n", 42 }, { "str_len_bytes", "package main;\n" "export fn main() i32 = {\n" "\tlet xs: []str = [];\n" "\tlet s: str = \"abcdef\";\n" "\tappend(xs, s);\n" "\treturn (xs[0].len: i32) + (xs[0][5]: i32) - (xs[0][0]: i32);\n" "};\n", 11 }, { "str_two_append", "package main;\n" "export fn main() i32 = {\n" "\tlet xs: []str = [];\n" "\tlet a: str = \"abc\";\n" "\tlet b: str = \"fg\";\n" "\tappend(xs, a);\n" "\tappend(xs, b);\n" "\treturn (xs[0].len * 10 + xs[1].len): i32;\n" "};\n", 32 }, { "slice_elem", "package main;\n" "export fn main() i32 = {\n" "\tlet hb: [4]u8; hb[0] = 9u8; hb[1] = 4u8;\n" "\tlet a: []u8; a.ptr = &hb[0]; a.len = 3; a.cap = 4;\n" "\tlet ys: [][]u8 = [];\n" "\tappend(ys, a);\n" "\treturn (ys[0].len: i32) + (ys[0][0]: i32);\n" "};\n", 12 }, { "struct_first8", "package main;\n" "type pt = struct { x: i32, y: i32, z: i64 };\n" "export fn main() i32 = {\n" "\tlet xs: []pt = [];\n" "\tlet p: pt = pt { x = 3, y = 5, z = 9 };\n" "\tappend(xs, p);\n" "\treturn (xs[0].x + xs[0].y): i32;\n" "};\n", 8 }, { "struct_tail_word", "package main;\n" "type pt = struct { x: i32, y: i32, z: i64 };\n" "export fn main() i32 = {\n" "\tlet xs: []pt = [];\n" "\tlet p: pt = pt { x = 3, y = 5, z = 9 };\n" "\tappend(xs, p);\n" "\treturn (xs[0].z - 1): i32;\n" "};\n", 8 }, { "struct_lit_two", "package main;\n" "type pt = struct { x: i32, y: i32, z: i64 };\n" "export fn main() i32 = {\n" "\tlet xs: []pt = [];\n" "\tappend(xs, pt { x = 1, y = 2, z = 5 });\n" "\tappend(xs, pt { x = 3, y = 4, z = 7 });\n" "\treturn (xs[0].z + xs[1].z): i32 - xs[0].x - xs[1].y;\n" "};\n", 7 }, { "spread_str", "package main;\n" "export fn main() i32 = {\n" "\tlet xs: []str = [];\n" "\tlet a: str = \"abc\";\n" "\tlet b: str = \"fg\";\n" "\tappend(xs, a);\n" "\tlet ys: []str = [];\n" "\tappend(ys, xs...);\n" "\tappend(ys, b);\n" "\treturn (ys[0].len + ys[1].len + ys.len): i32;\n" "};\n", 7 }, { "spread_tagged", "package main;\n" "type cell = (i64 | bool);\n" "export fn main() i32 = {\n" "\tlet xs: []cell = [];\n" "\tappend(xs, 40i64);\n" "\tappend(xs, 2i64);\n" "\tlet ys: []cell = [];\n" "\tappend(ys, xs...);\n" "\tlet r: i32 = 0;\n" "\tmatch (ys[0]) {\n" "\tcase let v: i64 => r += v: i32;\n" "\tcase bool => r = 99;\n" "\t};\n" "\tmatch (ys[1]) {\n" "\tcase let v: i64 => r += v: i32;\n" "\tcase bool => r = 98;\n" "\t};\n" "\treturn r;\n" "};\n", 42 }, { "tagged_ident_src", "package main;\n" "type cell = (i64 | bool);\n" "export fn main() i32 = {\n" "\tlet xs: []cell = [];\n" "\tlet c: cell = 40i64;\n" "\tlet b: cell = true;\n" "\tappend(xs, c);\n" "\tappend(xs, b);\n" "\tlet r: i32 = 0;\n" "\tmatch (xs[0]) {\n" "\tcase let v: i64 => r += v: i32;\n" "\tcase bool => r = 99;\n" "\t};\n" "\tmatch (xs[1]) {\n" "\tcase i64 => r = 98;\n" "\tcase let w: bool => { if (w) { r += 2; }; };\n" "\t};\n" "\treturn r;\n" "};\n", 42 }, { "struct_eight_appends", "package main;\n" "type option = struct { flag: rune, value: str };\n" "export fn main() i32 = {\n" "\tlet opts: []option = [];\n" "\tappend(opts, option { flag = 'a', value = \"v0\" });\n" "\tappend(opts, option { flag = 'b', value = \"v1\" });\n" "\tappend(opts, option { flag = 'c', value = \"v2\" });\n" "\tappend(opts, option { flag = 'd', value = \"v3\" });\n" "\tappend(opts, option { flag = 'e', value = \"v4\" });\n" "\tappend(opts, option { flag = 'f', value = \"v5\" });\n" "\tappend(opts, option { flag = 'g', value = \"v6\" });\n" "\tappend(opts, option { flag = 'h', value = \"seventh\" });\n" "\treturn (opts[7].value.len + opts.len): i32;\n" "};\n", 15 }, { "struct_call_loudstop", "package main;\n" "type pt = struct { x: i32, y: i32, z: i64 };\n" "fn mk() pt = {\n" "\treturn pt { x = 1, y = 2, z = 3 };\n" "};\n" "export fn main() i32 = {\n" "\tlet xs: []pt = [];\n" "\tappend(xs, mk());\n" "\treturn 0;\n" "};\n", BUILD_FAIL }, { "spread_call_loudstop", "package main;\n" "fn mks() []str = {\n" "\tlet xs: []str = [];\n" "\tlet a: str = \"abc\";\n" "\tappend(xs, a);\n" "\treturn xs;\n" "};\n" "export fn main() i32 = {\n" "\tlet ys: []str = [];\n" "\tappend(ys, mks()...);\n" "\treturn ys[0].len: i32;\n" "};\n", BUILD_FAIL }, }; 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/apwe_%d_%d.ww", getpid(), i); snprintf(tmpdir, sizeof tmpdir, "/tmp/apwe_%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) { if (r->want != BUILD_FAIL) 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. Subsumes the frame-size canary: a scratch-order or * esz divergence shows up on the TEXT main,$N line or the MOVQ $N, SI * line. Pre-fix the tagged/struct rows differed (wwstage SI=$8 vs $16); * the str rows were byte-id-blind (both wrong identically), which the * runtime rows above catch. */ 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/apwe_asm_%d_%d.ww", getpid(), i); snprintf(cs, sizeof cs, "/tmp/apwe_asm_%d_%d_c.s", getpid(), i); snprintf(ws, sizeof ws, "/tmp/apwe_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, "append_wide_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++; int bad = rows[i].want == BUILD_FAIL ? (got != -1) : (got != rows[i].want); if (bad) { fprintf(stderr, "append_wide_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++) { if (rows[i].want == BUILD_FAIL) continue; total++; if (asm_byte_identical(bin, &rows[i], i) != 0) fail++; } } if (fail) { fprintf(stderr, "append_wide_elem: %d/%d fixtures failed\n", fail, total); return 1; } printf("append_wide_elem: %d fixtures passed\n", total); return 0; }