From e515a08fb71e4cd382338c85933fc29544d76d76 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sat, 27 Jun 2026 14:49:36 +0900 Subject: [PATCH] check: autodereference a single-level *fn callee before the call gate (C6a) cstage rejected a deref-less function-pointer call `f(21)` (f: *fn...) with "calling non-function" while wwstage accepted it and ran correctly -- a cs!=ww divergence. Hare auto-dereferences a pointer callee to its fn type before the call. Peel one pointer level after the named-type chase so cstage accepts a `*fn` callee, matching wwstage and Hare. One level only, deliberately: the #181-cgen indirect-call path lowers the callee VALUE as the target (CALL AX), which is the fn address for a single `*fn` but only the address-of the fn-ptr for `**fn`. A multi-level peel would accept `**fn` past what cgen can lower -- a silent both-stage miscompile (returns garbage). cstage stays loud on `**fn` (rule 7); wwstage's loop-accept of all levels (check.ww:3763) is the over-permissive side, filed for align-down plus the deeper cgen multi-level autoderef. Surfaced by the codegen miscompile hunt (finding C6a). Pinned by test/lang/fnptr_derefless_call_test.ww (deref-less + canonical (*f)(21), multi-arg, alias-chain, tuple-return; reverting the peel reds the deref-less rows at compile). --- cmd/wcc/check.c | 12 +++++ test/lang/fnptr_derefless_call_test.ww | 63 ++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 test/lang/fnptr_derefless_call_test.ww diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 5b7f7a74..87112654 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -1898,6 +1898,18 @@ cexpr(Checker *c, Node *n) return n->type = ty_err; } Type *u = type_chase_named(ft); + /* Autodereference a pointer callee to its fn type before the + * gate (harec check_autodereference → type_dereference, + * ref/harec/src/check.c:1566, types.c:13). ONE level only: the + * #181-cgen path lowers an indirect call by using the callee + * VALUE as the target (CALL AX) — the fn address for a single + * `*fn`, but only the *address of* the fn-ptr for `**fn`. So a + * deref-less `**fn` call drops a `MOVQ (AX),AX` and miscompiles + * on BOTH stages (byte-id-blind, returns garbage). Stay loud on + * `**fn` (rule 7) rather than match wwstage's loop-accept + * (selfhost/cmd/wcc/check.ww:3763, which over-accepts past + * #181-cgen); multi-level fn-ptr autoderef is deferred (#181). */ + if (u && u->kind == TY_PTR) u = type_chase_named(u->sub); if (u == NULL || u->kind != TY_FN) return n->type = err(c, n->pos, "calling non-function %s", type_name(c->a, ft)); diff --git a/test/lang/fnptr_derefless_call_test.ww b/test/lang/fnptr_derefless_call_test.ww new file mode 100644 index 00000000..1c99cd65 --- /dev/null +++ b/test/lang/fnptr_derefless_call_test.ww @@ -0,0 +1,63 @@ +// fnptr_derefless_call_test — a deref-less call through a `*fn` pointer +// (C6a, task #7). `f(21)` where `f: *fn(...)...` autodereferences one +// pointer level to the fn before the call, exactly like the canonical +// `(*f)(21)`. wwstage already accepted this (it mirrors harec's +// check_autodereference, ref/harec/src/check.c:1566); cstage's N_CALL +// checker arm peeled only TY_NAMED and rejected the `*fn` callee +// ("calling non-function"), the under-permissive cs!=ww side. The fix +// peels ONE pointer level in cmd/wcc/check.c so BOTH stages accept it and +// emit byte-identical asm (rule-10) — cgen lowers a bare fn-ptr value as +// the call target (#181), identical to the deref form. +// +// REVERT MANIFESTATION: revert the cstage checker peel and the +// deref-less rows below no longer COMPILE under cstage ("calling +// non-function"), so this whole file fails to build in the cstage `ww +// test` / byte-id legs — a hard red, not a silent value drift. +// +// NOT covered here: a `**fn` deref-less call (`let pf: **fn(...) = &f; +// pf(21)`). The #181-cgen path uses the callee VALUE as the call target, +// so it autodereferences exactly ONE level; a `**fn` deref-less call +// drops a `MOVQ (AX),AX` and miscompiles on BOTH stages (byte-id-blind, +// returns garbage). cstage deliberately stays loud on `**fn` (one-level +// peel); multi-level fn-ptr autoderef is deferred (#181). The explicit +// `(**pf)(21)` is correct (cgexpr emits both loads). + +package fnptr_derefless_call_test; + +fn add1(x: i32) i32 = { return x + 21; }; +fn many(a: i32, b: i32, p: *i32) i32 = { return a + b + *p; }; +fn pair(x: i32) (i32, i32) = { return (x, x + 1); }; + +@test fn derefless_minimal() void = { + // the bug repro: `f(21)` through a `*fn` callee, no explicit deref. + let f: *fn(x: i32) i32 = &add1; + assert(f(21) == 42); +}; + +@test fn deref_form_same_value() void = { + // the canonical `(*f)(21)` must agree — both spellings call the same fn. + let f: *fn(x: i32) i32 = &add1; + assert((*f)(21) == 42); + assert(f(21) == (*f)(21)); +}; + +@test fn derefless_multi_arg() void = { + // multiple args, scalar + ptr mix, through a deref-less callee. + let z: i32 = 5; + let f: *fn(a: i32, b: i32, p: *i32) i32 = &many; + assert(f(3, 7, &z) == 15); +}; + +@test fn derefless_alias_chain() void = { + // the fn-ptr value survives a plain copy and still calls deref-less. + let f: *fn(x: i32) i32 = &add1; + let g: *fn(x: i32) i32 = f; + assert(g(21) == 42); +}; + +@test fn derefless_tuple_return() void = { + // multi-register return ABI through the deref-less call shape. + let f: *fn(x: i32) (i32, i32) = &pair; + let a, b = f(7); + assert(a + b == 15); +};