← board

-O3 alone computes a different result for a nested case statement

What happens

A generated program prints a running checksum. Five oracles, four agree:

oracle result
fpc-O0 16452949249337348755
fpc-O2 16452949249337348755
pxx-O0 16452949249337348755
pxx-O2 16452949249337348755
pxx-O3 16571182087083257235

No warning, no crash, no diagnostic — the program runs to completion and prints a wrong number. Four independent implementations agreeing is what makes this a fact rather than a judgement call: the program's meaning is not in dispute, and -O3 is the arm that is wrong.

Reproducing it

pasmith is deterministic, so the seed and the generator arguments ARE the repro:

python3 tools/pasmith.py --seed 91162 --vars 12 --funcs 2 --stmts 60 --depth 4 \
  --classes 3 --objs 3 --strs 3 --recs 2 --arrs 2 --enums 2 --shorts 2 \
  --excepts 3 --modeprocs 2 --intfs 0 --hier 4 --mptrs 0 --props 3 \
  --exdtor 3 --clsm 3 --checks 1 --consts 1 -o g.pas
./compiler/pascal26 -Fulib/rtl -O2 g.pas g2 && ./g2   # 16452949249337348755
./compiler/pascal26 -Fulib/rtl -O3 g.pas g3 && ./g3   # 16571182087083257235

Reproduced at d7e8e1dc6; first seen at 8e39c9ed5. Compiler binary sha256 dfb894303…, self-host fixedpoint verified at HEAD before the run.

Localisation

Adding --trace to the same command emits the checksum after every statement. Diffing the two traces puts the first divergence at checkpoint 8 of 67, a case statement — everything before it agrees, so the defect is at that statement and not accumulated drift. Inserting Halt; immediately after checkpoint 8 preserves the divergence, so the remaining 59 statements are not involved.

The statement is a nested case on longint(g4) and 3, whose arms contain a further two levels of case, for loops, record and array member access, and calls to the Safe* division helpers.

What has been ruled out

Not yet bisected to a pass. The remaining OptLevel >= 3 sites on x86-64 are in ir.inc (10603 — deeper inlining; 12072 — ruled out above), ir_codegen.inc (4547, 5561, 6234, 6258, 6402, 6431), emit.inc:211 and symtab.inc:6115. Track T deliberately did not bisect by patching those files: frankA holds ir_codegen.inc/emit.inc under a live Track A lock, and editing them to diagnose would be exactly the concurrent-edit hazard the lane letters exist to prevent.

Reduction: attempted, abandoned, and what it did establish

A line-delta reducer got the program from 2829 to 1635 lines — 42% removed with the -O2 vs -O3 disagreement intact — before it was killed at load 19.6 on a 12-core workstation. It writes its output only on completion, so there is no reduced artefact; what survives is the measurement that the program is heavily reducible, which is worth knowing before anyone assumes 2829 lines is the floor.

That partial figure is NOT validated and must not be leaned on. The reducer ran a cheap two-oracle predicate (pxx-O2 != pxx-O3) with a five-oracle revalidation deferred to the end, and the end never came. A reducer optimises for its predicate, so a deleted line that introduces an uninitialised read makes -O2 and -O3 disagreeing correct behaviour and the predicate still says "interesting". Everything above the reduction section was measured on the unreduced program, where all five oracles ran.

The unreduced repro is complete on its own: pasmith is deterministic, so the seed and the generator arguments reproduce it exactly.

Why prio 45 and not higher

-O3 is the free tier — nothing gates OptLevel >= 3, -O2 is the proven default, and no shipped artefact is built at -O3. So this is contained. It is not lower than 45 because the failure is silently wrong output from the optimizer, and because it blocks promoting whichever pass is at fault to -O2 — the promotion path in CLAUDE.md is exactly "land behind -O3, promote per-pass after the full gate", and a pass cannot be promoted while it is miscompiling.

Acceptance

-O3 agrees with -O0, -O2 and both FPC oracles on seed 91162, and the pass at fault is named in the fix so the promotion decision has something to cite.

Resolution (2026-08-30, frank-optimize-b4)

Same defect as bug-a-o3-drops-the-first-of-two-chained-qword-multiply-xor-statements, and that is a measurement rather than the assumption the two tickets were kept separate to avoid. Fixing that one and re-running seed 91162 — the acceptance criterion written into the sibling — gives:

g O0: 16452949249337348755
g O2: 16452949249337348755
g O3: 16452949249337348755

matching all four oracles. The case statement at checkpoint 8 was not the subject; it was the shape that happened to contain a chained assignment whose reload the -O3 store→reload pass elided after the emitter reordered the operands. Root cause, mechanism and the fix are written up in full in the sibling's Resolution section; there is nothing case-specific to record here.

Worth keeping from this ticket: the two rule-outs cost real time and both still stand — not DCE (--no-dce diverged identically) and not the for-to-MaxLongint bound at ir.inc:12072. A third rule-out was added while closing it: the IR is byte-identical at -O2 and -O3 for the diverging body (PXXDBG=a.ir:<proc>), which is what moved the search out of ir.inc entirely and into the x86-64 emitter. That is the cheapest single measurement in this hunt and it should be the first step next time — it eliminated two of the nine candidate gate sites before any of them was probed.

Also recorded because it was measured and came back negative: the three sites pre-ranked as suspects by shape (ir_codegen.inc op = tkStar + IRKind[left] = IR_LOAD_SYM, and the two in its family) were each disabled and rebuilt individually. All three still printed 222. The shape that matches is not the shape that fires, and one build each is what settled it.

Log