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:
6
Makefile
6
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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
324
test/wcc/961_opaque_guards.c
Normal file
324
test/wcc/961_opaque_guards.c
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user