← board

FPC seed drift: RExprRecId called before its definition in rparser.inc

The failure

rparser.inc(1416,12) Error: Identifier not found "RExprRecId"
compiler.pas(2111) Fatal: There were 1 errors compiling module, stopping
compiler/rparser.inc:1416:  recId := RExprRecId(scrutNode);   <-- use
compiler/rparser.inc:1754:function RExprRecId(node: Integer): Integer;   <-- definition
compiler/rparser.inc:2314:    optCi := RExprRecId(valNode);
compiler/rparser.inc:2924:      symIdx := RExprRecId(Result);

The other two call sites are after the definition; only :1416 is ahead of it.

Why it escaped the lane that introduced it

pxx does not need the forward and FPC does, so the whole per-fix loop is green on it: make compiler/pascal26 self-hosts and reaches its byte-identical fixedpoint, the Rust tests pass, and nothing in the mandatory loop compiles the compiler with FPC. Only the seed canary — which is in gate.sh, not in the per-fix loop — sees it. That is the fourth time this exact asymmetry has produced a seed drift, which is what decide-should-the-fpc-seed-canary-be-in-the-mandatory-loop is about.

Consequence while it stands: the compiler cannot be bootstrapped from source with FPC. Nothing an existing checkout does is affected, so it is invisible until someone cold-starts.

Provenance — measured, not inferred

Bisected only as far as "not mine": my working tree had exactly two modified files (compiler/pasparser_expr.inc, compiler/pasparser_lval.inc) and neither is rparser.inc. I did not stop at that reasoning — I stashed the diff and ran the seed build the gate runs, verbatim, at clean HEAD:

git stash push compiler/pasparser_expr.inc compiler/pasparser_lval.inc
fpc -Mobjfpc -O2 -Tlinux -Px86_64 -FU<tmp> -o<tmp>/seedbin compiler/compiler.pas
  -> rparser.inc(1416,12) Error: Identifier not found "RExprRecId"

Reproduces at HEAD without my changes. Last commits touching the file: fa22da207 ("feat(rust): if as an expression, statement and tail forms", 2026-08-29), 2d99f0ab1, fcfe1cba1RExprRecId's use at :1416 is inside the if-as-expression scrutinee handling, so fa22da207 is the likely author. Not bisected further; R will know in one look.

Fix

Add function RExprRecId(node: Integer): Integer; forward; above the first use (or move the definition ahead of :1416). Then confirm with the seed build above — not with make compiler/pascal26, which is green either way and is precisely why this class of break keeps landing.


ALREADY FIXED — this ticket was filed against a stale checkout. 2026-08-29.

No action needed. The forward is on master and the canary is green.

Measured at e2104bf4d: python3 tools/forwardlint.pyexit 0, zero FAILs, and compiler/rparser.inc:77 reads

function RExprRecId(node: Integer): Integer; forward;

The forward landed in 9d14b759d at 19:13, "fix(rust): forward-declare RExprRecId — the FPC bootstrap seed compiles again", verified two ways by frank-rust at the time: forwardlint clean and a direct fpc -Mobjfpc compiler.pas bootstrap linking, 210475 lines, 0 errors.

The line numbers in this filing are the proof, and they are the general lesson. This ticket cites used at :1416, defined at :1754 — which is exactly the pre-fix geometry. Current HEAD is used at :1642/:1746, defined at :2054, forward at :77. Those numbers cannot both describe the same file, so the reported "clean HEAD" was a local HEAD behind origin.

The stash-and-rebuild was correct method and reproduced faithfully; what it reproduced was the state of the checkout, not the state of master.

"I rebuilt at clean HEAD" answers is my tree consistent, not is my tree current. A stash removes local edits; it does not fetch. Both readings are spelled "clean HEAD", and only one of them is evidence about master.

Sibling of the rule the same session coined hours earlier — "the fix is in HEAD" and "the fix is in the binary I just ran" are different claims — with the ambiguity moved one step earlier, from which binary to which HEAD.

Before filing a bug about master, git fetch and name the sha you measured at. A sha in the filing makes this self-diagnosing; line numbers alone made it take three commands.

Closed as already-fixed rather than deleted, so 750ddf4aa resolves to something. The genuine filings are bug-r-rexprrecid-breaks-the-fpc-bootstrap-seed and bug-r-rparser-calls-rexprrecid-before-declaring-it-so-the-fpc-bootstrap-seed-does-not-compile, both in done/.

The family observation in this ticket is correct and worth keeping: this is the fourth instance of the FPC-seed forward-declaration hazard, and done/bug-n-fpc-seed-drift-pywiden-needs-a-forward-in-pars… is the NilPy one. Same asymmetry every time — pxx resolves across the unit, FPC in source order, so the entire mandatory loop including the self-host fixedpoint stays green and only the canary sees it. That is precisely why forwardlint is now wired into gate.sh, and why the canary caught this one before it reached master rather than after.