Measured, 2026-08-25 (pinned AND HEAD — identical)
type TIntArr = array of Integer;
TMat = array of array of Integer; { the named-alias spelling behaves the same }
function MakeMat: TMat;
var r0, r1: TIntArr;
begin
SetLength(r0, 2); SetLength(r1, 2);
r0[0] := 1; r0[1] := 2; r1[0] := 3; r1[1] := 4;
SetLength(Result, 2); Result[0] := r0; Result[1] := r1;
end;
var m: TMat;
begin
m := MakeMat;
WriteLn(m[1][0], ' ', Length(m), ' ', Length(m[1])); { fpc: 3 2 2 }
end.
pxx prints the leading literal and then SIGSEGV. Length(MakeMat[1]) dies
the same way. The one-level case (array of Integer returned and indexed) is
correct in every spelling — see
[[compat-pascal-index-a-function-call-result]] and
test/test_index_a_dynamic_array_call_result.pas.
Why it is filed separately
It was hit while landing the call-result indexing fix, and the first suspicion was that fix. It is not: the via-VARIABLE spelling crashes identically on the PINNED binary, so the defect is older and lives below the indexing question — somewhere in how a nested dyn-array RESULT is built, returned or stored, not in how it is subscripted.
A related smell found in passing, worth checking first: SetLength(Result[0], 2)
— sizing the inner array through Result — is refused outright with "SetLength
expects an array variable in IR codegen", which is why the repro above has to
build r0/r1 as locals and assign them in. Both point at Result of a nested
dyn-array type not carrying its full shape.
Suspected, NOT measured
ProcRetDynDepth is the obvious suspect (ArrTypeDynDepth for the alias arm,
the hardcoded retDynDepth := 1 for the literal array of arm in
compiler/pasparser_proc.inc ~944). But the via-variable crash means the value
itself is wrong, so check what the CALLEE writes before blaming what the caller
reads — PXXDBG=a.symptr:* on Result, then the IR of the return.
Gate
make compiler/pascal26 + the repro above matching fpc -O1 + tools/gate.sh quick. Add the two-level rows to
test/test_index_a_dynamic_array_call_result.pas, which deliberately omits them
today.
RESOLVED 2026-08-25 — one hardcoded 1, and two consumers keyed on a spelling
Root cause, measured not guessed. compiler/pasparser_proc.inc allocated the
Result slot as
idx := AllocDynArray('Result', retElemTk, 1);
— a hardcoded nesting of 1, while ProcRetDynDepth[procIdx] two hundred lines
above already held the declared one. So for function F: TMat an OUTER element
was read as a 4-byte Integer instead of an 8-byte handle. Nothing else about
the value was wrong, and the bisect below is what said so:
| spelling | before |
|---|---|
loc: TMat local inside the function |
correct (2 2 3) |
procedure Fill(var Res: TMat) |
correct |
Result := loc then read Result[1] |
garbage |
Result[1] := r1 then read in the CALLER |
Length(m[1]) = 0 |
SetLength(Result[0], 2) |
refused: "SetLength expects an array variable" |
The ticket's own suspicion (ProcRetDynDepth, retDynDepth := 1) was aimed at
the right family and the wrong line: the proc ROW was right all along — the
alias arm reads ArrTypeDynDepth correctly — and it was the SYMBOL built from
it that dropped the depth. The literal function F: array of array of T
spelling does hardcode retDynDepth := 1, but fpc rejects that syntax
outright ("Type identifier expected"), and pxx refuses the nested form
loudly, so nothing is lost there.
Two more halves fell out, both the same mistake in a different place — a consumer keyed on one SPELLING of a dyn-array value instead of on the value:
Length(MakeMat[1])printed25769803783=0x600000007, the row's first two elements read as one 8-byte word. The IR arm that lifts a dyn-array call result forLengthtestedAN_CALL and ProcRetIsDynArrayand hardcoded depth 1; the parser had already liftedMakeMat[1]into anAN_COMMA, which that test cannot see. It now keys on(not IsASTLValue) and NodeDynDepth > 0and takes depth/element from the node.for i in MakeMat[1]said "undefined variable (MakeMat)" — the for-in qualified-source dispatch resolved the name withFindSymonly, so a PROC arrived atParseLValueASTasidx = -1. Same fix as theHigh/Lowarms: parse the source withParseExprwhen the name is a proc.
And AN_COMMA — the node the call-result materialisation builds — was missing
from NodeDynDepth / NodeDynBaseTk / NodeDynBaseRec / NodeDynBaseSym
(ast_arena.inc) and from DynArrayNodeDepth (symtab.inc), so a lifted
dyn-array value stopped being a dyn array to every shape query at once.
ResolveNodeRec had had the comma arm since the csmith struct-through-a-comma
fix; the dyn-array twins never got it.
The two depth functions are twins and they drift.
NodeDynDepth(ast_arena) andDynArrayNodeDepth(symtab) answer the same question, andDynArrayNodeDepth's own AN_INDEX arm already carries a note about having fallen behind its sibling once before. Both got the comma arm in one edit, and a note now sits in each pointing at the other. Merging them is the real fix and is a Track A refactor, filed separately.
Also removed: the depth>1 refusal added to the for-in arm in 3fba47f2b, which
existed only to avoid trading a loud error for this crash. It is unnecessary now
— nested for-in over a call result matches fpc.
Regression: test/test_a_nested_dynamic_array_result.pas, wired into
test-core, .expected = fpc 3.2.2's own output. It asserts the callee-side
read, SetLength through Result, depth 3, a managed base element, the direct
call-result index, High, and both for-in spellings.
Gate: make compiler/pascal26 converged in 1 round, tools/gate.sh quick
GREEN, fpc-testsuite conformance unmoved.
Log
- 2026-08-25 — resolved, commit 1facc0a40.