← board

A defaulted trailing parameter disables argument type checking

Repro

program w1;
type TR = record a: Integer; end;
function P(const c: AnsiString; k: Integer = 0): Integer;
begin P := Length(c); end;
var r: TR;
begin r.a := 1; WriteLn(P(r)); end.
compiler result
fpc 3.2.2 Error: Incompatible type for arg no. 1: Got "TR", expected "AnsiString"
pxx, pin v410 (c599e8546121) compiles, rc=0, binary segfaults
pxx, fixed no overload of P matches these arguments / argument types: (record)

Nothing here is exotic. There is no overload, no generic, no set literal, no cross-target width question -- a single routine, one defaulted parameter, and an argument of the wrong type.

Why it survived

The check was never missing. One of the two spellings of the same call was not wired to it. Written out, P(r, 0) is refused by pxx today and always was; only the spelling that OMITS the defaulted argument reaches TryFillTrailingDefaults, and that function's candidate scan asks two questions -- does the name match, and does the arity fit once the tail is defaulted -- and no third. This is normalise-dont-special-case.md's sibling rule in its stated form: grep for the OTHER SPELLING'S HANDLER, not for the feature. Both spellings mean the same thing to whoever wrote the source, so no test corpus separates them.

It is also structurally invisible to the instrument that would catch it: the fallback runs only where the ordinary match has already refused, so the population it rescues is exactly the population of calls with a type error in them. A suite of correct programs cannot reach it.

Measured boundary

All rows fpc 3.2.2 -Mobjfpc against pxx at the commit before the fix.

argument parameter trailing default pxx before pxx after fpc
TR record AnsiString yes compiled, 1 REFUSED refused
TR record AnsiString no REFUSED REFUSED refused
AnsiString Integer yes compiled, 4265208 (an address) REFUSED refused
['x'] set literal AnsiString yes compiled, Length = 17297991344808736 REFUSED refused
['x'] set literal AnsiString no REFUSED REFUSED refused
record, in position 2 of 3 AnsiString yes compiled REFUSED refused
AnsiString, in position 3 of 4 Integer yes compiled REFUSED refused

The two no rows are the control: same argument, same parameter, same compiler, and the only thing that varies is whether the trailing default was written out.

Log

The fix

TrailingDefaultArgsAcceptable (pasparser_call.inc, beside FillMatchArgChannelsAt), asked from the candidate scan. Three properties are deliberate:

One gate, three callers: the expression site (pasparser_expr.inc), the statement site (pasparser_stmt.inc) and NilPy's (pyparser.inc). A check at the call sites would have been three spellings of one rule.

Tests

A REDUCTION OF THIS CAN LOOK COMPLETELY HEALTHY — the record shape is load-bearing

Measured 2026-09-17 by frankuser while verifying the control, and worth more than the verification. Reconstructing the repro from a description — a record of three Int64s rather than this ticket's one-Integer TR — compiles under the pin, runs cleanly, and prints 3. A small, sensible-looking integer. Reported from that run alone it reads as does not reproduce, with a clean run to point at.

It was never a false instance: HEAD refuses that program too, by the same fallback. Only the CONSEQUENCE differed. Length(c) reads whatever bytes sit at the argument, and a wide record happens to put a plausible small number there where a narrow one puts a crash. The observable is a function of the victim's memory layout, so the severity of any given reduction is luck. Use test/test_default_arg_typecheck_fail.pas rather than a reconstruction — it is in the tree, its shapes are chosen, and a reader who reduces this to "some record, some string parameter" can get a healthy run and close the ticket on it.

Corpus: no cost (expectation stated before the run)

This fix makes the compiler STRICTER, which on a corpus can only lose units and never gain them — and gate.sh quick cannot see that. Expectation recorded before the sweep: 21 / 10 / 176 unchanged; any movement in BOTH-OK is a regression, not a win.

Measured at 2de677672 over FPC's own compiler, 207 units, run as three foreground chunks of a partition asserted to union exactly to the glob (the probe's own header records that a backgrounded full sweep has been lost twice): 21 BOTH-OK / 10 ORACLE-NO / 176 PXX-FAIL, 207 rows, 207 distinct units. Unchanged. The null row is reportable only because the expectation was written down first.

What this does NOT fix

bug-p-an-array-constructor-in-argument-position-is-typed-as-a-set [55] is a separate and still-open defect that this one was found underneath. With the fallback no longer rescuing it, P(['x']) against an AnsiString / array of AnsiString overload pair now selects fpc's candidate (the array one) instead of the string one -- but the argument is still presented as a SET, so the callee reads a garbage length. Selecting right and lowering wrong is one defect where there were two; it is not a fix. See that ticket.