f(...)[i] — indexing a call result
- Type: compat (Pascal frontend parity) — Track P
- Status: backlog
- Opened: 2026-08-05
- Found by:
tools/fpc_diff_probe.sh, dynamic-array case batch (dynarray-copy-and-alias, now tagged[known]).
This is two gaps behind one syntax, and they fail differently.
Gap 1 — parse error: an UNQUALIFIED call, indexed
program dy3;
type TArr = array of Integer;
function Make: TArr;
begin SetLength(Result, 2); Result[0] := 7; Result[1] := 8; end;
var s: string;
begin
s := 'hello';
writeln(Copy(s, 2, 3)[1]); { FPC: e }
writeln(Make[1]); { FPC: 8 }
end.
pxx on either line:
Expected: ), but got: (Kind: 76, Line: 10)
pascal26:10: error: unexpected token
near: s >>>
The postfix [ is not accepted after a call in an unqualified expression
position. (Note the diagnostic is also nearly contentless — near: s >>> with
nothing after the marker.)
Gap 2 — parses, then cannot be lowered: a QUALIFIED call with arguments
type
TArr = array of Integer;
TBag = class
function Arr: TArr;
function ArrP(k: Integer): TArr;
end;
...
writeln(b.Arr[1]); { WORKS — prints 6 }
writeln(b.ArrP(3)[0]); { FPC: 3 }
writeln(TBag.Create.Arr[0]); { FPC: 5 }
pascal26:14: error: IR_UNSUPPORTED: frontend could not lower AST node (kind 8)
— a frontend gap, would miscompile
Kind 8 is AN_CALL (compiler/defs.inc:198). So the parser builds the index
over a call node and the lowering has no path for it — it needs a temporary to
hold the returned dynamic array before indexing, the same temp the paramless
case already gets.
b.Arr[1] — a paramless method returning a dynamic array — works and gives
the right value. That is the shape to follow: whatever gives the paramless
qualified call its temp is what the other three need.
Why it matters
SplitString(s, ',')[0], Copy(line, 1, 4)[1], GetItems(k)[0] are ordinary
Pascal. The failure mode is at least honest — a parse error or a loud
IR_UNSUPPORTED, never a wrong value — so this is a gap, not a corruption.
Gate
Track P: make test + self-host fixedpoint (byte-identical). Track P catch —
the Pascal frontend lives in the shared lexer.inc/parser.inc, so this must
not be edited concurrently with Track A. The [known] probe case starts
reporting the moment it works.
NARROWED 2026-08-05 — one of the two gaps is already closed; the rest is smaller than described
Measured at HEAD. The syntax does not fail uniformly:
| form | result |
|---|---|
Nm()[1] — explicit call, string result, indexed |
works |
MakeR.a / MakeR().a — call result, .field postfix |
works |
Make[1] — bare paramless call, dyn-array result |
parse error |
MakeN(1)[1] — explicit call, dyn-array result |
parse error |
Copy(s,2,3)[1] — builtin intrinsic, string result |
parse error |
a := Make; a[1] (control) |
works |
So the ticket's "either fails to parse or reaches IR lowering as an un-lowerable AN_CALL" is now only half true:
- The LOWERING half is done.
IRLowerAddressaccepted a call in address position only fortyRecord/tyVariant; it now also acceptstyAnsiStringand the frozen kinds (bug-p-index-getter-backed-string-property, fixed today). That is whyNm()[1]works, and it means a string-returning call no longer reaches lowering un-indexable. - What remains is purely PARSE-side, and is two narrower cases, not one
broad one:
- a dynamic-array-returning call, indexed — fails with parens or without, so it is the RESULT TYPE that matters, not the call spelling;
- a builtin intrinsic (
Copy) whose branchExits before any postfix chain runs — a different code path from a user call, which is whyNm()[1]andCopy(...)[1]disagree despite both returning a string.
Recording rather than fixing: the hook is inside the Pascal factor parser's
call branch, which is the densest function in the file with many early Exits,
and a mis-placed postfix loop there changes expression parsing everywhere. It
wants a session with room to gate properly, not a tail-end patch. The two cases
above are the actual scope, and the lowering they need already exists.