← board

A plain type alias gets its own RTTI blob, so TypeInfo pointer dispatch misses

Repro

program p;
uses typinfo;
type TMyInt = Integer;
begin
  WriteLn(PTypeInfo(TypeInfo(TMyInt))^.NamePtr^, ' ', PTypeInfo(TypeInfo(TMyInt))^.Kind);
  WriteLn(PTypeInfo(TypeInfo(Integer))^.NamePtr^, ' ', PTypeInfo(TypeInfo(Integer))^.Kind);
  if TypeInfo(TMyInt) = TypeInfo(Integer) then WriteLn('SAME') else WriteLn('DIFFER');
end.

pxx at 4d0642bfa917:

TMyInt  name=Integer kind=1
Integer name=Integer kind=1
ptr TMyInt=Integer DIFFER

Same name, same kind, different pointer. FPC 3.2.2 answers SAME.

Why it matters — the dispatch idiom, not the string

The reason RTTI carries an identity at all is so code can key on it:

if p = TypeInfo(Integer) then ...

With var m: TMyInt, TypeInfo(m) matches neither TypeInfo(Integer) nor TypeInfo(LongInt) nor TypeInfo(Int32) — it matches nothing in the program, while ^.NamePtr^ and ^.Kind both say it is an Integer. A serializer, a variant registry or a property dispatcher written the ordinary way takes the fall-through arm and is wrong with no diagnostic.

type TMyInt = Integer is a plain ALIAS. In FPC a distinct type needs type TMyInt = type Integer, and standard Pascal agrees an alias names the same type — so code expecting these equal is correct code, which is what makes this a bug rather than a compat entry.

What is NOT the cause

frankS's TypeInfo(variable) work (14e6ce592) is correct and is not implicated. Measured on the same binary:

question pxx fpc
TypeInfo(m) vs TypeInfo(TMyInt) SAME SAME
TypeInfo(i) vs TypeInfo(Integer) SAME SAME
TypeInfo(TMyInt) vs TypeInfo(Integer) DIFFER SAME

The variable resolves to exactly its own declared type in every case. The divergence is entirely in the TYPE-NAME path, which predates that commit — the pinned compiler refuses TypeInfo(m) outright, so the variable form could not have introduced it.

Nor is it the tyInteger/tyInt32 split. That is decide-typeinfo-scalar-name-spelling's option 3 and a separate, larger question. This defect reproduces within one kind: TMyInt and Integer are both tyInteger, both named Integer, and still have different identities. Collapsing tyInteger into tyInt32 would not fix it.

The premise it corrects

decide-typeinfo-scalar-name-spelling (decided, user, 2026-08-21) dismisses its own option 3 with:

option 3 ... buys nothing here — the observable behaviour is already identical and only the RTTI label differs.

That sentence is now falsified in the exact opposite direction: the label is identical and the behaviour differs. The decision itself — keep Integer by default, report LongInt under --strict-fpc — is about the NAME STRING, is unaffected, and is NOT reopened here. Only the supporting claim that nothing observable differs is wrong, and it is the sentence a future reader would use to dismiss this ticket.

Where to look

The alias category of the TypeInfo request table (RegisterTypeInfoReq, TYPEINFO_REQ_CAT_ALIAS, compiler/pasparser_expr.inc ~4519 and compiler/rtti_emit.inc). A plain scalar alias whose target is a builtin should resolve to the TARGET's blob rather than minting a row. Not attempted here.

Check before fixing: a = type Integer distinct-type spelling, if pxx has one, must keep its own blob — that is the shape the current behaviour is right for, and a fix that collapses both is trading one wrong answer for another. test/test_typeinfo_named_types.pas asserts TypeInfo(TMyInt) prints Integer, which a correct fix leaves green.

Provenance

Found while probing the keyword-vs-identifier spelling seam (refactor-p-five-dispatch-sites-for-one-named-type-cast); it is not that seam — both spellings resolve correctly.

The original filing said "not mine to touch while frankS is in the file". That is no longer true and the sentence is replaced rather than left standing: frankS and I agreed it is ONE question with two holders, the census below was run once to answer both halves, and this ticket is now frankA's. The reciprocal half is [[bug-a-typeinfo-does-not-return-one-shape-of-pointer]] and neither should be fixed without reading the other — they meet in the same table.

The census — 2026-09-06 at 0e3558e5a4d4, and it widens the ticket

Run once to answer this ticket's identity question and the shape question in [[bug-a-typeinfo-does-not-return-one-shape-of-pointer]] together. Pointer comparison only, no field reads, so it compiles under both compilers.

spelling fpc 3.2.2 pxx
TMyInt = Integer (plain alias) SAME DIFFER
TMyInt2 = type Integer (distinct) DIFFER DIFFER
LongInt vs Integer (synonym) SAME DIFFER
TMyColour = TColour (alias of ENUM) SAME DIFFER
TMyStr = string (alias of string) SAME DIFFER
TMyRec = TRec (alias of RECORD) SAME DIFFER

pxx answers DIFFER on every row. So the defect is not "a plain scalar alias mints a blob" as filed — there is no alias identity mechanism at all: every named type mints its own descriptor, for every type family.

And the one row where pxx agrees with fpc, it agrees with by having no opinion. = type Integer is supposed to differ, and pxx gets it right because it cannot tell that row from any other. That row is the obvious control for a fixer to reach for, it passes today, and it will pass after a wrong fix too — it can only fail if identity collapses too far. Do not use it as the control. The discriminating rows are the five that should say SAME.

The general form, because this row will be reached for again (frankS's framing, and it is the sharper statement): a guard cannot fail when its expected answer collides with the do-nothing answer. Here "DIFFER" is both the correct result for a distinct type and what a compiler with no alias mechanism at all emits for everything, so the row cannot separate the two — the same animal as CLAUDE.md's sizeof(int) = 4 case, where the unknown default equals the expected value. Ask of any row you plan to trust: if the machinery did nothing whatsoever, would this still pass? For this row the answer is yes, and it is the only row here for which it is.

Two further defects the same census turned up

Both are self-inconsistent and need no oracle.

1. An alias of an ENUM reports as an INTEGER header. TypeInfo(TMyColour) where TMyColour = TColour yields a TTypeInfo header with Kind = 1 and NamePtr^ = "Integer" — not an enum blob, and not named for anything the program declared. TColour itself correctly yields the enum blob. So the alias does not merely mint a separate descriptor, it mints a descriptor of the wrong shape and the wrong kind, and GetEnumName(TypeInfo(TMyColour), 0) is handed a header where its PEnumRTTI signature says blob. That is a second live instance of the shape bug, distinct from the subrange one already recorded there, and it is reached by an ordinary alias rather than by a subrange.

2. An alias of a RECORD yields a header whose NamePtr is unreadable. TypeInfo(TMyRec) gives Kind = 13 (correct for a record) and then segfaults on NamePtr^. TRec itself reads fine. A descriptor that type-checks, reports the right kind, and crashes on its own name field is worse than the identity miss this ticket was filed for, because the identity miss is at least silent and recoverable.

The probe must read Kind before NamePtr or it dies mid-census. The first cut did not and segfaulted on row 6, reporting nothing about rows 7–13 — where both defects above live. A probe that dies partway through reads as a short table, not as an error.