← board

What

tools/forwardlint.py's analyse() is a two-pass flat identifier scan:

for pos, (_path, _ln, code, _cond) in enumerate(stream):
    m = DEF.match(code)
    if m:
        declared.setdefault(m.group(1).lower(), pos)

DEF matches a procedure/function declaration wherever it appears, including one nested inside another routine's declaration part. The second pass then walks every identifier on every earlier line and reports any that resolves to a later declaration.

Nothing distinguishes a call from any other occurrence of the identifier, and nothing tracks scope. So the report fires on:

The measured instance

Adding a nested procedure Mark(slot: Integer) inside WasmDceRun (compiler/dce.inc) produced:

FAIL /home/neo/frankB/compiler/pyparser.inc:38397: calls mark,
     declared at /home/neo/frankB/compiler/dce.inc:812,
     which FPC has not seen yet

pyparser.inc:38397 is:

  while PyPendLamCount > mark do

mark there is a local variable. The line contains no call. And the reported declaration is nested inside another routine, so it is invisible to every line in the file the lint is pointing at — under FPC and under pxx. The build self-hosted fine (converged after 1 round(s)) both before and after the rename; only the lint objected.

What this is NOT

Not a reason to loosen the lint. Its header states the hazard it exists for and it is real: "pxx resolves names across a whole unit; FPC — the bootstrap seed — resolves them in SOURCE ORDER. So a file can self-host perfectly and still break the seed build." It has caught that twice in ir_codegen_wasm32.inc. A false positive here is cheap; a false negative costs a seed build, and those are found by gate.sh quick's FPC canary once at the end of a phase.

So the failure mode this ticket is about is the other one CLAUDE.md names: a guard that cries wolf on a case its author did not intend teaches that it can be ignored. The next person to hit this may reach for the wrong lever.

Options, in the order I would rank them

  1. Skip nested declarations. Track procedure/function nesting depth while scanning — a declaration inside another routine's declaration part is not a unit-level name and cannot be forward-referenced from elsewhere. This is the correct fix and it narrows nothing the lint legitimately catches.
  2. Require the occurrence to look like a call or a reference, i.e. not immediately preceded by var/:/., and not a bare operand in a comparison. Weaker, more heuristic, and it would still fire here (> mark is a bare operand, which is exactly what it would have to learn to ignore).
  3. Document a naming rule — nested routines take a distinctive prefix — and leave the lint alone. Cheapest, and it is what the call site does today, but it is a rule nobody will find until the lint fires at them.

I did not implement (1) because forwardlint.py is Track T's file and the lint is load-bearing for every lane's seed build; a change to it wants its own positive control (a real forward-reference violation it must still catch) rather than a drive-by from a Track A fix.

Current state

Not red. The nested routines in dce.inc were renamed to WasmDceMark / WasmDceMarkProc, with a comment recording why the generic name was wrong there. python3 tools/forwardlint.py compiler/compiler.pas is clean.