Files
ww/test/wcc/650_dot_chain.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

430 lines
14 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 650_dot_chain — chained dotted access through value-struct fields
* must compile to a single load/store at (base + sum-of-offsets),
* not silently lower to an undefined global symbol or shuffle stale
* registers. Drew filed this from worker-bufio when the Hare-shaped
* `scanner` embed in lib/bufio tripped the cgen.
*
* Each fixture exercises a different chain shape. The repro from
* drew's bug report is row[0]; the rest pin: 3-deep chains, slice
* pseudo-field tails (`s.buf.len`), global-rooted chains, and
* mixed leaf widths (u8 / i32 / u32). `&o.i.a` address-of and i8
* sign-extend leaves are deferred (tasks #9 / #10).
*
* Exercises both stages via `ww` (cstage) and `ww_ww` (wwstage)
* when present, so a regression on either side is caught here.
*/
#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; };
static const struct row rows[] = {
/* Drew's repro: 2-deep value-struct read AND write. Returns
* o.i.a (=10) + o.x (=5) = 15. */
{ "drew_2deep_read_write",
"type inner = struct { a: i32, b: i32, c: i32 };\n"
"type outer = struct { i: inner, x: i32 };\n"
"fn main() i32 = {\n"
" let o: outer; o.i.a = 10; o.x = 5;\n"
" return o.i.a + o.x;\n"
"};\n",
15 },
/* 3-deep value-struct chain. Proves the spine walker is loop-
* shaped, not hardcoded to depth 2. */
{ "value_struct_3deep",
"type a3 = struct { a: i32 };\n"
"type a2 = struct { a: a3 };\n"
"type a1 = struct { a: a2 };\n"
"fn main() i32 = {\n"
" let v: a1;\n"
" v.a.a.a = 7;\n"
" return v.a.a.a;\n"
"};\n",
7 },
/* Slice pseudo-field tail through a value-struct field: write
* .ptr/.len/.cap on s.buf where buf: []u8 is a struct field.
* Read back .len through the same chain. */
{ "slice_field_pseudo",
"type bag = struct { buf: []u8, x: i32 };\n"
"fn main() i32 = {\n"
" let arr: [8]u8;\n"
" let i: i32 = 0;\n"
" for (i < 8) { arr[i] = i: u8; i = i + 1; };\n"
" let b: bag;\n"
" b.buf.ptr = &arr[0];\n"
" b.buf.len = 5;\n"
" b.buf.cap = 8;\n"
" b.x = 0;\n"
" return b.buf.len;\n"
"};\n",
5 },
/* u8 leaf through a chained struct: round-trips a single byte
* unchanged (no narrow cast in play — direct MOVB write +
* MOVZBQ read). Pins the leaf-width dispatch in the walker. */
{ "u8_leaf_through_chain",
"type holder = struct { val: u8, pad: u8 };\n"
"type box = struct { h: holder, tag: i32 };\n"
"fn main() i32 = {\n"
" let b: box;\n"
" b.h.val = 99u8;\n"
" b.h.pad = 0u8;\n"
" b.tag = 0;\n"
" return b.h.val: i32;\n"
"};\n",
99 },
/* i32 leaf through a chained struct, value > 255 to confirm the
* MOVL store width (not silently narrowed to MOVB). Returns 42
* iff the full 4-byte value round-trips. */
{ "i32_leaf_through_chain",
"type holder = struct { val: i32, pad: i32 };\n"
"type box = struct { h: holder, tag: i32 };\n"
"fn main() i32 = {\n"
" let b: box;\n"
" b.h.val = 0x12345;\n"
" b.h.pad = 0;\n"
" b.tag = 0;\n"
" if (b.h.val == 0x12345) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
/* u32 leaf through a chained struct. Distinct write + read at
* the chained leaf, value chosen so the MOVL store + load
* round-trip preserves all four bytes. */
{ "u32_leaf_through_chain",
"type holder = struct { val: u32, pad: u32 };\n"
"type box = struct { h: holder, tag: i32 };\n"
"fn main() i32 = {\n"
" let b: box;\n"
" b.h.val = 4321u32;\n"
" b.h.pad = 0u32;\n"
" b.tag = 0;\n"
" if (b.h.val == 4321u32) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
/* Global-rooted chain: write+read through a top-level `let g`.
* Pins the LEAQ name(SB), CX → MOVQ disp(CX) path on both
* stages (the local-rooted rows above pin the BP-relative
* path). 11 + 3 = 14. */
{ "global_root_chain",
"type inner = struct { a: i32, b: i32 };\n"
"type outer = struct { i: inner, x: i32 };\n"
"let g: outer;\n"
"fn main() i32 = {\n"
" g.i.a = 11; g.x = 3;\n"
" return g.i.a + g.x;\n"
"};\n",
14 },
/* `*T` parameter, single-dot read: `fn f(o: *outer) i32 = o.f`.
* Already worked via the existing pointer-to-struct-field
* branch; pinned here so future refactors don't regress it. */
{ "ptr_root_single_read",
"type outer = struct { f: i32, g: i32 };\n"
"fn get(o: *outer) i32 = { return o.f; };\n"
"fn main() i32 = {\n"
" let x: outer; x.f = 42; x.g = 0;\n"
" return get(&x);\n"
"};\n",
42 },
/* `*T` parameter, single-dot write: `fn f(o: *outer) void = …`.
* Already worked via the via_ptr branch in N_ASSIGN; pinned
* for the same reason. */
{ "ptr_root_single_write",
"type outer = struct { f: i32, g: i32 };\n"
"fn set(o: *outer) void = { o.f = 42; };\n"
"fn main() i32 = {\n"
" let x: outer; x.f = 0; x.g = 0;\n"
" set(&x);\n"
" return x.f;\n"
"};\n",
42 },
/* Drew's report shape — `*T` parameter, 2-deep read.
* Used to lower to `MOVQ a(SB), AX` (link-time undefined ref).
* Now the spine walker dereferences the root pointer. */
{ "ptr_root_2deep_read",
"type inner = struct { a: i32, b: i32 };\n"
"type outer = struct { i: inner, x: i32 };\n"
"fn get(o: *outer) i32 = { return o.i.a; };\n"
"fn main() i32 = {\n"
" let v: outer; v.i.a = 7; v.i.b = 0; v.x = 0;\n"
" return get(&v);\n"
"};\n",
7 },
/* `*T` parameter, 2-deep write — bufio.init shape.
* Previously silently dropped; now MOVQ off(BP), CX +
* store at total_off(CX). */
{ "ptr_root_2deep_write",
"type inner = struct { a: i32, b: i32 };\n"
"type outer = struct { i: inner, x: i32 };\n"
"fn set(o: *outer) void = { o.i.a = 5; };\n"
"fn main() i32 = {\n"
" let v: outer; v.i.a = 0; v.i.b = 0; v.x = 0;\n"
" set(&v);\n"
" return v.i.a;\n"
"};\n",
5 },
/* 3-deep `*T`-rooted: proves the spine walker loops past the
* pointer-root hop and isn't hardcoded at depth 2. */
{ "ptr_root_3deep",
"type a3 = struct { x: i32 };\n"
"type a2 = struct { a: a3 };\n"
"type a1 = struct { a: a2 };\n"
"fn put(p: *a1) void = { p.a.a.x = 9; };\n"
"fn get(p: *a1) i32 = { return p.a.a.x; };\n"
"fn main() i32 = {\n"
" let v: a1; v.a.a.x = 0;\n"
" put(&v);\n"
" return get(&v);\n"
"};\n",
9 },
/* `*T` *local* (not just param) — same emit path through
* localfind, but exercised separately for symmetry. */
{ "ptr_root_local",
"type inner = struct { a: i32, b: i32 };\n"
"type outer = struct { i: inner, x: i32 };\n"
"fn main() i32 = {\n"
" let v: outer; v.i.a = 0; v.i.b = 0; v.x = 0;\n"
" let p: *outer = &v;\n"
" p.i.a = 13;\n"
" return p.i.a;\n"
"};\n",
13 },
/* str leaf field through a `*T`-rooted chain — exercises the
* (AX=ptr, BX=len) convention through the CX-base load. */
{ "ptr_root_str_leaf",
"type holder = struct { s: str, pad: i32 };\n"
"type box = struct { h: holder, tag: i32 };\n"
"fn put(b: *box) void = { b.h.s = \"abc\"; };\n"
"fn main() i32 = {\n"
" let b: box;\n"
" let empty: str; b.h.s = empty; b.h.pad = 0; b.tag = 0;\n"
" put(&b);\n"
" if (b.h.s.len == 3) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
/* slice pseudo-field on a `*T`-rooted chain. .len of an empty
* slice round-trips through the CX-base load. */
{ "ptr_root_slice_pseudo",
"type bag = struct { buf: []u8, x: i32 };\n"
"fn setlen(b: *bag, n: i32) void = { b.buf.len = n; };\n"
"fn main() i32 = {\n"
" let b: bag;\n"
" let nope: []u8; b.buf = nope; b.x = 0;\n"
" setlen(&b, 5);\n"
" return b.buf.len: i32;\n"
"};\n",
5 },
/* f64 leaf field through a `*T`-rooted chain — exercises the
* X0 spill / MOVSD store path off the CX base. */
{ "ptr_root_f64_leaf",
"type holder = struct { v: f64, pad: i32 };\n"
"type box = struct { h: holder, tag: i32 };\n"
"fn put(b: *box) void = { b.h.v = 0.5f64; };\n"
"fn main() i32 = {\n"
" let b: box; b.h.v = 0.0f64; b.h.pad = 0; b.tag = 0;\n"
" put(&b);\n"
" if (b.h.v == 0.5f64) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
/* Sub-word leaf widths through a `*T`-rooted chain — pins
* fldloadop / fldstoreop on the CX-base path. u8/i8/u16/i16/i32
* all stored then read back through the inner pointer. */
{ "ptr_root_subword_u8",
"type holder = struct { v: u8, pad: u8 };\n"
"type box = struct { h: holder, tag: i32 };\n"
"fn put(b: *box) void = { b.h.v = 99u8; };\n"
"fn main() i32 = {\n"
" let b: box; b.h.v = 0u8; b.h.pad = 0u8; b.tag = 0;\n"
" put(&b);\n"
" return b.h.v: i32;\n"
"};\n",
99 },
{ "ptr_root_subword_i32",
"type holder = struct { v: i32, pad: i32 };\n"
"type box = struct { h: holder, tag: i32 };\n"
"fn put(b: *box) void = { b.h.v = 0x12345; };\n"
"fn main() i32 = {\n"
" let b: box; b.h.v = 0; b.h.pad = 0; b.tag = 0;\n"
" put(&b);\n"
" if (b.h.v == 0x12345) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
{ "ptr_root_subword_i16",
"type holder = struct { v: i16, pad: i16 };\n"
"type box = struct { h: holder, tag: i32 };\n"
"fn put(b: *box) void = { b.h.v = 4321i16; };\n"
"fn main() i32 = {\n"
" let b: box; b.h.v = 0i16; b.h.pad = 0i16; b.tag = 0;\n"
" put(&b);\n"
" if (b.h.v == 4321i16) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
/* Negative signed leaf widths through a `*T`-rooted chain.
* Stored value's sign bit must round-trip — pins localloadop /
* fldloadop sign-extension on the CX-base path so `*T`-rooted
* chains don't silently widen to a positive bit pattern. i8, i16
* and i32 each store 7 and check `< 0` after the chained read. */
{ "ptr_root_subword_i8_neg",
"type holder = struct { v: i8, pad: i8 };\n"
"type box = struct { h: holder, tag: i32 };\n"
"fn put(b: *box) void = { b.h.v = -7i8; };\n"
"fn main() i32 = {\n"
" let b: box; b.h.v = 0i8; b.h.pad = 0i8; b.tag = 0;\n"
" put(&b);\n"
" let r: i32 = b.h.v: i32;\n"
" if (r == -7) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
{ "ptr_root_subword_i16_neg",
"type holder = struct { v: i16, pad: i16 };\n"
"type box = struct { h: holder, tag: i32 };\n"
"fn put(b: *box) void = { b.h.v = -7i16; };\n"
"fn main() i32 = {\n"
" let b: box; b.h.v = 0i16; b.h.pad = 0i16; b.tag = 0;\n"
" put(&b);\n"
" let r: i32 = b.h.v: i32;\n"
" if (r == -7) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
{ "ptr_root_subword_i32_neg",
"type holder = struct { v: i32, pad: i32 };\n"
"type box = struct { h: holder, tag: i32 };\n"
"fn put(b: *box) void = { b.h.v = -7; };\n"
"fn main() i32 = {\n"
" let b: box; b.h.v = 0; b.h.pad = 0; b.tag = 0;\n"
" put(&b);\n"
" if (b.h.v < 0) { return 42; };\n"
" return 0;\n"
"};\n",
42 },
/* Round-trip across the aliasing seam: caller writes via the
* local value, callee reads via the `*T`-rooted chain — and
* vice versa. Pins that both branches index the SAME slot
* (same field offsets resolved off the right base). */
{ "ptr_root_roundtrip_local_write_ptr_read",
"type inner = struct { a: i32, b: i32 };\n"
"type outer = struct { i: inner, x: i32 };\n"
"fn get(o: *outer) i32 = { return o.i.a; };\n"
"fn main() i32 = {\n"
" let o: outer; o.i.a = 0; o.i.b = 0; o.x = 0;\n"
" let p: *outer = &o;\n"
" o.i.a = 42;\n"
" return get(p);\n"
"};\n",
42 },
{ "ptr_root_roundtrip_ptr_write_local_read",
"type inner = struct { a: i32, b: i32 };\n"
"type outer = struct { i: inner, x: i32 };\n"
"fn main() i32 = {\n"
" let o: outer; o.i.a = 0; o.i.b = 0; o.x = 0;\n"
" let p: *outer = &o;\n"
" p.i.a = 42;\n"
" return o.i.a;\n"
"};\n",
42 },
};
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/wdc_%d_d_%d", getpid(), i);
mkdir(tmpdir, 0755);
snprintf(src, sizeof src, "%s/wdc_%d_%d.ww", tmpdir, getpid(), i);
snprintf(outbin, sizeof outbin, "%s/wdc_%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",
driver, outbin, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: build via %s failed\n",
r->label, driver);
runwait(rmcmd);
return -1;
}
int got = runwait(outbin);
runwait(rmcmd);
return got;
}
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, "dot_chain: 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,
"dot_chain[%s][%s]: exit=%d want=%d\n",
drivers[d].name, rows[i].label,
got, rows[i].want);
fail++;
}
}
}
if (fail) {
fprintf(stderr,
"dot_chain: %d/%d fixtures failed\n", fail, total);
return 1;
}
printf("dot_chain: %d/%d ok\n", total, total);
return 0;
}