/* * 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 #include #include #include #include #include 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; }