rtl-generics (Generics.Collections) — rung 3 of the Pascal OOP corpus
- Type: feature (compat — generics × classes × interfaces)
- Track: P — tag: compat
- Status: unfinished runs, fpjson's suite is 203/203).
- Follows: [[feature-pascal-corpus-fpjson]] (done). Parent umbrella: [[feature-pascal-corpus-oop]].
Why this rung
~9.5k LOC (generics.collections/defaults/hashes/helpers/memoryexpanders): generic
classes, IComparer<T>/IEqualityComparer<T> interface constraints, class
constraints — the generics × classes × interfaces intersection nothing else
touches. Stage dir prepared: /tmp/generics-stage (symlinks + inc/), driver g1.pp
(TList<Integer> smoke).
Walls cleared during recon (b329 batch, landed)
{$I inc\file.inc}backslash include paths (ExpandIncludes translates).rtlconstsunit (minimal FPC-compat message consts, lib/rtl/rtlconsts.pas).array[Byte] of X— small ordinal type as a whole index range.- PUInt8/PInt8/PUInt16/PInt16/PUInt32/PInt32 builtin pointer names.
- LOCAL var-section initializers
var a: UInt32 = 1;(ordinal/float consts via the LocalInit prologue machinery; STRING literals still unsupported there — they take a different decl path, small follow-up). - Compound-assign STATEMENTS
a += e;(expression side + IR already existed for the C frontend).
The current wall
{$MACRO ON} + {$define mix_abc := <multi-line statement text>} —
FPC compile-time TEXT MACROS, used by generics.hashes' bottom-up Jenkins mixer
(mix_abc; / final_abc; splice statement blocks). A lexer-level feature:
store the replacement text at {$define name := ...}, splice it when the bare
identifier appears. FPC also allows parameterless value macros. Scope carefully:
macros interact with the include expander and the token pre-scan.
After that
Unknown — generics themselves. pxx has "generic class in program" support; a
full TList<T> with specialization-per-instantiation across UNITS is the real
test. Expect walls in: generic TList<T> = class header syntax, specialize
vs Delphi-mode implicit specialization, nested generic types
(TDictionary<K,V>.TPair), interface constraints, TArray<T> = array of T.
Gate
Suite: rtl-generics has FPC tests (packages/rtl-generics/tests). Same recipe as fpjson: stage dir + driver + tjrun-style walker once it compiles.
Recon continued (same night) — b330 landed
{$MACRO ON}text macros: ExpandPasMacros textual pre-pass (elfwriter.inc, runs after ExpandIncludes; guarded so only value-define sources pay). Bodies flatten to one line, directives blank to spaces — line numbers preserved.- Int8/Int16/Int32 as value-cast names (OrdinalNameToTk).
- RolDWord/RorDWord/RolQWord/RorQWord System rotates (__pxx soft-alias helpers in builtin, UpCase/Pos pattern; prescan pull).
The NEXT wall (where this rung actually starts costing)
type TValueAnsiStringHelper = record helper for AnsiString — TYPE HELPERS
(generics.helpers.pas). A real language feature: helper method dispatch on
plain types, Self = the value. After that: the generic classes themselves
(TList<T>/TDictionary<K,V> across units, specialize, interface constraints).
Both are full sessions, not walls.
Next-wall inventory (generics.defaults) — methods NAMED after TYPE KEYWORDS
class function Integer(constref ALeft, ARight: Integer): Integer; etc — ~30
each in TCompare/TEquals/THashFactory. Needs: member-NAME position accepting
type-keyword tokens (tkInteger_T/tkLongWord_T/...; NOTE their SVal is empty —
read via GetTokenStr, the class-body property path already does), impl headers
class function TCompare.Integer(...), and call sites TCompare.Integer(a,b)
(selector paths guard on CurTok.Kind = tkIdent). Plus UNTYPED constref params
(constref ALeft, ARight): Integer). Type helpers are DONE through statics
(b331 v1+v2, see feature-pascal-type-helpers).
Recon round 3 (b332 landed) — and THE architectural wall
&keywordescaped identifiers (lexer: '&'+letter = plain tkIdent, no keyword lookup; '&777' stays octal).- Methods NAMED after type keywords (
class function Integer(...)) — IsMethodNameTok at decl/impl/call-site name positions; names via GetTokenStr (keyword tokens carry no SVal). class ofFORWARD references mint a forward class row.- PVariant builtin pointer name.
The wall recon stops at: generics.defaults selects comparers through RTTI — PTypeInfo/PTypeData over TypeInfo(T), incl. TypeInfo of GENERIC PARAMS. pxx's TypeInfo() is enum-only today. NOT a decision — see the plan below; no fork, no byte-layout cloning.
2026-07-14 — NO DECISION NEEDED. Settled approach: facade typinfo over our own blobs
The apparent fork-or-clone dilemma dissolves on inspection. Nothing real reads
FPC's RTTI bytes directly. Consumers — generics.defaults, fpjsonrtti,
LFM/TPersistent streaming, mORMot-style serializers, the script embeds — reach
RTTI through the typinfo UNIT's record declarations and accessors
(GetTypeData, GetPropInfo, GetEnumName, PropType), and those
declarations live inside typinfo itself. So we supply typinfo.
Consequences:
- No byte-layout parity. We are free to keep our own blob layout.
- No fork of Generics.Defaults. The vendor source compiles unmodified against our typinfo.
- Layout only leaks if a library does pointer arithmetic PAST the published API. Rare; deal with it if a corpus target actually does, not before.
The work (two items, both ordinary)
- Widen
TypeInfo(T)beyond enums: emit a per-TYPE info blob for scalars, strings, records, classes and (the one that matters here) GENERIC PARAMETERS at specialization time. This is the real compiler gap and the only interesting part. Track A/P. lib/rtl/typinfo.pasfacade: declare FPC'sTTypeKind/PTypeInfo/PTypeData/PPropInfoAPI SHAPES and fill them from our blobs. Track B. (The existing typinfo already does exactly this for enums — GetEnumName / GetEnumValue / GetEnumNameCount — so this is growing a proven pattern, not a new one.)
Same facade unblocks [[feature-embed-dwscript-rtti]] and the RTTI->streaming->LFM line, which is why it is worth doing properly rather than shimming per corpus.
2026-08-01 — item 1+2 LANDED: TypeInfo(T) widened + typinfo.pas facade
Both halves of the settled plan above are done and verified (self-host
fixedpoint + testmgr --tier quick green; make test running as the fuller
confirm since this touches shared RTTI emission).
Compiler side (Track A/P, compiler/defs.inc, symtab.inc, parser.inc,
ir.inc, rtti_emit.inc, compiler.pas):
TypeInfo(T)now accepts scalars (Integer/Boolean/Char/Int64/QWord/Single/ Double/Extended/...),string/AnsiString/ShortString, any user CLASS, and any user RECORD — not just enums. EnumTypeInfo(TEnum)is byte-for-byte UNCHANGED (still yields the barePEnumRTTIaddress) specifically so fpjson'sGetEnumName/GetEnumNameCountgate stays untouched — verified by a regression check in the smoke test below.- New machinery:
RegisterTypeInfoReqdedups each distinct (category, key)TypeInfo(T)use at parse time;EmitTypeInfoHeaders(rtti_emit.inc) runs right afterEmitRTTIand builds one uniform 24-byte "PTypeInfo" header per request —{Kind:Int64; NamePtr:PString; DataPtr:Pointer}— withKindmapped onto FPC's actualTTypeKindordinals (PxxTkToFPCKind, kept faithful to FPC's declared order on purpose, even though no byte-layout parity is required — costs nothing, avoids surprises).DataPtris nil for scalars, and for class/record points at the ALREADY-existingUClsRTTIOff[ci]blob (the sameTClassRTTI/ layout-descriptor typinfo already read viaAN_CLASSREF) — no new class/record blob format, pure reuse. - New sentinel base
TYPEINFO_REQ_DATAREF_BASE = 500000(most negative of the data-ref sentinel family, tested first incompiler.pas's post-EmitRTTI fixup pass — same pre-link-fixup convention asCLASSREF_DATAREF_BASE/RECORD_RTTI_DATAREF_BASE/ENUM_RTTI_DATAREF_BASE). - Generic parameters needed NO special handling. pxx generics specialize
by literal TOKEN-STREAM substitution (
SpecializeStreamin parser.inc) —Tinside a template body is textually replaced by the concrete type's spelling before the parser ever sees the specialized body. SoTypeInfo(T)insideTBox<Integer>is justTypeInfo(Integer)by the time the widened TypeInfo() parsing runs. Verified end-to-end with a hand-written generic class (see smoke test below):TBox<Integer>.KindOfTandTBox<AnsiString>.KindOfTeach report the correct FPCTTypeKindordinal for their specialization.
Library side (Track B, lib/rtl/typinfo.pas):
- Added
TTypeKind(FPC's own 30-member enum, same declared order — cheap fidelity, not required by the "no parity" decision but avoids surprises for any vendor code doingarray[TTypeKind] of Xor a rawOrd()compare). - Added
TTypeInfoHdr/PTypeInfo/PTypeData(PTypeData = PTypeInfofor now — one header, no separate TTypeData split yet; see "Known gaps" below) matching the compiler's new header layout exactly. GetEnumName/GetEnumNameCount/the whole existing enum facade is UNTOUCHED — enumTypeInfo()still returns a barePEnumRTTI, not this new header, on purpose (see compiler-side note above).
Verified with hand-written smoke tests (not yet checked into test/ —
follow-up, see below):
TypeInfo(Integer) -> Kind=1 (tkInteger), NamePtr^='Integer'
TypeInfo(Boolean) -> Kind=18 (tkBool), NamePtr^='Boolean'
TypeInfo(TAnimal) -> Kind=15 (tkClass), DataPtr -> the class's real TClassRTTI (GetClassName works through it)
TypeInfo(TPoint) -> Kind=13 (tkRecord)
TypeInfo(TColor) -> unchanged PEnumRTTI path, GetEnumName/GetEnumNameCount still work (regression check)
generic TBox<T>.KindOfT calling TypeInfo(T):
TBox<Integer>.KindOfT = 1 (tkInteger)
TBox<AnsiString>.KindOfT = 9 (tkAString)
Process note: briefly mis-suspected case x of Ord(EnumConst): ... was
broken in this compiler and reflexively rewrote two functions to if/elseif
chains as a "workaround" without testing the actual claim first. Caught (by
the user) before it shipped: a 4-line repro proved case/Ord() labels work
completely correctly, and the real error was an unrelated const-declared-
inside-a-var-block syntax mistake in defs.inc. Reverted to the natural
case form. No compiler bug here, no ticket needed — noted only so the
mistake (reasoning instead of measuring) isn't repeated.
Known gaps / natural follow-ups (not blocking, noted for the next session)
- No
TTypeDatafields beyondDataPtr.GetTypeDatadoesn't exist yet as an FPC-shaped function (min/max/ordinal size for scalars,PropCountetc for classes read straight off the class blob today). Add when a real corpus target reads a specific field FPC'sTTypeDatahas and ours doesn't. - generics.defaults itself not yet retried. This session's remaining
budget went to verifying the widening in isolation (hand-written smoke
tests + the generic-specialization check above) rather than restaging
/tmp/generics-stage(which no longer exists —findon this box turned up nothing) and re-running the vendor tree. generics.defaults expectsATypeInfo.Kindas a DIRECT field read (confirmed by readingpackages/rtl-generics/src/generics.defaults.pasfrom a local FPC source checkout) plusarray[TTypeKind] of TInstancelookup tables andGetTypeData(ATypeInfo)for extra fields (e.g. ordinal size) — theKindfield and thearray[TTypeKind]shape are both now supported by this landing;GetTypeDatabeyondDataPtris the likely next wall. Re-stage pertools/install_lib_candidates.shand retry as the next step. - No
test/test_typeinfo_widen*.paschecked in yet. The smoke tests above were hand-run from/tmpscratch, not committed as a gated regression test. Should land as a realtest/file wired intotestmgrbefore this ticket is considered done, so the widening itself is gated going forward (not just fpjson's enum-only regression). test-fpjsonSKIPped locally (no fcl-json tree staged on this box —tools/install_lib_candidates.sh fcl-jsonwas not re-run this session) so the "fpjson stays green" claim above rests on the unchanged-enum-codepath argument + the regression check in the smoke test, not a fresh fpjson run. Track T's watcher (or the next session, if it re-stages fcl-json) should confirm.