← board

--strict-fpc: a narrow shr by zero loses the operand's sign

var i: Integer;
begin i := -16; WriteLn(i shr 0); end.
result
fpc 3.2.2 -16
pxx, default dialect -16
pxx --strict-fpc 4294967280

A shift by nothing changed the value. Every count >= 1 was already correct (i shr 1 = 2147483640, matching FPC exactly), which is what kept this hidden.

Cause

--strict-fpc reproduces FPC's shift widths: a narrow operand shifts at its declared 32-bit width rather than at native width (decide-shift-operator-promotion-width). The x86-64 emitter does that by zero-extending the operand into rax (mov eax, eax) and shifting there.

But FPC's shr result keeps the operand's typeLongInt shr n is a LongInt, signed. So the 32-bit answer has to be read back as signed. pxx left it zero-extended, so the value was correct as a bit pattern and wrong as a number.

Only a count of zero can expose that: shr by 1 or more always clears bit 31, so the sign-extend that was missing would have been a no-op anyway. The bug was one value wide.

Fix

Three lines in ir_codegen.inc's Pascal shr arm — after shr rax, cl, when the operand and the result are narrow and the operand is signed, cdqe to re-interpret the low 32 bits as signed. Costs one instruction on the strict path only; the default dialect already takes the sign-extend branch before the shift and never reaches it.

Verification

test/test_strict_fpc_shr_keeps_the_sign.pas, wired into test-core and compiled twice, once per dialect, because the two must disagree:

The strict-mode output is byte-identical to fpc 3.2.2 across all eleven rows. make compiler/pascal26 fixedpoint converged in 1 round; tools/gate.sh quick green.

Found by

A 34-program integer-arithmetic differential (div/mod with negative operands and divisors, shifts across the full count range, wraparound at every width, mixed-signedness comparison, Int64/QWord boundaries, Hi/Lo/Swap, constant folding). Four rows diverged from FPC in the default dialect and all four are the decided native-width behaviour — decide-shift-operator-promotion-width, resolved 2026-08-10. Re-running those four under --strict-fpc, which that decision says must reproduce FPC exactly, left two: this one, and a QWord rendering bug that turned out not to be about shifts at all (bug-b-inttostr-of-a-qword-above-2-63-renders-negative).

Everything else in the family matched: signed division truncating toward zero on both operands' signs, all four wraparound widths, Round's banker's rounding, not on each unsigned width, Abs(Low(Int64)+1), and 3037000500^2.