wcc: accept bare &fn into a fn-pointer-alias slot via a caller-site gate (#206)

A bare `&fn_name` was not assignable into a `*reader` / `(*reader | void)`
vtable field without an explicit cast: cstage type_eq on TY_NAMED is
pointer-identity, so a structural `*fn(...)` referent never matched the named
`*reader` variant; wwstage accepted it only via an accidental catch-all
leniency. harec accepts bare &fn through hint-directed alias adoption at the
address-of site (check.c:3594-3626) while keeping pointer assignability
strictly nominal (types.c:1039-1066), so a materialized `*fn` value never
launders across alias names.

Mirror that decision without threading a type hint through the bottom-up
cexpr: keep type_assignable / isassignable fully nominal, and add a
caller-site helper (assignable_addrfn) at the assignment boundaries
(let-init, struct-literal field-init, assign, return, call-arg, array
element) that accepts iff the rhs is a DIRECT &-of-fn-ident and the
destination (or exactly one tagged variant) is a pointer-to-fn-alias whose
underlying fn signature structurally matches. A materialized `*fn` value, a
distinct same-signature alias, and an ambiguous multi-variant target all stay
rejected. Both stages share the rule; wwstage's lenient pointer-fn punt
becomes a confident reject. ww has no methods, so a `value.leaf` slot is only
ever a fn-pointer field and this never over-admits.

The tightening surfaced a wwstage typeeqast gap: a TY_FN result that is a
tuple (`*fn(...)(i32,i32)`) compared false where cstage type_eq handled it,
newly rejecting a legitimate structural assign. Add the N_TTUPLE structural
case (rule-10), restoring test 766.

cgen-neutral (the cast was a no-op reinterpret); pre/post bootstrap .s
zero-delta. Test 783 covers the positive paths (incl. a byte-id-clean
three-field-vtable dispatcher) and the negatives. Tagged-slot negatives
(ambiguous / tagged-laundering) are rejected on cstage but wwstage's separate
`(X|void)` void-variant leniency (#214) still admits them; 783 pins them
cstage-only, to graduate when #214 closes (required before wwstage becomes
the authoritative selfhost checker).

Note: `make clean && make test` is RED at HEAD on 4 alloc fixtures
(700/748/758/915) via a pre-existing clean-build defect (#215, malloc vs
rt_malloc); identical with or without this change, so bisect-clean for #206.
This commit is contained in:
2026-05-29 18:05:53 +09:00
parent 4e1181fd8c
commit f6ac7fb2f8
6 changed files with 908 additions and 22 deletions

View File

@@ -322,6 +322,58 @@ def_cast_fits(Type *t, u64 v)
return ext == v;
}
/* addrfn_ptr_matches — true iff ptr is a pointer whose referent
* (after one NAMED peel) is a fn type structurally equal to fnty. */
static int
addrfn_ptr_matches(Type *ptr, Type *fnty)
{
if (ptr == NULL || ptr->kind != TY_PTR) return 0;
Type *ref = ptr->sub;
if (ref && ref->kind == TY_NAMED) ref = ref->under;
if (ref == NULL || ref->kind != TY_FN) return 0;
return type_eq(ref, fnty);
}
/* assignable_addrfn — project #206. A bare `&fn` types structurally
* as `*fn(...)`, which is nominally distinct from a `*alias`
* fn-pointer slot; type_assignable stays fully nominal (preserving
* harec's nominal pointer rule, ref/harec/src/types.c:1039-1066) so
* any materialized `*fn` value laundered into a `*alias` is rejected.
* This admits the one shape harec accepts via its address-of hint
* (ref/harec/src/check.c:3594-3626 adopts the alias when the operand
* dealiases to the hint's referent): a DIRECT `&`-of-fn-ident whose
* signature structurally matches the destination's pointed-to fn
* alias, or — for a tagged `(*alias | void)` destination — the single
* ptr-to-fn variant it matches (>=2 same-signature variants is
* ambiguous → reject, never silently pick). Lives at the assignment
* boundary, not in cexpr, because ww's tinfo is nominal-lossy and
* cexpr is hint-free (the alias identity is unrecoverable post-typing);
* the caller-site rhs node is the only place the direct-&fn shape
* survives. "Direct" is strict: the gate fires only when the rhs IS
* the address-of node, never on `&fn` nested in a larger expr. */
static int
assignable_addrfn(Checker *c, Type *dst, Node *rhs)
{
if (dst == NULL || rhs == NULL) return 0;
if (rhs->kind != N_UN || rhs->op != TK_AMP) return 0;
Node *id = rhs->lhs;
if (id == NULL || id->kind != N_IDENT || id->str == NULL) return 0;
Sym *s = scope_lookup_prefer(c->cur, c->cur_mod, id->str);
if (s == NULL || s->kind != SK_FN) return 0;
Type *fnty = s->type;
if (fnty == NULL || fnty->kind != TY_FN) return 0;
Type *du = (dst->kind == TY_NAMED) ? dst->under : dst;
if (du == NULL) return 0;
if (du->kind == TY_PTR) return addrfn_ptr_matches(du, fnty);
if (du->kind == TY_TAGGED) {
int n = 0;
for (Tparam *p = du->params; p; p = p->next)
if (addrfn_ptr_matches(p->type, fnty)) n++;
return n == 1;
}
return 0;
}
/* arrlit_init_fits — #130: accept-if-fits for `let/def A: [N]T = [..]`
* where the whole-array type_assignable failed (bare-int elements
* synthesize [N]i32 via type_default, losing the literal flavor that
@@ -360,7 +412,8 @@ arrlit_init_fits(Checker *c, Type *dt, Node *rhs)
}
continue;
}
if (!type_assignable(et, e->type)) return 0;
if (!type_assignable(et, e->type) && !assignable_addrfn(c, et, e))
return 0;
}
return 1;
}
@@ -1415,7 +1468,8 @@ cexpr(Checker *c, Node *n)
err(c, a->pos,
"spread arg must be the last");
} else if (elem != ty_err && at != ty_err) {
if (!type_assignable(elem, at))
if (!type_assignable(elem, at) &&
!assignable_addrfn(c, elem, a))
err(c, a->pos,
"variadic arg: %s not assignable to %s",
type_name(c->a, at),
@@ -1423,7 +1477,8 @@ cexpr(Checker *c, Node *n)
}
continue;
}
if (!type_assignable(p->type, at) && at != ty_err && p->type != ty_err)
if (!type_assignable(p->type, at) && at != ty_err && p->type != ty_err
&& !assignable_addrfn(c, p->type, a))
err(c, a->pos, "argument type %s not assignable to %s",
type_name(c->a, at), type_name(c->a, p->type));
p = p->next;
@@ -1449,7 +1504,8 @@ cexpr(Checker *c, Node *n)
}
Type *l = cexpr(c, n->lhs);
Type *r = cexpr(c, n->rhs);
if (l != ty_err && r != ty_err && !type_assignable(l, r))
if (l != ty_err && r != ty_err && !type_assignable(l, r) &&
!assignable_addrfn(c, l, n->rhs))
err(c, n->pos, "cannot assign %s to %s",
type_name(c->a, r), type_name(c->a, l));
return n->type = l;
@@ -1483,7 +1539,8 @@ cexpr(Checker *c, Node *n)
err(c, f->pos, "no field '%s' in %s",
f->str, type_name(c->a, t));
else if (vt != ty_err &&
!type_assignable(match->type, vt))
!type_assignable(match->type, vt) &&
!assignable_addrfn(c, match->type, f->lhs))
err(c, f->pos, "field %s: %s not assignable to %s",
f->str, type_name(c->a, vt),
type_name(c->a, match->type));
@@ -1837,7 +1894,8 @@ clet(Checker *c, Node *n)
}
}
if (declared && initt && initt != ty_err && !has_arr_repeat &&
!type_assignable(declared, initt))
!type_assignable(declared, initt) &&
!assignable_addrfn(c, declared, n->rhs))
err(c, n->pos, "init %s not assignable to declared %s",
type_name(c->a, initt), type_name(c->a, declared));
/* #104 fold-2: `let x: f32 = 1.0` — narrow the init literal to f32. */
@@ -1877,7 +1935,8 @@ cstmt(Checker *c, Node *n)
if (c->ret == ty_void && n->lhs)
err(c, n->pos, "return value in void function");
else if (c->ret != ty_void && rt != ty_err && c->ret != ty_err
&& !type_assignable(c->ret, rt))
&& !type_assignable(c->ret, rt)
&& !assignable_addrfn(c, c->ret, n->lhs))
err(c, n->pos, "return %s not assignable to %s",
type_name(c->a, rt), type_name(c->a, c->ret));
/* #104 fold-2: `fn g() f32 = { return 1.0; }` — narrow to f32. */