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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user