← board

Member access on a plain Pointer is SILENTLY ACCEPTED and yields the pointer

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

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:

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