← board

@BodiedProc passed as an ARGUMENT escapes the cdecl soundness reject and silently miscompiles

A bodied Pascal proc marked cdecl receives the internal convention (every param in a GP register by position, >6 all-stack), while an indirect call through a cdecl proc-type value marshals true SysV (floats in xmm0..7, int and SSE classes counted independently). The two coincide only for <=6 integer/pointer params. compiler/ir.inc has a loud reject for the unsound binding — but it is keyed on the assignment shape, AN_ASSIGN whose RHS is AN_PROCADDR. Passing the same @proc as a call argument is a different AST shape and is not checked.

Measured at 82c135761, self-hosted binary f2bfbb3c94a5, clean tree:

type TCb = function(a: Double; b: Integer): Integer; cdecl;
function MyCb(a: Double; b: Integer): Integer; cdecl;
begin Result := Trunc(a) + b; end;          { want 9 for (2.5, 7) }
procedure Take(f: TCb);
begin writeln(f(2.5, 7)); end;
shape result
p := @MyCb (assignment) error: ... by-value float parameter is not SysV-callable yet
Take(@MyCb) (argument) compiles clean, prints 4261032
8 integer params, argument shape compiles clean, prints 1126281949 (want 36)

Both halves of the reject — the by-value-float half and the ParamCount > 6 half — escape through the argument shape. Deterministic across runs. 4261032 is 0x410268, code-address-shaped, consistent with the callee reading a from a GP register while the caller placed it in xmm0.

The control that could have falsified this: the same argument shape with two integer params prints 9, correctly. So this is the calling-convention mismatch the reject describes, not function-pointer arguments being broken in general.

Not currently breaking anything in-tree

Every bodied cdecl proc in the tree is a GTK trampoline in lib/pcl with <=3 pointer params (ControlClickTramp, TimerTramp, ...), which sits inside the range where the internal convention and SysV coincide. Real but not on fire.

The pattern

The reject is not a conservative wall with a feature behind it. It is a wall with a door in it. A guard calibrated to a SPELLING rather than a SHAPE, which is the third instance of this class recorded on 2026-08-30 alone:

Worth naming, because a repo-wide sweep for guards keyed on one AST shape or one source spelling is a real piece of work someone should eventually do.

Fix

Subsumed by feature-cdecl-bodied-sysv-prologue: give bodied cdecl procs a genuine SysV prologue on x86-64, after which both shapes are sound on x86-64 and the reject is obsolete there rather than incomplete.

SCOPE CORRECTION, made when the fix landed. The sentence above originally read "both shapes are sound", unqualified, and that was an overclaim. The fix is x86-64 only, because only x86-64 got the prologue arm. On i386/arm32/aarch64/riscv32 the argument-shaped door is exactly as open as it was — measured on aarch64 after slice 1: p := @MyCb is refused while Take(@MyCb) on the same source compiles clean and prints 0 where 9 is correct.

That remainder is bug-a-the-cdecl-soundness-reject-still-has-its-argument-shaped-door-on-four-targets, and this ticket is closed against its x86-64 repro only. A closed ticket whose claim is broader than its fix is worse than an open one, because nobody re-reads it. Filed separately because a silent wrong value needs its own slug for tstate and regression citation, because "fixed as a side effect of a feature ticket" is invisible to anyone searching the symptom, and because if that feature slips this must not slip with it.

Gate: the repro above, on x86-64, prints 9 and 36. Covered by test/test_cdecl_bodied_sysv.pas (12 checks, in test-core).

Log