/* * 809_delete_range — cstage and wwstage agree, byte-for-byte and at * runtime, that the range form `delete(xs[lo:hi])` removes elements * [lo, hi) from a slice: the tail [hi..len) shifts down count = hi-lo * strides, len -= count, cap unchanged; lo defaults 0, hi defaults * len (so delete(xs[:]) clears the slice, storage retained). The * range half of the delete() builtin — fold-5a prereq P2 for the * regex.ha:333 consumer `delete(jump_idxs[group_level][..])` (harec * ref/harec/src/check.c:1981-2027 accepts EXPR_SLICE alongside the * 804-covered ACCESS_INDEX form). * * Lowering (BOTH stages, converged byte-identical by construction): * push &hdr, lo and count = hi-lo, then an ascending word-copy loop * moves element j+count onto element j until j+count >= len, then * hdr.len -= count. The per-element move is 804's same-slice * whole-stride word copy with a DYNAMIC src offset (count*esz via a * src register) instead of the constant one-stride; the esz spread * below (1/2/4/8/16/24) re-proves every copy-tail arm against the * dynamic src. Base shapes: local slice ident, deref-of-local * ptr-to-slice, and — new for the range arm — an INDEXED local slice * base xs[g][lo:hi] (the regex fold-5a consumer shape). Bounds are * implicit (no range check), matching the 804 single-element arm and * the rest of cgen — so no out-of-bounds abort rows here; a * violating range is as undefined as a violating index. * * row | shape | want * -----------------+----------------------------------------+------ * full_range | [7,11,13], delete(xs[:]) -> len 0 | 7 * full_explicit | [7,11,13], delete(xs[0:3]) -> len 0 | 8 * full_twice | delete(xs[:]) twice — second is a | 9 * | no-op on the emptied slice (count 0) | * head | [1,2,3], delete(xs[:2]) — lo default 0 | 13 * mid | [1..5], delete(xs[1:3]) | 148 * tail_open | [1,2,3], delete(xs[1:]) — hi default | 11 * | len, copy loop never entered | * empty_range | [1,2], delete(xs[1:1]) — count 0, | 23 * | self-copy loop, len unchanged | * end_boundary | [1,2], delete(xs[len:len]) — lo==hi== | 212 * | len, loop exits at entry, no deref | * | past the end | * empty_explicit | delete(xs[0:0]) on a never-appended | 31 * | empty slice (ptr nil) — count 0 and | * | len 0, nothing dereferenced | * single_via_range | delete(xs[1:2]) == delete(ys[1]) | 42 * | element-for-element | * cap_preserved | manual {ptr,len=3,cap=8} header, | 80 * | delete(xs[:]); cap must still read 8 | * u8_narrow | []u8 esz=1 — MOVB tail, dynamic src | 215 * u16_narrow | []u16 esz=2 — MOVW tail, dynamic src | 25 * i32_narrow | []i32 esz=4 — MOVL tail, dynamic src | 50 * struct16 | []pair esz=16 — 2 whole qwords, no | 216 * | sub-word tail (804's struct esz twin) | * str_elem | []str esz=24 — 3-qword headers move | 235 * | whole across a 2-stride shift | * regex_exact | [][]size, delete(jj[lvl][:]) — the | 10 * | EXACT regex.ha:333 consumer shape | * | (indexed base, size-typed index) | * deref_shape | delete((*p)[:]) behind *[]u8 — the | 17 * | 804 regex_shape twin | * order_of_eval | delete(xs[lof():hif()]) — lo then hi, | 16 * | each CALLED exactly once (trace must | * | read 12; harec evaluates the slicing | * | operands left-to-right, once) | * alias_visible | delete(xs[poke(&xs):]) — lo's write | 91 * | through &xs lands BEFORE the shift | * | (loop reads ptr/len/elems fresh from | * | the header, after operand eval) | * reject_array | delete(t[0:2]) on [3]i64 — "delete | BUILD_FAIL * | must operate on a slice" | * reject_str | delete(s[:]) on str — slicing a str | BUILD_FAIL * | yields str, not a slice | * * 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 rdl_l/rdl_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[] = { { "full_range", "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[:]);\n" "\treturn len(xs): i32 + 7;\n" "};\n", 7, NULL }, { "full_explicit", "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:3]);\n" "\treturn len(xs): i32 + 8;\n" "};\n", 8, NULL }, /* Second full-range delete sees len == 0: count = len - 0 = 0, * the copy loop exits at entry, len -= 0. Pins that an emptied * slice survives a re-clear (the regex `)` arm clears * jump_idxs[group_level] whether or not `|` populated it). */ { "full_twice", "package main;\n" "export fn main() i32 = {\n" "\tlet xs: []i64 = [];\n" "\tappend(xs, 7);\n" "\tappend(xs, 11);\n" "\tdelete(xs[:]);\n" "\tdelete(xs[:]);\n" "\treturn len(xs): i32 + 9;\n" "};\n", 9, NULL }, { "head", "package main;\n" "export fn main() i32 = {\n" "\tlet xs: []i64 = [];\n" "\tappend(xs, 1);\n" "\tappend(xs, 2);\n" "\tappend(xs, 3);\n" "\tdelete(xs[:2]);\n" "\treturn xs[0]: i32 + len(xs)*10;\n" "};\n", 13, NULL }, { "mid", "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" "\tappend(xs, 5);\n" "\tdelete(xs[1:3]);\n" "\treturn (xs[0]*100 + xs[1]*10 + xs[2]): i32 + len(xs);\n" "};\n", 148, NULL }, { "tail_open", "package main;\n" "export fn main() i32 = {\n" "\tlet xs: []i64 = [];\n" "\tappend(xs, 1);\n" "\tappend(xs, 2);\n" "\tappend(xs, 3);\n" "\tdelete(xs[1:]);\n" "\treturn xs[0]: i32 + len(xs)*10;\n" "};\n", 11, NULL }, { "empty_range", "package main;\n" "export fn main() i32 = {\n" "\tlet xs: []i64 = [];\n" "\tappend(xs, 1);\n" "\tappend(xs, 2);\n" "\tdelete(xs[1:1]);\n" "\treturn (xs[0] + xs[1]): i32 + len(xs)*10;\n" "};\n", 23, NULL }, /* lo == hi == len: the loop guard j+count >= len holds at entry * with j = len, so nothing past the end is ever addressed. */ { "end_boundary", "package main;\n" "export fn main() i32 = {\n" "\tlet xs: []i64 = [];\n" "\tappend(xs, 1);\n" "\tappend(xs, 2);\n" "\tdelete(xs[len(xs):len(xs)]);\n" "\treturn (xs[0]*10 + xs[1]): i32 + len(xs)*100;\n" "};\n", 212, NULL }, /* Never-appended empty slice: ptr is nil. count 0 and len 0 mean * the loop body (the only dereferencing code) never runs. */ { "empty_explicit", "package main;\n" "export fn main() i32 = {\n" "\tlet xs: []i64 = [];\n" "\tdelete(xs[0:0]);\n" "\treturn len(xs): i32 + 31;\n" "};\n", 31, NULL }, { "single_via_range", "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:2]);\n" "\tlet ys: []i64 = [];\n" "\tappend(ys, 7);\n" "\tappend(ys, 11);\n" "\tappend(ys, 13);\n" "\tdelete(ys[1]);\n" "\tif (len(xs) != len(ys)) { return 99; };\n" "\tif (xs[0] != ys[0]) { return 98; };\n" "\tif (xs[1] != ys[1]) { return 97; };\n" "\treturn 42;\n" "};\n", 42, NULL }, { "cap_preserved", "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[:]);\n" "\treturn (xs.cap*10 + xs.len): i32;\n" "};\n", 80, NULL }, { "u8_narrow", "package main;\n" "export fn main() i32 = {\n" "\tlet xs: []u8 = [];\n" "\tappend(xs, 1u8);\n" "\tappend(xs, 2u8);\n" "\tappend(xs, 3u8);\n" "\tappend(xs, 4u8);\n" "\tappend(xs, 5u8);\n" "\tdelete(xs[1:4]);\n" "\treturn (xs[0]*10u8 + xs[1]): i32 + len(xs)*100;\n" "};\n", 215, NULL }, { "u16_narrow", "package main;\n" "export fn main() i32 = {\n" "\tlet xs: []u16 = [];\n" "\tappend(xs, 1u16);\n" "\tappend(xs, 2u16);\n" "\tappend(xs, 3u16);\n" "\tappend(xs, 4u16);\n" "\tdelete(xs[1:3]);\n" "\treturn (xs[0] + xs[1]): i32 + len(xs)*10;\n" "};\n", 25, NULL }, { "i32_narrow", "package main;\n" "export fn main() i32 = {\n" "\tlet xs: []i32 = [];\n" "\tappend(xs, 10i32);\n" "\tappend(xs, 20i32);\n" "\tappend(xs, 30i32);\n" "\tappend(xs, 40i32);\n" "\tdelete(xs[2:4]);\n" "\treturn xs[0] + xs[1] + len(xs)*10;\n" "};\n", 50, NULL }, /* esz 16: exactly two whole qwords per element — the only spread * point with multiple MOVQs and NO sub-word tail. */ { "struct16", "package main;\n" "type pair = struct { a: i64, b: i64 };\n" "export fn main() i32 = {\n" "\tlet xs: []pair = [];\n" "\tappend(xs, pair{ a = 1, b = 2 });\n" "\tappend(xs, pair{ a = 3, b = 4 });\n" "\tappend(xs, pair{ a = 5, b = 6 });\n" "\tdelete(xs[1:2]);\n" "\treturn (xs[0].a*10 + xs[1].b): i32 + len(xs)*100;\n" "};\n", 216, 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" "\tlet h: str = \"jklmn\";\n" "\tappend(xs, a);\n" "\tappend(xs, b);\n" "\tappend(xs, g);\n" "\tappend(xs, h);\n" "\tdelete(xs[1:3]);\n" "\treturn (xs[0].len*10 + xs[1].len): i32 + len(xs)*100;\n" "};\n", 235, NULL }, /* The fold-5a consumer, shape-EXACT: indexed local-slice base, * size-typed outer index, defaulted full range (regex.ha:333 * `delete(jump_idxs[group_level][..])`; ww spells `[..]` as * `[:]`, D7). The header lives at jj.ptr + g*size([]size) — * outer stride off the type table, never a literal. */ { "regex_exact", "package main;\n" "export fn main() i32 = {\n" "\tlet jump_idxs: [][]size = [];\n" "\tlet lvl0: []size = [];\n" "\tlet a: size = 3;\n" "\tlet b: size = 9;\n" "\tappend(lvl0, a);\n" "\tappend(lvl0, b);\n" "\tappend(jump_idxs, lvl0);\n" "\tlet group_level: size = 0;\n" "\tdelete(jump_idxs[group_level][:]);\n" "\treturn len(jump_idxs[0]): i32 + len(jump_idxs): i32 * 10;\n" "};\n", 10, NULL }, { "deref_shape", "package main;\n" "fn clear(p: *[]u8) void = {\n" "\tdelete((*p)[:]);\n" "};\n" "export fn main() i32 = {\n" "\tlet xs: []u8 = [];\n" "\tappend(xs, 5u8);\n" "\tappend(xs, 6u8);\n" "\tclear(&xs);\n" "\treturn len(xs): i32 + 17;\n" "};\n", 17, NULL }, /* lo and hi as CALLs: each side effect fires exactly once, lo * before hi (trace 12, never 21/121/122) — pins that neither * bound expression is re-evaluated by the count computation or * the copy loop. */ { "order_of_eval", "package main;\n" "let trace: i64 = 0;\n" "fn lof() size = {\n" "\ttrace = trace*10 + 1;\n" "\treturn 1;\n" "};\n" "fn hif() size = {\n" "\ttrace = trace*10 + 2;\n" "\treturn 3;\n" "};\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" "\tdelete(xs[lof():hif()]);\n" "\tif (trace != 12) { return 90; };\n" "\treturn (xs[0]*10 + xs[1]): i32 + len(xs);\n" "};\n", 16, NULL }, /* lo mutates the slice it is deleting from: only the header * ADDRESS is taken before operand eval; ptr/len/elements are read * after, so poke's write to xs[0] survives into the result. */ { "alias_visible", "package main;\n" "fn poke(p: *[]i64) size = {\n" "\t(*p)[0] = 9;\n" "\treturn 1;\n" "};\n" "export fn main() i32 = {\n" "\tlet xs: []i64 = [];\n" "\tappend(xs, 1);\n" "\tappend(xs, 2);\n" "\tappend(xs, 3);\n" "\tdelete(xs[poke(&xs):]);\n" "\treturn xs[0]: i32 * 10 + len(xs);\n" "};\n", 91, NULL }, { "reject_array", "package main;\n" "export fn main() i32 = {\n" "\tlet t: [3]i64 = [1, 2, 3];\n" "\tdelete(t[0:2]);\n" "\treturn 0;\n" "};\n", BUILD_FAIL, "delete must operate on a slice" }, { "reject_str", "package main;\n" "export fn main() i32 = {\n" "\tlet s: str = \"abc\";\n" "\tdelete(s[:]);\n" "\treturn 0;\n" "};\n", BUILD_FAIL, "delete must operate on a slice" }, }; /* 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/delr_%d_%d.ww", getpid(), i); snprintf(tmpdir, sizeof tmpdir, "/tmp/delr_%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 — compile the row's source standalone with w6c and * w6c_ww and diff. Both range-delete lowerings are written fresh, so * this is the converged-by-construction gate: any drift in the shift * loop, the rdl_l/rdl_e label sequence, the dynamic-src copy, or the * indexed-base header address 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/delr_asm_%d_%d.ww", getpid(), i); snprintf(cs, sizeof cs, "/tmp/delr_asm_%d_%d_c.s", getpid(), i); snprintf(ws, sizeof ws, "/tmp/delr_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_range: 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_range[%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_range: %d/%d fixtures failed\n", fail, total); return 1; } printf("delete_range: %d fixtures passed\n", total); return 0; }