/* * 804_delete_elem — cstage and wwstage agree, byte-for-byte and at * runtime, that `delete(xs[i])` removes element i from a slice: the * tail [i+1..len) shifts down one stride, len -= 1, cap unchanged * (the delete-half of task #35; insert() stays deferred; regex * fold-2b's delete_thread/failed-thread/PC-dedup consumers). * * Lowering (BOTH stages, converged byte-identical by construction — * there was no pre-existing reference side): push i and &hdr, then an * ascending word-copy loop moves element j+1 onto element j until * j >= len-1, then hdr.len -= 1. The element move is a same-type * whole-stride byte copy — src and dst are elements of the SAME * slice, so no boxing/coercion exists for any element kind; the rows * below prove that across scalar (8B), narrow (4B/1B — the MOVL/MOVB * copy tails), str (24B header), struct (16B) and tagged (56B) esz. * esz comes off the STAMPED base type (the #34/#48 discipline), so a * named-alias element cannot mis-stride. * * Hare's delete also accepts the range form delete(xs[i..j]) * (ref/harec/src/check.c:1981-2027 EXPR_SLICE) — implemented as the * fold-5a prereq P2; covered by 809_delete_range. * * row | shape | want * ----------------+----------------------------------------+------ * i64_first | [7,11,13], delete(xs[0]) | 44 * i64_middle | [7,11,13], delete(xs[1]) | 40 * i64_last | [7,11,13], delete(xs[2]) (no copy, | 38 * | loop body never runs) | * i32_narrow | []i32 esz=4 — the MOVL copy tail | 229 * u16_narrow | []u16 esz=2 — the MOVW copy tail (the | 26 * | last otherwise-unexercised tail arm) | * u8_narrow | []u8 esz=1 — the MOVB copy tail | 28 * cap_unchanged | manual {ptr,len=3,cap=8} header over a | 91 * | stack backing; delete middle; cap must | * | still read 8, tail value intact | * delete_to_empty | 1-element slice, delete(xs[0]), len=0 | 7 * str_elem | []str esz=24 — 3-qword header moves | 234 * | whole | * tagged_56b | [](s6|bool) esz=56 — 7-qword raw move, | 231 * | tag+payload survive; match head + | * | raw-byte tail readback (#43 caveat) | * regex_shape | delete((*threads)[i]) behind *[]thread | * | — the fold-2b delete_thread shape | 24 * | (deref-of-local base, struct element) | * delete_in_loop | drain [1,2,3,4] to empty via repeated | 112 * | delete(xs[0]) — len bookkeeping under | * | iteration, base-4 order accumulation | * reject_array | delete(t[0]) on [3]i64 — "delete must | BUILD_FAIL * | operate on a slice" | * reject_nonindex | delete(xs) — operand must be an index- | BUILD_FAIL * | ing or slicing expression | * reject_arity | delete(xs[0], xs[1]) | BUILD_FAIL * * 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, * which subsumes the frame canary (TEXT main,$N) and the del_l/del_e * label-counter symmetry. */ #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 (the checker reject set — message text * included — is part of the contract: 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[] = { { "i64_first", "package main;\n" "export fn main() i32 = {\n" "\tlet xs: []i64 = [];\n" "\tappend(xs, 7);\n" "\tappend(xs, 11);\n" "\tappend(xs, 13);\n" "\tdelete(xs[0]);\n" "\treturn (xs[0] + xs[1]): i32 + len(xs)*10;\n" "};\n", 44, NULL }, { "i64_middle", "package main;\n" "export fn main() i32 = {\n" "\tlet xs: []i64 = [];\n" "\tappend(xs, 7);\n" "\tappend(xs, 11);\n" "\tappend(xs, 13);\n" "\tdelete(xs[1]);\n" "\treturn (xs[0] + xs[1]): i32 + len(xs)*10;\n" "};\n", 40, NULL }, { "i64_last", "package main;\n" "export fn main() i32 = {\n" "\tlet xs: []i64 = [];\n" "\tappend(xs, 7);\n" "\tappend(xs, 11);\n" "\tappend(xs, 13);\n" "\tdelete(xs[2]);\n" "\treturn (xs[0] + xs[1]): i32 + len(xs)*10;\n" "};\n", 38, NULL }, { "i32_narrow", "package main;\n" "export fn main() i32 = {\n" "\tlet xs: []i32 = [];\n" "\tappend(xs, 4i32);\n" "\tappend(xs, 9i32);\n" "\tappend(xs, 2i32);\n" "\tdelete(xs[0]);\n" "\treturn xs[0] + xs[1]*10 + len(xs)*100;\n" "};\n", 229, NULL }, { "u16_narrow", "package main;\n" "export fn main() i32 = {\n" "\tlet xs: []u16 = [];\n" "\tappend(xs, 4u16);\n" "\tappend(xs, 9u16);\n" "\tappend(xs, 2u16);\n" "\tdelete(xs[1]);\n" "\treturn (xs[0] + xs[1]): i32 + len(xs)*10;\n" "};\n", 26, NULL }, { "u8_narrow", "package main;\n" "export fn main() i32 = {\n" "\tlet xs: []u8 = [];\n" "\tappend(xs, 5u8);\n" "\tappend(xs, 9u8);\n" "\tappend(xs, 3u8);\n" "\tdelete(xs[1]);\n" "\treturn (xs[0] + xs[1]): i32 + len(xs)*10;\n" "};\n", 28, NULL }, { "cap_unchanged", "package main;\n" "export fn main() i32 = {\n" "\tlet hb: [8]u8; hb[0] = 4u8; hb[1] = 6u8; hb[2] = 9u8;\n" "\tlet xs: []u8; xs.ptr = &hb[0]; xs.len = 3; xs.cap = 8;\n" "\tdelete(xs[1]);\n" "\treturn (xs.cap*10 + xs.len): i32 + xs[1]: i32;\n" "};\n", 91, NULL }, { "delete_to_empty", "package main;\n" "export fn main() i32 = {\n" "\tlet xs: []u8 = [];\n" "\tappend(xs, 5u8);\n" "\tdelete(xs[0]);\n" "\treturn len(xs) + 7;\n" "};\n", 7, NULL }, { "str_elem", "package main;\n" "export fn main() i32 = {\n" "\tlet xs: []str = [];\n" "\tlet a: str = \"abc\";\n" "\tlet b: str = \"de\";\n" "\tlet g: str = \"fghi\";\n" "\tappend(xs, a);\n" "\tappend(xs, b);\n" "\tappend(xs, g);\n" "\tdelete(xs[1]);\n" "\treturn (xs[0].len*10 + xs[1].len): i32 + len(xs)*100;\n" "};\n", 234, NULL }, /* 56B element: tag qword + 48B struct payload — seven whole-qword * moves; tag AND payload must both survive the raw move (a tagged * element at rest is just bytes; no boxing on a same-slice move). * Readback is split: match proves tag + head (s.a), and the tail * qword (f, element byte 48 -> absolute byte 104 of element 1) is * read RAW via a *u8 over xs.ptr — the indexed-match payload * cursor itself truncates past 32B (pre-existing, task #43), so a * match on s.f would mis-fail regardless of the move. Verified * truncation-free against a no-delete control. */ { "tagged_56b", "package main;\n" "type s6 = struct { a: i64, b: i64, c: i64, d: i64, e: i64, f: i64 };\n" "type cell = (s6 | bool);\n" "export fn main() i32 = {\n" "\tlet xs: []cell = [];\n" "\tlet p0: s6 = s6 { a = 1, b = 2, c = 3, d = 4, e = 5, f = 6 };\n" "\tlet p1: s6 = s6 { a = 7, b = 8, c = 9, d = 10, e = 11, f = 12 };\n" "\tlet p2: s6 = s6 { a = 13, b = 14, c = 15, d = 16, e = 17, f = 18 };\n" "\tappend(xs, p0);\n" "\tappend(xs, p1);\n" "\tappend(xs, p2);\n" "\tdelete(xs[1]);\n" "\tlet r: i32 = 0;\n" "\tmatch (xs[1]) {\n" "\tcase let s: s6 => r = s.a: i32;\n" "\tcase bool => r = 99;\n" "\t};\n" "\tlet bp: *u8 = xs.ptr: *u8;\n" "\tr += bp[104]: i32;\n" "\treturn r + len(xs)*100;\n" "};\n", 231, NULL }, /* The fold-2b consumer shape: regex.ha:547-551 delete_thread takes * threads: *[]thread and deletes through the pointer. ww spells the * autoderef explicitly: delete((*threads)[i]). */ { "regex_shape", "package main;\n" "type thread = struct { pc: i64, matched: bool };\n" "fn delete_thread(i: i64, threads: *[]thread) void = {\n" "\tdelete((*threads)[i]);\n" "};\n" "export fn main() i32 = {\n" "\tlet xs: []thread = [];\n" "\tappend(xs, thread { pc = 1, matched = false });\n" "\tappend(xs, thread { pc = 2, matched = false });\n" "\tappend(xs, thread { pc = 3, matched = false });\n" "\tdelete_thread(1, &xs);\n" "\tlet r: i32 = (xs[0].pc + xs[1].pc): i32;\n" "\treturn r + len(xs)*10;\n" "};\n", 24, NULL }, /* Pins the len bookkeeping under iteration: each pass reads the * new head then deletes it; base-4 positional accumulation makes * any wrong order, double-shift, or stale len visible (a stuck * len would never terminate; the row would time out as a wrong * exit via the harness). 1,2,3,4 -> ((1*4+2)*4+3)*4+4 = 112. */ { "delete_in_loop", "package main;\n" "export fn main() i32 = {\n" "\tlet xs: []i64 = [];\n" "\tappend(xs, 1);\n" "\tappend(xs, 2);\n" "\tappend(xs, 3);\n" "\tappend(xs, 4);\n" "\tlet acc: i64 = 0;\n" "\tfor (len(xs) > 0) {\n" "\t\tacc = acc*4 + xs[0];\n" "\t\tdelete(xs[0]);\n" "\t};\n" "\treturn acc: i32 + len(xs)*1000;\n" "};\n", 112, NULL }, { "reject_array", "package main;\n" "export fn main() i32 = {\n" "\tlet t: [3]i64 = [1, 2, 3];\n" "\tdelete(t[0]);\n" "\treturn 0;\n" "};\n", BUILD_FAIL, "delete must operate on a slice" }, { "reject_nonindex", "package main;\n" "export fn main() i32 = {\n" "\tlet xs: []u8 = [];\n" "\tappend(xs, 5u8);\n" "\tdelete(xs);\n" "\treturn 0;\n" "};\n", BUILD_FAIL, "delete: operand must be an indexing or slicing expression" }, { "reject_arity", "package main;\n" "export fn main() i32 = {\n" "\tlet xs: []u8 = [];\n" "\tappend(xs, 5u8);\n" "\tappend(xs, 6u8);\n" "\tdelete(xs[0], xs[1]);\n" "\treturn 0;\n" "};\n", BUILD_FAIL, "delete: takes exactly one argument" }, }; /* 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/dele_%d_%d.ww", getpid(), i); snprintf(tmpdir, sizeof tmpdir, "/tmp/dele_%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. Both delete lowerings are written fresh, so this is * the converged-by-construction gate: any drift in the shift loop, the * del_l/del_e label sequence, or the esz word-copy 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/dele_asm_%d_%d.ww", getpid(), i); snprintf(cs, sizeof cs, "/tmp/dele_asm_%d_%d_c.s", getpid(), i); snprintf(ws, sizeof ws, "/tmp/dele_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, "delete_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, "delete_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, "delete_elem: %d/%d fixtures failed\n", fail, total); return 1; } printf("delete_elem: %d fixtures passed\n", total); return 0; }