← board

A builtin pointer-name cast is refused as an ASSIGNMENT TARGET

Found 2026-08-22 by an FPC differential sweep (fpc 3.2.2 -Mobjfpc -O1 vs pxx 3dba78808). Loud, not silent — a compile error, no wrong values.

Symptom

var p: Pointer;
begin
  p := GetMem(16);
  writeln(PInteger(p)^);   { works }
  PInteger(p)^ := 42;      { error: undefined variable (PInteger) }
end.

FPC accepts both. A user-declared PI = ^Integer accepts both. Inc(PInteger(p)^) works. Assigning the cast to a variable first works. Only the assignment-TARGET position fails, and the receiver does not matter — a plain var, a record field and a class var all fail identically.

Which names, and what that says about the cause

Every builtin pointer name, as a statement target:

works refused
PByte PWord PInt32 PInt64 PDouble PInteger PLongInt PCardinal PLongWord PChar PPointer PQWord PBoolean PSingle PShortInt PSmallInt PUInt8 PUInt32 PNativeInt PPtrInt (and the rest of BuiltinPtrNameElemTk's ~25 names)

The working five are exactly the names declared as real type aliases in the always-linked prelude (compiler/builtin/builtinheap.pas declares PWord, PByte, PInt64, PInt32; builtin.pas declares PDouble). So they are not "supported" — they are found by FindTypeAlias like any source declaration, and the statement path never falls through to BuiltinPtrNameElemTk / EnsureBuiltinPtrAlias the way the expression path does (pasparser_expr.inc:5515).

That makes this the double-case shape of devdocs/dev/normalise-dont-special-case.md: one concept, two lookup paths, and the second one stayed broken. The fix is the same one-line fallback the expression path already has, at the statement/lvalue-target site — not a second name table. Note EnsureBuiltinPtrAlias' ordering rule is load-bearing (it must run only AFTER FindTypeAlias misses; registering builtins up front breaks the self-host gate — see bug-pascal-builtin-pointer-type-cast), so the fallback belongs after the alias lookup, mirroring the expression site exactly.

Gate

make compiler/pascal26 + a differential test over the whole name list as both an expression and a statement target + tools/gate.sh quick.


Resolved 2026-08-24 — the one-line fallback, plus the name the table never had

The statement lvalue path (pasparser_stmt.inc, the "C4" block) now falls through to EnsureBuiltinPtrAlias when FindTypeAlias misses, in that order — the same two lines the expression site has carried, and the order is the load-bearing part (registering the builtins up front shadows a source declaration and silently re-types this compiler's own PWord = ^NativeInt, which is bug-pascal-builtin-pointer-type-cast).

PChar needed one thing more, and it is the reason not to "just register the names". PChar is deliberately absent from BuiltinPtrNameElemTk: the expression site lowers it as the -2 adapter, which skips a STRING operand's inline length prefix. A plain ^Char alias would have made PChar(s)^ := 'H' write into the string HANDLE instead of its first character — a wrong value, silently, where today there is a clean compile error. The statement path therefore builds the same adapter, spelled the same way; PChar(s)^ := 'H' now edits the string and matches fpc.

Verified

Left open, deliberately

The two lvalue paths are still two. This is the third bug of the shape "the expression path learned something and the statement path did not", and the real fix is for the statement path to delegate to the expression lvalue parser the way the cast-headed-CALL case already does. Filed as [[refactor-p-one-lvalue-path-for-statements-and-expressions]].

Log