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

@@ -10797,9 +10797,27 @@ fn typeeqast(a: *node, b: *node) bool = {
};
return pb == nil;
};
// Conservative: anything else (struct/tagged/tuple/array) fails
// the cheap check. Selfhost code doesn't currently rely on
// equality at these shapes for the targeted checks.
// #206: tuple structural equality — mirror of cstage type.c:261-268
// (TY_TUPLE). Needed since a tuple-RETURN fn pointer compares its
// N_TFN return node (aa.lhs) here; without it `*fn(x)(a,b)` never
// proves structurally equal to itself, so the #206 punt-tightening
// would confidently reject a bare-&fn into a structural `*fn(...)`
// slot (test 766 fn_tuple_return). Elements are N_TPARAM-wrapped
// (parse.ww:302-318), so compare each link's .lhs.
if (k == nkind.N_TTUPLE) {
let pa: *node = aa.list;
let pb: *node = bb.list;
for (pa != nil) {
if (pb == nil) { return false; };
if (!typeeqast(pa.lhs, pb.lhs)) { return false; };
pa = pa.next;
pb = pb.next;
};
return pb == nil;
};
// Conservative: anything else (struct/tagged/array) fails the
// cheap check. Selfhost code doesn't currently rely on equality
// at these shapes for the targeted checks.
return false;
};
@@ -12069,6 +12087,35 @@ fn unoptype(c: *checker, e: *node) *node = {
};
};
};
// #206: `&fn` must type as `*fn(...)` — a pointer to the fn's
// full signature — not `*<rettype>`. exprtype's N_IDENT arm
// returns a fn decl's lhs (the return type), so the generic
// `*opt` below would mistype `&myread` as `*i32`; a `*fn`
// laundered into a `*alias` slot then slips past the nominal
// pointer-fn reject in isassignable. Synthesize the N_TFN from
// the fn decl (N_FNDECL and N_TFN share parseparams' param
// shape) so the address-of carries the signature. Mirrors
// cstage, where a fn ident already types as TY_FN
// (cmd/wcc/check.c:668), so `&fn` is `*fn` natively.
if (e.lhs != nil) {
if (e.lhs.kind == nkind.N_IDENT) {
let fs: *sym = scopelookup(c.cur, e.lhs.str);
if (fs != nil) {
if (fs.skind == skind.SK_FN) {
if (fs.decl != nil) {
if (fs.decl.kind == nkind.N_FNDECL) {
let synth: *node = newnode(nkind.N_TFN, "", 0, 0);
synth.lhs = fs.decl.lhs;
synth.list = fs.decl.list;
let pf: *node = newnode(nkind.N_TPTR, "", 0, 0);
pf.lhs = synth;
return pf;
};
};
};
};
};
};
// opt nil → propagation from inherent-IDENT bail (5-lite-b
// #34). Generic &expr widens to *opt; without opt we can't
// synthesize the pointer node.
@@ -13020,6 +13067,70 @@ fn isstrtname(t: *node) bool = {
return streq(t.str, "str");
};
// addrfnptrmatches — true iff `ptr` (after alias-resolve) is a
// pointer whose referent resolves to a fn type structurally equal to
// `synth` (a synthetic N_TFN built from a fn decl's ret + params).
// Mirror of cstage addrfn_ptr_matches (cmd/wcc/check.c, project #206);
// reuses typeeqast — the same structural-fn comparator cstage uses via
// type_eq(TY_FN,TY_FN) — for symmetry.
fn addrfnptrmatches(c: *checker, ptr: *node, synth: *node) bool = {
if (ptr == nil) { return false; };
let pu: *node = resolvealias(c, unwrapbang(ptr));
if (pu == nil) { return false; };
if (pu.kind != nkind.N_TPTR) { return false; };
let ref: *node = resolvealias(c, unwrapbang(pu.lhs));
if (ref == nil) { return false; };
if (ref.kind != nkind.N_TFN) { return false; };
return typeeqast(synth, ref);
};
// assignableaddrfn — project #206 Option C gate. Mirror of cstage
// assignable_addrfn (cmd/wcc/check.c). A bare `&fn` types structurally
// as `*fn(...)`, nominally distinct from a `*alias` fn-pointer slot;
// isassignable stays nominal (the pointer-fn arm below confidently
// rejects a laundered `*fn` value, like harec types.c:1039-1066). This
// admits only the shape harec adopts via its address-of hint (harec
// check.c:3594-3626): a DIRECT `&`-of-fn-ident whose signature
// structurally matches the destination's pointed-to fn alias, or the
// single matching ptr-to-fn variant of a tagged dst (>=2 same-sig
// variants → ambiguous, reject). Lives at the assignment-boundary
// caller sites — not in exprtype — because the alias identity is
// nominal-lossy once typed and the direct-&fn shape survives only on
// the rhs node. N_FNDECL and N_TFN share parseparams' param-node shape
// (lib/ww/parse/decl.ww + parse.ww), so a synthetic N_TFN over the fn
// decl's lhs/list compares correctly under typeeqast.
fn assignableaddrfn(c: *checker, dst: *node, rhs: *node) bool = {
if (dst == nil) { return false; };
if (rhs == nil) { return false; };
if (rhs.kind != nkind.N_UN) { return false; };
if (rhs.op != tkind.TK_AMP) { return false; };
let id: *node = rhs.lhs;
if (id == nil) { return false; };
if (id.kind != nkind.N_IDENT) { return false; };
let s: *sym = scopelookup(c.cur, id.str);
if (s == nil) { return false; };
if (s.skind != skind.SK_FN) { return false; };
if (s.decl == nil) { return false; };
let d: *node = s.decl;
if (d.kind != nkind.N_FNDECL) { return false; };
let synth: *node = newnode(nkind.N_TFN, "", 0, 0);
synth.lhs = d.lhs;
synth.list = d.list;
let du: *node = resolvealias(c, unwrapbang(dst));
if (du == nil) { return false; };
if (du.kind == nkind.N_TPTR) { return addrfnptrmatches(c, du, synth); };
if (du.kind == nkind.N_TTAGGED) {
let nmatch: i32 = 0;
let v: *node = du.list;
for (v != nil) {
if (addrfnptrmatches(c, v, synth)) { nmatch = nmatch + 1; };
v = v.next;
};
return nmatch == 1;
};
return false;
};
// isassignable — AST-level approximation of C check.c
// type_assignable. Returns true when we know the assignment is
// OK, false only when we're confident it isn't, and "skip" (true)
@@ -13171,6 +13282,31 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
};
};
};
// #206: two pointers whose referents both resolve to fn types are
// NOMINALLY assignable only when structurally equal — and that case
// already returned true via typeeqast at the top. Reaching here
// means the fn signatures differ, or a bare structural `*fn` value
// is being laundered into a `*alias` slot: confidently NOT
// assignable, mirror of cstage's nominal type_assignable (harec
// types.c:1039-1066). The direct `&fn` adopt-the-alias case is
// handled at the assignment caller sites via assignableaddrfn, NOT
// here. Without this the lenient catch-all below silently accepted
// the laundering shape.
if (du.kind == nkind.N_TPTR) {
if (su.kind == nkind.N_TPTR) {
let dref: *node = resolvealias(c, unwrapbang(du.lhs));
let sref: *node = resolvealias(c, unwrapbang(su.lhs));
if (dref != nil) {
if (sref != nil) {
if (dref.kind == nkind.N_TFN) {
if (sref.kind == nkind.N_TFN) {
return false;
};
};
};
};
};
};
// Anything else: don't claim confidence.
*confident = false;
return true;
@@ -13429,8 +13565,11 @@ fn checkletassign(c: *checker, n: *node) void = {
if (est != nil) {
if (!isassignable(c, elemtn, est, &conf2)) {
if (conf2) {
errnotassign(c, elemtn, est, "let");
return;
// #206: direct `&fn` array element.
if (!assignableaddrfn(c, elemtn, ev)) {
errnotassign(c, elemtn, est, "let");
return;
};
};
};
};
@@ -13443,6 +13582,8 @@ fn checkletassign(c: *checker, n: *node) void = {
};
let conf: bool = false;
let ok: bool = isassignable(c, n.lhs, src, &conf);
// #206: direct `&fn` → `*alias` / `(*alias | void)` slot.
if (!ok) { if (assignableaddrfn(c, n.lhs, n.rhs)) { ok = true; }; };
if (!conf) { return; };
if (!ok) { errnotassign(c, n.lhs, src, "let"); };
};
@@ -13460,6 +13601,8 @@ fn checkretassign(c: *checker, n: *node) void = {
if (src == nil) { return; };
let conf: bool = false;
let ok: bool = isassignable(c, c.fnret, src, &conf);
// #206: direct `&fn` returned into a `*alias` / `(*alias | void)`.
if (!ok) { if (assignableaddrfn(c, c.fnret, n.lhs)) { ok = true; }; };
if (!conf) { return; };
if (!ok) { errnotassign(c, c.fnret, src, "return"); };
};

View File

@@ -767,9 +767,27 @@ fn typeeqast(a: *node, b: *node) bool = {
};
return pb == nil;
};
// Conservative: anything else (struct/tagged/tuple/array) fails
// the cheap check. Selfhost code doesn't currently rely on
// equality at these shapes for the targeted checks.
// #206: tuple structural equality — mirror of cstage type.c:261-268
// (TY_TUPLE). Needed since a tuple-RETURN fn pointer compares its
// N_TFN return node (aa.lhs) here; without it `*fn(x)(a,b)` never
// proves structurally equal to itself, so the #206 punt-tightening
// would confidently reject a bare-&fn into a structural `*fn(...)`
// slot (test 766 fn_tuple_return). Elements are N_TPARAM-wrapped
// (parse.ww:302-318), so compare each link's .lhs.
if (k == nkind.N_TTUPLE) {
let pa: *node = aa.list;
let pb: *node = bb.list;
for (pa != nil) {
if (pb == nil) { return false; };
if (!typeeqast(pa.lhs, pb.lhs)) { return false; };
pa = pa.next;
pb = pb.next;
};
return pb == nil;
};
// Conservative: anything else (struct/tagged/array) fails the
// cheap check. Selfhost code doesn't currently rely on equality
// at these shapes for the targeted checks.
return false;
};
@@ -2039,6 +2057,35 @@ fn unoptype(c: *checker, e: *node) *node = {
};
};
};
// #206: `&fn` must type as `*fn(...)` — a pointer to the fn's
// full signature — not `*<rettype>`. exprtype's N_IDENT arm
// returns a fn decl's lhs (the return type), so the generic
// `*opt` below would mistype `&myread` as `*i32`; a `*fn`
// laundered into a `*alias` slot then slips past the nominal
// pointer-fn reject in isassignable. Synthesize the N_TFN from
// the fn decl (N_FNDECL and N_TFN share parseparams' param
// shape) so the address-of carries the signature. Mirrors
// cstage, where a fn ident already types as TY_FN
// (cmd/wcc/check.c:668), so `&fn` is `*fn` natively.
if (e.lhs != nil) {
if (e.lhs.kind == nkind.N_IDENT) {
let fs: *sym = scopelookup(c.cur, e.lhs.str);
if (fs != nil) {
if (fs.skind == skind.SK_FN) {
if (fs.decl != nil) {
if (fs.decl.kind == nkind.N_FNDECL) {
let synth: *node = newnode(nkind.N_TFN, "", 0, 0);
synth.lhs = fs.decl.lhs;
synth.list = fs.decl.list;
let pf: *node = newnode(nkind.N_TPTR, "", 0, 0);
pf.lhs = synth;
return pf;
};
};
};
};
};
};
// opt nil → propagation from inherent-IDENT bail (5-lite-b
// #34). Generic &expr widens to *opt; without opt we can't
// synthesize the pointer node.
@@ -2990,6 +3037,70 @@ fn isstrtname(t: *node) bool = {
return streq(t.str, "str");
};
// addrfnptrmatches — true iff `ptr` (after alias-resolve) is a
// pointer whose referent resolves to a fn type structurally equal to
// `synth` (a synthetic N_TFN built from a fn decl's ret + params).
// Mirror of cstage addrfn_ptr_matches (cmd/wcc/check.c, project #206);
// reuses typeeqast — the same structural-fn comparator cstage uses via
// type_eq(TY_FN,TY_FN) — for symmetry.
fn addrfnptrmatches(c: *checker, ptr: *node, synth: *node) bool = {
if (ptr == nil) { return false; };
let pu: *node = resolvealias(c, unwrapbang(ptr));
if (pu == nil) { return false; };
if (pu.kind != nkind.N_TPTR) { return false; };
let ref: *node = resolvealias(c, unwrapbang(pu.lhs));
if (ref == nil) { return false; };
if (ref.kind != nkind.N_TFN) { return false; };
return typeeqast(synth, ref);
};
// assignableaddrfn — project #206 Option C gate. Mirror of cstage
// assignable_addrfn (cmd/wcc/check.c). A bare `&fn` types structurally
// as `*fn(...)`, nominally distinct from a `*alias` fn-pointer slot;
// isassignable stays nominal (the pointer-fn arm below confidently
// rejects a laundered `*fn` value, like harec types.c:1039-1066). This
// admits only the shape harec adopts via its address-of hint (harec
// check.c:3594-3626): a DIRECT `&`-of-fn-ident whose signature
// structurally matches the destination's pointed-to fn alias, or the
// single matching ptr-to-fn variant of a tagged dst (>=2 same-sig
// variants → ambiguous, reject). Lives at the assignment-boundary
// caller sites — not in exprtype — because the alias identity is
// nominal-lossy once typed and the direct-&fn shape survives only on
// the rhs node. N_FNDECL and N_TFN share parseparams' param-node shape
// (lib/ww/parse/decl.ww + parse.ww), so a synthetic N_TFN over the fn
// decl's lhs/list compares correctly under typeeqast.
fn assignableaddrfn(c: *checker, dst: *node, rhs: *node) bool = {
if (dst == nil) { return false; };
if (rhs == nil) { return false; };
if (rhs.kind != nkind.N_UN) { return false; };
if (rhs.op != tkind.TK_AMP) { return false; };
let id: *node = rhs.lhs;
if (id == nil) { return false; };
if (id.kind != nkind.N_IDENT) { return false; };
let s: *sym = scopelookup(c.cur, id.str);
if (s == nil) { return false; };
if (s.skind != skind.SK_FN) { return false; };
if (s.decl == nil) { return false; };
let d: *node = s.decl;
if (d.kind != nkind.N_FNDECL) { return false; };
let synth: *node = newnode(nkind.N_TFN, "", 0, 0);
synth.lhs = d.lhs;
synth.list = d.list;
let du: *node = resolvealias(c, unwrapbang(dst));
if (du == nil) { return false; };
if (du.kind == nkind.N_TPTR) { return addrfnptrmatches(c, du, synth); };
if (du.kind == nkind.N_TTAGGED) {
let nmatch: i32 = 0;
let v: *node = du.list;
for (v != nil) {
if (addrfnptrmatches(c, v, synth)) { nmatch = nmatch + 1; };
v = v.next;
};
return nmatch == 1;
};
return false;
};
// isassignable — AST-level approximation of C check.c
// type_assignable. Returns true when we know the assignment is
// OK, false only when we're confident it isn't, and "skip" (true)
@@ -3141,6 +3252,31 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
};
};
};
// #206: two pointers whose referents both resolve to fn types are
// NOMINALLY assignable only when structurally equal — and that case
// already returned true via typeeqast at the top. Reaching here
// means the fn signatures differ, or a bare structural `*fn` value
// is being laundered into a `*alias` slot: confidently NOT
// assignable, mirror of cstage's nominal type_assignable (harec
// types.c:1039-1066). The direct `&fn` adopt-the-alias case is
// handled at the assignment caller sites via assignableaddrfn, NOT
// here. Without this the lenient catch-all below silently accepted
// the laundering shape.
if (du.kind == nkind.N_TPTR) {
if (su.kind == nkind.N_TPTR) {
let dref: *node = resolvealias(c, unwrapbang(du.lhs));
let sref: *node = resolvealias(c, unwrapbang(su.lhs));
if (dref != nil) {
if (sref != nil) {
if (dref.kind == nkind.N_TFN) {
if (sref.kind == nkind.N_TFN) {
return false;
};
};
};
};
};
};
// Anything else: don't claim confidence.
*confident = false;
return true;
@@ -3399,8 +3535,11 @@ fn checkletassign(c: *checker, n: *node) void = {
if (est != nil) {
if (!isassignable(c, elemtn, est, &conf2)) {
if (conf2) {
errnotassign(c, elemtn, est, "let");
return;
// #206: direct `&fn` array element.
if (!assignableaddrfn(c, elemtn, ev)) {
errnotassign(c, elemtn, est, "let");
return;
};
};
};
};
@@ -3413,6 +3552,8 @@ fn checkletassign(c: *checker, n: *node) void = {
};
let conf: bool = false;
let ok: bool = isassignable(c, n.lhs, src, &conf);
// #206: direct `&fn` → `*alias` / `(*alias | void)` slot.
if (!ok) { if (assignableaddrfn(c, n.lhs, n.rhs)) { ok = true; }; };
if (!conf) { return; };
if (!ok) { errnotassign(c, n.lhs, src, "let"); };
};
@@ -3430,6 +3571,8 @@ fn checkretassign(c: *checker, n: *node) void = {
if (src == nil) { return; };
let conf: bool = false;
let ok: bool = isassignable(c, c.fnret, src, &conf);
// #206: direct `&fn` returned into a `*alias` / `(*alias | void)`.
if (!ok) { if (assignableaddrfn(c, c.fnret, n.lhs)) { ok = true; }; };
if (!conf) { return; };
if (!ok) { errnotassign(c, c.fnret, src, "return"); };
};

View File

@@ -10797,9 +10797,27 @@ fn typeeqast(a: *node, b: *node) bool = {
};
return pb == nil;
};
// Conservative: anything else (struct/tagged/tuple/array) fails
// the cheap check. Selfhost code doesn't currently rely on
// equality at these shapes for the targeted checks.
// #206: tuple structural equality — mirror of cstage type.c:261-268
// (TY_TUPLE). Needed since a tuple-RETURN fn pointer compares its
// N_TFN return node (aa.lhs) here; without it `*fn(x)(a,b)` never
// proves structurally equal to itself, so the #206 punt-tightening
// would confidently reject a bare-&fn into a structural `*fn(...)`
// slot (test 766 fn_tuple_return). Elements are N_TPARAM-wrapped
// (parse.ww:302-318), so compare each link's .lhs.
if (k == nkind.N_TTUPLE) {
let pa: *node = aa.list;
let pb: *node = bb.list;
for (pa != nil) {
if (pb == nil) { return false; };
if (!typeeqast(pa.lhs, pb.lhs)) { return false; };
pa = pa.next;
pb = pb.next;
};
return pb == nil;
};
// Conservative: anything else (struct/tagged/array) fails the
// cheap check. Selfhost code doesn't currently rely on equality
// at these shapes for the targeted checks.
return false;
};
@@ -12069,6 +12087,35 @@ fn unoptype(c: *checker, e: *node) *node = {
};
};
};
// #206: `&fn` must type as `*fn(...)` — a pointer to the fn's
// full signature — not `*<rettype>`. exprtype's N_IDENT arm
// returns a fn decl's lhs (the return type), so the generic
// `*opt` below would mistype `&myread` as `*i32`; a `*fn`
// laundered into a `*alias` slot then slips past the nominal
// pointer-fn reject in isassignable. Synthesize the N_TFN from
// the fn decl (N_FNDECL and N_TFN share parseparams' param
// shape) so the address-of carries the signature. Mirrors
// cstage, where a fn ident already types as TY_FN
// (cmd/wcc/check.c:668), so `&fn` is `*fn` natively.
if (e.lhs != nil) {
if (e.lhs.kind == nkind.N_IDENT) {
let fs: *sym = scopelookup(c.cur, e.lhs.str);
if (fs != nil) {
if (fs.skind == skind.SK_FN) {
if (fs.decl != nil) {
if (fs.decl.kind == nkind.N_FNDECL) {
let synth: *node = newnode(nkind.N_TFN, "", 0, 0);
synth.lhs = fs.decl.lhs;
synth.list = fs.decl.list;
let pf: *node = newnode(nkind.N_TPTR, "", 0, 0);
pf.lhs = synth;
return pf;
};
};
};
};
};
};
// opt nil → propagation from inherent-IDENT bail (5-lite-b
// #34). Generic &expr widens to *opt; without opt we can't
// synthesize the pointer node.
@@ -13020,6 +13067,70 @@ fn isstrtname(t: *node) bool = {
return streq(t.str, "str");
};
// addrfnptrmatches — true iff `ptr` (after alias-resolve) is a
// pointer whose referent resolves to a fn type structurally equal to
// `synth` (a synthetic N_TFN built from a fn decl's ret + params).
// Mirror of cstage addrfn_ptr_matches (cmd/wcc/check.c, project #206);
// reuses typeeqast — the same structural-fn comparator cstage uses via
// type_eq(TY_FN,TY_FN) — for symmetry.
fn addrfnptrmatches(c: *checker, ptr: *node, synth: *node) bool = {
if (ptr == nil) { return false; };
let pu: *node = resolvealias(c, unwrapbang(ptr));
if (pu == nil) { return false; };
if (pu.kind != nkind.N_TPTR) { return false; };
let ref: *node = resolvealias(c, unwrapbang(pu.lhs));
if (ref == nil) { return false; };
if (ref.kind != nkind.N_TFN) { return false; };
return typeeqast(synth, ref);
};
// assignableaddrfn — project #206 Option C gate. Mirror of cstage
// assignable_addrfn (cmd/wcc/check.c). A bare `&fn` types structurally
// as `*fn(...)`, nominally distinct from a `*alias` fn-pointer slot;
// isassignable stays nominal (the pointer-fn arm below confidently
// rejects a laundered `*fn` value, like harec types.c:1039-1066). This
// admits only the shape harec adopts via its address-of hint (harec
// check.c:3594-3626): a DIRECT `&`-of-fn-ident whose signature
// structurally matches the destination's pointed-to fn alias, or the
// single matching ptr-to-fn variant of a tagged dst (>=2 same-sig
// variants → ambiguous, reject). Lives at the assignment-boundary
// caller sites — not in exprtype — because the alias identity is
// nominal-lossy once typed and the direct-&fn shape survives only on
// the rhs node. N_FNDECL and N_TFN share parseparams' param-node shape
// (lib/ww/parse/decl.ww + parse.ww), so a synthetic N_TFN over the fn
// decl's lhs/list compares correctly under typeeqast.
fn assignableaddrfn(c: *checker, dst: *node, rhs: *node) bool = {
if (dst == nil) { return false; };
if (rhs == nil) { return false; };
if (rhs.kind != nkind.N_UN) { return false; };
if (rhs.op != tkind.TK_AMP) { return false; };
let id: *node = rhs.lhs;
if (id == nil) { return false; };
if (id.kind != nkind.N_IDENT) { return false; };
let s: *sym = scopelookup(c.cur, id.str);
if (s == nil) { return false; };
if (s.skind != skind.SK_FN) { return false; };
if (s.decl == nil) { return false; };
let d: *node = s.decl;
if (d.kind != nkind.N_FNDECL) { return false; };
let synth: *node = newnode(nkind.N_TFN, "", 0, 0);
synth.lhs = d.lhs;
synth.list = d.list;
let du: *node = resolvealias(c, unwrapbang(dst));
if (du == nil) { return false; };
if (du.kind == nkind.N_TPTR) { return addrfnptrmatches(c, du, synth); };
if (du.kind == nkind.N_TTAGGED) {
let nmatch: i32 = 0;
let v: *node = du.list;
for (v != nil) {
if (addrfnptrmatches(c, v, synth)) { nmatch = nmatch + 1; };
v = v.next;
};
return nmatch == 1;
};
return false;
};
// isassignable — AST-level approximation of C check.c
// type_assignable. Returns true when we know the assignment is
// OK, false only when we're confident it isn't, and "skip" (true)
@@ -13171,6 +13282,31 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
};
};
};
// #206: two pointers whose referents both resolve to fn types are
// NOMINALLY assignable only when structurally equal — and that case
// already returned true via typeeqast at the top. Reaching here
// means the fn signatures differ, or a bare structural `*fn` value
// is being laundered into a `*alias` slot: confidently NOT
// assignable, mirror of cstage's nominal type_assignable (harec
// types.c:1039-1066). The direct `&fn` adopt-the-alias case is
// handled at the assignment caller sites via assignableaddrfn, NOT
// here. Without this the lenient catch-all below silently accepted
// the laundering shape.
if (du.kind == nkind.N_TPTR) {
if (su.kind == nkind.N_TPTR) {
let dref: *node = resolvealias(c, unwrapbang(du.lhs));
let sref: *node = resolvealias(c, unwrapbang(su.lhs));
if (dref != nil) {
if (sref != nil) {
if (dref.kind == nkind.N_TFN) {
if (sref.kind == nkind.N_TFN) {
return false;
};
};
};
};
};
};
// Anything else: don't claim confidence.
*confident = false;
return true;
@@ -13429,8 +13565,11 @@ fn checkletassign(c: *checker, n: *node) void = {
if (est != nil) {
if (!isassignable(c, elemtn, est, &conf2)) {
if (conf2) {
errnotassign(c, elemtn, est, "let");
return;
// #206: direct `&fn` array element.
if (!assignableaddrfn(c, elemtn, ev)) {
errnotassign(c, elemtn, est, "let");
return;
};
};
};
};
@@ -13443,6 +13582,8 @@ fn checkletassign(c: *checker, n: *node) void = {
};
let conf: bool = false;
let ok: bool = isassignable(c, n.lhs, src, &conf);
// #206: direct `&fn` → `*alias` / `(*alias | void)` slot.
if (!ok) { if (assignableaddrfn(c, n.lhs, n.rhs)) { ok = true; }; };
if (!conf) { return; };
if (!ok) { errnotassign(c, n.lhs, src, "let"); };
};
@@ -13460,6 +13601,8 @@ fn checkretassign(c: *checker, n: *node) void = {
if (src == nil) { return; };
let conf: bool = false;
let ok: bool = isassignable(c, c.fnret, src, &conf);
// #206: direct `&fn` returned into a `*alias` / `(*alias | void)`.
if (!ok) { if (assignableaddrfn(c, c.fnret, n.lhs)) { ok = true; }; };
if (!conf) { return; };
if (!ok) { errnotassign(c, c.fnret, src, "return"); };
};