diff --git a/Makefile b/Makefile index f3186475..916c92dc 100644 --- a/Makefile +++ b/Makefile @@ -338,6 +338,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_types_sizelim_run \ $(BIN)/test_types_intlim_run \ $(BIN)/test_opaque_decl_run \ + $(BIN)/test_opaque_guards \ $(BIN)/test_bufio_run $(BIN)/test_random_run $(BIN)/test_smoke: test/wcc/000_smoke.c $(LIB)/libwcc.a | $(BIN) @@ -1101,6 +1102,11 @@ $(BIN)/test_opaque_decl_run: test/wcc/960_opaque_decl_run.c $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_opaque_guards: test/wcc/961_opaque_guards.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_f64crossmod_run: test/wcc/953_f64crossmod_run.c $(BIN)/ww \ $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 327789e2..c8dcf4f1 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -436,6 +436,30 @@ stamp_intlit(Checker *c, Node *n, u64 v) /* n->type left intact (the type cexpr inferred for the rhs). */ } +/* + * require_sized — #108(b): opaque is abstract + UNSIZED + * (size == SIZE_UNDEFINED) and is legal ONLY behind indirection: + * `*opaque` (8B) and `[]opaque` (24B header) size themselves + * independently of the element, so they pass this guard. A use that + * needs a concrete byte size — a bare local/param/return value, a + * struct field, an array element — would otherwise fabricate a + * (u64)-1-byte slot: a silent miscompile (rule 7). Reject loud here. + * Mirrors harec's `size == SIZE_UNDEFINED` binding/field/return guards + * (ref/harec/src/check.c:1524 "Cannot create binding for type of + * undefined size", :3931 return-by-value). Returns 1 when the type is + * sized (caller proceeds), 0 when it emitted the error. + */ +static int +require_sized(Checker *c, Type *t, Pos pos, const char *where) +{ + if (t == NULL || t->size != SIZE_UNDEFINED) + return 1; + err(c, pos, "unsized type '%s' cannot be %s; use '*%s' or '[]%s'", + type_name(c->a, t), where, type_name(c->a, t), + type_name(c->a, t)); + return 0; +} + static Type * resolve_type(Checker *c, Node *n) { @@ -475,7 +499,9 @@ resolve_type(Checker *c, Node *n) } else { err(c, n->pos, "array length must be an integer literal"); } - return type_array(c->a, resolve_type(c, n->lhs), len); + Type *elem = resolve_type(c, n->lhs); + require_sized(c, elem, n->pos, "an array element"); /* #108(b) */ + return type_array(c->a, elem, len); } case N_TCHAN: return type_chan(c->a, resolve_type(c, n->lhs)); @@ -486,6 +512,10 @@ resolve_type(Checker *c, Node *n) for (Node *e = n->list; e; e = e->next) { Tparam *tp = amalloc(c->a, sizeof *tp); tp->type = resolve_type(c, e); + /* #108(b): an unsized member would poison sz with the + * (u64)-1 sentinel. harec ref/harec/src/type_store.c:1147. */ + if (!require_sized(c, tp->type, e->pos, "a tuple member")) + continue; if (tp->type && tp->type->align > al) al = tp->type->align; if (tp->type) sz += tp->type->size; if (head == NULL) head = tp; @@ -516,6 +546,10 @@ resolve_type(Checker *c, Node *n) for (Node *e = n->list; e; e = e->next) { Type *vt = resolve_type(c, e); if (vt == ty_never) continue; + /* #108(b): an unsized variant has no slot in the union + * payload. harec ref/harec/src/type_store.c:449. */ + if (!require_sized(c, vt, e->pos, "a tagged union member")) + continue; int spread = (e->op == TK_ELLIPSIS); /* `...inner` spread: flatten the variants of the * (possibly NAMED) inner tagged union into the @@ -624,6 +658,10 @@ resolve_type(Checker *c, Node *n) u64 off = 0, maxalign = 1; for (Node *f = n->list; f; f = f->next) { Type *ft = resolve_type(c, f->lhs); + /* #108(b): an unsized field would overflow the offset + * accumulator (align/size == (u64)-1); reject + skip it. */ + if (!require_sized(c, ft, f->pos, "a struct field")) + continue; if (ft->align > maxalign) maxalign = ft->align; off = (off + ft->align - 1) & ~(ft->align - 1); if (f->str != NULL) { @@ -1027,8 +1065,17 @@ cexpr(Checker *c, Node *n) err(c, n->pos, "index must be integer"); if (base == ty_err) return n->type = ty_err; Type *u = (base->kind == TY_NAMED) ? base->under : base; - if (u && (u->kind == TY_SLICE || u->kind == TY_ARRAY)) + if (u && (u->kind == TY_SLICE || u->kind == TY_ARRAY)) { + /* #108(b): indexing needs the element size; `[]opaque` + * is a legal (sized) header but its element is unsized. + * harec ref/harec/src/check.c:384. */ + if (u->sub && u->sub->size == SIZE_UNDEFINED) + err(c, n->pos, "cannot index %s: element type " + "'%s' has undefined size", + type_name(c->a, base), + type_name(c->a, u->sub)); return n->type = u->sub; + } if (u && u->kind == TY_STR) return n->type = ty_u8; /* `*[N]T` auto-decays to `[N]T` indexing — drill into the @@ -1068,7 +1115,20 @@ cexpr(Checker *c, Node *n) int is_size = strcmp(n->lhs->str, "size") == 0; Type *t = resolve_type(c, n->list); u64 v = 0; - if (t && t != ty_err) v = is_size ? t->size : t->align; + if (t && t != ty_err) { + u64 m = is_size ? t->size : t->align; + /* #108(b): size(opaque)/align(opaque) has no + * concrete answer; folding the (u64)-1 sentinel + * would be a silent miscompile (rule 7). harec + * ref/harec/src/check.c:2720. */ + if (m == SIZE_UNDEFINED) + err(c, n->pos, + "cannot take %s of unsized type '%s'", + is_size ? "size" : "align", + type_name(c->a, t)); + else + v = m; + } n->kind = N_INTLIT; n->uval = v; n->str = aprintf(c->a, "%llu", (unsigned long long)v); @@ -1626,6 +1686,10 @@ clet(Checker *c, Node *n) else err(c, n->pos, "[_]T needs an array-literal initialiser"); } + /* #108(b): a bare `let x: opaque` would fabricate a (u64)-1-byte + * local. `*opaque` / `[]opaque` locals are sized and pass. */ + if (declared) + require_sized(c, declared, n->pos, "a variable"); Type *t = declared; if (t == NULL && initt) t = type_default(initt); if (t == NULL) { @@ -1888,6 +1952,10 @@ build_fn_type(Checker *c, Node *fn) Type *t = newtype(c->a, TY_FN); t->size = 8; t->align = 8; t->ret = fn->lhs ? resolve_type(c, fn->lhs) : ty_void; + /* #108(b): opaque can't be returned by value (undefined size); harec + * ref/harec/src/check.c:3931. `*opaque` / `[]opaque` returns are + * sized and pass. */ + require_sized(c, t->ret, fn->pos, "a return type"); Tparam *head = NULL, *tail = NULL; for (Node *p = fn->list; p; p = p->next) { if (p->str && strcmp(p->str, "...") == 0) { @@ -1904,6 +1972,10 @@ build_fn_type(Checker *c, Node *fn) } else { tp->type = pt; } + /* #108(b): a by-value opaque param has undefined size. The + * `T...` variadic form wraps in []T (sized) above, so guard + * tp->type after the wrap, not pt. */ + require_sized(c, tp->type, p->pos, "a parameter"); if (head == NULL) head = tp; else tail->next = tp; tail = tp; @@ -2130,6 +2202,10 @@ check_file(Checker *c, Node *file) } case N_LET: { Type *t = d->lhs ? resolve_type(c, d->lhs) : NULL; + /* #108(b): a top-level `let x: opaque` is the same + * undefined-size footgun as a local one. */ + if (t) + require_sized(c, t, d->pos, "a variable"); d->type = t; if (d->str && d->str[0]) { Sym *prev = scope_lookup_local(c->cur, d->str); diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 32559cd5..3ab14743 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -7999,6 +7999,59 @@ fn astsize(c: *checker, t: *node) i64 = { return 0i64; }; +// astunsized — #108(b): true iff `t` contains an unsized component. A +// type is unsized iff it is the abstract `opaque` (size/align == +// SIZE_UNDEFINED) OR an aggregate (array / struct / tuple / tagged) +// with a recursively-unsized member. The wwstage has NO type-decl +// construction guards (those are cstage-only, rule-10), so its size()/ +// align() FOLD must detect every opaque-containing type itself — a +// leaf-only check would silently fold size([4]opaque) / size(struct{x: +// opaque}) / size((opaque, i32)) to garbage (rule 7). Does NOT peel +// TPTR/TSLICE/TCHAN/TFN — `*opaque` (8B) and `[]opaque` (24B header) +// are sized and legal behind indirection. Cstage twin: the leaf +// `m == SIZE_UNDEFINED` size/align guard PLUS the per-construction +// require_sized guards that reject unsized aggregates at the type decl +// (so the cstage size/align fold only ever sees a leaf opaque); harec +// ref/harec/src/check.c:2720, type_store.c:1147 (tuple) / :449 (tagged). +fn astunsized(c: *checker, t: *node) bool = { + if (t == nil) { return false; }; + let u: *node = resolvealias(c, unwrapbang(t)); + if (u == nil) { return false; }; + let k: nkind = u.kind; + if (k == nkind.N_TNAME) { + if (streq(u.str, "opaque")) { return true; }; + return false; + }; + if (k == nkind.N_TARRAY) { return astunsized(c, u.lhs); }; + if (k == nkind.N_TTUPLE) { + let p: *node = u.list; + for (p != nil) { + if (astunsized(c, p.lhs)) { return true; }; + p = p.next; + }; + return false; + }; + if (k == nkind.N_TSTRUCT) { + let f: *node = u.list; + for (f != nil) { + if (f.kind == nkind.N_TFIELD) { + if (astunsized(c, f.lhs)) { return true; }; + }; + f = f.next; + }; + return false; + }; + if (k == nkind.N_TTAGGED) { + let v: *node = u.list; + for (v != nil) { + if (astunsized(c, v)) { return true; }; + v = v.next; + }; + return false; + }; + return false; +}; + // matchyieldtype — port of cstage cmd/wcc/check.c:110-135. Walks a // match arm body for the first `yield expr;` and returns its operand // type. Returns nil if no yield is reachable from `body`. Doesn't @@ -9183,12 +9236,20 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { // own type. let utn: *node = mktname(c, "untyped_int"); if (issize) { + // #108(b): rule-10 twin of the cstage + // size/align unsized guard. + if (astunsized(c, e.list)) { + deffolderr(c, e, "cannot take size of unsized type 'opaque'"); + }; let v: i64 = astsize(c, e.list); foldtointlit(c, e, v); e.type_ = tinfofornode(c, utn): *void; return mktname(c, "i32"); }; if (isalign) { + if (astunsized(c, e.list)) { + deffolderr(c, e, "cannot take align of unsized type 'opaque'"); + }; let v: i64 = astalign(c, e.list); foldtointlit(c, e, v); e.type_ = tinfofornode(c, utn): *void; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 3fc78e5b..71cc2f54 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -848,6 +848,59 @@ fn astsize(c: *checker, t: *node) i64 = { return 0i64; }; +// astunsized — #108(b): true iff `t` contains an unsized component. A +// type is unsized iff it is the abstract `opaque` (size/align == +// SIZE_UNDEFINED) OR an aggregate (array / struct / tuple / tagged) +// with a recursively-unsized member. The wwstage has NO type-decl +// construction guards (those are cstage-only, rule-10), so its size()/ +// align() FOLD must detect every opaque-containing type itself — a +// leaf-only check would silently fold size([4]opaque) / size(struct{x: +// opaque}) / size((opaque, i32)) to garbage (rule 7). Does NOT peel +// TPTR/TSLICE/TCHAN/TFN — `*opaque` (8B) and `[]opaque` (24B header) +// are sized and legal behind indirection. Cstage twin: the leaf +// `m == SIZE_UNDEFINED` size/align guard PLUS the per-construction +// require_sized guards that reject unsized aggregates at the type decl +// (so the cstage size/align fold only ever sees a leaf opaque); harec +// ref/harec/src/check.c:2720, type_store.c:1147 (tuple) / :449 (tagged). +fn astunsized(c: *checker, t: *node) bool = { + if (t == nil) { return false; }; + let u: *node = resolvealias(c, unwrapbang(t)); + if (u == nil) { return false; }; + let k: nkind = u.kind; + if (k == nkind.N_TNAME) { + if (streq(u.str, "opaque")) { return true; }; + return false; + }; + if (k == nkind.N_TARRAY) { return astunsized(c, u.lhs); }; + if (k == nkind.N_TTUPLE) { + let p: *node = u.list; + for (p != nil) { + if (astunsized(c, p.lhs)) { return true; }; + p = p.next; + }; + return false; + }; + if (k == nkind.N_TSTRUCT) { + let f: *node = u.list; + for (f != nil) { + if (f.kind == nkind.N_TFIELD) { + if (astunsized(c, f.lhs)) { return true; }; + }; + f = f.next; + }; + return false; + }; + if (k == nkind.N_TTAGGED) { + let v: *node = u.list; + for (v != nil) { + if (astunsized(c, v)) { return true; }; + v = v.next; + }; + return false; + }; + return false; +}; + // matchyieldtype — port of cstage cmd/wcc/check.c:110-135. Walks a // match arm body for the first `yield expr;` and returns its operand // type. Returns nil if no yield is reachable from `body`. Doesn't @@ -2032,12 +2085,20 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { // own type. let utn: *node = mktname(c, "untyped_int"); if (issize) { + // #108(b): rule-10 twin of the cstage + // size/align unsized guard. + if (astunsized(c, e.list)) { + deffolderr(c, e, "cannot take size of unsized type 'opaque'"); + }; let v: i64 = astsize(c, e.list); foldtointlit(c, e, v); e.type_ = tinfofornode(c, utn): *void; return mktname(c, "i32"); }; if (isalign) { + if (astunsized(c, e.list)) { + deffolderr(c, e, "cannot take align of unsized type 'opaque'"); + }; let v: i64 = astalign(c, e.list); foldtointlit(c, e, v); e.type_ = tinfofornode(c, utn): *void; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 8f08c8b8..c0ed90b8 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -7999,6 +7999,59 @@ fn astsize(c: *checker, t: *node) i64 = { return 0i64; }; +// astunsized — #108(b): true iff `t` contains an unsized component. A +// type is unsized iff it is the abstract `opaque` (size/align == +// SIZE_UNDEFINED) OR an aggregate (array / struct / tuple / tagged) +// with a recursively-unsized member. The wwstage has NO type-decl +// construction guards (those are cstage-only, rule-10), so its size()/ +// align() FOLD must detect every opaque-containing type itself — a +// leaf-only check would silently fold size([4]opaque) / size(struct{x: +// opaque}) / size((opaque, i32)) to garbage (rule 7). Does NOT peel +// TPTR/TSLICE/TCHAN/TFN — `*opaque` (8B) and `[]opaque` (24B header) +// are sized and legal behind indirection. Cstage twin: the leaf +// `m == SIZE_UNDEFINED` size/align guard PLUS the per-construction +// require_sized guards that reject unsized aggregates at the type decl +// (so the cstage size/align fold only ever sees a leaf opaque); harec +// ref/harec/src/check.c:2720, type_store.c:1147 (tuple) / :449 (tagged). +fn astunsized(c: *checker, t: *node) bool = { + if (t == nil) { return false; }; + let u: *node = resolvealias(c, unwrapbang(t)); + if (u == nil) { return false; }; + let k: nkind = u.kind; + if (k == nkind.N_TNAME) { + if (streq(u.str, "opaque")) { return true; }; + return false; + }; + if (k == nkind.N_TARRAY) { return astunsized(c, u.lhs); }; + if (k == nkind.N_TTUPLE) { + let p: *node = u.list; + for (p != nil) { + if (astunsized(c, p.lhs)) { return true; }; + p = p.next; + }; + return false; + }; + if (k == nkind.N_TSTRUCT) { + let f: *node = u.list; + for (f != nil) { + if (f.kind == nkind.N_TFIELD) { + if (astunsized(c, f.lhs)) { return true; }; + }; + f = f.next; + }; + return false; + }; + if (k == nkind.N_TTAGGED) { + let v: *node = u.list; + for (v != nil) { + if (astunsized(c, v)) { return true; }; + v = v.next; + }; + return false; + }; + return false; +}; + // matchyieldtype — port of cstage cmd/wcc/check.c:110-135. Walks a // match arm body for the first `yield expr;` and returns its operand // type. Returns nil if no yield is reachable from `body`. Doesn't @@ -9183,12 +9236,20 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { // own type. let utn: *node = mktname(c, "untyped_int"); if (issize) { + // #108(b): rule-10 twin of the cstage + // size/align unsized guard. + if (astunsized(c, e.list)) { + deffolderr(c, e, "cannot take size of unsized type 'opaque'"); + }; let v: i64 = astsize(c, e.list); foldtointlit(c, e, v); e.type_ = tinfofornode(c, utn): *void; return mktname(c, "i32"); }; if (isalign) { + if (astunsized(c, e.list)) { + deffolderr(c, e, "cannot take align of unsized type 'opaque'"); + }; let v: i64 = astalign(c, e.list); foldtointlit(c, e, v); e.type_ = tinfofornode(c, utn): *void; diff --git a/test/wcc/961_opaque_guards.c b/test/wcc/961_opaque_guards.c new file mode 100644 index 00000000..69987260 --- /dev/null +++ b/test/wcc/961_opaque_guards.c @@ -0,0 +1,324 @@ +/* + * 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; +}