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

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