Symptom
unit rootmethunit;
interface
type TThing = class public Tag: Integer; end;
function Describe(t: TThing): string;
implementation
function Describe(t: TThing): string;
begin Describe := t.ToString; end; { <-- unknown method 'ToString' }
end.
The identical expression in the main program's body compiles and runs.
Root cause
EnsureTObjectRootMethods materialises Equals, GetHashCode and ToString
into the root VMT lazily — a program is only charged for them if it names one.
The trigger was a token pre-scan for .equals / .gethashcode / .tostring
that ran over the main program's token range only. A unit's tokens are
parsed in ParseUsesUnitBody, which had no such scan, so by the time the unit's
implementation referenced the method the root VMT had no slot for it — and
whether the program later mentioned ToString did not help, because the unit is
parsed first.
Same family as [[bug-p-a-class-const-cannot-take-the-address-of-a-sibling-class-const]]: a facility that works at top level and is missing on the unit path.
Fix (landed 2026-08-25)
ParseUsesUnitBody (compiler/pasparser_proc.inc) gained the same pre-scan over
the unit's own token range, guarded by RootVMTSlotCount > 0; on a hit it pulls
in builtin via ParseUsesUnitAmbient and calls EnsureTObjectRootMethods.
Suppressed under NoDefaultRtl and for ESP class targets, as at top level.
EnsureTObjectRootMethods needed a forward in compiler/pasparser_name.inc.
Regression test test/test_tobject_root_methods_inside_a_unit.pas +
test/tobject_units/rootmethunit.pas, .expected taken from fpc 3.2.2.
Where it was found
Driving uses generics.defaults for [[feature-pascal-corpus-generics]] — the wall
at line 1569, after the class-const @ fix moved it up from 411.
Log
- 2026-08-25 — resolved, commit bd50e76ed.