#108 sub-fold (b): close the footgun #108(a) opened. opaque is abstract and UNSIZED (size = align = SIZE_UNDEFINED = (u64)-1), legal only behind indirection. Without guards a bare use would fabricate a (u64)-1-byte slot — a silent miscompile (rule 7). opaque is illegal by-value in FOUR aggregate positions (array element, struct field, tuple member, tagged- union variant) + as a bare value, under size/align, and as a []opaque element-index. LOUD guards, mirroring harec's scattered `size == SIZE_UNDEFINED` checks: 1. bare value/local/param/return-by-value (check.c clet, build_fn_type, top-level let; harec check.c:1524, :3931) 2. opaque struct field (resolve_type N_TSTRUCT) 3. [N]opaque array element (resolve_type N_TARRAY) 3t. opaque tuple member (resolve_type N_TTUPLE; harec type_store.c:1147) 3u. opaque tagged-union variant (resolve_type N_TTAGGED; harec type_store.c:449) 4. size(opaque) / align(opaque) (size/align fold; harec check.c:2720) 5. indexing []opaque (N_INDEX; harec check.c:384) Detection is via the SIZE_UNDEFINED sentinel the guard consults, so the sized forms `*opaque` (8B) and `[]opaque` (24B header) pass untouched. Rule-10 per-guard stage placement: - Guards 1/2/3/3t/3u/5 are CSTAGE-ONLY. The wwstage check.ww is an AST-level approximation with no binding-size computation (g1) and no type-decl field/element/member validation walk (g2/g3/3t/3u); its N_INDEX indexresult returns the element type without consulting its size and defers invalid-index rejection to the cstage (g5). Same cstage-only neg-case precedent as 712_redecl / 708_param_shadow_mod. - Guard 4 is BOTH-STAGES. The wwstage HAS the size()/align() fold (astsize/astalign would otherwise fold opaque to a bogus 0 — a silent miscompile); twinned via astunsized + deffolderr. Because the wwstage has NO per-construction guards, its fold alone must catch every opaque-containing type: astunsized is RECURSIVE — a type is unsized iff it is opaque OR an aggregate (array/struct/tuple/tagged) with a recursively-unsized member. This both reaches the tuple/tagged folds AND closes the leaf-only size([4]opaque)/size(struct{x:opaque})→0 leak. The cstage size/align guard stays leaf — the cstage rejects unsized aggregates at construction, so its fold only ever sees a leaf. opaque is unused by the bootstrap, so every guard is inert on the selfhost corpus — 990-997 stay byte-identical. Regenerates the w6c/wwdump combined.ww (check.ww embed). New compile-fail probe 961_opaque_guards (14 build-fails rows incl tuple/tagged/nested + 2 *opaque/[]opaque positive controls); 960 positive probe unchanged.
325 lines
10 KiB
C
325 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 <string.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], cmd[2048];
|
|
snprintf(src, sizeof src, "/tmp/wcopg_%d_%d.ww", getpid(), i);
|
|
snprintf(tmpdir, sizeof tmpdir, "/tmp/wcopg_%d_d_%d", getpid(), i);
|
|
|
|
FILE *f = fopen(src, "wb");
|
|
if (!f) return -1;
|
|
fputs(r->src, f);
|
|
fclose(f);
|
|
|
|
mkdir(tmpdir, 0755);
|
|
snprintf(cmd, sizeof cmd, "cd %s && %s build %s >/dev/null 2>&1",
|
|
tmpdir, driver, src);
|
|
int rc = runwait(cmd);
|
|
|
|
const char *base = strrchr(src, '/');
|
|
base = base ? base + 1 : src;
|
|
char outbin[256];
|
|
snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base);
|
|
char *dot = strrchr(outbin, '.');
|
|
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
|
|
|
|
char combined[256];
|
|
snprintf(combined, sizeof combined, "/tmp/wcopg_%d_%d.combined.ww",
|
|
getpid(), i);
|
|
|
|
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);
|
|
unlink(outbin);
|
|
}
|
|
unlink(src);
|
|
unlink(combined);
|
|
rmdir(tmpdir);
|
|
return bad ? -1 : 0;
|
|
}
|
|
|
|
/* Positive — build then run. */
|
|
if (rc != 0) {
|
|
fprintf(stderr, "opaque-guard[%s]: build failed\n", r->label);
|
|
unlink(src);
|
|
unlink(combined);
|
|
rmdir(tmpdir);
|
|
return -1;
|
|
}
|
|
int got = runwait(outbin);
|
|
unlink(src);
|
|
unlink(outbin);
|
|
unlink(combined);
|
|
rmdir(tmpdir);
|
|
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;
|
|
}
|