Files
ww/test/lang/fnptr_derefless_call_test.ww
Hojun-Cho 3d79735964 check: reject a deref-less multi-level **fn call, not silently miscompile (#14)
wwstage exprtype's N_CALL fn-value arm peeled ALL pointer levels before the
TY_FN gate, so a deref-less `pf(21)` where pf:**fn type-checked and lowered --
then segfaulted at runtime (a silent miscompile). cstage peels exactly one
level and loud-rejects (the C6a discipline, check.c:1947).

Align wwstage DOWN: peel one level (loop -> if); a remaining non-TY_FN callee
hits a loud "calling non-function" reject mirroring cstage's message. Both
stages now reject the exotic deref-less `**fn`/`***fn` shape; the legitimate
`(*pf)(21)` and one-level deref-less `f(21)` (f:*fn) still compile + run.
Multi-level autoderef is a separate deferred FEATURE, not a miscompile to lower
(rule 7/10 -- align the richer stage down to the leaner, no value ships).

ww-only change (cstage is the correct oracle); a reject emits no asm, so the
byte-id baselines and LANGBYTEID floor are unchanged.

Pins: cfail test/wcc/data/fnptr_pp_derefless_reject (both stages reject,
reddens-on-revert -- the silent miscompile resurfaces if the fix is reverted) +
test/lang/fnptr_derefless_call_test (positive guard (*pf)(21)==42 and one-level
f(21), value-asserted + byte-id, so the fix does not over-reject the legitimate
one-level autoderef).
2026-06-29 15:49:01 +09:00

80 lines
3.4 KiB
Plaintext

// 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.
//
// The #181-cgen path uses the callee VALUE as the call target, so it
// autodereferences exactly ONE level: a `*fn` callee is the fn address, but
// a `**fn` callee is only the *address of* the fn-ptr. So a deref-less
// `**fn` call (`let pf: **fn(...) = &f; pf(21)`) drops a `MOVQ (AX),AX` and
// is a hard reject in BOTH stages — cstage at cmd/wcc/check.c:1947 (one-level
// peel), wwstage aligned DOWN at selfhost/cmd/wcc/check.ww (task #14, was the
// SILENT-miscompile side). That loud reject is pinned in
// test/wcc/data/fnptr_pp_derefless_reject. The LEGAL one-explicit-deref
// `(*pf)(21)` (which yields a `*fn`, then autoderefs the remaining single
// level) is covered by derefless_pp_one_explicit_deref below — the shape the
// one-level peel must NOT over-reject. Multi-level fn-ptr autoderef is a
// deferred FEATURE (#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);
};
@test fn derefless_pp_one_explicit_deref() void = {
// #14 positive guard: a `**fn` with ONE explicit deref `(*pf)` yields a
// `*fn`, whose call autodereferences the remaining single level — legal,
// the shape the one-level peel must NOT over-reject. Its deref-less twin
// `pf(21)` is the loud reject pinned in fnptr_pp_derefless_reject.
let f: *fn(x: i32) i32 = &add1;
let pf: **fn(x: i32) i32 = &f;
assert((*pf)(21) == 42);
};