← board

T: the forward-decl lint counts nested functions as globals

What happened

tools/gate.sh quick step fpc seed compiles (forward decls) went RED on a Track P change that had no forward-declaration problem:

FAIL compiler/pasparser_generic.inc:1437: calls argName, declared at
     compiler/rparser.inc:504, which FPC has not seen yet

compiler/pasparser_generic.inc:1437 was:

  if specArgKind = tkIdent then argCi := FindUClass(specArg);

...before renaming; the offending identifier argName was a const parameter of the enclosing procedure, not a call.

And rparser.inc:504 is a nested function:

function RResultClassForRec(okTk: TTypeKind; okRec: Integer;
                            errTk: TTypeKind; errRec: Integer): Integer;
var name: AnsiString; ci, vi, payOff, align, endOff: Integer;

  function ArgName(tk: TTypeKind; rec: Integer): AnsiString;    { <-- nested }
  begin

ArgName is local to RResultClassForRec and unreachable from any other routine, let alone another include. There is no FPC ordering hazard here in either direction.

Repro

Name a parameter, local variable or field in any compiler/*.inc after rparser.inc in compiler.pas's include order — argName will do — and run tools/gate.sh quick. The step fails. Renaming the local makes it pass; the compiler is unaffected either way (the self-host fixedpoint produced a byte-identical binary across the rename, sha 9c8f23be1d4c both sides, which is the proof that nothing about the build changed).

Why it matters

The lint guards something real — pxx resolves across the unit while FPC resolves in source order, so a genuine missing forward; breaks the bootstrap seed — and it is a gate step, so a false positive stops a lane. The cost is not just the noise: the sanctioned workaround is to rename your own local, which means the lint quietly reserves every nested function name in the tree as a global identifier nobody may reuse. ArgName, Emit, Flush and friends are exactly the names a nested helper gets.

Fix

Skip function/procedure declarations that are nested inside another routine when building the "declared globals" table — a nested declaration is indented inside an enclosing routine's declaration part, between its header and its begin, and is not addressable from outside it. Only top-level (column-0) declarations belong in that table.

Filed by Track P per T owns the tool, never the bug — worked around locally by renaming the parameter rather than editing Track T tooling. Found while resolving [[bug-p-generic-type-constraints-are-parsed-and-discarded]].

Deprioritised 2026-09-02 — the Track T tooling backlog was cut as a pile

This ticket is not being called wrong. It was moved as part of a pile, not judged individually, and nothing here disputes its finding.

Owner decision. 73 of the 74 open track: T tickets were filed between 2026-08-31 and 2026-09-02, 58 on one day. The pile was too large to work through and returned almost nothing, and a ticket nobody will fix does not sit neutrally — it stays in the ranker forever at zero value, which is the argument CLAUDE.md already makes for a terminal folder over a low prio.

Four were kept in the ranker on a purely structural test — an active umbrella or a hard blocked-by: edge from live work: umbrella-one-full-tier-run-with-no-red-tier, feature-t-freebsd-image-and-runner, and the two regression-test-core-* reds that block the umbrella.

Kept, not deleted, for two reasons: so the finding is not rediscovered and refiled from scratch by the next agent who trips over it, and so it can be pulled back if what it touches becomes load-bearing.

To revive it: move it to the owning lane's backlog, set status: backlog, and say in the ticket WHAT CHANGED to make it matter now. Restoring it because it reads well is how the pile comes back.