← board

Repro

program p1;
{$mode objfpc}{$H+}
type TBox = class FV: LongInt; end;
function Outer: TBox;
  procedure P;
  begin
    Outer.FV := 33;        { qualified enclosing name, from a nested PROCEDURE }
  end;
begin
  Result := TBox.Create;
  Result.FV := 1;
  P;
  WriteLn('fv=', Result.FV);
end;
begin WriteLn('final=', Outer.FV); end.
fpc 3.2.2 fv=33 / final=33
pxx, pin v407 and HEAD 14d483f74974 prints final= then SIGSEGV (stack exhausted by unbounded recursion)

Result.FV := 33 in the same position works. So does Outer := .... It is the combination — enclosing NAME, then a qualifier — that has no path.

Cause

ParseNestedRoutine, compiler/pasparser_decl.inc, the enclosing-name rewrite:

if (CurProc >= 0) and (i + 1 < TokCount) and
   (Tokens[i + 1].Kind = tkAssign) and
   CaseEqual(nm, UnqualifiedRoutineName(Procs[CurProc].Name)) and
   (FindSym('Result') >= 0) then

The tkAssign test is deliberate and its comment says why: inside a function the bare name is ALSO a recursive call, and only a following := separates the two readings — Recurse := Recurse(k - 1) + 1 has both in one statement, and test_a_nested_routine_assigns_the_enclosing_functions_result.pas carries that control. The test is right for a bare name and does not generalise. After a ., recursion is not a competing reading at all: Outer.FV cannot be a call, because a call's argument list cannot start with a field selector. The rewrite declines anyway and the name falls through to the ordinary identifier path, which resolves it as the routine and emits a call.

The failure is quiet in the worst way

No diagnostic. The compiler accepts the unit, and the only symptom is a segfault at run time in a program with no obvious recursion in it. A wrong answer would at least be a value; this is a hang that ends in a signal, and the crash location is a stack frame of Outer that names nothing about the nested routine.

Shape of the fix

Widen the discriminator from "next token is :=" to "next token is := OR the name is being QUALIFIED" — ., [, ^ — since none of those can begin a recursive call's arguments. Keep the existing control green: a bare name NOT followed by := must stay a recursive call.

Both readings of a bare Outer are already exercised; add a qualified row for each of ., [ and ^, and one for a nested PROCEDURE as well as a nested FUNCTION, because the two reach this line by different routes.

FIXED — and the ticket's own "shape of the fix" was right where I was cautious

compiler/pasparser_decl.inc, ParseNestedRoutine's enclosing-name rewrite: the discriminator is now := or a selector (., [, ^) on the next token, for a nested PROCEDURE. Fixture test/test_a_nested_routine_can_write_the_enclosing_result_through_a_selector.pas (test_nestselwrite26), six rows, all against fpc 3.2.2.

The cautious rule is the wrong one, and I built it before measuring

I first wrote a helper that walked the selector chain and rewrote only if it ended at :=, reasoning that a qualified name on the RIGHT must be a call — x := Outer.FV calls Outer and reads a field of the result. That is what this ticket implies too, in the sentence "after a ., recursion is not a competing reading at all". fpc 3.2.2 says the reading is not competing in EITHER position:

row source fpc
rmw Outer.FV := Outer.FV + 6, result starts at 60 66
read nested gRead := Outer.FV, entered once behind a depth counter 70, not 71

Under the cautious rule the right-hand Outer.FV in rmw stays a call and the program never returns. So a function's name carrying a selector is the result variable in either position, and only a BARE name has the two readings the := test exists to separate. The helper is gone; the condition is a token-kind set.

The two values in the read row differ deliberately: the outer call sets FV := 70 + depth so a re-entry would answer 71. With both at 70 the row could not tell a call from a read — the expected value would have collided with the failure value.

( is deliberately not in the set, so Outer(x).F stays a call.

Nested PROCEDURE only, and that is the other ticket rather than caution

The rewrite's target is the token Result, which inside a nested function names THAT function's result. Widening this to a nested function would trade an unbounded recursion for a silent write to the wrong variable, which is strictly worse — the enclosing result needs a distinct spelling first. [[bug-p-the-enclosing-functions-name-inside-a-nested-function-writes-the-nested-results]] A nested function keeps today's behaviour, and the fixture says so in its header rather than asserting it: the row still recurses and cannot be run, and asserting a crash is not an assertion.

Controls green

test_a_nested_routine_assigns_the_enclosing_functions_result (the bare name's two readings) and test_a_nested_functions_own_result_is_not_the_enclosing_ones both pass unchanged.

Log