← board

(pc + 2)^ derefs as an integer

Repro

var ca: array[0..7] of Char; pc: PChar;
begin
  ca := 'abcdefg'#0;
  pc := @ca[0];
  Writeln(pc^, ' ', (pc + 2)^, ' ', pc[2]);
end.
FPC: a c c
pxx: a 1717920867 c

1717920867 is $66656463 — the four bytes dcef read as an integer. So the deref happened at the right ADDRESS and read the right memory; it read four bytes instead of one because the node's type is not Char.

Why it is the arithmetic form specifically

pc^ and pc[2] both keep the pointee type, and pc[2] is defined as (pc + 2)^, so the two spellings of one access disagree — the double case normalise-dont-special-case.md describes. pc + 2 is typed as a plain pointer (or an integer) by the binop typing, and the ^ then has nothing to go on and falls back to the machine word.

Sketch

The binop that adds an integer to a typed pointer should carry the LEFT operand's pointer type through, which is also what makes the scaling correct for a wider pointee. Check PByte, PInteger and a ^TRec while fixing it — the same expression on those is the case where a 4-byte read is not merely the wrong type but the wrong span.

Resolution — 2026-08-20

NodePtrElem in parser.inc: the pointee of a pointer-valued expression, seen through the arithmetic. A bare identifier is the leaf (Syms[].PtrElemTk), an arr[i] recurses, and a +/- binop takes the pointee from whichever operand carries one. The parenthesised-suffix ^ arm calls it instead of hand-matching two node shapes and defaulting the rest to Integer.

The default was the whole bug, and it hid because it is correct for a ^Integer — the pointer everyone tests with. Measured against FPC, the four other pointer widths in the probe were all wrong, in both directions:

before FPC
(xb + 3)^ (^Byte) 117835012 4
(pc + 2)^ (PChar) 1717920867 'c'
(xs + 1)^ (^SmallInt) 100992003 1027
(x64 + 0)^ = x64^ (^Int64) False True
(xi + 2)^ (^Integer) 102 102

The ^Int64 row is the one that reads SHORT rather than long, so "it reads too much" is not the invariant — "it reads the machine int" is.

test/test_pointer_arith_deref_keeps_pointee.pas — 17 assertions across ^Byte, ^Integer, PChar, ^SmallInt and ^Int64, each against the two spellings that were already right (p^, p[k]), plus nested and subtracted offsets and the plain (p)^ grouping.

Gate

make compiler/pascal26 fixedpoint + tools/gate.sh quick.

Log