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.