test/wcc: carrier ownership repair and driver-contract adaptation

Every surviving carrier now owns its artifacts: checked mkdir/mkdtemp/
fopen acquisition, one all-exit cleanup funnel per carrier, ENOENT-
tolerant checked unlinks, exact-path deletion (rm -rf only for an
owned pid-keyed dir or a .sepwork beneath one), and cleanup failure
fails a passing carrier without overwriting its diagnostic. In the
same pass the carriers adapt to the driver contract this branch lands:
--sep and WW_PKGCACHE are gone, -S and the /tmp/ww_run_<pid> scratch
contract are asserted, and rows whose runtime or reject coverage moved
to test/wcc/data fixtures or test/lang @test owners are trimmed to the
byte/artifact/diagnostic observations only they can make.

Repair and adaptation ride together because most files interleave both
in the same hunks; splitting would manufacture intermediate carrier
states that never existed and cannot run against either driver.
This commit is contained in:
2026-08-07 23:02:47 +09:00
parent b2899dd8d3
commit a95a7a316b
132 changed files with 7573 additions and 6172 deletions

View File

@@ -1,75 +1,20 @@
/*
* 961_opaque_guards — #108 sub-fold (b): the opaque USE-GUARDS.
* 961_opaque_guards — C-stage-only guards for the unsized opaque type.
*
* #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 legal behind indirection (*opaque is 8 bytes and []opaque is a
* 24-byte header), but it cannot appear by value or be indexed through a
* slice. The WW frontend currently accepts the eight programs below because
* it does not validate those construction and binding sites. That polarity
* cannot be represented by the both-stage fixture protocol, so this carrier
* requires each program to fail with the C-stage driver.
*
* 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`.
* Both-stage size/align rejections, including nested aggregates, are owned by
* the six r961_neg_* fixtures. The sized pointer and slice controls are
* owned by r961_pos_ptr_opaque and r961_pos_slice_opaque.
*/
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
@@ -83,196 +28,112 @@ runwait(const char *cmd)
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; };
struct row { const char *label; const char *src; };
static const struct row rows[] = {
/* guard 1 — bare local. */
{ "neg_bare_local", 0,
{ "neg_bare_local",
"package main;\n"
"export fn main() i32 = {\n"
" let x: opaque;\n"
" return 0;\n"
"};\n",
0 },
"\tlet x: opaque;\n"
"\treturn 0;\n"
"};\n" },
/* guard 1 — by-value parameter. */
{ "neg_param", 0,
{ "neg_param",
"package main;\n"
"fn f(x: opaque) i32 = { return 0; };\n"
"export fn main() i32 = { return 0; };\n",
0 },
"export fn main() i32 = { return 0; };\n" },
/* guard 1 — return-by-value. */
{ "neg_return", 0,
/*
* A declaration isolates the return-type guard. A function body returning
* 0 would add an unrelated untyped_int-to-opaque rejection.
*/
{ "neg_return",
"package main;\n"
"fn f() opaque = { return 0; };\n"
"export fn main() i32 = { return 0; };\n",
0 },
"fn f() opaque;\n"
"export fn main() i32 = { return 0; };\n" },
/* guard 2 — opaque struct field. */
{ "neg_struct_field", 0,
{ "neg_struct_field",
"package main;\n"
"type S = struct { x: opaque };\n"
"export fn main() i32 = { return 0; };\n",
0 },
"export fn main() i32 = { return 0; };\n" },
/* guard 3 — [N]opaque array element. */
{ "neg_array_elem", 0,
{ "neg_array_elem",
"package main;\n"
"export fn main() i32 = {\n"
" let a: [4]opaque;\n"
" return 0;\n"
"};\n",
0 },
"\tlet a: [4]opaque;\n"
"\treturn 0;\n"
"};\n" },
/* 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,
/*
* The cast keeps this row on the slice-index guard rather than creating
* a bare opaque local.
*/
{ "neg_slice_index",
"package main;\n"
"export fn main() i32 = {\n"
" let s: []opaque;\n"
" let v: i32 = s[0]: i32;\n"
" return v;\n"
"};\n",
0 },
"\tlet s: []opaque;\n"
"\tlet v: i32 = s[0]: i32;\n"
"\treturn v;\n"
"};\n" },
/* 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,
{ "neg_tuple_member",
"package main;\n"
"export fn main() i32 = {\n"
" let t: (opaque, i32);\n"
" return 0;\n"
"};\n",
0 },
"\tlet t: (opaque, i32);\n"
"\treturn 0;\n"
"};\n" },
/* 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,
{ "neg_tagged_variant",
"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 },
"\tlet x: (opaque | i32);\n"
"\treturn 0;\n"
"};\n" },
};
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);
char tmpdir[64] = "/tmp/wcopg_XXXXXX";
if (mkdtemp(tmpdir) == NULL) {
perror("opaque-guard: mkdtemp");
return -1;
}
char src[128], outbin[128], sepwork[160], rmcmd[192], cmd[2048];
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);
snprintf(sepwork, sizeof sepwork, "%s.sepwork", outbin);
snprintf(rmcmd, sizeof rmcmd, "rm -rf %s", sepwork);
int result = -1;
FILE *f = fopen(src, "wb");
if (!f) { runwait(rmcmd); return -1; }
if (!f) goto cleanup;
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);
int bad = (rc == 0);
if (bad) {
fprintf(stderr, "opaque-guard[%s]: build unexpectedly succeeded\n",
r->label);
}
result = bad ? -1 : 0;
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;
cleanup: {
int cleanbad = 0;
if (unlink(src) != 0 && errno != ENOENT) cleanbad = 1;
if (unlink(outbin) != 0 && errno != ENOENT) cleanbad = 1;
if (runwait(rmcmd) != 0) cleanbad = 1;
if (rmdir(tmpdir) != 0) cleanbad = 1;
if (cleanbad) {
fprintf(stderr, "opaque-guard[%s]: temporary cleanup failed\n",
r->label);
result = -1;
}
/* 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;
return result;
}
int