Member access on a plain Pointer is SILENTLY ACCEPTED and yields the pointer
- Type: bug (diagnostics / silent wrong value)
- Track: A — core (member-access resolution)
- Status: done
- Found by: the class-reference-array work for fpjson —
ClassTable[k].Tag(a class method through a metaclass array element) printed the blob ADDRESS instead of erroring, which led to the general case below.
Reproduction
var p: Pointer;
begin
p := nil;
writeln(PtrUInt(p.NoSuchThing)); { compiles. prints the pointer. }
end.
No error, no warning. Any member name on a pointer-typed value is accepted and the
expression evaluates to the pointer itself. So a typo (obj.Nmae) on a pointer-typed
receiver silently becomes a no-op rather than a compile error.
Why it matters beyond typos
It hides real missing features. ClassTable[k].Tag — calling a class METHOD through a
metaclass array element — is not supported (the dispatch needs a compile-time class id,
which an AN_INDEX does not carry). Instead of saying so, it silently produced the class
reference and dropped the call. That is the worst possible failure mode for a gap: it looks
like it works.
Note the class-reference OPERATIONS (ClassName / ClassType / InheritsFrom) DO now work
on any pointer-typed node, because they need only the blob value. It is the class-METHOD
call through such a node that is missing.
Care needed
The C frontend may lean on lax pointer member access (C-mode struct access through pointers). Any tightening must be gated on Pascal mode, or run the full C corpus before landing. That is why this is filed rather than fixed inline.
Wanted
- A plain
Pointer(no element type) with a member access → compile error naming the member. - A class-reference (
tyPointerwhose element istyClass) with a member that is NOT a class-reference operation → an error saying a class method needs a typed metaclass (class of T), pointing at [[feature-pascal-typed-metaclass]].
Gate
make test + self-host byte-identical + the C corpus (see the care note).
2026-07-13 — ATTEMPTED and REVERTED. The naive guard is wrong, and here is why.
Tried the obvious thing: at the AN_FIELD builder, reject a member when the receiver's recName is
not a record/class (Pascal mode only, to leave the C frontend alone). It correctly rejected
p.NoSuchThing — and it BROKE a legitimate case immediately:
type PNode = ^TNode; TNode = record v: Integer; next: PNode; end;
...
writeln(p^.next^.v); { rejected by the naive guard }
A typed pointer's record identity is NOT carried in recName at that point — it lives in the
symbol's PtrElemRec (and, for a field, UFldPtrElemRec), and the deref chain resolves it
elsewhere. So "recName is REC_NONE" does not mean "this has no members"; it means "the record
id is somewhere else". Reverted; make test (b266) caught it in one run.
The real fix must ask the right question. Reject only when the receiver genuinely has no member namespace to search — i.e. it is a pointer whose PtrElemRec is REC_NONE and not a record/class — rather than keying on the one field that happens to be empty at that site. That means threading the pointee record id to the member-access decision, which is the same information the deref chain already uses.
Do NOT retry this by tightening the same predicate; it will keep hitting valid typed-pointer
chains. Start by finding where p^.next^ resolves its record and use that.
2026-07-14 — RESOLVED (commit d5785451, b338)
The previous attempt failed because it asked the question at the wrong time, not because the
question was wrong. recName at the field site was empty for a valid typed-pointer chain —
but the reason it was empty turned out to be a real bug of its own: a pointer field
declared through a FORWARD alias (PNode = ^TNode before TNode — the linked-list idiom)
never had its pointee record patched, so p^.next^ genuinely lost the record identity and
every field after it resolved at offset 0. See
[[bug-pascal-forward-pointer-field-loses-pointee-record]].
So the order was: fix the pointee resolution first, and the guard the ticket asked for then becomes both correct and safe.
Landed:
- plain
Pointer+ any member → error naming the member ("a pointer has no members"). - pointer to a RECORD + a member it has → implicit deref (
p.v=p^.v, FPC/Delphi). This previously read an offset into the pointer VALUE and printed garbage — a second silent bug the ticket had not seen. - metaclass (
class of T) receivers keep their own dispatch: their pointee is a class, not a record, so the auto-deref does not fire.
The C-corpus hazard the ticket flagged does not apply: ParseLValueAST is Pascal-only
(the C frontend has its own selector parser), and C struct fields are excluded from the
alias patch pass. make test green, self-host byte-identical.
Still open, as the ticket noted: a class METHOD called through an untyped metaclass value needs a compile-time class id — that remains [[feature-pascal-typed-metaclass]]. The array element case was fixed separately (b328).
Log
- 2026-07-14 — resolved, commit d5785451.