← board

rtl-generics (Generics.Collections) — rung 3 of the Pascal OOP corpus

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)

  1. {$I inc\file.inc} backslash include paths (ExpandIncludes translates).
  2. rtlconsts unit (minimal FPC-compat message consts, lib/rtl/rtlconsts.pas).
  3. array[Byte] of X — small ordinal type as a whole index range.
  4. PUInt8/PInt8/PUInt16/PInt16/PUInt32/PInt32 builtin pointer names.
  5. 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).
  6. 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

  1. {$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.
  2. Int8/Int16/Int32 as value-cast names (OrdinalNameToTk).
  3. 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

  1. &keyword escaped identifiers (lexer: '&'+letter = plain tkIdent, no keyword lookup; '&777' stays octal).
  2. Methods NAMED after type keywords (class function Integer(...)) — IsMethodNameTok at decl/impl/call-site name positions; names via GetTokenStr (keyword tokens carry no SVal).
  3. class of FORWARD references mint a forward class row.
  4. 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:

The work (two items, both ordinary)

  1. 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.
  2. lib/rtl/typinfo.pas facade: declare FPC's TTypeKind/PTypeInfo/ PTypeData/PPropInfo API 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):

Library side (Track B, lib/rtl/typinfo.pas):

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)