Files
ww/test/wcc/800_append_wide_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

492 lines
16 KiB
C

/*
* 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 <stdio.h>
#include <stdlib.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 (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 tmpdir[64], src[128], outbin[128], rmcmd[160], cmd[1024];
snprintf(tmpdir, sizeof tmpdir, "/tmp/apwe_%d_d_%d", getpid(), i);
mkdir(tmpdir, 0755);
snprintf(src, sizeof src, "%s/apwe_%d_%d.ww", tmpdir, getpid(), i);
snprintf(outbin, sizeof outbin, "%s/apwe_%d_%d", tmpdir, getpid(), i);
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>/dev/null",
driver, outbin, src);
if (runwait(cmd) != 0) {
if (r->want != BUILD_FAIL)
fprintf(stderr, "row[%s]: build via %s failed\n",
r->label, driver);
runwait(rmcmd);
return -1;
}
int got = runwait(outbin);
runwait(rmcmd);
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;
}