feat(types): resolve qualified type names UnitName.TypeName

A type written as 'UnitName.TypeName' (the unit qualifier may itself be
dotted, e.g. System.SysUtils.TFormatSettings) was a parse error: after
reading the leading identifier ParseTypeName only accepted a generic
'<...>' argument list, not a '.' continuation.

Parse the full dotted path into the type name, then resolve it by its
final component — a type is always a single identifier; the leading
components name a unit already loaded by the 'uses' clause. The strip is
centralised in TSymbolTable.FindType (via UnitQualifierTail) so every
type position benefits uniformly — var/field decls, parameters, and the
several routine return-type sites — and FindTypeOrInstantiate strips the
qualifier off a generic instance (Unit.TList<Integer>) before
instantiation. Plain dotted identifier paths only: the bracket/space/
caret/angle encodings for array, pointer, set and generic types are left
untouched.

Tested: dotted-name capture in the parser, semantic resolution of a
qualified builtin (System.Integer), and end-to-end compile+run of a
program using a cross-unit qualified type.

(cherry picked from commit 7e9c3d1f61d42769bc3341195220a25190637e15)
This commit is contained in:
Andrew Haines 2026-06-19 00:05:29 -04:00 committed by Graeme Geldenhuys
parent cf994dcca4
commit 9d876b46aa
5 changed files with 129 additions and 4 deletions

View file

@ -385,6 +385,17 @@ begin
[FCurrent.Line, FCurrent.Col, FLexer.Filename]));
Result := FCurrent.Value;
Advance();
{ Qualified type name: 'UnitName.TypeName'. The unit qualifier may itself
be dotted (e.g. System.SysUtils.TFormatSettings). A type is always the
final identifier; the leading components name the (possibly dotted) unit.
Carry the full dotted form through — the semantic type resolver strips the
unit qualifier and resolves the tail via the uses chain. }
while Check(tkDot) and (PeekKind() = tkIdent) do
begin
Advance(); { consume '.' }
Result := Result + '.' + FCurrent.Value;
Advance();
end;
if Check(tkLessThan) then
begin
Advance(); { consume '<' }

View file

@ -2352,6 +2352,7 @@ var
EnumDesc: TEnumTypeDesc;
SAT: TStaticArrayTypeDesc;
DAT: TDynArrayTypeDesc;
I, LtPos, DotPos: Integer;
begin
Result := FTable.FindType(AName);
if Result <> nil then Exit;
@ -2539,6 +2540,25 @@ begin
[BaseType.Name]), 0, 0);
Exit;
end;
{ Qualified type name 'UnitName.TypeName' — the unit qualifier may itself be
dotted (System.SysUtils.TFormatSettings). A type is a single identifier,
so strip everything up to the final '.' that precedes any generic '<...>'
argument list and resolve the tail via the normal uses chain (the qualifier
names the unit, already loaded by the 'uses' clause). A qualified generic
instance (Unit.TList<Integer>) reduces to the bare 'TList<Integer>', which
the generic branch above instantiates. }
LtPos := StrPos('<', AName);
if LtPos < 0 then
LtPos := Length(AName);
DotPos := -1;
for I := 0 to LtPos - 1 do
if StrAt(AName, I) = Ord('.') then
DotPos := I;
if DotPos >= 0 then
begin
Result := FindTypeOrInstantiate(StrCopyTail(AName, DotPos + 1));
Exit;
end;
if StrPos('<', AName) >= 0 then
begin
Result := InstantiateGeneric(AName);

View file

@ -1932,15 +1932,53 @@ begin
Result := Sym;
end;
{ For a plain dotted identifier path 'A.B.C' (every component an identifier,
single-dot separators) return the final component 'C'. In a qualified type
reference 'UnitName.TypeName' the type is always that final identifier and
the leading components name the (possibly dotted) unit. Returns '' when
AName is not such a path — e.g. the bracket/space/caret/angle-bracket forms
used to encode array, pointer, set and generic-instance types — leaving
those untouched for the on-demand resolver. }
function UnitQualifierTail(const AName: string): string;
var
I, LastDot: Integer;
B: Integer;
begin
Result := '';
LastDot := -1;
for I := 0 to Length(AName) - 1 do
begin
B := AName[I];
if B = Ord('.') then
LastDot := I
else if not (((B >= Ord('A')) and (B <= Ord('Z'))) or
((B >= Ord('a')) and (B <= Ord('z'))) or
((B >= Ord('0')) and (B <= Ord('9'))) or
(B = Ord('_'))) then
Exit; { contains a non-identifier byte — not a qualified name }
end;
if (LastDot >= 0) and (LastDot < Length(AName) - 1) then
Result := Copy(AName, LastDot + 1, MaxInt);
end;
function TSymbolTable.FindType(const AName: string): TTypeDesc;
var
Sym: TSymbol;
Sym: TSymbol;
Tail: string;
begin
Sym := Lookup(AName); { honour uses-chain visibility }
if (Sym <> nil) and (Sym.Kind = skType) then
Result := Sym.TypeDesc
else
Result := nil;
Exit(Sym.TypeDesc);
{ Qualified type name 'UnitName.TypeName' — resolve by the final dotted
component through the same uses chain (the qualifier names a used unit). }
Tail := UnitQualifierTail(AName);
if Tail <> '' then
begin
Sym := Lookup(Tail);
if (Sym <> nil) and (Sym.Kind = skType) then
Exit(Sym.TypeDesc);
end;
Result := nil;
end;
function IsUnmangledUnit(const AUnitName: string): Boolean;

View file

@ -30,6 +30,9 @@ type
procedure TestSingleVarDecl;
procedure TestMultipleVarDecls;
procedure TestMultiNameVarDecl;
{ Qualified type name 'UnitName.TypeName' is captured whole. }
procedure TestQualifiedTypeName;
procedure TestQualifiedTypeName_DottedUnit;
{ Statements }
procedure TestEmptyBeginEnd;
@ -173,6 +176,35 @@ begin
end;
end;
procedure TParserTests.TestQualifiedTypeName;
var
Prog: TProgram;
begin
Prog := ParseSource('program P; var x: System.Integer; begin end.');
try
AssertEquals('qualified type captured whole', 'System.Integer',
TVarDecl(Prog.Block.Decls.Items[0]).TypeName);
finally
Prog.Free();
end;
end;
procedure TParserTests.TestQualifiedTypeName_DottedUnit;
var
Prog: TProgram;
begin
{ The unit qualifier may itself be dotted; the whole dotted path is
carried through to the type name. }
Prog := ParseSource('program P; var x: System.SysUtils.TFoo; begin end.');
try
AssertEquals('dotted-unit qualified type captured whole',
'System.SysUtils.TFoo',
TVarDecl(Prog.Block.Decls.Items[0]).TypeName);
finally
Prog.Free();
end;
end;
procedure TParserTests.TestMultipleVarDecls;
var
Prog: TProgram;

View file

@ -55,6 +55,8 @@ type
{ ------------------------------------------------------------------ }
procedure TestSemantic_Unit_OK;
procedure TestSemantic_Unit_WithType_OK;
{ Qualified type name 'System.Integer' resolves to the builtin. }
procedure TestSemantic_Unit_QualifiedType_OK;
procedure TestSemantic_Unit_ImplBodyUsesIntfType;
procedure TestSemantic_Unit_SignatureMismatch_ParamCount_RaisesError;
procedure TestSemantic_Unit_MissingImpl_RaisesError;
@ -233,6 +235,23 @@ const
end.
''';
{ Qualified type names resolve by their final dotted component through the
uses chain. 'System.Integer' needs no cross-unit load — System is
implicit and Integer is builtin — so it self-containedly exercises the
qualifier-stripping type resolver. }
SrcUnitQualifiedType =
'''
unit QualT;
interface
function Twice(N: System.Integer): System.Integer;
implementation
function Twice(N: System.Integer): System.Integer;
begin
Result := N * 2
end;
end.
''';
{ ------------------------------------------------------------------ }
{ Lexer tests }
{ ------------------------------------------------------------------ }
@ -394,6 +413,11 @@ begin
AnalyseUnit(SrcUnitWithType).Free();
end;
procedure TUnitTests.TestSemantic_Unit_QualifiedType_OK;
begin
AnalyseUnit(SrcUnitQualifiedType).Free();
end;
procedure TUnitTests.TestSemantic_Unit_ImplBodyUsesIntfType;
begin
{ TBox is declared in interface; impl body can reference it }