Files
ww/test/wcc/744_letcopy_struct.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

376 lines
11 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.
/*
* 744_letcopy_struct — sentinel for STATUS-6 #32. Class B-symmetric
* cgen miscompile: `let p2: T = p1;` where T is a struct >8B and rhs
* is a local ident silently zero-inits p2 (cstage emitted nothing for
* the copy; wwstage emitted a single MOVQ AX + stale BX from the
* sz==16 str-init tail). Reads after the let saw whatever the stack
* held — silent partial-copy / silent zero on a fresh frame.
*
* Both stages now byte-copy the source slot to the dest slot
* per-qword with a sized tail (MOVL/MOVB) for natural sizes not
* 8-aligned. Mirrors cg_widen_tagged_store's struct-ident payload
* copy. See cmd/w6c/cgen.c N_LET and selfhost/cmd/wcc/cgenstmt.ww
* cglet.
*
* Rows exercise the 4 struct shapes the task brief called out:
* (a) 3-field i32 (sz=12, MOVQ + MOVL tail).
* (b) i32 + str field (sz=24, 3 × MOVQ).
* (c) i32 + []u8 slice field (sz=32, 4 × MOVQ).
* (d) i32 + tagged (i32|str) field (sz=32, 4 × MOVQ).
*
* Each row pins:
* - asm-presence in both stages: produce body emits `min_loads`
* `MOVQ -K(BP), AX` source-slot loads (the bug pin — pre-fix
* cstage emitted 0; wwstage emitted 1 with garbage BX tail).
* - asm byte-id between stages (cmp -s on the produce-bearing .s).
* - runtime: produce() returns 0 (every field round-tripped).
*/
#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;
}
static int
slurp(const char *path, char *buf, size_t cap)
{
FILE *f = fopen(path, "rb");
if (!f) return -1;
size_t n = fread(buf, 1, cap - 1, f);
fclose(f);
buf[n] = '\0';
return (int)n;
}
static int
count_substr(const char *start, const char *end, const char *needle)
{
int n = 0;
size_t nlen = strlen(needle);
const char *p = start;
while (p + nlen <= end) {
if (memcmp(p, needle, nlen) == 0) { n++; p += nlen; }
else { p++; }
}
return n;
}
struct row {
const char *label;
/* Direct-w6c source: bare fn produce, no package/imports. */
const char *asm_src;
/* Driver source: `package main; import os; ...` runtime wrapper. */
const char *run_src;
/* Source-slot loads expected in produce: at least N for the copy.
* Pre-fix cstage emitted 0; pre-fix wwstage emitted 1 with stale BX.
* `MOVQ -K(BP), AX` is also emitted by p2.a / p2.s.len reads in the
* checks, so set min_loads to a value the copy alone surpasses. */
int min_loads;
};
static const struct row rows[] = {
/* (a) 3-field i32 struct, sz=12. Copy = 1 × MOVQ + 1 × MOVL.
* p1 inits via structlit to sidestep the pre-existing no-rhs
* zero-init divergence for non-8-multiple struct sizes
* (cstage uses natural sz=12 → MOVQ+MOVL, wwstage uses slot
* sz=16 → MOVQ+MOVQ). Tracked as the #15/#26c rule-10
* size-strategy convergence follow-up. */
{ "tri_i32",
"type tri = struct { a: i32, b: i32, c: i32 };\n"
"export fn produce() i32 = {\n"
" let p1: tri = tri{a=11, b=22, c=33};\n"
" let p2: tri = p1;\n"
" if (p2.a != 11) { return 11; };\n"
" if (p2.b != 22) { return 12; };\n"
" if (p2.c != 33) { return 13; };\n"
" return 0;\n"
"};\n",
"package main;\n"
"import os;\n"
"type tri = struct { a: i32, b: i32, c: i32 };\n"
"fn produce() i32 = {\n"
" let p1: tri = tri{a=11, b=22, c=33};\n"
" let p2: tri = p1;\n"
" if (p2.a != 11) { return 11; };\n"
" if (p2.b != 22) { return 12; };\n"
" if (p2.c != 33) { return 13; };\n"
" return 0;\n"
"};\n"
"export fn main() i32 = { os.exit(produce()); };\n",
1 },
/* (b) i32 + str field, sz=24. Copy = 3 × MOVQ. */
{ "field_str",
"type ws = struct { a: i32, s: str };\n"
"export fn produce() i32 = {\n"
" let p1: ws;\n"
" p1.a = 7; p1.s = \"hi\";\n"
" let p2: ws = p1;\n"
" if (p2.a != 7) { return 11; };\n"
" if (p2.s.len != 2) { return 12; };\n"
" return 0;\n"
"};\n",
"package main;\n"
"import os;\n"
"type ws = struct { a: i32, s: str };\n"
"fn produce() i32 = {\n"
" let p1: ws;\n"
" p1.a = 7; p1.s = \"hi\";\n"
" let p2: ws = p1;\n"
" if (p2.a != 7) { return 11; };\n"
" if (p2.s.len != 2) { return 12; };\n"
" return 0;\n"
"};\n"
"export fn main() i32 = { os.exit(produce()); };\n",
3 },
/* (c) i32 + []u8 slice field, sz=32. Copy = 4 × MOVQ. */
{ "field_slice",
"type wsl = struct { a: i32, b: []u8 };\n"
"export fn produce() i32 = {\n"
" let raw: [3]u8 = [1: u8, 2: u8, 3: u8];\n"
" let p1: wsl;\n"
" p1.a = 9; p1.b = raw[0:3];\n"
" let p2: wsl = p1;\n"
" if (p2.a != 9) { return 11; };\n"
" if (p2.b.len != 3) { return 12; };\n"
" return 0;\n"
"};\n",
"package main;\n"
"import os;\n"
"type wsl = struct { a: i32, b: []u8 };\n"
"fn produce() i32 = {\n"
" let raw: [3]u8 = [1: u8, 2: u8, 3: u8];\n"
" let p1: wsl;\n"
" p1.a = 9; p1.b = raw[0:3];\n"
" let p2: wsl = p1;\n"
" if (p2.a != 9) { return 11; };\n"
" if (p2.b.len != 3) { return 12; };\n"
" return 0;\n"
"};\n"
"export fn main() i32 = { os.exit(produce()); };\n",
4 },
/* (d) i32 + tagged (i32|str) field, sz=32. Copy = 4 × MOVQ.
* Runtime checks only p2.a; the asm load-count (>=4) and the
* byte-id gate together prove the full 32B slot copied — match
* on p2.t avoided because wwstage's match scrutinee spill grows
* the frame and reorders ABI loads vs cstage (pre-existing
* #15/#26c rule-10 size-strategy / match-spill divergence). */
{ "field_tagged",
"type tag = (i32 | str);\n"
"type wtg = struct { a: i32, t: tag };\n"
"export fn produce() i32 = {\n"
" let p1: wtg = wtg{a=5, t=42: tag};\n"
" let p2: wtg = p1;\n"
" if (p2.a != 5) { return 11; };\n"
" return 0;\n"
"};\n",
"package main;\n"
"import os;\n"
"type tag = (i32 | str);\n"
"type wtg = struct { a: i32, t: tag };\n"
"fn produce() i32 = {\n"
" let p1: wtg = wtg{a=5, t=42: tag};\n"
" let p2: wtg = p1;\n"
" if (p2.a != 5) { return 11; };\n"
" match (p2.t) {\n"
" case let x: i32 => if (x != 42) { return 12; };\n"
" case let s: str => return 13;\n"
" };\n"
" return 0;\n"
"};\n"
"export fn main() i32 = { os.exit(produce()); };\n",
4 },
};
/* Locate `TEXT produce` body in the asm file and count source-slot
* loads (`MOVQ\t-K(BP), AX`) inside it. Pre-fix cstage produce had 0;
* pre-fix wwstage produce had 1 (no tail). */
static int
check_copy_loads(const char *spath, const struct row *r)
{
static char buf[1 << 16];
if (slurp(spath, buf, sizeof buf) < 0) return -1;
const char *body = strstr(buf, "TEXT produce");
if (!body) {
fprintf(stderr, "row[%s]: no TEXT produce label in %s\n",
r->label, spath);
return -1;
}
/* End at the next TEXT (or EOF). */
const char *end = strstr(body + 1, "\nTEXT ");
if (!end) end = buf + strlen(buf);
int loads = count_substr(body, end, "MOVQ\t-");
if (loads < r->min_loads) {
fprintf(stderr,
"row[%s]: only %d `MOVQ -K(BP), AX` source loads in "
"produce; expected >= %d (copy missing)\n",
r->label, loads, r->min_loads);
return -1;
}
return 0;
}
static int
emit_s(const char *w6c, const struct row *r, int i, int stage,
char *out_s, size_t cap)
{
char src[64], cmd[1024];
snprintf(src, sizeof src, "/tmp/lcs_asm_%d_%d_%d.ww",
getpid(), i, stage);
snprintf(out_s, cap, "/tmp/lcs_asm_%d_%d_%d.s",
getpid(), i, stage);
FILE *f = fopen(src, "wb");
if (!f) return -1;
fputs(r->asm_src, f);
fclose(f);
snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, out_s, src);
int rc = runwait(cmd);
unlink(src);
return rc;
}
static int
run_driver(const char *driver, const struct row *r, int i, int stage)
{
char tmpdir[64], src[128], outbin[128], rmcmd[160], cmd[1024];
snprintf(tmpdir, sizeof tmpdir, "/tmp/lcs_run_%d_d_%d_%d",
getpid(), i, stage);
mkdir(tmpdir, 0755);
snprintf(src, sizeof src, "%s/lcs_run_%d_%d_%d.ww",
tmpdir, getpid(), i, stage);
snprintf(outbin, sizeof outbin, "%s/lcs_run_%d_%d_%d",
tmpdir, getpid(), i, stage);
snprintf(rmcmd, sizeof rmcmd, "rm -rf %s", tmpdir);
FILE *f = fopen(src, "wb");
if (!f) { runwait(rmcmd); return -1; }
fputs(r->run_src, f);
fclose(f);
snprintf(cmd, sizeof cmd, "%s build -o %s %s 2>/dev/null",
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[512];
if (bin[0] != '/') {
char cwd[256];
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
bin = absbin;
}
char w6c[640], w6c_ww[640], cdrv[640], wdrv[640];
snprintf(w6c, sizeof w6c, "%s/w6c", bin);
snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin);
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin);
int have_ww = (access(w6c_ww, X_OK) == 0);
int have_wdrv = (access(wdrv, X_OK) == 0);
int n = (int)(sizeof rows / sizeof rows[0]);
int total = 0, fail = 0;
for (int i = 0; i < n; i++) {
const struct row *r = &rows[i];
char cs_path[160], ws_path[160];
/* cstage asm-presence. */
if (emit_s(w6c, r, i, 0, cs_path, sizeof cs_path) != 0) {
fprintf(stderr,
"letcopy_struct[cstage][%s]: w6c failed\n",
r->label);
fail++; total++; continue;
}
total++;
if (check_copy_loads(cs_path, r) != 0) fail++;
/* wwstage asm-presence + byte-id. */
if (have_ww) {
if (emit_s(w6c_ww, r, i, 1, ws_path, sizeof ws_path)
!= 0) {
fprintf(stderr,
"letcopy_struct[wwstage][%s]: "
"w6c_ww failed\n", r->label);
fail++; total++;
unlink(cs_path);
continue;
}
total++;
if (check_copy_loads(ws_path, r) != 0) fail++;
total++;
char cmd[512];
snprintf(cmd, sizeof cmd, "cmp -s %s %s",
cs_path, ws_path);
if (runwait(cmd) != 0) {
fprintf(stderr,
"letcopy_struct[%s]: cstage vs wwstage "
"asm differs\n", r->label);
fail++;
}
unlink(ws_path);
}
unlink(cs_path);
/* Runtime correctness via cstage driver. */
total++;
int got = run_driver(cdrv, r, i, 2);
if (got != 0) {
fprintf(stderr,
"letcopy_struct[ww/%s]: exit=%d want=0\n",
r->label, got);
fail++;
}
/* Runtime correctness via wwstage driver. */
if (have_wdrv) {
total++;
got = run_driver(wdrv, r, i, 3);
if (got != 0) {
fprintf(stderr,
"letcopy_struct[ww_ww/%s]: exit=%d want=0\n",
r->label, got);
fail++;
}
}
}
if (fail) {
fprintf(stderr,
"letcopy_struct: %d/%d fixtures failed\n", fail, total);
return 1;
}
printf("letcopy_struct: %d/%d ok\n", total, total);
return 0;
}