Files
ww/test/wcc/804_delete_elem.c
Hojun-Cho ce3a25a0b4 test: contain sepwork scratch per-driver tmpdir, fix /tmp+in-repo leak (#8)
The wcc test drivers ran `ww build <bare-/tmp src>` with no -o, so the
compiler's <stem>.sepwork scratch landed beside the source and was never
cleaned: unbounded /tmp growth (2195 stale dirs observed) that fills tmpfs
and fabricates phantom test failures + silent harness aborts, and for
in-repo fixture builds leaked .sepwork into the tracked tree.

Each leaking build now writes its source + output inside a per-invocation
tmpdir, passes -o <tmpdir>/<stem> so the .sepwork lands inside it, and
rm -rf's the tmpdir on every exit path -- including fopen-fail and the
expected-fail reject builds (scratch is mkdir'd before the build can fail).
`ww run` and explicit-`-o`/byte-id helpers are left as-is; the 990/993
byte-id comparison logic is byte-for-byte unchanged.

Two items filed separately (this commit holds the no-Makefile / no-main.c
rail):
- #13: a stale <src>.s byte-id readback (749) silently no-ops since
  separate-compile emits .s to <ostem>.sepwork/__root.s; documented inline.
- #14: build-system Makefile recipes build selfhost/cmd/*/main.ww with no
  -o and leak main.sepwork in-tree (bounded, gitignored; own commit).

One concern -- sepwork leak hygiene -- across 228 drivers; uniform
transform applied per-file and two-round reviewed. make test: all 402
passed, zero net-new /tmp scratch, zero test-driven in-repo .sepwork.
2026-06-22 23:29:39 +09:00

483 lines
15 KiB
C

/*
* 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
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 tmpdir[64], src[128], outbin[128], errlog[160], rmcmd[160];
char cmd[1200];
snprintf(tmpdir, sizeof tmpdir, "/tmp/dele_%d_d_%d", getpid(), i);
mkdir(tmpdir, 0755);
snprintf(src, sizeof src, "%s/dele_%d_%d.ww", tmpdir, getpid(), i);
snprintf(outbin, sizeof outbin, "%s/dele_%d_%d", tmpdir, getpid(), i);
snprintf(errlog, sizeof errlog, "%s/err", tmpdir);
snprintf(rmcmd, sizeof rmcmd, "rm -rf %s", tmpdir);
FILE *f = fopen(src, "wb");
if (!f) { runwait(rmcmd); return -1; }
fputs(r->src, f);
fclose(f);
snprintf(cmd, sizeof cmd, "%s build -o %s %s 2>%s",
driver, outbin, 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 */
}
runwait(rmcmd);
return rc;
}
int got = runwait(outbin);
runwait(rmcmd);
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;
}