Files
ww/test/wcc/961_opaque_guards.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

307 lines
10 KiB
C

/*
* 961_opaque_guards — #108 sub-fold (b): the opaque USE-GUARDS.
*
* #108(a) made `opaque` an abstract, UNSIZED type (size = align =
* SIZE_UNDEFINED = (u64)-1), legal only behind indirection (`*opaque`
* 8B, `[]opaque` 24B header). That opened a footgun: a bare use of
* opaque where a concrete byte size is needed would fabricate a
* (u64)-1-byte slot — a silent miscompile (rule 7). This fold makes
* every such use a LOUD compile error.
*
* opaque is illegal by-value in FOUR aggregate positions (array elem,
* struct field, tuple member, tagged-union variant) + as a bare value,
* under size/align, and as a []opaque element-index. The guards (cstage
* cmd/wcc/check.c), each a build-fails row:
*
* guard | misuse | message
* ------+--------------------------------+---------------------------------
* 1 | bare local `let x: opaque` | unsized type 'opaque' cannot be a
* | param `fn f(x: opaque)` | variable / a parameter /
* | return-by-value `fn f() opaque` | a return type
* 2 | struct field `{ x: opaque }` | ... cannot be a struct field
* 3 | array elem `[N]opaque` | ... cannot be an array element
* 3t | tuple member `(opaque, i32)` | ... cannot be a tuple member
* 3u | tagged variant `(opaque|i32)` | ... cannot be a tagged union member
* 4 | size(opaque) / align(opaque) | cannot take size/align of unsized
* | | type 'opaque'
* 5 | indexing `s[i]`, s: []opaque | cannot index []opaque: element
* | | type 'opaque' has undefined size
*
* Nested aggregates (size([4]opaque), size(struct{x:opaque}),
* size((opaque,i32))) are caught transitively: the cstage rejects the
* inner aggregate at its own construction, and the wwstage size/align
* fold detects them via a RECURSIVE astunsized (an aggregate is unsized
* iff any member is). The tuple/tagged size() rows are the exact
* gate-blind miscompile the first #108(b) attempt (c9e98ca) left open —
* size((opaque,i32)) built and folded to 3.
*
* Detection is via the SIZE_UNDEFINED sentinel (the size/align the guard
* consults), so the legal sized forms `*opaque` (8B) and `[]opaque` (24B
* header) pass untouched — positive rows below + the 960 probe pin that.
* Mirrors harec's scattered `size == SIZE_UNDEFINED` guards
* (ref/harec/src/check.c:1524 binding, :3931 return-by-value, :2720
* size-of, :384 slice-index; ref/harec/src/type_store.c:1147 tuple
* member / :449 tagged variant; field/array at the type-construction
* sites).
*
* Stage placement (rule 10, per-guard — see also the worker report):
* - Guards 1/2/3/3t/3u/5 are CSTAGE-ONLY. The wwstage check.ww is an
* AST-level approximation: it has no binding-size computation (g1),
* no type-decl field/element/member validation walk (g2/g3/3t/3u),
* and its N_INDEX `indexresult` returns the element type without
* consulting its size and is documented to defer invalid-index
* rejection to the cstage (g5). Adding twins there would mean
* building check-sites the leaner stage doesn't have — same
* cstage-only precedent as 712_redecl / 708_param_shadow_mod.
* - Guard 4 is BOTH-STAGES. The wwstage HAS the size()/align() fold
* (exprtype + astsize/astalign), which would otherwise fold opaque
* (and any opaque-containing aggregate) to a bogus 0 (a silent
* miscompile); the twin is a RECURSIVE astunsized + deffolderr,
* since the wwstage lacks the cstage's per-construction guards and
* so its fold alone must detect tuple/tagged/nested opaque uses.
* Verified by hand on w6c_ww / wwdump_ww (this cstage-driver test
* does not exercise the wwstage, like 712/960).
*
* opaque is unused by the bootstrap, so every guard is inert on the
* selfhost corpus — 990-997 stay byte-identical.
*
* Driven like 712_redecl: kind==0 rows must FAIL to build; kind==1 rows
* must build AND exit with `want`.
*/
#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;
}
/*
* kind == 0: negative — build must fail (any nonzero exit).
* kind == 1: positive — build must succeed AND binary exits with `want`.
*/
struct row { const char *label; int kind; const char *src; int want; };
static const struct row rows[] = {
/* guard 1 — bare local. */
{ "neg_bare_local", 0,
"package main;\n"
"export fn main() i32 = {\n"
" let x: opaque;\n"
" return 0;\n"
"};\n",
0 },
/* guard 1 — by-value parameter. */
{ "neg_param", 0,
"package main;\n"
"fn f(x: opaque) i32 = { return 0; };\n"
"export fn main() i32 = { return 0; };\n",
0 },
/* guard 1 — return-by-value. */
{ "neg_return", 0,
"package main;\n"
"fn f() opaque = { return 0; };\n"
"export fn main() i32 = { return 0; };\n",
0 },
/* guard 2 — opaque struct field. */
{ "neg_struct_field", 0,
"package main;\n"
"type S = struct { x: opaque };\n"
"export fn main() i32 = { return 0; };\n",
0 },
/* guard 3 — [N]opaque array element. */
{ "neg_array_elem", 0,
"package main;\n"
"export fn main() i32 = {\n"
" let a: [4]opaque;\n"
" return 0;\n"
"};\n",
0 },
/* guard 4 — size(opaque). */
{ "neg_size_of", 0,
"package main;\n"
"export fn main() i32 = { return size(opaque): i32; };\n",
0 },
/* guard 4 — align(opaque). */
{ "neg_align_of", 0,
"package main;\n"
"export fn main() i32 = { return align(opaque): i32; };\n",
0 },
/* guard 5 — indexing a []opaque (legal sized header, unsized elem).
* The result is cast to i32 so this trips ONLY the index guard, not
* the bare-local guard. */
{ "neg_slice_index", 0,
"package main;\n"
"export fn main() i32 = {\n"
" let s: []opaque;\n"
" let v: i32 = s[0]: i32;\n"
" return v;\n"
"};\n",
0 },
/* guard tuple — opaque as a tuple member. The tuple type is rejected
* at construction (cstage type_store.c:1147); the wwstage twin folds
* it via the recursive astunsized. */
{ "neg_tuple_member", 0,
"package main;\n"
"export fn main() i32 = {\n"
" let t: (opaque, i32);\n"
" return 0;\n"
"};\n",
0 },
/* guard tuple — size((opaque, i32)). This is the exact gate-blind
* miscompile c9e98ca left: it built + folded to 3. Now rejected. */
{ "neg_size_tuple", 0,
"package main;\n"
"export fn main() i32 = { return size((opaque, i32)): i32; };\n",
0 },
/* guard tagged — opaque as a tagged-union variant (an unsized variant
* has no payload slot; cstage type_store.c:449). */
{ "neg_tagged_variant", 0,
"package main;\n"
"export fn main() i32 = {\n"
" let x: (opaque | i32);\n"
" return 0;\n"
"};\n",
0 },
/* guard tagged — size((opaque | i32)). */
{ "neg_size_tagged", 0,
"package main;\n"
"export fn main() i32 = { return size((opaque | i32)): i32; };\n",
0 },
/* nested — size([4]opaque): the unsized leaf is one level down. Proves
* the wwstage's astunsized descends (cstage rejects at array constr). */
{ "neg_size_nested_array", 0,
"package main;\n"
"export fn main() i32 = { return size([4]opaque): i32; };\n",
0 },
/* nested — size(struct { x: opaque }): an unsized field one level down. */
{ "neg_size_nested_struct", 0,
"package main;\n"
"export fn main() i32 = { return size(struct { x: opaque }): i32; };\n",
0 },
/* pos control — `*opaque` is sized (8B): a local, a param, a return,
* and size()/align() of it all compile. Round-trips a real *i32. */
{ "pos_ptr_opaque", 1,
"package main;\n"
"fn id(p: *opaque) *opaque = { return p; };\n"
"export fn main() i32 = {\n"
" let n: i32 = 42;\n"
" let po: *opaque = (&n): *opaque;\n"
" let back: *i32 = id(po): *i32;\n"
" let w: i32 = size(*opaque): i32;\n"
" if (w != 8) { return 1; };\n"
" return *back;\n"
"};\n",
42 },
/* pos control — `[]opaque` is sized (24B header): a local + size()
* of it compile. size([]opaque) == 24. */
{ "pos_slice_opaque", 1,
"package main;\n"
"export fn main() i32 = {\n"
" let s: []opaque;\n"
" let h: i32 = size([]opaque): i32;\n"
" if (h != 24) { return 1; };\n"
" return s.len: i32 + 7;\n"
"};\n",
7 },
};
static int
run_row(const char *driver, const struct row *r, int i)
{
char src[128], tmpdir[128], outbin[128], rmcmd[160], cmd[2048];
snprintf(tmpdir, sizeof tmpdir, "/tmp/wcopg_%d_d_%d", getpid(), i);
mkdir(tmpdir, 0755);
snprintf(src, sizeof src, "%s/wcopg_%d_%d.ww", tmpdir, getpid(), i);
snprintf(outbin, sizeof outbin, "%s/wcopg_%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 >/dev/null 2>&1",
driver, outbin, src);
int rc = runwait(cmd);
if (r->kind == 0) {
/* Negative — build must fail. */
int bad = (rc == 0);
if (bad) {
fprintf(stderr,
"opaque-guard[%s]: build unexpectedly succeeded\n",
r->label);
}
runwait(rmcmd);
return bad ? -1 : 0;
}
/* Positive — build then run. */
if (rc != 0) {
fprintf(stderr, "opaque-guard[%s]: build failed\n", r->label);
runwait(rmcmd);
return -1;
}
int got = runwait(outbin);
runwait(rmcmd);
if (got != r->want) {
fprintf(stderr, "opaque-guard[%s]: exit=%d want=%d\n",
r->label, got, r->want);
return -1;
}
return 0;
}
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 cdrv[640];
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
int n = (int)(sizeof rows / sizeof rows[0]);
int fail = 0;
for (int i = 0; i < n; i++) {
if (run_row(cdrv, &rows[i], i) != 0) fail++;
}
if (fail) {
fprintf(stderr, "opaque-guards: %d/%d row(s) failed\n", fail, n);
return 1;
}
printf("opaque-guards: %d/%d ok\n", n, n);
return 0;
}