← board

Builtin TObject class — var o: TObject + TObject.Create + root methods

Symptom

TObject works as a PARAMETER type (tyPointer/elem tyClass, landed bug-tobject-param-truncated-32bit) and as an implicit class PARENT (class(TObject) — parent resolution special-cases it). But there is no real TObject class ROW, so:

var o: TObject;
begin
  o := TObject.Create;   { undefined variable (TObject) }
  writeln(o.GetHashCode);
end.

fails. Blocks tobject5, tclassinfo1 (also needs RTTI ClassInfo), and any real-world code using a bare TObject instance (very common — Pascal Script, Synapse hooks, LCL).

Fix shape

Mirror RegisterBuiltinTGuid (a builtin record minted at ParseProgram start), but as a CLASS: a zero-field TObject with a VMT, a Create constructor (GetMem instance + stamp VMT + return), and Free/Destroy (the obj.Free desugar already exists). FPC's TObject also has Equals/GetHashCode/ClassName/ UnitName/InstanceSize — add the simple ones (GetHashCode = PtrInt(self), Equals = pointer compare); ClassName/UnitName/ClassInfo need RTTI (defer / separate ticket).

The tricky part vs TGuid: a class needs a VMT slot table + a constructor proc with a synthesized body. Look at how metaclass New / GenMakeFreeObject build instances for the allocation shape.

Slice 1 landed (2026-07-12, opus-p)

Instantiation works: var o: TObject; o := TObject.Create; o.Free, and a child instance assigns to a TObject ref. RegisterBuiltinTObject mints the root row (fieldless, VMT slot); the class(TObject) parent guard forces the implicit-root model so no VMT relocates (an early real-parent version RED'd test-core + cross ARC — fixed). Test: test_builtin_tobject.

Remaining: RTTI-backed root methods — GetHashCode (= PtrInt(self)), Equals (pointer compare), ClassName / UnitName / InstanceSize / ClassInfo — for tobject5/tclassinfo1. Needs the RTTI-on-IR surface; separate slice.

Corpus evidence, and the fork (2026-08-20, frank1-ACP)

The "remaining" slice above is now the only thing between the rtl-generics corpus rung and its next measurement, and it blocks it twice: generics.defaults.pas:1569 needs TObject.Equals and :1780 needs TObject.GetHashCode. Every default comparer in that unit overrides both. The walls past them ([[feature-pascal-corpus-generics]], walls 28-33) could only be measured by stubbing the two out in a throwaway copy of the tree.

That raises the slice's real value well above its prio: 42, but it is a decision, not a task, and the ticket should not be picked up until it is settled:

The intercept is not a cheaper version of the slots; it is a different, weaker guarantee, and the corpus is the caller that would notice. Recommend the slots, scheduled deliberately rather than squeezed into a corpus session.

2026-08-20, frank1-ACP: that fork is now filed as [[decide-tobject-root-methods-dispatch-model]] and this ticket is blocked on it. Written as prose inside a feature ticket, the "do not pick this up until it is settled" instruction was invisible to the ranker, which kept offering the ticket as the top of Track A's queue. The decide ticket adds a third option the write-up above did not consider (reserved leading VMT slots in every class, TObject still implicit) and carries the recommendation.

Gate

make test + self-host byte-identical; a compile-run test (var o: TObject; o := TObject.Create; o.Free); unskip tobject5 (partial — the RTTI-free assertions).

2026-08-22 — UNBLOCKED, and nearly done. Two members left.

The blocker [[decide-tobject-root-methods-dispatch-model]] was answered on 2026-08-21 (option C, reserved leading VMT slots, N = 4, with --compact-classes) and its implementation ticket [[feature-a-tobject-root-method-vmt-slots]] is in done/. Slot 0 (Destroy) was the last unfilled one and landed in 24585b403. So this ticket is no longer blocked on anything.

Re-measured against a self-hosted binary at 0332839bc — the six members the ticket and [[feature-p-tobject-api-classparent-instancesize-tostring]] list, on a TD = class(TObject) instance:

member today
ClassParent works (non-nil for a class with a parent)
InstanceSize works (12 for a one-Integer class)
ClassName works
ToString works — 'TD', and a descendant's override dispatches
Equals works — including a descendant's override through a static TObject receiver
GetHashCode works — same, virtual
ClassInfo PXX-REJECT
UnitName PXX-REJECT

The virtual-dispatch check is the one that mattered: a TFoo overriding all three, called through function EqRoot(const L, R: TObject), agrees with fpc -Mobjfpc -O1 {$H+} line for line. That is exactly the shape generics.defaults' TEquals.&class / THashFactory.&Class use, so the corpus walls at generics.defaults.pas:1569 and :1780 are cleared — [[feature-pascal-corpus-generics]] is unblocked from here.

Remaining, and neither is on the corpus path:

Moved out of blocked/.

2026-08-25 — UnitName LANDED. Only ClassInfo left, and it is a decide.

UnitName is done, exactly as the note above scoped it: RTTI_CLS_SIZE grew 96 → 104 with the declaring unit's interned name at +96, written in rtti_emit.inc from UClsUnitIdx[ci], read by __pxxUnitName and reached through the IsClassRefOpName / GenMakeClassRefOp pair that already serves ClassName / InstanceSize.

Two details worth keeping:

Growing the header was safe by construction and now demonstrably so: nothing strides over these blobs (rtti_emit records each one's offset) and every reader — builtin.pas' PXX_RTTI_*, lib/rtl/rtti.pas' RTTI_OFS_* — names a FIELD offset, never the total. 141 lib units compile unchanged; the self-host fixedpoint converged.

tobject5.pp is unskipped and passing: the conformance suite went 344 → 345 pass, 171 → 170 skip, one pre-existing failure unmoved.

Regression test: test/test_tobject_unitname.pas + its helper unit, wired into test-core, .expected taken from fpc 3.2.2.

Remaining: ClassInfo only, and it is now filed as the Track U call it always was — [[decide-tobject-classinfo-blob-or-refusal]]. Do not implement it by picking a side here.

2026-09-09 — ClassInfo LANDED. This ticket is closed.

The last PXX-REJECT member, and the one that was a Track U call rather than an implementation choice. Built to the decision ([[decide-tobject-classinfo-blob-or-refusal]], option 3): x.ClassInfo answers the typinfo facade's PTypeInfo header, the same value TypeInfo(TThatClass) mints, so identity holds AND a layout walker reads a real kind byte.

Work and evidence are in [[feature-a-classinfo-returns-the-typinfo-header]] — including the second, older defect it turned up: UnitName, landed here on 2026-08-25, shipped without its builtin-pull pre-scan trigger, so it was refused in any program with no uses clause. This ticket's own note above says UnitName is done, and it was right about the value and the accessor and wrong about the LINKAGE; the row that would have caught it did not exist, because test_tobject_unitname.pas uses a helper unit and so always pulled the trigger by a neighbour. Fixed, with a bare-program test of its own.

Blob is 112 bytes now (+96 unitName, +104 classInfo). The "safe to grow" note at RTTI_CLS_SIZE has been narrowed to say WHY it is safe here and to point at the record layout descriptor, which is the counterexample (30ed522b3) and may not grow at all.

Log