{$IFOPT X+} is hardwired False, so the wrong arm compiles
{$IFOPT} is a CONDITIONAL. Getting it wrong does not produce a diagnostic —
it compiles the other branch. That is the same class as
[[bug-p-macpas-conditional-directives-are-ignored-so-both-arms-compile]] and it
is worse than a mis-set switch, because nothing downstream can tell that a
branch was chosen rather than written.
Found by censusing fpc 3.2.2's own sources for directive VALUES rather than
NAMES (the unknown-directive warning keys on the name, so a known directive
with an unhandled value is invisible to it — see
[[bug-p-a-spurious-unknown-directive-warning-cannot-fail-any-test-we-have]]).
{$IFOPT} is 139 uses there.
What was fixed, and why the rest was not
ProcessPasDirective said
if CaseEqual(command, 'ifopt') then
cond := False { compiler option switches are not modelled; branch off }
and the comment was stale — RChecksVal, QChecksVal, IChecksVal and
AssertionsVal are set a few lines below it, in the same procedure. Those four
now answer from their own state. They are also where the corpus is: {$IFOPT R+} alone is 113 of the 139 uses.
{$IFOPT R+} was RIGHT BEFORE THE FIX, by coincidence — fpc's R defaults
off, and "always False" agrees with "off". The uses that dominate the corpus
are exactly the ones a broken implementation gets right, which is why this
survived: the discriminating case is {$R+} followed by {$IFOPT R+}, where
fpc says TRUE and pxx said FALSE.
The open part: letters with no variable but a fixed behaviour
Measured against fpc 3.2.2 on x86-64, 2026-09-05 — one probe per letter for the
default, one for whether an explicit {$L+} moves it:
| default ON | default OFF | not tracked by fpc at all | |
|---|---|---|---|
| letters | I G J X |
R Q C B D H M P S T V | A L O |
Z was closed 2026-09-05 — {$PACKENUM} landed and gave the letter a
variable, so {$IFOPT Z+} now answers PackEnumVal = 4. Measured across every
spelling against fpc 3.2.2, both signs, with the dead A beside it as the
control: (none) ON, $Z1 OFF, $Z2 OFF, $Z4 ON, $Z+ ON, $Z- OFF,
$PACKENUM 1 OFF, $PACKENUM 4 ON. A and Z are both NUMERIC switches and
only a probe separates them — A is dead and Z is live — so the analogy that
would have settled either one settles neither. Rows are in
test_ifopt_tracks_the_switch_it_names, whose .expected is fpc's own output.
G, J and X remain, and they are the ones the table above was really about.
A not being tracked is the useful negative: it is a NUMERIC switch, so
{$IFOPT A+} is false however {$A8} was set. Modelling it from
PackRecordsVal would have been wrong, and only the measurement says so.
pxx answers False for every letter, so it is wrong by default for the five in
the first column. {$IFOPT X+} guarding extended-syntax code takes the else arm
on a compiler that has extended syntax.
Each of those needs its own claim — does pxx behave as X+? — and that is a
per-letter determination about this compiler, not a lookup in fpc's table. Two
that are not obvious: H (ansistrings) is mode-dependent in fpc, and G (286
code generation) is a switch whose subject no longer exists.
Where pxx will DIFFER from fpc on purpose
{$IFOPT C+} answers TRUE by default here and FALSE in fpc, because
AssertionsVal defaults ON — a decision already recorded at its declaration in
defs.inc (flipping it to fpc's default would turn every existing pxx assertion
into dead code silently). IFOPT reports this compiler's state truthfully;
matching fpc's answer would mean lying about ours.
Not part of this, deliberately
fpc diagnoses a malformed {$IFOPT} — {$IFOPT R} and {$IFOPT R*} are
Wrong switch toggle, {$IFOPT RR+} and {$IFOPT 9+} are Illegal compiler
switch — and pxx silently answers False for all of them. Us accepting what fpc
rejects is not a defect, and a differing diagnostic is deferred; recorded so the
next reader does not have to re-measure it. The letter IS case-insensitive in
fpc ({$IFOPT r+} works), which the fix does honour.
Still reproduces at HEAD — G, J and X all answer OFF (frankS, 2026-09-05)
Measured at 0bbd82cd7, compiler/pascal26 sha 7fca108e4b85
(converged after 1 round(s)). One program per letter,
{$ifopt L+} WriteLn('L ON') {$else} WriteLn('L OFF') {$endif}:
| letter | HEAD | |
|---|---|---|
| G | G OFF |
the residual — fpc defaults it ON |
| J | J OFF |
residual |
| X | X OFF |
residual |
| A | A OFF |
the documented negative: numeric and untracked, so OFF is CORRECT |
| R | R ON with {$R+}, R OFF with {$R-} |
already fixed, tracks both ways |
Read the R row as a warning, not as a result. A staleness probe written from this ticket's SLUG picks R or Q — a letter the summary already records as fixed — and comes back green on a ticket whose residual is untouched. It is a control drawn from the wrong population, and it very nearly closed this ticket today. The summary is where the residual lives; the slug is a year out of date by construction, because a slug cannot be edited without breaking citations.
The residual stands as the summary states it: G, J, X, each needing its own measured behavioural claim about pxx.
Log
- 2026-09-05 — resolved, commit G, J and X answer True unconditionally, each on its own measured behavioural claim about pxx rather than a lookup in fpc's table (6f84891c5); and the second copy of the decision in ExpandIncludes, which was dropping {$I} includes silently, now shares PasIfOptState (4038b32d0). Oracle test byte-identical to fpc 3.2.2 at 20 rows; two more tests for the chosen divergence and the guarded include, pin scores 3/20, 4/7 and 0/2. Residual split out as bug-p-the-include-pre-pass-cannot-see-a-switch-directive-written-above-an-ifopt (low-prio)..