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.
307 lines
10 KiB
C
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;
|
|
}
|