A pointer difference is typed as a pointer
Found 2026-08-22 by an FPC differential sweep over language shapes
(fpc -Mobjfpc -O1 3.2.2 vs pxx 3b980e9f0).
The measurement
type TPC = ^Char;
var ac: array[0..7] of Char; pc, pc0: TPC; n: PtrInt;
...
n := pc - pc0; Writeln(n); { fpc 3, pxx 3 }
Writeln(pc - pc0); { fpc 3, pxx SIGSEGV }
| element type | via a variable | directly in Writeln |
|---|---|---|
^Integer |
3 | 3 |
^Int64 |
3 | 3 |
^Char |
3 | SIGSEGV |
^Byte |
3 | SIGSEGV |
Reproduced on stable_linux_amd64/default/pinned, so it is not a regression.
Root cause
TypeIsOrdinal reports tyPointer as an ordinal — deliberately, it is a
pointer-width integer for every arithmetic purpose. So in ParseSimpleExpr the
arm
else if ((op = tkPlus) or (op = tkMinus)) and
(ASTTk[left] = Ord(tyPointer)) and
TypeIsOrdinal(IntToTypeKind(ASTTk[right])) then
ASTTk[node] := Ord(tyPointer)
claimed p - q as pointer minus ordinal and typed the result a POINTER.
The IR lowering has its own, correct arm — it detects two pointer operands,
subtracts, divides by the stride and tags the value tyNativeInt. The two
disagreed, and only the parser's answer reaches Writeln's dispatch: a
^Char/^Byte-elemented pointer is a PChar there, so it printed the value 3
as a string address.
The reason n := p - q was fine is that an assignment converts through the
destination's type and never asks the node what it is. That is what kept a
crashing expression looking like it worked.
The fix
One arm, placed above the pointer-arithmetic one: two tyPointer operands under
- produce tyNativeInt — the same type the lowering already assigned, so the
parser and the IR now agree. Pointer arithmetic (p + n, p - n, n + p) is
untouched.
Verified against fpc
^Integer, ^Byte, ^Char, ^Int64 and ^TRec differences, each through a
variable and directly in an expression; the difference composing as a number
((p - q) * 2 + 1, (p - q) > 2); zero and negative differences; and pointer
arithmetic itself (p0 + 5, Inc(p, -2)) as control rows. Output
byte-identical to fpc -Mobjfpc -O1.
Left open deliberately
FPC's p - q answers BYTES when either operand is an untyped Pointer — which
includes @x, since {$TYPEDADDRESS OFF} is FPC's default — and ELEMENTS when
both are the same typed pointer. pxx always answers elements. That is a dialect
choice rather than a defect, so it is filed as decide-pointer-difference-unit
(Track U) rather than changed here.
Gate
make compiler/pascal26 (self-host fixedpoint) + tools/gate.sh quick GREEN.
Test test/test_pointer_difference_is_a_number.pas, 18 assertions, wired into
test-core.