← board

An UNKNOWN type name silently becomes a 4-byte Integer (pointers truncate)

Symptom

ParseTypeKind's final fallback is Result := tyInteger — meant for enum names, but an unknown name lands there too. So a typo compiles:

var x: Integr;      { typo for Integer — compiles, becomes a 4-byte Integer }

and, far worse, a pointer type the source never declared becomes a 32-bit int, so every address stored through it is TRUNCATED:

var p: PSomethingUndeclared;   { silently a 4-byte Integer }
p := GetMem(64);
heap-addr = 131751131217928
via-p     = 18446744072423997448     <- corrupted, no diagnostic

This hole has bitten before

bug-tobject-param-truncated-32bit was exactly this: Sender: TObject fell through to tyInteger and truncated. That fix special-cased TObject. It patched the symptom, not the hole — and the hole then swallowed TClass (a class reference, truncated) and Int8/Int16/Int32 (all silently 4 bytes) until 2026-07-13, when those were fixed the same one-at-a-time way. The next missing name will do it again.

Confirmed symptoms so far

Each was found and fixed one at a time, and each came through THIS hole:

The hole keeps manufacturing these. It is worth closing.

Why it is not closed yet — what is VERIFIED, and what is not

Making it an error is a one-line change, and the compiler still self-hosts with it on. make test then fails in exactly ONE place: the flagship fgl-compiles test, with unknown type: TPoint. Everything else in the suite passes.

Verified facts (do not re-derive)

What is NOT known

Which construct actually trips it. Line numbers are useless here: the error reports "line 19", which is a COMMENT in typshrdh.inc and a {$modeswitch} in types.pp — they do not track across includes. Two hypotheses were tried and BOTH were wrong (an unresolved include; then a self-referencing type). Do not guess a third.

The diagnostic is now IN, and it found the construct

Every Error now prints the near: token window (2026-07-13). With the strict error re-applied, it says:

pascal26:19: error: unknown type (TPoint)
  near: dupError   PPoint   >>> TPoint  TPoint

So the failing token is the TPoint in PPoint = ^TPoint;typshrdh.inc line 90. TPoint itself is declared at line 62 of the same file, i.e. BEFORE it. So the question is not "where" any more, it is:

Why is TPoint not registered by the time PPoint = ^TPoint is parsed, given it is declared 28 lines earlier?

The likely answer: TPoint is a full ADVANCED RECORD — public, constructor Create, class function, class operator — and pxx cannot parse any of that ([[feature-pascal-advanced-records]]). A minimal advanced record fails outright under the LAX compiler too (Expected: :, but got: function), yet the real one somehow survives and SizeOf(TPoint) = 8 with p.X working — so something about that declaration is being partially skipped rather than parsed, and the name never lands in the record table even though the type ends up usable. THAT is the thing to understand.

Instrumented — and the answer is ORDER, not the record

Done 2026-07-13. AddUClass DOES run for TPoint and ParseRecordFields DOES complete:

DBG unknown TPoint: IsRecordType=0 alias=-1 UClsCount=4 TypeSectionDepth=0
DBG named-record branch for TPoint, ci=4
DBG TPoint fields done; IsRecordType=20

Read the order. The unknown-TPoint hit comes FIRST, while the record table is still empty for it (IsRecordType=0, UClsCount=4) — and TPoint's record declaration is processed AFTERWARDS. So PPoint = ^TPoint is being parsed BEFORE TPoint = record, even though TPoint is declared 28 lines EARLIER in the file.

Declarations are not reaching the parser in source order. That is the actual bug to chase — not advanced records, and not the include. The advanced-record body is presumably why TPoint's declaration gets deferred/re-walked, but the failure itself is an ordering one.

This also explains something that should have been suspicious all along: forward pointer refs (PNode = ^TNode; before TNode) currently WORK. If a pointer alias can be parsed before its element type exists, then the unknown-name fallback is what is silently absorbing that case too — which means closing the hole may require the forward-pointer path to be made explicit (a real forward declaration + fixup), not just an error.

Next: find why the declaration order is not source order here (the decl pre-scan / excision machinery is the place to look), and how forward pointer aliases are meant to resolve their element type.

Advanced-record support is the likely prerequisite either way, since that declaration cannot currently be parsed as written.

Old notes (superseded)

Why it was not simply made an error

Tried 2026-07-13 (one line: error instead of Result := tyInteger). It does NOT break forward references — the declaration pre-scan registers the type section up front. It DOES break the flagship fgl-compiles test with unknown type: TPoint.

A wrong lead, recorded so nobody re-walks it

My first hypothesis was that FPC's types.pp declares TPoint via {$i typshrdh.inc} and that we fail to resolve that include. That is WRONG. Checked directly:

uses types;         -> SizeOf(TPoint) = 8   (the record from the include: two Longints)
uses fgl;           -> SizeOf(TPoint) = 8   (transitively, same)
uses fgl, types;    -> SizeOf(TPoint) = 8

The include IS found and TPoint IS registered — and a missing include in a unit is already a hard error (verified), so it could not have been silently skipped anyway. So the "headline FPC-compat leans on this bug" claim in the first draft of this ticket was false; ignore it.

What the strict error's true source is has NOT been isolated. Something in the fgl -> types chain references a type name that is unknown AT THAT POINT, and today it quietly becomes an Integer. Note that typshrdh.inc's TPoint is an ADVANCED RECORD (methods, public, a self-referencing constructor Create(apt: TPoint)), and pxx does not parse a record with a public constructor at all — so how much of that declaration actually lands is worth checking first.

How to pick this up

  1. Re-apply the one-line strict error in ParseTypeKind's final else.
  2. Compile test/test_fgl_use.pas --mimic-fpc -Fu/usr/share/fpcsrc/3.2.2/rtl/objpas and find WHICH name is unknown and where. Do not assume it is the include.
  3. Fix that, then keep the error.

The truncation hole is the prize and it is worth the dig: it is a silent wrong-code bug, and it has already produced two shipped symptoms (TObject, then TClass/Int8/Int16).

Gate

make test + self-host byte-identical + cross. The fgl-compiles test is the one to watch.

CLOSED 2026-07-13

The insight was that the fallback was only ever legitimately needed in one place: the ELEMENT of a ^. PNode = ^TNode; before TNode is a legal forward reference, and ResolvePendingPointerAliases already fixes the element up afterwards BY NAME — the fallback was just the placeholder that let the parse get that far. Everywhere else it was pure hazard.

So: a PtrElemDepth counter is raised around both ^-element parses, an unknown name is tolerated only while it is > 0, and everywhere else it is now Error('unknown type: ...').

Two more name tables had to be consulted first, because they hold names that ARE declared and simply were not reachable from ParseTypeKind:

What the strict check immediately caught

Gate

make test green, self-host byte-identical, testmgr --tier full GREEN. Regression b266: the POSITIVE half (forward ^ refs, named dyn-array types, AnsiChar/Int16 widths) in the test file, and the NEGATIVE half (a typo'd name must FAIL to compile) in the Makefile, since it must not compile at all.