← board

Stray tokens in a unit declaration section are silently skipped

RESOLVED 2026-08-26. Both unit sections now reject a stray token, as the program path already did. The blocker turned out to be operator declarations -- see Outcome at the end.

Measured (pxx stable_linux_amd64/default/pinned, VERSION 374; oracle FPC 3.2.2)

uu.pas:

unit uu;
interface
zzz                          { <- starts no declaration }
function F: Integer;
implementation
function F: Integer; begin F := 1; end;
end.
where the stray token sits FPC 3.2.2 pxx
unit interface section Fatal: Syntax error, "IMPLEMENTATION" expected but "identifier ZZZ" found accepted silently
unit implementation section rejected accepted silently
program declaration section Fatal: Syntax error, "BEGIN" expected but "identifier ZZZ" found rejected: error: unexpected token
inside a procedure body rejected rejected: undefined variable (ZZZ)

Accepted silently in the unit case for an identifier, an integer literal, and a bare ). Declarations after the stray token are still registered — the parser skips the offending token and carries on.

The harmful shape is a mistyped section header:

unit uu;
interface
cosnt K = 5;                 { typo for `const` }
function F: Integer;
implementation
function F: Integer; begin F := K; end;
end.

Root shape

Program and unit declaration sections are two paths through one concept, and the unit one has an "unrecognised token → skip it and continue" recovery the program one does not. Normalise onto the program path's behaviour (devdocs/dev/normalise-dont-special-case.md) rather than adding a diagnostic to the recovery. Possibly related to the open note about error recovery swallowing diagnostics (ticket(A): error recovery silences every lowering-only diagnostic) — check whether that is the same mechanism before fixing either.

Second, smaller finding in the same probe

A unit's header name is not checked against its filename: a file uu.pas containing unit notuu; compiles and satisfies uses uu. FPC rejects with Illegal unit name. Lower severity (it cannot silently change behaviour, only tolerate a rename mistake) — worth folding into the same fix if the parser is open, otherwise leave it.

Why it matters beyond the diagnostic

This weakens every "it compiled" signal on unit-shaped corpus code. A corpus rung whose oracle is "the unit built" cannot distinguish a unit that built from a unit that had text quietly thrown away — which is precisely the reporting failure the corpus ladder exists to avoid.

Gate

make compiler/pascal26 (self-host fixedpoint) + tools/gate.sh quick. Note the fix tightens acceptance, so re-run the FPC conformance sweep (tools/run_pascal_conformance.sh) before landing — some test/*.pas or vendored units may be relying on the silent skip.

Found under [[feature-pascal-corpus-fgl]] · umbrella [[feature-pascal-corpus-expansion]]


Outcome (2026-08-26)

Normalised onto the program path's behaviour, as the ticket asked: both else Next recoveries -- one per unit section -- are now UnitSectionStrayToken, which errors and names the likely cause. The typo now points at the typo:

pascal26:5: error: unexpected token in a unit interface section:
            it starts no declaration (a mistyped section header?)
  in: test/units/ustray.pas

rather than undefined variable (K) in another file, or nothing at all.

The blocker, which only measurement would have found

The ticket's gate note -- "the fix tightens acceptance, so re-run the FPC conformance sweep ... some test/*.pas or vendored units may be relying on the silent skip" -- was right, and the thing relying on it was not a test.

Instrumenting both skip paths and compiling all of test/ and lib/rtl: the path fired 1949 times, and every single event was a piece of an operator DECLARATION header -- +, (, a, :, TBigInt, ;. The implementation loop's copy of the skip never fired at all.

The interface loop had no operator arm. Its implementation twin got one in [[bug-unit-operator-def-silently-skipped]], whose comment says the else Next below it "silently skipped the whole definition" -- and the interface half was left with exactly that. So an interface-side operator header was being chewed one token at a time, and it looked harmless only because the implementation-side definition registers the real operator.

SkipOperatorDeclHeader consumes it as a UNIT instead: paren depth is tracked, because a two-operand header spells its operands (a: Double; b: TCx) with a semicolon INSIDE the parens that does not end the declaration. lib/rtl's bignum, ucomplex, pathlib and vecmath all declare operators this way, and test/lib_ucomplex.pas is wired in as the regression guard.

Had the error gone in without that arm, every operator-declaring unit in the RTL would have stopped compiling -- so the 1949 events are the whole reason this landed green rather than red.

Gate results

The second finding is deliberately left

A unit's header name is still not checked against its filename (uu.pas containing unit notuu; compiles). The ticket says to fold it in "if the parser is open, otherwise leave it". Left: it cannot silently change behaviour, only tolerate a rename, and tightening it risks vendored units whose filename and header genuinely differ -- a different blast radius from this fix, and one nothing measured here says anything about.