← board

A declaration-only = nil default on a dynamic-array parameter is lost

type
  TArr = array of Integer;
  TC = class
    procedure M(const a: TArr = nil);   { default HERE only }
  end;
procedure TC.M(const a: TArr);          { implementation does not repeat it }
begin WriteLn('M len=', Length(a)); end;
...
o.M;
row pxx fpc 3.2.2
o.M (default on the declaration only) error: wrong number of parameters in call to TC.M M len=0
n: Integer = 5, declaration only, same class works works
const s: AnsiString = 'hi', declaration only works works
const a: TArr = nil written on both sides works works
the same shape as a free routine works works

Four controls, and each removes a different explanation: it is not class methods, not nil, not the declaration-only spelling, and not the type on its own.

Done when

Row 1 prints M len=0, with the four controls kept beside it in one file — the value is plausible in every row, so only the rows' disagreement carries the finding.

Found while closing [[bug-p-a-default-value-is-accepted-on-an-open-array-parameter]], whose positive control writes the default on both sides for exactly this reason and says so.

How it was actually closed

Nobody worked on this ticket. It was fixed by [[bug-p-an-interface-dispatched-call-passing-a-named-dynamic-array-segfaults]], whose fix taught all four method parameter parsers about named array types — which is precisely the disagreement this ticket's summary guessed at and marked unverified. The declaration row and the implementation row now record the same parameter, and the default binds.

The guess being right is not why it is closed; the measurement is. The row prints M len=0 and the four controls still print their own values, which is what the "Done when" asked for.

The regression that came with it, and the instrument that did not see it

Making IsArray true for a named array type broke the sibling refusal. The open-array-default guard asks its CALLER "is this an open array", because IsArray alone is the wrong half of the type — that is the whole point of test_a_named_dynamic_array_parameter_still_takes_nil_as_a_default.pas. The three method parsers were passing the bare flag, correctly, while it could only mean the literal spelling. It now also means a named array, so they pass IsArray and (dynDepth <= 0), matching ParseSubroutine.

gate.sh quick was GREEN for that commit and was not wrong. These rows live in test-core, which the gate does not run and the full-suite hook denies. GREEN was a true statement about 33 unit tests.

And "invisible to the per-fix loop" is what I concluded, and it is wrong. There is a sanctioned one-case runner and the full-suite hook's own refusal text names it:

tools/testmgr.py --tier native --job src:test/<file>.pas      # ~1s, one case

--job <literal> (no glob) is allowed at any tier — it is the repro line auto-filed regression tickets already print. Measured 2026-09-06: the row above runs in 0.5s against a compiler snapshot at the current sha. So the row was not unreachable, it was unrun, and the reason it was unrun is that I believed gate.sh quick and make test-core were the only two things that existed. Run it after any change to a parser your rows exercise; it costs a second and it is the difference between a GREEN that covers your assertion and one that does not.

Log