Still live at HEAD 2026-09-16, with a standalone repro the ORACLE ACCEPTS
Re-measured because this ticket's last line — "both are behind the unit-cycle bug anyway, so neither buys a compiling unit today" — was written 2026-09-11 and the unit cycle was fixed that same evening. That caveat is stale; whether this now buys units is unmeasured and is the first thing to check. The defect itself has not moved:
program s2;
type toptswitch = (cs_opt_level1, cs_opt_level2, cs_opt_use_load_modify_store);
const genericlevel1 = [cs_opt_level1];
genericlevel2 = [cs_opt_use_load_modify_store];
supported_optimizerswitches = genericlevel1 + genericlevel2;
begin
{$if (cs_opt_use_load_modify_store in supported_optimizerswitches)}
WriteLn('IN: yes');
{$else}
WriteLn('IN: no');
{$endif}
end.
| compiler | result |
|---|---|
| fpc 3.2.2 | IN: yes |
| pxx at HEAD | pascal26:0: error: conditional directive: expected operator |
It exercises all three things the summary names — set-union folding across two
constants, an enum MEMBER name, and in on the directive's value stack.
THE FIRST VERSION OF THIS REPRO WAS INVALID AND THE ORACLE IS WHAT SAID SO.
It declared the constants as TYPED (const x : toptimizerswitches = [...]),
which reads naturally and is what FPC's own globtype.pas looks like at a glance.
fpc refuses that with Illegal expression — a typed constant is not a constant
expression — so pxx's failure on it would have been evidence about nothing.
Only the untyped form is a question about us.
So in this area the oracle is not a nice-to-have, it is the ONLY instrument
— pxx emits conditional directive: expected operator for the typed form and
for the untyped form alike, so our own output can never separate a valid repro
from an invalid one here. Any probe against this defect must be compiled under
fpc FIRST; a pxx-only reading of it cannot fail, and it was written as a
hand-off artefact, which is the thing whose whole job is to be trusted by
someone who did not build it.
{$if} over a set-valued constant
Split from [[bug-p-a-conditional-directive-cannot-read-a-const-whose-value-is-not-an-integer-literal]] when its other shape was fixed. Keeping the two together made that ticket's summary describe work of two very different sizes as one thing.
- Umbrella:
umbrella-pxx-compiles-fpc-itself
The directive, and everything it needs
nld.pas:700:
{$if (cs_opt_use_load_modify_store in supported_optimizerswitches)}
x86_64/cpuinfo.pas:139:
supported_optimizerswitches = genericlevel1optimizerswitches+
genericlevel2optimizerswitches+ ...
globtype.pas:428-430:
genericlevel1optimizerswitches = [cs_opt_level1,cs_opt_peephole];
genericlevel2optimizerswitches = [cs_opt_level2,cs_opt_remove_emtpy_proc];
genericlevel3optimizerswitches = [cs_opt_level3,cs_opt_constant_propagate, ...];
So one directive needs, and none of it exists in the conditional evaluator:
- A set value on the directive's value stack. It is tagged today with
kind 0 = bool, 1 = int, and 2 = no-value (added 2026-09-11 for the
short-circuit fix). A set needs a fourth and a payload wider than an
Int64. - Set-union folding across three named set constants, two of which are in a different unit from the third.
- Enum MEMBER resolution —
cs_opt_use_load_modify_storeis a member of an enum, not a const, and nothing in this evaluator resolves one. - The
inoperator, which the expression grammar does not have at all.
Why it is worth less than its unit count
2 units of 207, and they are one directive: ncnv's interface uses nld, so
it is the same line reached twice. Both are behind
[[bug-p-a-unit-cycle-closed-through-an-implementation-uses-cannot-see-the-other-interface]]
anyway — no unit here compiles when this is fixed.
Contrast with the half that WAS fixed on 2026-09-11, which is the argument for
splitting rather than for doing this next: that one reused walks the evaluator
already had (PasCondTypeAlias, OrdinalNameToTk, OrdinalTypeBound) and added
no new value kind, and it still bought zero compiling units — it moved rgobj to
the next wall. This one builds four new mechanisms for the same yield.
The cheaper question to ask first: whether the evaluator should answer in
at all, or whether a directive it cannot evaluate should be reported with the
operator named rather than as expected operator, which describes the grammar
and not the program. That is a smaller change and it is what a reader of
nld.pas:700 actually needs.
FIXED 2026-09-16 (frankb-56) — and the stale caveat is now MEASURED, with the answer it feared
The four mechanisms the summary predicted were all genuinely required and all
four are in paslexer.inc: a SET value kind (3) with a 32-byte mask payload,
PasCondSetConstIn for +-joined term folding, PasCondEnumOrdIn for an enum
member's ordinal, and in as a relational operator. The mask shape is
deliberately the one ConstSetTerm (pasparser_decl.inc) already uses — that
walk cannot be reused, because it reads baked masks out of Data[] and nothing
is baked during LexAll, but a set now has ONE spelling in this compiler.
THE STALE CAVEAT WAS THE RIGHT THING TO CHECK AND ITS CONCLUSION SURVIVED FOR
A DIFFERENT REASON. The line said both units were behind the unit-cycle bug,
which was fixed 2026-09-11. Measured with fpc_compiler_corpus_probe.sh before
the fix: the cycle is indeed no longer in front — both units reach THIS wall
first (oracle-no=0, so fpc accepts both). Measured again after:
| unit | before | after |
|---|---|---|
| nld | expected operator at :2 |
Unsupported tcompilerwidechar size at :3266 |
| ncnv | expected operator at :3440 |
unknown type: TDoubleRec at :36 (errs=2) |
So it still buys ZERO compiling units — exactly as the previously-fixed half
of this ticket did, and that is the honest result rather than a disappointment:
both units advance to different, older walls. TDoubleRec is
[[feature-b-rtl-has-no-tdoublerec]], already ranked p85.
nld's new wall is a WRONG BRANCH and is not this fix's doing. ncon.pas:968
is {$if sizeof(tcompilerwidechar) = 2} / {$elseif ... = 4} / {$else} {$error Unsupported tcompilerwidechar size} — FPC's own error arm, and fpc
compiles the unit, so we are taking an arm fpc does not. It was always there,
hidden behind the wall this ticket removed. The reduction does NOT
reproduce: $elseif works, a local alias sizes correctly, and a cross-unit
tcompilerwidechar = word in a used unit also sizes correctly — all three
measured. So the cause is context-dependent and the leading unconfirmed
hypothesis is probe nesting (PasCondProbeUsedUnits refuses at
ProbeDepth > 0, so a sizeof needing its own probe while already inside one
declines, and under PasCondQuiet an unresolved expression answers False,
which walks every arm down to the {$else}). Filed separately rather than
guessed at here.
WHAT THIS DELIBERATELY REFUSES. An enum with an explicit value in its body
((a, b := 5, c)) makes position stop meaning ordinal — c is 6, not 2 — so the
walk declines the whole declaration rather than counting commas. fpc answers
that case correctly and we refuse it. Refusing is the safe direction where the
alternative is a plausible wrong ordinal, which in a set test does not fail but
takes the other branch. Asserted as a must-not-compile control.
THE FPC SEED CANARY EARNED ITS KEEP. The first version walked the enum body
on the for variable itself, which pxx allows and FPC refuses
(Illegal assignment to for-loop variable). It built, self-hosted and passed
its own tests; only the canary saw it. That is the declaration-order/seed class
the canary exists for, arriving in a new subsystem.
Gate: gate.sh quick GREEN. Tests byte-identical to fpc 3.2.2 on all 10 rows,
every membership row paired with a non-membership row so an over-approximating
fold cannot pass, and the member under test not always first.
Log
- 2026-09-16 — resolved; this names the commit that carried the resolve, which is not always the one that carried the change — commit eaf776dd8.