wcc: opaque use-guards — reject every unsized use (incl tuple/tagged, recursive) (#108)

#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.
This commit is contained in:
2026-05-26 09:23:50 +09:00
parent 3a18d2cfe6
commit f4970d886c
6 changed files with 592 additions and 3 deletions

View File

@@ -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)

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 <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;
}