Files
ww/test/wcc/809_idx_structlit_store.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

551 lines
18 KiB
C

/*
* 809_idx_structlit_store — cstage and wwstage agree, byte-for-byte and
* at runtime, that a struct-LITERAL store into an INDEXED element place
* (`a[i] = pt{...}`, `cs[i] = capture{...}`, `(*ts)[i].caps[k] =
* capture{...}`) and into a DEREF place (`*p = pt{...}`) writes every
* field (task #20; ken's p13 array find + prober PG4 slice extension;
* gates regex fold-5a's run_thread groupstart capture store,
* regex.ha:643-651).
*
* Pre-fix BOTH stages compiled these byte-identically WRONG (gate-blind
* — only a runtime pin can hold this): the N_ASSIGN N_INDEX-lhs arm's
* aggregate branch (#270-1b) gated its rhs on ident/dot/deref shapes,
* so an N_STRUCTLIT rhs fell to the scalar store tail; cgexpr on a
* struct literal emits NOTHING (AX stays 0) and the tail stored ONE
* zero word at the element base. Net effect: every literal field
* silently dropped, the element's first 8 bytes zeroed — for a
* str-leading element (the regex capture shape) that nulls content.ptr
* and the next read of the str field SEGFAULTS. The N_UN(STAR) deref
* arm (`*p = pt{...}`) had the same fall-to-scalar tail — same class.
*
* The fix (BOTH stages, converged byte-identical): a struct-lit rhs
* aimed at an N_INDEX, N_UN(STAR), or indexed-base N_DOT place
* (`a[i].f = pt{...}`, reviewer-20 sibling — the a[i].f legacy arm's
* fldstoreop tail had the same silent drop) skips the legacy arms and
* routes through the F6 assign-resolver (cgplaceaddr) — the SAME C1.25
* aggregate arm that wires `(*ts)[i].field = capture{...}` N_DOT
* places: materialise the literal into a FRESH per-use @placescr slot
* (cg_structlit_fill_bp / cgstructlitfillbp: nested literals, str
* fields, TK_ELLIPSIS autofill), resolve the place address, word-copy
* esz bytes. Close-by-construction: every non-ident place kind (DOT /
* INDEX / UN-STAR) now funnels struct-lit stores through that single
* arm; ident places keep their enumerated fill paths. Class boundary
* for OTHER rhs kinds at these places: tuple-lit/str-lit into INDEX
* and DOT places work (pinned below); array-lit rhs at assignment is
* unwired for every place kind and now dies LOUD (task #32, build-fail
* rows below); tuple-lit through deref truncates (filed #31-E); CALL
* rhs into INDEX/UN-STAR places stores only RAX (filed #31-G).
*
* row | shape | want
* ------------------+-----------------------------------------+------
* arr_elem | a[1] = pt{3,4}; a[1].x*10+a[1].y | 34
* arr_neighbor | store a[1]; a[0]/a[2] field-set intact | 82
* arr_var_idx | a[geti()] = pt{...} (runtime index) | 34
* small_elem | 8B one-field elem (esz<=8 word-copy) | 52
* nested_lit | a[1] = outer{ in = inner{3,4}, t=5 } | 45
* partial_ellipsis | prefill 9,9; a[1] = pt{x=3, ...} → y=0 | 30
* alias_lit | type qt = pt; a[1] = qt{3,4} (chase | 34
* | peels the NAMED alias before TY_STRUCT) |
* all_default | prefill 9,9; a[1] = pt{} zeroes EVERY | 7
* | word (fill-loop floor) + 7 |
* tuple_elem | a[1] = (3,4); copy-out readback | 34
* dotidx_field | a[1].p = pt{3,4} (indexed-base DOT | 34
* | place, reviewer-20 sibling fix) |
* slice_elem_str | cs[1] = capture{...} 56B str-leading | 128
* fieldplace_slice | ts[0].caps[1] = capture{...} | 128
* derefspine_slice | (*tsp)[0].caps[1] = capture{...} | 128
* callee_capture | (*ts)[i].caps[k] = capture{...} in a | 197
* | callee w/ *[]thr param; str readback via |
* | strings.compare + len + both neighbors |
* | (the EXACT fold-5a consumer shape) |
* deref_lit | *p = pt{3,4} (N_UN place, same class) | 34
* global_arr | g[1] = pt{3,4}, g: [3]pt module let | 34
* arrlit_idx_loud | a[1] = [3,4] — LOUD build-fail (#32) | -1
* arrlit_deref_loud | *p = [3,4] — LOUD build-fail (#32) | -1
*
* global_arr is RUNTIME-ONLY (byteid=0): an uninitialised [N]struct
* value-global already diverges on master — wwstage emits a zero
* `DATAW main.g(SB)` record (under-sized: 24B for a 48B array),
* cstage emits none. Pre-existing, independent of this fix (task #11
* family: value-global data emission); the store/readback this row
* pins is symmetric and correct on both stages. The arrlit_* rows pin
* task #32's loud boundary (want -1 = driver build MUST fail; pre-#32
* both stages silently zeroed one word); flip them to runtime rows
* when #32 wires the fill.
*/
#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;
}
struct row { const char *label; const char *src; int want; int byteid; };
/* The 56B regex capture shape: str header leading + 4 size fields.
* Shared preamble for the slice rows; only the store site differs. */
#define CAPTURE_DEFS \
"package main;\n" \
"type capture = struct {\n" \
"\tcontent: str,\n" \
"\tstart: size,\n" \
"\tstart_bytesize: size,\n" \
"\tend: size,\n" \
"\tend_bytesize: size,\n" \
"};\n"
#define CAPTURE_SEED \
"\tlet cs: []capture = [];\n" \
"\tappend(cs, capture { content = \"a\", start = 1: size, " \
"start_bytesize = 1: size, end = 1: size, " \
"end_bytesize = 1: size });\n" \
"\tappend(cs, capture { content = \"b\", start = 2: size, " \
"start_bytesize = 2: size, end = 2: size, " \
"end_bytesize = 2: size });\n"
#define CAPTURE_STORE(place) \
"\t" place " = capture { content = \"grp\", start = 3: size, " \
"start_bytesize = 4: size, end = 60: size, " \
"end_bytesize = 61: size };\n"
static const struct row rows[] = {
{ "arr_elem",
"package main;\n"
"type pt = struct { x: u64, y: u64 };\n"
"export fn main() i32 = {\n"
"\tlet a: [3]pt;\n"
"\ta[1] = pt { x = 3u64, y = 4u64 };\n"
"\treturn (a[1].x * 10u64 + a[1].y): i32;\n"
"};\n",
34, 1 },
{ "arr_neighbor",
"package main;\n"
"type pt = struct { x: u64, y: u64 };\n"
"export fn main() i32 = {\n"
"\tlet a: [3]pt;\n"
"\ta[0].x = 7u64; a[2].y = 9u64;\n"
"\ta[1] = pt { x = 3u64, y = 4u64 };\n"
"\treturn (a[0].x * 10u64 + a[2].y + a[1].x): i32;\n"
"};\n",
82, 1 },
{ "arr_var_idx",
"package main;\n"
"type pt = struct { x: u64, y: u64 };\n"
"fn geti() size = { return 1: size; };\n"
"export fn main() i32 = {\n"
"\tlet a: [3]pt;\n"
"\ta[geti()] = pt { x = 3u64, y = 4u64 };\n"
"\treturn (a[1].x * 10u64 + a[1].y): i32;\n"
"};\n",
34, 1 },
{ "small_elem",
"package main;\n"
"type one = struct { v: u64 };\n"
"export fn main() i32 = {\n"
"\tlet a: [2]one;\n"
"\ta[0].v = 2u64;\n"
"\ta[1] = one { v = 5u64 };\n"
"\treturn (a[1].v * 10u64 + a[0].v): i32;\n"
"};\n",
52, 1 },
{ "nested_lit",
"package main;\n"
"type inner = struct { a: u64, b: u64 };\n"
"type outer = struct { in: inner, t: u64 };\n"
"export fn main() i32 = {\n"
"\tlet a: [2]outer;\n"
"\ta[1] = outer { in = inner { a = 3u64, b = 4u64 }, "
"t = 5u64 };\n"
"\treturn (a[1].in.b * 10u64 + a[1].t): i32;\n"
"};\n",
45, 1 },
{ "partial_ellipsis",
"package main;\n"
"type pt = struct { x: u64, y: u64 };\n"
"export fn main() i32 = {\n"
"\tlet a: [3]pt;\n"
"\ta[1].x = 9u64; a[1].y = 9u64;\n"
"\ta[1] = pt { x = 3u64, ... };\n"
"\treturn (a[1].x * 10u64 + a[1].y): i32;\n"
"};\n",
30, 1 },
{ "alias_lit",
"package main;\n"
"type pt = struct { x: u64, y: u64 };\n"
"type qt = pt;\n"
"export fn main() i32 = {\n"
"\tlet a: [3]pt;\n"
"\ta[1] = qt { x = 3u64, y = 4u64 };\n"
"\treturn (a[1].x * 10u64 + a[1].y): i32;\n"
"};\n",
34, 1 },
{ "all_default",
"package main;\n"
"type pt = struct { x: u64, y: u64 };\n"
"export fn main() i32 = {\n"
"\tlet a: [3]pt;\n"
"\ta[1].x = 9u64; a[1].y = 9u64;\n"
"\ta[1] = pt {};\n"
"\treturn (a[1].x * 10u64 + a[1].y + 7u64): i32;\n"
"};\n",
7, 1 },
{ "tuple_elem",
"package main;\n"
"export fn main() i32 = {\n"
"\tlet a: [2](u64, u64);\n"
"\ta[1] = (3u64, 4u64);\n"
"\tlet t: (u64, u64) = a[1];\n"
"\treturn (t.0 * 10u64 + t.1): i32;\n"
"};\n",
34, 1 },
{ "dotidx_field",
"package main;\n"
"type pt = struct { x: u64, y: u64 };\n"
"type box = struct { p: pt, t: u64 };\n"
"export fn main() i32 = {\n"
"\tlet a: [2]box;\n"
"\ta[1].p.x = 9u64; a[1].p.y = 9u64;\n"
"\tif (a[1].p.x != 9u64 || a[1].p.y != 9u64) { return 250; };\n"
"\ta[1].p = pt { x = 3u64, y = 4u64 };\n"
"\treturn (a[1].p.x * 10u64 + a[1].p.y): i32;\n"
"};\n",
34, 1 },
{ "slice_elem_str",
CAPTURE_DEFS
"export fn main() i32 = {\n"
CAPTURE_SEED
CAPTURE_STORE("cs[1]")
"\tif (cs[0].start != 1) { return 250; };\n"
"\treturn (cs[1].start + cs[1].start_bytesize + cs[1].end + "
"cs[1].end_bytesize): i32;\n"
"};\n",
128, 1 },
{ "fieldplace_slice",
CAPTURE_DEFS
"type thr = struct { pc: size, caps: []capture };\n"
"export fn main() i32 = {\n"
CAPTURE_SEED
"\tlet ts: []thr = [];\n"
"\tappend(ts, thr { pc = 5: size, caps = cs });\n"
CAPTURE_STORE("ts[0].caps[1]")
"\tif (ts[0].caps[0].start != 1) { return 250; };\n"
"\treturn (ts[0].caps[1].start + ts[0].caps[1].start_bytesize + "
"ts[0].caps[1].end + ts[0].caps[1].end_bytesize): i32;\n"
"};\n",
128, 1 },
{ "derefspine_slice",
CAPTURE_DEFS
"type thr = struct { pc: size, caps: []capture };\n"
"export fn main() i32 = {\n"
CAPTURE_SEED
"\tlet ts: []thr = [];\n"
"\tappend(ts, thr { pc = 5: size, caps = cs });\n"
"\tlet tsp = &ts;\n"
CAPTURE_STORE("(*tsp)[0].caps[1]")
"\tif (ts[0].caps[0].start != 1) { return 250; };\n"
"\treturn (ts[0].caps[1].start + ts[0].caps[1].start_bytesize + "
"ts[0].caps[1].end + ts[0].caps[1].end_bytesize): i32;\n"
"};\n",
128, 1 },
/* The fold-5a consumer pin: regex.ha:643-651 verbatim shape —
* callee takes *[]thr, double-indexes through the deref spine,
* stores the 56B capture literal. Readback covers the str field
* (strings.compare + len — pre-fix this SEGFAULTED on the nulled
* content.ptr), both neighbor elements (56B store must not
* smear), and all four size fields. 3+4+60+61+3*23 = 197. */
{ "callee_capture",
"package main;\n"
"\n"
"import strings;\n"
"\n"
"type capture = struct {\n"
"\tcontent: str,\n"
"\tstart: size,\n"
"\tstart_bytesize: size,\n"
"\tend: size,\n"
"\tend_bytesize: size,\n"
"};\n"
"type thr = struct { pc: size, caps: []capture };\n"
"fn store(ts: *[]thr, i: size, idx: size) void = {\n"
"\t(*ts)[i].caps[idx] = capture {\n"
"\t\tcontent = \"grp\",\n"
"\t\tstart = 3: size,\n"
"\t\tstart_bytesize = 4: size,\n"
"\t\tend = 60: size,\n"
"\t\tend_bytesize = 61: size,\n"
"\t};\n"
"};\n"
"export fn main() i32 = {\n"
"\tlet cs: []capture = [];\n"
"\tappend(cs, capture { content = \"a\", start = 1: size, "
"start_bytesize = 1: size, end = 1: size, "
"end_bytesize = 1: size });\n"
"\tappend(cs, capture { content = \"b\", start = 2: size, "
"start_bytesize = 2: size, end = 2: size, "
"end_bytesize = 2: size });\n"
"\tappend(cs, capture { content = \"c\", start = 9: size, "
"start_bytesize = 9: size, end = 9: size, "
"end_bytesize = 9: size });\n"
"\tlet ts: []thr = [];\n"
"\tappend(ts, thr { pc = 5: size, caps = cs });\n"
"\tstore(&ts, 0: size, 1: size);\n"
"\tif (strings.compare(ts[0].caps[1].content, \"grp\") != 0) "
"{ return 250; };\n"
"\tif (len(ts[0].caps[1].content) != 3) { return 249; };\n"
"\tif (ts[0].caps[0].end != 1 || ts[0].caps[2].start != 9) "
"{ return 248; };\n"
"\tif (strings.compare(ts[0].caps[2].content, \"c\") != 0) "
"{ return 247; };\n"
"\tlet acc = ts[0].caps[1].start + ts[0].caps[1].start_bytesize\n"
"\t\t+ ts[0].caps[1].end + ts[0].caps[1].end_bytesize\n"
"\t\t+ ts[0].caps[1].start * 23;\n"
"\treturn acc: i32;\n"
"};\n",
197, 1 },
{ "deref_lit",
"package main;\n"
"type pt = struct { x: u64, y: u64 };\n"
"export fn main() i32 = {\n"
"\tlet a: pt = pt { x = 1u64, y = 2u64 };\n"
"\tlet p: *pt = &a;\n"
"\t*p = pt { x = 3u64, y = 4u64 };\n"
"\treturn (a.x * 10u64 + a.y): i32;\n"
"};\n",
34, 1 },
{ "global_arr",
"package main;\n"
"type pt = struct { x: u64, y: u64 };\n"
"let g: [3]pt;\n"
"export fn main() i32 = {\n"
"\tg[1] = pt { x = 3u64, y = 4u64 };\n"
"\treturn (g[1].x * 10u64 + g[1].y): i32;\n"
"};\n",
34, 0 },
{ "arrlit_idx_loud",
"package main;\n"
"export fn main() i32 = {\n"
"\tlet a: [2][2]u64;\n"
"\ta[1] = [3u64, 4u64];\n"
"\treturn (a[1][0] * 10u64 + a[1][1]): i32;\n"
"};\n",
-1, 0 },
{ "arrlit_deref_loud",
"package main;\n"
"export fn main() i32 = {\n"
"\tlet a: [2]u64;\n"
"\tlet p: *[2]u64 = &a;\n"
"\t*p = [3u64, 4u64];\n"
"\treturn (a[0] * 10u64 + a[1]): i32;\n"
"};\n",
-1, 0 },
};
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/isls_%d_d_%d", getpid(), i);
mkdir(tmpdir, 0755);
snprintf(src, sizeof src, "%s/isls_%d_%d.ww", tmpdir, getpid(), i);
snprintf(outbin, sizeof outbin, "%s/isls_%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) {
/* want == -1 rows pin a LOUD build refusal (task #32). */
if (r->want != -1)
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 — #103 sep layout: the driver flip moves build
* artifacts from next-to-source <stem>.s to a <stem>.sepwork/ scratch
* dir, so drive `ww build --sep -o <stem>` twice — once with cstage's w6c
* and once with the wwstage compiler (WW_W6C override; w6a/w6l stay
* cstage — only the .s is compared) — then concat each build's
* per-package .sepwork asm (sorted glob, identical package set:
* callee_capture pulls strings, so this is multi-package) and diff. The
* driver route — not a bare `w6c src.ww` — so importing rows resolve;
* --sep is flip-invariant (green pre- and post-flip). WW_PKGCACHE pins
* the package cache to the scratch dir so out/.pkgcache stays untouched.
* Pre-fix the asm was byte-identically WRONG (both stages dropped the
* fill), so these rows pin only that the converged fix stays symmetric;
* the runtime rows above carry correctness. */
static int
asm_byte_identical(const char *bin, const struct row *r, int i)
{
char src[96], tmpdir[64], outc[128], outw[128];
char cs[96], ws[96], cmd[1024];
snprintf(tmpdir, sizeof tmpdir, "/tmp/isls_asm_%d_d_%d", getpid(), i);
snprintf(src, sizeof src, "%s/main809.ww", tmpdir);
snprintf(outc, sizeof outc, "%s/main809c", tmpdir);
snprintf(outw, sizeof outw, "%s/main809w", tmpdir);
snprintf(cs, sizeof cs, "%s/all_cs.s", tmpdir);
snprintf(ws, sizeof ws, "%s/all_ww.s", tmpdir);
mkdir(tmpdir, 0755);
FILE *f = fopen(src, "wb");
if (!f) {
snprintf(cmd, sizeof cmd, "rm -rf %s", tmpdir);
if (system(cmd)) {}
return -1;
}
fputs(r->src, f);
fclose(f);
int rc = 0;
snprintf(cmd, sizeof cmd,
"cd %s && WW_PKGCACHE=%s/pkgc_c %s/ww build --sep -o %s %s 2>/dev/null",
tmpdir, tmpdir, bin, outc, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: cstage sep build failed\n", r->label);
rc = -1;
}
if (rc == 0) {
snprintf(cmd, sizeof cmd,
"cd %s && WW_PKGCACHE=%s/pkgc_w WW_W6C=%s/w6c_ww "
"%s/ww build --sep -o %s %s 2>/dev/null",
tmpdir, tmpdir, bin, bin, outw, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: wwstage sep build failed\n",
r->label);
rc = -1;
}
}
if (rc == 0) {
snprintf(cmd, sizeof cmd, "cat %s.sepwork/*.s > %s 2>/dev/null",
outc, cs); if (system(cmd)) {}
snprintf(cmd, sizeof cmd, "cat %s.sepwork/*.s > %s 2>/dev/null",
outw, ws); if (system(cmd)) {}
FILE *fc = fopen(cs, "rb");
FILE *fw = fopen(ws, "rb");
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);
}
snprintf(cmd, sizeof cmd, "rm -rf %s", tmpdir);
if (system(cmd)) {}
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,
"idx_structlit_store: 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++;
if (got != rows[i].want) {
fprintf(stderr,
"idx_structlit_store[%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].byteid) continue;
total++;
if (asm_byte_identical(bin, &rows[i], i) != 0)
fail++;
}
}
if (fail) {
fprintf(stderr,
"idx_structlit_store: %d/%d fixtures failed\n",
fail, total);
return 1;
}
printf("idx_structlit_store: %d/%d ok\n", total, total);
return 0;
}