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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user