/* * 806_append_place — cstage and wwstage agree, byte-for-byte and at * runtime, that `append(*p, v)` / `append(*p, items...)` through a * pointer-to-slice place lands the element (FA1, task #15 of the regex * fold-2b blockers; the add_thread hot shape `threads: *[]thread`). * * Pre-fix this was ONE mirrored choke-point with TWO failure modes: * cstage 0-defaulted the header base for any non-ident target * (`sn_off = (sn->kind==N_IDENT) ? localfind : 0`), so 0(BP)/8(BP) * became the "slice header" and rt_ensure corrupted the CALLER frame * (SIGSEGV); wwstage cgappend silently emitted NOTHING (gate-blind * cs≠ww). The fix re-keys the append lowering from BP-displacement * assumptions onto a resolver-provided header PLACE (cgplaceaddr's * third consumer after C1 assign-stores and C1.25 aggregate-field * stores): the derived header address is spilled to a per-fn * @apphdrscr slot so it survives rt_ensure (realloc moves .ptr, never * the header), and every header access reloads from the slot. * Ident-local targets keep the legacy BP-disp emission byte-identical. * Any target place the resolver can't address dies LOUD (rule 7) — * the old silent corruption can't come back through an unwired shape. * * row | shape | want * ---------------------+-----------------------------------------+------ * scalar_param | append(*p, v) i64 via *[]i64 param | 60 * scalar_u8_multi | append(*q, a, b) u8 via local ptr | 61 * spread_param | append(*p, src...) i64 spread | 62 * spread_narrow_signed | append(*p, src...) i32 (MOVSXD load | 63 * | keyed off stamped tinfo, no tnode) | * spread_wide | append(*p, src...) str (24B word-copy) | 64 * str_elem | append(*p, "hi") 3-word header | 65 * slice_elem | append(*p, one) [][]u8 3-word header | 66 * tagged_elem | append(*p, 42) (i64|void) widen-box | 67 * struct_lit | pA6 verbatim: append(*p, box{...}) | 68 * struct_ident | append(*p, b) local struct word-copy | 69 * realloc_loop | 100 appends via param, branched callee, | 70 * | cap-crossing reallocs, full multi-field | * | readback + caller-frame sentinels | * deref_spine | append((*q)[i].xs, v) resolver spine | 71 * neutral_direct | ident-local appends (the legacy arm — | 72 * | runtime-pins the asm-neutrality claim) | * reentrant_value | append(*p, match{.. append(*q,..) ..}) | 33 * | — nested indirect append inside the | * | outer's value expr; pins the per-SITE | * | @apphdrscr slot (a shared slot clobbers)| * identroot_dot | h.xs via *holder param — C1.5's reject, | 73 * | graduated by C2's resolver ident root | * reject_spread_src | non-ident spread SOURCE through a deref | BUILD_FAIL * | target (the #35 designed boundary) | * * BUILD_FAIL rows also assert the diagnostic TEXT (stderr substring, * both stages) — a build that fails for any other reason (parse error, * crash) is a vacuous reject and fails the row. * * Every non-BUILD_FAIL row also asserts cstage/wwstage asm byte-id. * * Readbacks deliberately avoid the still-open F2/F5 read shapes * (`len(xs[i].field)`, `let s = xs[i].field`) — those are tracked * separately; this test pins the append WRITE path. */ #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 AND * emit expect_err on stderr (rule 7 — never a silent acceptance; * without the message check a row would pass vacuously on any * unrelated build failure). */ #define BUILD_FAIL (-2147483647 - 1) struct row { const char *label; const char *src; int want; const char *expect_err; /* BUILD_FAIL rows: required stderr substring */ }; static const struct row rows[] = { /* The FA1 minimum: two appends through a *[]i64 param, then a * third through a local pointer binding. */ { "scalar_param", "package main;\n" "fn add(p: *[]i64, v: i64) void = { append(*p, v); };\n" "export fn main() i32 = {\n" "\tlet xs: []i64 = [];\n" "\tadd(&xs, 5);\n" "\tadd(&xs, 9);\n" "\tif (len(xs) != 2) { return 1; };\n" "\tif (xs[0] != 5 || xs[1] != 9) { return 2; };\n" "\tlet q: *[]i64 = &xs;\n" "\tappend(*q, 11);\n" "\tif (len(xs) != 3 || xs[2] != 11) { return 3; };\n" "\treturn 60;\n" "};\n", 60, NULL }, /* esz=1 store (MOVB) + multi-value list through one resolve. */ { "scalar_u8_multi", "package main;\n" "export fn main() i32 = {\n" "\tlet bs: []u8 = [];\n" "\tlet q: *[]u8 = &bs;\n" "\tappend(*q, 1u8, 2u8);\n" "\tif (len(bs) != 2) { return 1; };\n" "\tif (bs[0] != 1u8 || bs[1] != 2u8) { return 2; };\n" "\treturn 61;\n" "};\n", 61, NULL }, { "spread_param", "package main;\n" "fn addall(p: *[]i64, src: []i64) void = { append(*p, src...); };\n" "export fn main() i32 = {\n" "\tlet xs: []i64 = [];\n" "\tappend(xs, 1, 2);\n" "\tlet ys: []i64 = [];\n" "\taddall(&ys, xs);\n" "\taddall(&ys, xs);\n" "\tif (len(ys) != 4) { return 1; };\n" "\tif (ys[0] != 1 || ys[1] != 2 || ys[3] != 2) { return 2; };\n" "\treturn 62;\n" "};\n", 62, NULL }, /* Indirect mode has no declared tnode — the spread element LOAD * op (sign extension) must come off the stamped tinfo; negative * i32 elements pin MOVSXD. */ { "spread_narrow_signed", "package main;\n" "fn addall(p: *[]i32, src: []i32) void = { append(*p, src...); };\n" "export fn main() i32 = {\n" "\tlet xs: []i32 = [];\n" "\tappend(xs, -7i32, 9i32);\n" "\tlet ys: []i32 = [];\n" "\taddall(&ys, xs);\n" "\tif (len(ys) != 2) { return 1; };\n" "\tif (ys[0] != -7i32) { return 2; };\n" "\tif (ys[1] != 9i32) { return 3; };\n" "\treturn 63;\n" "};\n", 63, NULL }, /* Wide (24B) spread element word-copy through the deref target. */ { "spread_wide", "package main;\n" "fn addall(p: *[]str, src: []str) void = { append(*p, src...); };\n" "export fn main() i32 = {\n" "\tlet ss: []str = [];\n" "\tappend(ss, \"ab\");\n" "\tappend(ss, \"cde\");\n" "\tlet tt: []str = [];\n" "\taddall(&tt, ss);\n" "\tif (len(tt) != 2) { return 1; };\n" "\tlet a: str = tt[0];\n" "\tlet b: str = tt[1];\n" "\tif (a.len != 2 || b.len != 3) { return 2; };\n" "\tif (b[0] != 'c') { return 3; };\n" "\treturn 64;\n" "};\n", 64, NULL }, /* str element: cgexpr leaves {ptr,len,cap} in AX/BX/CX; all three * must survive rt_ensure AND the indirect header reload. */ { "str_elem", "package main;\n" "fn adds(p: *[]str, v: str) void = { append(*p, v); };\n" "export fn main() i32 = {\n" "\tlet ss: []str = [];\n" "\tadds(&ss, \"hi\");\n" "\tadds(&ss, \"world\");\n" "\tif (len(ss) != 2) { return 1; };\n" "\tlet a: str = ss[0];\n" "\tlet b: str = ss[1];\n" "\tif (a.len != 2 || b.len != 5) { return 2; };\n" "\tif (a[0] != 'h' || b[0] != 'w') { return 3; };\n" "\treturn 65;\n" "};\n", 65, NULL }, { "slice_elem", "package main;\n" "fn addv(p: *[][]u8, v: []u8) void = { append(*p, v); };\n" "export fn main() i32 = {\n" "\tlet vv: [][]u8 = [];\n" "\tlet one: []u8 = [];\n" "\tappend(one, 5u8, 6u8);\n" "\taddv(&vv, one);\n" "\tif (len(vv) != 1) { return 1; };\n" "\tlet got: []u8 = vv[0];\n" "\tif (len(got) != 2 || got[1] != 6u8) { return 2; };\n" "\treturn 66;\n" "};\n", 66, NULL }, /* Tagged element: the #12 widen choke-point boxes through the * resolver-derived slot pointer (grow-first, no register form). */ { "tagged_elem", "package main;\n" "type tu = (i64 | void);\n" "fn addt(p: *[]tu, v: i64) void = { append(*p, v); };\n" "export fn main() i32 = {\n" "\tlet ts: []tu = [];\n" "\taddt(&ts, 42);\n" "\tif (len(ts) != 1) { return 1; };\n" "\tlet t0: tu = ts[0];\n" "\tif (!(t0 is i64)) { return 2; };\n" "\treturn 67;\n" "};\n", 67, NULL }, /* The pA6 repro, verbatim shapes: struct-literal element fill * through @appendscr while the header address sits in @apphdrscr. */ { "struct_lit", "package main;\n" "type box = struct { pc: size, matched: bool };\n" "fn add(p: *[]box, v: size) void = {\n" "\tappend(*p, box { pc = v, matched = false });\n" "};\n" "export fn main() i32 = {\n" "\tlet bs: []box = [];\n" "\tadd(&bs, 5);\n" "\tadd(&bs, 9);\n" "\tif (len(bs) != 2) { return 1; };\n" "\tif (bs[0].pc != 5) { return 2; };\n" "\tif (bs[1].pc != 9) { return 3; };\n" "\treturn 68;\n" "};\n", 68, NULL }, { "struct_ident", "package main;\n" "type box = struct { pc: size, matched: bool };\n" "fn add(p: *[]box, v: size) void = {\n" "\tlet b: box = box { pc = v, matched = true };\n" "\tappend(*p, b);\n" "};\n" "export fn main() i32 = {\n" "\tlet bs: []box = [];\n" "\tadd(&bs, 7);\n" "\tif (len(bs) != 1) { return 1; };\n" "\tif (bs[0].pc != 7) { return 2; };\n" "\tif (!bs[0].matched) { return 3; };\n" "\treturn 69;\n" "};\n", 69, NULL }, /* The corruption symptom row: 100 appends through the param in a * loop with a branched callee — crosses several cap-doubling * reallocs. Sentinels on BOTH sides of the slice local pin the * caller frame (pre-fix cstage incremented 8(BP) and passed (BP) * to rt_ensure → caller-frame corruption); the full readback pins * every element across the realloc moves. */ { "realloc_loop", "package main;\n" "type box = struct { pc: size, matched: bool };\n" "fn add(p: *[]box, v: size) void = {\n" "\tif (v % 2 == 0) {\n" "\t\tappend(*p, box { pc = v, matched = true });\n" "\t} else {\n" "\t\tappend(*p, box { pc = v, matched = false });\n" "\t};\n" "};\n" "export fn main() i32 = {\n" "\tlet lo: i64 = 0x5151;\n" "\tlet bs: []box = [];\n" "\tlet hi: i64 = 0x7272;\n" "\tlet i: size = 0;\n" "\tfor (i < 100) {\n" "\t\tadd(&bs, i);\n" "\t\ti += 1;\n" "\t};\n" "\tif (len(bs) != 100) { return 1; };\n" "\tlet j: size = 0;\n" "\tfor (j < 100) {\n" "\t\tif (bs[j].pc != j) { return 2; };\n" "\t\tif (bs[j].matched != (j % 2 == 0)) { return 3; };\n" "\t\tj += 1;\n" "\t};\n" "\tif (lo != 0x5151 || hi != 0x7272) { return 4; };\n" "\treturn 70;\n" "};\n", 70, NULL }, /* Deeper resolver spine: the header is a slice FIELD of an element * behind a deref ((*q)[i].xs — N_DOT over N_INDEX over N_UN). * Readback via a *holder ptr-dot, a sound read path. */ { "deref_spine", "package main;\n" "type holder = struct { tag: i64, xs: []i64 };\n" "fn addspine(q: *[]holder, i: i64, v: i64) void = {\n" "\tappend((*q)[i].xs, v);\n" "};\n" "export fn main() i32 = {\n" "\tlet hs: []holder = [];\n" "\tlet h0: holder = holder { tag = 1, xs = [] };\n" "\tappend(hs, h0);\n" "\taddspine(&hs, 0, 41);\n" "\taddspine(&hs, 0, 43);\n" "\tlet hp: *holder = &hs[0];\n" "\tlet g: []i64 = hp.xs;\n" "\tif (len(g) != 2) { return 1; };\n" "\tif (g[0] != 41 || g[1] != 43) { return 2; };\n" "\tif (hp.tag != 1) { return 3; };\n" "\treturn 71;\n" "};\n", 71, NULL }, /* Ident-local targets stay with the legacy BP-disp emission (the * resolver must never fire for them) — this row runtime-pins the * shapes the before/after asm sweep diffed statically. */ { "neutral_direct", "package main;\n" "type box = struct { pc: size, matched: bool };\n" "export fn main() i32 = {\n" "\tlet xs: []i64 = [];\n" "\tappend(xs, 7);\n" "\tappend(xs, 8, 9);\n" "\tlet ys: []i64 = [];\n" "\tappend(ys, xs...);\n" "\tlet ss: []str = [];\n" "\tappend(ss, \"hi\");\n" "\tlet ps: []box = [];\n" "\tappend(ps, box { pc = 1, matched = false });\n" "\tif (len(xs) != 3 || xs[2] != 9) { return 1; };\n" "\tif (len(ys) != 3 || ys[0] != 7) { return 2; };\n" "\tif (len(ss) != 1 || len(ps) != 1) { return 3; };\n" "\treturn 72;\n" "};\n", 72, NULL }, /* Reentrancy: a nested append-through-pointer inside the outer * append's VALUE expression (match-yield arm) spills its own * header address. Pins the per-SITE @apphdrscr slot — a shared * per-fn slot hands the outer's post-rt_ensure reloads the inner * target's header and the outer element lands in the wrong * slice (silent, both counts wrong). */ { "reentrant_value", "package main;\n" "type tu = (i64 | void);\n" "fn nest(p: *[]i64, q: *[]i64, t: tu) void = {\n" "\tappend(*p, match (t) {\n" "\tcase let x: i64 => {\n" "\t\tappend(*q, 500);\n" "\t\tyield x;\n" "\t};\n" "\tcase void => {\n" "\t\tyield 0;\n" "\t};\n" "\t});\n" "};\n" "export fn main() i32 = {\n" "\tlet a: []i64 = [];\n" "\tlet b: []i64 = [];\n" "\tlet t: tu = 7;\n" "\tnest(&a, &b, t);\n" "\tif (len(a) != 1) { return 1; };\n" "\tif (len(b) != 1) { return 2; };\n" "\tif (a[0] != 7) { return 3; };\n" "\tif (b[0] != 500) { return 4; };\n" "\treturn 33;\n" "};\n", 33, NULL }, /* Ident-rooted dot target (h.xs through *holder): C1.5's loud * boundary (the resolver had no ident root then; pre-FA1 this * shape silently corrupted the frame in cstage), graduated by * C2's cgplaceaddr N_IDENT root — the *holder base derefs once * inside the N_DOT hop and the header place lands on &h.xs. * Readbacks use the .len pseudo, not len(): len() of a non-tuple * N_DOT is the pre-existing F2 enumeration gap (task #10). */ { "identroot_dot", "package main;\n" "type holder = struct { tag: i64, xs: []i64 };\n" "fn addfield(h: *holder, v: i64) void = { append(h.xs, v); };\n" "export fn main() i32 = {\n" "\tlet hl: holder = holder { tag = 2, xs = [] };\n" "\taddfield(&hl, 9);\n" "\taddfield(&hl, 11);\n" "\tif (hl.xs[0] != 9) { return 1; };\n" "\tif (hl.xs[1] != 11) { return 2; };\n" "\tif (hl.xs.len != 2) { return 3; };\n" "\tif (hl.tag != 2) { return 4; };\n" "\treturn 73;\n" "};\n", 73, NULL }, /* The FA4 designed boundary (task #35, old #37): a spread SOURCE that is * not an ident local must stay loud even now that the deref * TARGET resolves. */ { "reject_spread_src", "package main;\n" "type holder = struct { tag: i64, xs: []i64 };\n" "fn dup(p: *[]i64, q: *[]holder, i: i64) void = {\n" "\tappend(*p, (*q)[i].xs...);\n" "};\n" "export fn main() i32 = {\n" "\tlet ys: []i64 = [];\n" "\tlet hs: []holder = [];\n" "\tdup(&ys, &hs, 0);\n" "\treturn 0;\n" "};\n", BUILD_FAIL, "#34: append() spread source shape unsupported (rule-7)" }, }; /* errlog_has — the build-failure stderr must carry the row's expected * diagnostic; any other failure (parse error, crash) is a vacuous * reject and must not pass. */ static int errlog_has(const char *path, const char *needle) { FILE *f = fopen(path, "rb"); if (!f) return 0; char buf[8192]; size_t got = fread(buf, 1, sizeof buf - 1, f); fclose(f); buf[got] = '\0'; return strstr(buf, needle) != NULL; } static int run_driver(const char *driver, const struct row *r, int i) { char src[64], tmpdir[64], errlog[80], cmd[1200]; snprintf(src, sizeof src, "/tmp/applp_%d_%d.ww", getpid(), i); snprintf(tmpdir, sizeof tmpdir, "/tmp/applp_%d_d_%d", getpid(), i); snprintf(errlog, sizeof errlog, "%s.err", src); 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>%s", tmpdir, driver, src, errlog); if (runwait(cmd) != 0) { int rc = -1; if (r->want != BUILD_FAIL) { fprintf(stderr, "row[%s]: build via %s failed\n", r->label, driver); } else if (r->expect_err && !errlog_has(errlog, r->expect_err)) { fprintf(stderr, "row[%s]: %s build failed without " "expected diagnostic \"%s\"\n", r->label, driver, r->expect_err); rc = -3; /* failed, but for the wrong reason */ } unlink(src); unlink(errlog); rmdir(tmpdir); return rc; } 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(errlog); unlink(outbin); rmdir(tmpdir); return got; } /* asm_byte_identical — generate .s via cstage's w6c and wwstage's * w6c_ww and diff. The header-place emission is written fresh on both * sides, so this is the converged-by-construction gate: any drift in * the resolve/spill/reload sequence shows here. */ 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/applp_asm_%d_%d.ww", getpid(), i); snprintf(cs, sizeof cs, "/tmp/applp_asm_%d_%d_c.s", getpid(), i); snprintf(ws, sizeof ws, "/tmp/applp_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[2080]; 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[2120]; snprintf(cdrv, sizeof cdrv, "%s/ww", bin); char wdrv[2120]; 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_place: 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_place[%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_place: %d/%d fixtures failed\n", fail, total); return 1; } printf("append_place: %d fixtures passed\n", total); return 0; }