Files
ww/test/wcc/832_tuple_elem_overlong.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

304 lines
10 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.
/*
* 832_tuple_elem_overlong — an OVERLONG array literal in a TUPLE ELEMENT
* position is INVALID ww; BOTH stages must LOUDLY REJECT at check time
* (#20, the #12 + #106 follow-up; rob spec .ai/rob-20-spec.md). #12/#106
* wired checkarrlitfits for a DIRECT array lhs (+ alias) at the decl /
* return / call-arg positions, but the tuple-element position was never
* wired — `let t: ([2]int, i32) = ([1,2,3], 5)` over-fills the [2]int slot
* with 3 initialisers.
*
* WWSTAGE-ONLY fix — cstage already rejects (tuple element-wise
* type_assignable counts elements: `[3]int` vs `[2]int`). Pre-fix wwstage
* divergence (the mutation-sanity target):
* - tuple_arr_over `let t:([2]int,i32)=([1,2,3],5)` : ww silently ACCEPTED
* - tuple_nested_arr `let t:([2][3]int,i32)=([...x3],5)` : the outer [2]
* slot over-filled by 3 sub-arrays (checkarrlitfits
* nested-array recursion under the tuple walk)
*
* The diagnostic TEXT may differ between stages ("over-fill" vs "not
* assignable") — byte-id-blind (stderr is not asm). Both REJECT and emit no
* asm; selfhost has no overlong tuple-elements, so 990-997 byte-id is
* untouched. Do NOT chase message parity.
*
* #24 (DISP-B broad reject, rob spec .ai/rob-24-spec.md): a tuple whose
* ELEMENT is a composite (array / struct / nested-tuple >8B) cannot ride the
* 8B cursor slot (#60 layout) — it silently DROPS on construction and SEGVs
* on the t.N[i] read. Both stages now REJECT such a type at N_TTUPLE
* resolution (kind ∈ {TY_ARRAY, TY_STRUCT, TY_TUPLE} after TY_NAMED chase),
* converting two silent miscompiles into one loud checker error. This FLIPS
* the former tuple_arr_exact / tuple_nested_exact positive controls to the
* neg table (their types are now outlawed) and adds slice/str/tagged-element
* positive controls proving DISP-B does NOT over-reject the inline-header
* kinds. Full inline support deferred to task #60 / DISP-A.
*
* neg row | shape | gate
* -------------------+----------------------------------------------+--------
* tuple_arr_over | let t:([2]int,i32)=([1,2,3],5) | b. FAIL
* tuple_nested_arr | let t:([2][3]int,i32)=([[..],[..],[..]],5) | b. FAIL
* tuple_in_tuple | let t:([2]int,([2]int,i32))=([..],([1,2,3],.))| #26 FAIL
* tuple_return_over | fn()([2]int,i32){return([1,2,3],5)} | #25 FAIL
* tuple_return_nested| fn()([2]int,([2]int,i32)){return(..,([..3],.))| #25 FAIL
* tuple_arr_exact | let t:([2]int,i32)=([1,2],5) | #24 FAIL
* tuple_nested_exact | let t:(i32,([2]int,i32))=(9,([3,4],7)) | #24 FAIL
* tuple_struct_elem | type P=struct{x:int}; let t:(P,i32)=(P{x=1},5)| #24 FAIL
*
* pos row | shape | want
* -------------------+----------------------------------------------+------
* tuple_scalar | let t:(i32,i32)=(1,2); t.1 | 2
* tuple_slice_elem | let t:([]u8,i32)=(a,5); t.1 | 5
* tuple_str_elem | let t:(str,i32)=("hi",7); t.1 | 7
* tuple_tagged_elem | let t:((void|size),i32)=(3,9); t.1 | 9
*/
#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[] = {
/* a scalar-only tuple — all elements ride the 8B slot. */
{ "tuple_scalar",
"package main;\n"
"export fn main() i32 = {\n"
"\tlet t: (i32, i32) = (1, 2);\n"
"\treturn t.1;\n"
"};\n",
2 },
/* #24 positive control — a SLICE element is DISP-B-allowed (its 24B
* header rides the cursor). Must NOT be over-rejected. Readout is the
* scalar t.1 (=5). */
{ "tuple_slice_elem",
"package main;\n"
"export fn main() i32 = {\n"
"\tlet hb: [8]u8;\n"
"\tlet a: []u8; a.ptr = &hb[0]; a.len = 3; a.cap = 8;\n"
"\tlet t: ([]u8, i32) = (a, 5);\n"
"\treturn t.1;\n"
"};\n",
5 },
/* #24 positive control — a STR element is DISP-B-allowed (24B header).
* Readout is the scalar t.1 (=7). */
{ "tuple_str_elem",
"package main;\n"
"export fn main() i32 = {\n"
"\tlet t: (str, i32) = (\"hi\", 7);\n"
"\treturn t.1;\n"
"};\n",
7 },
/* #24 positive control — a TAGGED-UNION element is DISP-B-allowed (its
* tag+payload box rides the slot). Readout is the scalar t.1 (=9). */
{ "tuple_tagged_elem",
"package main;\n"
"export fn main() i32 = {\n"
"\tlet t: ((void | size), i32) = (3, 9);\n"
"\treturn t.1;\n"
"};\n",
9 },
};
/* An overlong array literal in a tuple element — both stages must FAIL the
* build (loud checker diagnostic, not silent accept). */
static const char *neg[] = {
/* tuple_arr_over (the #20 repro) — [2]int slot gets 3 inits. */
"package main;\n"
"export fn main() i32 = {\n"
"\tlet t: ([2]int, i32) = ([1, 2, 3], 5);\n"
"\treturn t.0[1]: i32;\n"
"};\n",
/* tuple_nested_arr — outer [2] slot over-filled by 3 sub-arrays
* (checkarrlitfits recursion under the tuple walk). */
"package main;\n"
"export fn main() i32 = {\n"
"\tlet t: ([2][3]int, i32) = "
"([[1, 2, 3], [4, 5, 6], [7, 8, 9]], 5);\n"
"\treturn t.0[0][0]: i32;\n"
"};\n",
/* tuple_in_tuple (#26) — nested tuple element; inner [2]int over-
* filled by 3. The walk must RECURSE the nested tuple (let pos). */
"package main;\n"
"export fn main() i32 = {\n"
"\tlet t: ([2]int, ([2]int, i32)) = ([1, 2], ([3, 4, 5], 6));\n"
"\treturn t.0[0]: i32;\n"
"};\n",
/* tuple_return_over (#25) — overlong [2]int in a tuple RETURN. */
"package main;\n"
"fn f() ([2]int, i32) = {\n"
"\treturn ([1, 2, 3], 5);\n"
"};\n"
"export fn main() i32 = {\n"
"\tlet t = f();\n"
"\treturn t.1;\n"
"};\n",
/* tuple_return_nested (#25 path × #26 recursion) — nested tuple in a
* RETURN, inner [2]int over-filled. */
"package main;\n"
"fn f() ([2]int, ([2]int, i32)) = {\n"
"\treturn ([1, 2], ([3, 4, 5], 6));\n"
"};\n"
"export fn main() i32 = {\n"
"\tlet t = f();\n"
"\treturn t.0[0]: i32;\n"
"};\n",
/* tuple_arr_exact (#24) — was a GREEN positive control; the DISP-B
* broad reject now OUTLAWS an ARRAY tuple element (silent-drop on
* construction + segv on t.0[i] read). MIGRATED to the neg table. */
"package main;\n"
"export fn main() i32 = {\n"
"\tlet t: ([2]int, i32) = ([1, 2], 5);\n"
"\treturn t.1;\n"
"};\n",
/* tuple_nested_exact (#24) — was a GREEN positive control; a NESTED
* TUPLE element is now outlawed by DISP-B. MIGRATED to the neg table. */
"package main;\n"
"export fn main() i32 = {\n"
"\tlet t: (i32, ([2]int, i32)) = (9, ([3, 4], 7));\n"
"\treturn t.0;\n"
"};\n",
/* tuple_struct_elem (#24) — a STRUCT tuple element is now a checker
* loud (was a cgen "unsupported field-read shape" loud). */
"package main;\n"
"type P = struct { x: int };\n"
"export fn main() i32 = {\n"
"\tlet t: (P, i32) = (P { x = 1 }, 5);\n"
"\treturn t.1;\n"
"};\n",
};
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/teo_%d_d_%d", getpid(), i);
mkdir(tmpdir, 0755);
snprintf(src, sizeof src, "%s/teo_%d_%d.ww", tmpdir, getpid(), i);
snprintf(outbin, sizeof outbin, "%s/teo_%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) {
fprintf(stderr, "row[%s]: build via %s failed\n",
r->label, driver);
runwait(rmcmd);
return -1;
}
int got = runwait(outbin);
runwait(rmcmd);
return got;
}
/* build_should_fail — an overlong tuple-element array must error on
* `driver`; returns 0 when the build correctly FAILS, non-zero when it
* wrongly succeeded. */
static int
build_should_fail(const char *driver, const char *src, int i)
{
char tmpdir[64], s[128], outbin[128], rmcmd[160], cmd[1024];
snprintf(tmpdir, sizeof tmpdir, "/tmp/teon_%d_d_%d", getpid(), i);
mkdir(tmpdir, 0755);
snprintf(s, sizeof s, "%s/teon_%d_%d.ww", tmpdir, getpid(), i);
snprintf(outbin, sizeof outbin, "%s/teon_%d_%d", tmpdir, getpid(), i);
snprintf(rmcmd, sizeof rmcmd, "rm -rf %s", tmpdir);
FILE *f = fopen(s, "wb");
if (!f) { runwait(rmcmd); return -1; }
fputs(src, f);
fclose(f);
snprintf(cmd, sizeof cmd, "%s build -o %s %s 2>/dev/null",
driver, outbin, s);
int rc = runwait(cmd);
runwait(rmcmd);
return rc == 0 ? -1 : 0; /* build must NOT succeed */
}
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 nn = (int)(sizeof neg / sizeof neg[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, "tuple_elem_overlong: 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,
"tuple_elem_overlong[%s][%s]: exit=%d want=%d\n",
drivers[d].name, rows[i].label,
got, rows[i].want);
fail++;
}
}
for (int i = 0; i < nn; i++) {
total++;
if (build_should_fail(drivers[d].path, neg[i],
100 + i) != 0) {
fprintf(stderr,
"tuple_elem_overlong[%s][neg%d]: built ok, "
"expected a loud error\n",
drivers[d].name, i);
fail++;
}
}
}
if (fail) {
fprintf(stderr,
"tuple_elem_overlong: %d/%d fixtures failed\n", fail, total);
return 1;
}
printf("tuple_elem_overlong: %d/%d ok\n", total, total);
return 0;
}