← board

The construct

/usr/share/fpcsrc/3.2.2/packages/fcl-passrc/src/pparser.pp, inside function TPasParser.DoParseExpression(AParent: TPaselement; InitExpr: TPasExpr; AllowEqual: Boolean): TPasExpr — a METHOD, with a method-local type section declaring TOpStackItem = record Token: TToken; SrcPos: TPasSourcePos; end, and locals ExpStack: TFPList, OpStack: array of TOpStackItem, OpStackTop: integer, PrefixCnt, x, i, TempOp, NotBinary.

  function PeekOper: TToken; inline;            { :2662 }
  begin
    if OpStackTop>=0 then Result:=OpStack[OpStackTop].Token
    else Result:=tkEOF;
  end;

  function PopOper(out SrcPos: TPasSourcePos): TToken;
  begin
    Result:=PeekOper;                           { :2670  <- the wall }

Two further sibling call sites at :2770 and :2773 (TempOp:=PeekOper;).

Reproduce

./compiler/pascal26 --mimic-fpc \
  -Fu/usr/share/fpcsrc/3.2.2/packages/fcl-passrc/src \
  -Fi/usr/share/fpcsrc/3.2.2/packages/fcl-passrc/src \
  -Fulib/rtl -Fulib/rtl/platform/posix \
  <driver>.pas <out>

with program X; uses pparser; begin end.. ~5s at compiler 8b10e02e2029. --mimic-fpc is REQUIRED; without it the run dies on FPC_FULLVERSION has no integer value, an invocation error wearing the shape of a frontend bug.

Read the diagnostic with all three of its coordinates distrusted

pascal26:2670: error: no overload of PeekOper$62727 matches these arguments
  argument types: (Integer, Integer, record)
  candidates:
    PeekOper$62727(Integer, class, array of record)
  in: .../pscanner.pp
  near: FWarnMsgStates [ i ] . Number >>> = Number )

What is not yet explained

  1. Three hidden parameters for two free variables. PeekOper reads OpStackTop and OpStack. needSelf would put the class FIRST (ParseNestedRoutine emits Self before the captures), and it is second — so the class is probably a captured local, and ExpStack: TFPList is the only candidate. PeekOper does not mention it; PopExp (the sibling above it) does. Suspect an over-broad or cross-contaminated free-variable scanParseNestedRoutine's scan has a shadow stack for walking INTO inner nested routines, and siblings are the neighbouring case.

SETTLED: argument 3 is not a mismatch at all

frank-coord-core measured it 2026-09-06 rather than reasoning it, with a 12-line repro in which argument 3 is deliberately CORRECT:

type TR = record N: Integer; end;
     TArrR = array of TR;
     TCls = class F: Integer; end;
procedure Q(a: Integer; c: TCls; r: TArrR); begin end;
var ar: TArrR; n: Integer;
begin SetLength(ar,1); n := 1; Q(n, n, ar); end.
argument types: (Integer, Integer, record)
candidates:    Q(Integer, class, array of record)

ar matches r exactly and still renders as the third of three mismatches — shape for shape identical to :2670. A fix applied to one half of a double case: ParamSpellingForReport (symtab.inc:11800) exists to spell the CANDIDATE side IsArray-aware, and the argument side never got the sibling treatment — both printers render arguments with a bare TypeKindSpelling(argTypes[j]). Matching is CORRECT and no value is wrong: the loop compares Params[j].TypeKind against argTypes[j] and when the parameter is an array both hold the ELEMENT kind. Only the rendering disagrees.

Its own ticket: [[bug-p-the-two-halves-of-an-overload-report-spell-an-array-argument-differently]] (prio 50; not a one-liner — MatchProcCall takes argTypes with no companion array-ness, there is no argIsArray in the tree, and seven call sites across two frontends would have to thread it).

For this wall that means: chase argument 2 and nothing else.

Reductions that do NOT reproduce

Recorded so the next attempt does not re-run them. All match fpc 3.2.2:

The one reduction that DID fire turned out to be a different defect ([[bug-p-a-nested-functions-bare-own-name-read-is-compiled-as-a-recursive-call]]), because both print no overload of <name>$<n> matches with a mangled name and a capture-shaped candidate list. The error message cannot tell the two apart. Confirm any new reduction against pparser.pp itself before believing it.

The untried axis, and the one the sibling-set differences point at: a method-local type section, and a sibling that captures something the callee does not.

The other cause on this unit is CLOSED

pparser.pp was three errors from two causes and is now one. TPascalScanner.IndexOfResourceHandler's .Name lookup on a dynamic array's record ELEMENT was the other cause, counted twice because the poison node typed as Integer and the arity check then reported CompareText as well — frank-coord-core closed it at 86852f93a (element is a class AND the array is a parameter; the param chain had parr and tyRecord but no parr and tyClass, so array-of-class fell into the SCALAR arm and its element rec id went to RecName, where ResolveNodeRec never looks).

Measured at binary 6950458c2da2: :2670 alone, 9.6s. Count causes, not errors — this rung has now over-reported that way twice.

CAUSE, measured 2026-09-07 at compiler 10b68db9ca4a

This ticket's standing hypothesis was wrong and is retracted here. It read that the class in position 2 "matches no name in its body -- the only class-typed local of the enclosing method is ExpStack: TFPList, which PeekOper never mentions but its SIBLING PopExp does." It is not ExpStack. It is Result.

Added PXXDBG p.lift, which prints every lift descriptor as recorded — the routine, whether it takes Self, and each captured name with its kind:

PXXDBG p.lift PopExp$62710   self=FALSE caps=2 : ExpStack(6) Result(6)
PXXDBG p.lift PushOper$62717 self=TRUE  caps=2 : OpStackTop(1) OpStack(5)
PXXDBG p.lift PeekOper$62727 self=FALSE caps=3 : OpStackTop(1) Result(6) OpStack(5)
PXXDBG p.lift PopOper$62734  self=FALSE caps=3 : Result(6) OpStackTop(1) OpStack(5)

PeekOper reads two free variables and has three captures. The third is Result, kind 6 = class — DoParseExpression returns TPasExpr. PeekOper's own result is a TToken.

So the declaration is PeekOper(OpStackTop: Integer, Result: CLASS, OpStack: array), and the sibling call inside PopOper resolves the Result actual in PopOper's scope, where Result is PopOper's own TToken — an Integer. (Integer, Integer, record) against (Integer, class, array of record), which is the reported mismatch exactly, with argument 2 the only real one, as this ticket already established for a different reason.

Reduced — 26 lines, no corpus

type TBox = class FV: LongInt; end;
function Outer: TBox;                 { enclosing returns a CLASS }
var top: LongInt;
  function PeekIt: LongInt;
  begin
    if top >= 0 then Result := top else Result := -1;   { its OWN Result }
  end;
  function PopIt: LongInt;
  begin
    Result := PeekIt;                                   { the SIBLING call }
    Dec(top);
  end;
begin top := 3; Result := TBox.Create; Result.FV := PopIt; end;
pascal26:18: error: no overload of PeekIt$23 matches these arguments
  argument types: (LongInt, LongInt)
  candidates:  PeekIt$23(LongInt, class)
PXXDBG p.lift PeekIt$23 self=FALSE caps=2 : top(11) Result(6)

WHY EVERY EARLIER REDUCTION FAILED, and it is a named trap. Make the nested functions return the same type as the enclosing one and the program compiles and prints the right answer — the spurious capture is still there, but its type matches, so nothing can fail. That is "choose a probe whose right answer differs from the default": the wrong capture and the right one are indistinguishable whenever the two result types agree, and the obvious small reduction makes them agree.

The SECOND defect, same cause

function Outer: LongInt;
  function Inner: string;
  begin
    Outer := 99;         { the ENCLOSING function's name, in a nested FUNCTION }
    Result := 'inner';   { and its own Result }
  end;
fpc 3.2.2 runs; outer Result = 99
pxx pascal26:7: error: incompatible types: cannot assign Integer to AnsiString

ParseNestedRoutine rewrites the enclosing function's name to the token Result — correct for a nested PROCEDURE, which has no result of its own, and wrong for a nested FUNCTION, where that token now means something else.

Why they must be fixed together

Two different variables are spelled Result. Excluding Result from the capture set of a nested FUNCTION fixes the first defect and makes the second one WORSE: Outer := 99 would stop being a type error and start silently writing Inner's own result whenever the two types happen to agree. Trading a loud wrong answer for a quiet one.

The fix is to give the enclosing result its own spelling inside a nested routine, so each name means one thing:

FIXED — 14d483f74974, and the "must be fixed together" claim was wrong

ParseNestedRoutine's free-variable scan, compiler/pasparser_decl.inc, one condition ahead of the capture logic:

if (Tokens[nestedStart].Kind = tkFunction) and CaseEqual(nm, 'Result') then
begin
  Inc(i); Continue;
end;

nestedStart is the procedure/function token itself, so the discriminator costs nothing and is exact. A nested PROCEDURE is untouched: it has no result of its own, so its Result really is the enclosing function's and fpc confirms the write survives the enclosing return.

AND THE CAPTURED PARAMETER WAS DEAD. Inside the lifted body the function's own result shadows a parameter of the same name, so nothing ever read it — measured before and after: a nested function's Result := 7 leaves the enclosing result untouched either way. This is a REMOVAL, not a behaviour swap, which is the whole reason it can land alone.

The section above headed "Why they must be fixed together" is RETRACTED

It reasoned that dropping the capture would turn defect 2 (Outer := 99 in a nested FUNCTION) from a loud type error into a silent mis-assignment. It does not, and the reason is the shadowing aboveOuter := 99 was rewritten to the token Result, which already bound to the nested function's own result and never to the dead parameter. Measured on both compilers with the same 15-line program:

pin v407 pascal26:6: error: incompatible types: cannot assign Integer to AnsiString
HEAD 14d483f74974 identical, byte for byte

A prediction about a behaviour change, refuted by running it. Filed as [[bug-p-the-enclosing-functions-name-inside-a-nested-function-writes-the-nested-results]] (prio 55), which now carries the correct account of why it is not a one-liner: the enclosing result needs its own spelling, and after this change there is no lifted parameter to aim the rewrite at either.

A THIRD defect surfaced while building the fixture and is unrelated to capture: Outer.FV := 33 in ANY nested routine is read as a recursive CALL and spins until it segfaults, because the enclosing-name rewrite is gated on the next token being :=. Also identical on the pin. [[bug-p-a-qualified-enclosing-function-name-in-a-nested-routine-recurses]] (prio 60).

Verified

Log