← board

{$if} over a declared constant or type

FPC's scanner and parser are interleaved, so by the time {$if max_operands>2} is scanned the constant is already in the symbol table. pxx lexes a file in its own pass, so the evaluator sees defines and nothing else.

Minimal repro — all four rows, measured 2026-09-09 at 24dbb0b37

{ ucfg.pas }
unit ucfg;
interface
const MAXOPS = 4;
type TBig = Double;
implementation
end.
program m;
uses ucfg;                 { drop the unit and declare both here: same result }
begin
{$if MAXOPS > 2}       WriteLn('a'); {$else} WriteLn('b'); {$endif}
{$if sizeof(TBig) = 8} WriteLn('c'); {$else} WriteLn('d'); {$endif}
end.
pxx, const:  conditional directive: `MAXOPS` has no integer value here
pxx, sizeof: conditional directive: sizeof cannot size this type here: TBig
fpc:         compiles, prints `a` then `c`

The boundary is the DECLARATION, not the unit. Both rows fail with the declarations in the same file as the directive, so the used-unit half is not what is missing; the evaluator has no route to a declaration at all. What DOES work: {$if sizeof(Double) = 8} (a builtin name) and {$if declared(X)} (including for a name in a used unit).

Where it lives, and why the shape is already decided

paslexer.inc's EvalPasCondExprText resolves an identifier through PasDefineLookupValue and then falls back to "is it defined". Two questions beside it are already answered by the parser side rather than by a table in the lexer:

So the work is a third forwarded question -- "does this name have an integer constant value" -- plus teaching PasCondSizeOfTypeName to follow one level of type A = B before giving up, both reusing the walk and the probe that exist. Not verified: whoever takes this should confirm the walk can see a const value (it currently records only that a name IS declared, not what it equals).

What must NOT be widened

The sizeof arm's own comment is the constraint: "A NAME THIS DOOR CANNOT SIZE IS A DIAGNOSTIC, NOT A DEFAULT" -- records and frozen strings have no layout during LexAll, and answering 0 or a pointer width picks a branch silently. A conditional that takes the wrong arm does not produce a wrong value, it produces a different program. Same for the const half: a name whose value is an EXPRESSION rather than a literal should keep the current error rather than be guessed at.

Why prio 60

From the corpus, not from the backlog: 24 of 207 units of FPC's compiler stop here, second only to [[bug-p-a-unit-cycle-closed-through-an-implementation-uses-cannot-see-the-other-interface]] at 144. Both numbers are first-failure counts and therefore lower bounds -- a unit that stops on the cycle may stop here next.

Log

Resolved 2026-09-09, frankH — abc681636 + fcbe280b7

Both halves work and are fpc-verified. A source-level const is readable by {$if} in the file that declares it and through a uses clause, and sizeof follows a type ALIAS up to eight hops before giving up. Fixture: test/test_p_a_conditional_directive_can_read_a_source_const.pas + test/condsrc_unit.pas, 23 rows, byte-identical to fpc 3.2.2, and the PIN refuses the file on its first row.

PasCondNameDeclaredInUses became PasCondProbeUsedUnits(nm, arity, question) with PCQ_DECLARED / PCQ_CONSTINT / PCQ_TYPEALIAS dispatched inside the walk — generalised rather than copied, because the WALK is the subtle half (ResolveUsesUnitSource rather than a Pascal-only file search, the ProbeDepth guard, the per-unit reset to the command-line define baseline) and none of it differs by question.

The constraint this ticket set was kept. A name the sizeof door still cannot size is a diagnostic, not a default; a const whose value is not an integer literal keeps the old error. The two shapes FPC's compiler actually asks for there are filed as [[bug-p-a-conditional-directive-cannot-read-a-const-whose-value-is-not-an-integer-literal]] (3 units: set membership over a set-valued const, and high(T) as a const value).

Corpus, per-unit diff both times (tools/fpc_compiler_corpus_probe.sh, fpc as the oracle): the family went 26 units of 207 to 3. On the first commit exactly 25 rows moved and every one of them was in this family; on the second, 12 more moved and ALU not defined (5) and unterminated conditional directive (1) cleared. No unit newly compiles — 9 of 207 before and after — because everything behind these rows queues on [[bug-p-a-unit-cycle-closed-through-an-implementation-uses-cannot-see-the-other-interface]].

Two probe defects were found underneath and fixed in fcbe280b7, both predating this work: a probe launched inside a conditional re-lexed the file it was inside (it scans the whole token stream for uses and finds the parent's uses <thisunit>) and unbalanced the {$if} stack; and the probe never expanded the probed unit's {$I} includes, so PUint = qword — inside {$ifdef cpu64bitaddr}, a define fpcdefs.inc sets — was invisible. {$if declared(X)} had both.

Gate: quick GREEN on each commit, and the FULL tier was run for the pair because this touches the conditional evaluator and the lexer's probe re-entrancy, which every file goes through — quick cannot bound that. gate: GREEN (exit 0) at ab1d60ab8, 23 rows, 0 FAIL, including make test (2008s) and make test-nilpy (2100s).

The first attempt at that tier went RED, and the red was not this change: 9b0c07c2d had grown test/c_vla.c by a row and updated the x86-64 expectation beside it while the three CROSS copies of the same string still said what the file used to print. Fixed in 8cbe22841 after checking all three targets already print the new row correctly, so it was a stale expectation and not a codegen gap. Recorded here because "my change went RED on the tier" is the reading that terminates the search, and the discriminator was one git log -S on the expected string.