← board

Repro — the counter is the point

program bare; {$mode objfpc}{$H+}
var calls, depth: LongInt;
function F: LongInt;
  function Inner: LongInt;
  begin
    if depth = 0 then begin Inc(depth); Inner := F; end else Inner := -1;
  end;
begin
  Inc(calls);
  Result := 40 + depth;
  WriteLn('  inner=', Inner, ' res=', Result, ' calls=', calls);
end;
begin calls := 0; depth := 0; WriteLn('final=', F, ' calls=', calls); end.
fpc 3.2.2      final=  inner=40 res=40 calls=1
               40 calls=1
pxx HEAD       inner=-1 res=41 calls=2 ... 40 calls=2
pin v407       identical to HEAD

A value comparison alone could not have settled this — the two readings can produce the same number for the right depth. calls is the row that separates them, and it is why the probe carries a counter rather than only a result.

Why this is not one more token kind in the existing rewrite

ParseNestedRoutine's enclosing-name rewrite now fires on :=, ., [ and ^. Every one of those is unambiguously a result POSITION. A bare name in a read position is the one spelling where a recursive call is a real competing reading, and test_a_nested_routine_assigns_the_enclosing_functions_result.pas carries Recurse := Recurse(k - 1) + 1 as the control — though note that row is parenthesised, so it does not actually exercise this question. There is no in-tree assertion either way for a bare parameterless read.

So the choice is a dialect decision:

Whichever is chosen, the two positions (own body vs nested routine) currently agree with each other and disagree with fpc in the nested one. Say which behaviour is intended in the ticket before touching the rewrite, and add the counter probe as a row — a fixture asserting only the returned value will pass under both readings for most depths.

Not urgent

No in-tree code and no corpus row is known to spell it. Found while fixing bug-p-the-enclosing-functions-name-inside-a-nested-function-writes-the-nested-results, as the one remaining position after every WRITE spelling was closed.

RESOLVED — and the ticket's central claim was wrong: this was not a dialect decision

The ticket says the fix "has to decide what F with no parentheses means, and that is a dialect decision more than a parser one", and asks that the intent be settled before the rewrite is touched. It was already settled, and already implemented, in the sibling path. The own-body read in ParseFactorCore has keyed on not DelphiMode since the paramless flip and reproduces both of fpc's answers exactly, warning under --warn-self-result. ParseNestedRoutine's enclosing-name rewrite never consulted the mode at all, so it took the Delphi reading in objfpc too. One concept, two paths, second path broken — normalise-dont-special-case, not Track U.

Why the ticket read it as a fork: it measured one mode. Everything it reports is true of {$mode objfpc} and it never ran {$mode delphi}, where our behaviour was already correct. Measured 2026-09-08 against fpc 3.2.2, both modes, with the ticket's own call counter:

in a nested routine fpc objfpc fpc delphi pxx before pxx after
Inner := F bare read result var CALL CALL matches both
Outer.FV := 33 qualified result var result var result var unchanged
F; bare, statement Illegal expression Illegal expression accepted see below

The "widening would rewrite a recursion into a result read" worry, which is written into the code twice, is true in Delphi mode only. In objfpc there is no competing call reading to protect: fpc rejects a bare F; outright in both modes, so no legal program spells a recursive call that way from a nested routine. Nothing had to be given up.

The fix

bareRead := (not DelphiMode) and (i + 1 < TokCount) and (Tokens[i + 1].Kind <> tkLParen), OR-ed into the existing :=/./[/^ test. No special cases — see below for the two I wrote before measuring and then deleted.

F; in objfpc now becomes Result; and is refused (expected ':=' before ';') where it used to compile as a recursive call. That is a deliberate consequence and it moves us toward fpc, which refuses it too; the diagnostics differ in wording, which is deferred.

Two exceptions I invented on reasoning, and fpc refused both

Recorded because both looked like prudence and neither survived a measurement:

Fixtures — two files, and each is proven to fail

test/test_nested_bare_enclosing_name_objfpc.pas and ..._delphi.pas, wired into test-core, .expected generated from fpc rather than written.

The counter is the assertion, as the ticket correctly insisted: both readings return 40, so a value-only row passes either way.

Each row was shown to bite, which is the part that is easy to skip because both files are green:

Self-host fixedpoint holds (converged, 006274f6d15d).

Log