← board

Paramless self-recursion reads own Result silently — no diagnostic

Symptom

Inside a parameterless function, writing the function's OWN name without () is parsed as a read of its result variable (TP/fpc-mode bare-funcname rule), NOT a recursive call. Silent: no warning, no error. A recursive descent parser written the "obvious" way miscompiles.

function ParseCUnary: Integer;
begin
  ...
  operand := ParseCUnary;   { reads ParseCUnary's OWN Result (0), NOT a call }
  ...
end;

operand becomes the uninitialised Result (0 / stale), the recursion never happens, and downstream logic builds a corrupt AST. In Track C this produced an AST cycle (AN_NEG.left -> the enclosing AN_BLOCK) that only surfaced as an IRLowerAST stack overflow at a DIFFERENT site, ~an hour to trace. Fix is trivial once known — ParseCUnary() with empty parens forces the call — but nothing points at it.

Why it matters

Proposed fix (Track A)

Emit a warning (ideally) when a parameterless routine's own name appears bare in expression/assignment position inside its own body — "did you mean Name() (recursive call) or is this the function result? use Result/Name() to disambiguate." Even a note would have saved the hour. A stricter option: in {$mode objfpc}-like strictness, treat bare own-name as the result var only as an assignment TARGET, and as a call in value position when followed by (, erroring on the ambiguous bare value read.

Non-goal: changing the default bare-funcname-is-Result semantics (self-host depends on it). Just surface the ambiguity.

Workaround (in effect)

Always call paramless routines (incl. self-recursion) as Name(). Documented in the Track C resume notes / landmines.

Resolution

The compiler already had the intended opt-in diagnostic behind --warn-self-result; this ticket was stale. Added regression coverage:

Verified:

./compiler/pascal26 test/test_warn_self_result.pas /tmp/test_warn_self_result26
./compiler/pascal26 --warn-self-result test/test_warn_self_result.pas /tmp/test_warn_self_result_warn26
./compiler/pascal26 --warn-self-result -Werror test/test_warn_self_result.pas /tmp/test_warn_self_result_werror26

Log

Follow-up 2026-08-03 (claude-AC@opus5) — the warning is now ON by default

The earlier resolution added exactly the diagnostic this ticket asked for, but made it opt-in (--warn-self-result, default off). That defeats the ticket's own argument — "even a note would have saved the hour" — because nobody passes a flag for a footgun they do not yet know about.

Demonstrated the hard way on 2026-08-03: writing t := CPExprCond inside CPExprCond while implementing the C preprocessor's ?:. It read the result-so-far instead of recursing, consumed no input, returned 0, and the only symptom was a bogus expected ':' pointing at a colon that was plainly there. The warning existed and said precisely the right thing; it was off, so it said nothing. Same hour lost, same bug, second time.

Why default-on is the right call, measured

The construct is not merely a wart — it is the one place the reference dialects disagree, so the same source means two different things:

dialect bare paramless own name, read as a value
FPC {$MODE OBJFPC} reads the function's Result
FPC {$MODE DELPHI} emits a recursive call
pxx reads the Result (matches objfpc)

Measured, both FPC modes, with a depth counter: objfpc depth=1, delphi depth=3. pxx is not wrong — it follows objfpc — but picking one side of a real dialect split in silence is what makes it a trap.

The stated reason for opt-in no longer held

The flag's comment justified the default as "the compiler's own source uses the bare-name=Result idiom". Compiling compiler/compiler.pas with the warning on found exactly one such site: PyEnsureExceptionClass (pyparser.inc), correct but written in the ambiguous form. Rewritten to explicit Result, which it should have been anyway.

Noise, measured before flipping

So default-on costs nothing on this codebase. It is a warning, not an error, so the idiom stays legal and old sources still build; --no-warn-self-result silences it once a codebase has been audited.

Verified

check result
the landmine repro warns, naming both fixes (Name() or Result)
test/test_pascal_self_result_warn.pas new, gated: exactly 1 warning
explicit Result / F() / with-param forms silent — the warning must not become noise, or it gets tuned out
runtime behaviour unchanged 5 1 8 42 6 42 100depth=1 proves the bare form does not recurse, depth=6 proves F() does
--no-warn-self-result silences it (gated)
self-host fixedpoint byte-identical, converged in 1 round
tools/gate.sh quick GREEN

Log

{$MODE DELPHI} — already obeyed, now pinned

Checked rather than assumed, since the delphi delta lives in the same ParseFactor branch as the warning (the branch is guarded by not DelphiMode) and was therefore exactly what a change to that warning could silently break.

pxx already follows the mode, and matches FPC 3.2.2 in both:

mode pxx FPC meaning
default / {$MODE OBJFPC} depth=1 depth=1 (-Mobjfpc) reads Result
{$MODE DELPHI} depth=3 depth=3 (-Mdelphi) recursive call

The warning also correctly stays silent in delphi mode — there the construct is unambiguous, and "reads the result" would be a false statement.

Nothing pinned any of this before. Two gated tests now do, and each is oracle-verified against FPC in the matching mode:

The outputs differ between the two modes, so the tests cannot both pass unless the mode is actually being honoured.

Found while doing this and filed separately: [[compat-pascal-no-command-line-mode-switch]] — the {$MODE} directive works, but FPC's -Mdelphi / -Mobjfpc command-line switch has no pxx equivalent. That matters because real Delphi projects set the mode in the build rather than in each file, so their sources carry no directive at all and pxx compiles them in the wrong dialect silently — the same failure through a different door.