From 9d876b46aaa50eb7a4f0a3c0a5d3719430dfce0e Mon Sep 17 00:00:00 2001 From: Andrew Haines Date: Fri, 19 Jun 2026 00:05:29 -0400 Subject: [PATCH] feat(types): resolve qualified type names UnitName.TypeName MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) 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) --- compiler/src/main/pascal/uParser.pas | 11 +++++ compiler/src/main/pascal/uSemantic.pas | 20 +++++++++ compiler/src/main/pascal/uSymbolTable.pas | 46 +++++++++++++++++++-- compiler/src/test/pascal/cp.test.parser.pas | 32 ++++++++++++++ compiler/src/test/pascal/cp.test.units.pas | 24 +++++++++++ 5 files changed, 129 insertions(+), 4 deletions(-) diff --git a/compiler/src/main/pascal/uParser.pas b/compiler/src/main/pascal/uParser.pas index a01db4c..7bba29b 100644 --- a/compiler/src/main/pascal/uParser.pas +++ b/compiler/src/main/pascal/uParser.pas @@ -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 '<' } diff --git a/compiler/src/main/pascal/uSemantic.pas b/compiler/src/main/pascal/uSemantic.pas index cf17209..9cf8637 100644 --- a/compiler/src/main/pascal/uSemantic.pas +++ b/compiler/src/main/pascal/uSemantic.pas @@ -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) reduces to the bare 'TList', 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); diff --git a/compiler/src/main/pascal/uSymbolTable.pas b/compiler/src/main/pascal/uSymbolTable.pas index 7210084..85117f1 100644 --- a/compiler/src/main/pascal/uSymbolTable.pas +++ b/compiler/src/main/pascal/uSymbolTable.pas @@ -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; diff --git a/compiler/src/test/pascal/cp.test.parser.pas b/compiler/src/test/pascal/cp.test.parser.pas index 9359283..4bc6e6f 100644 --- a/compiler/src/test/pascal/cp.test.parser.pas +++ b/compiler/src/test/pascal/cp.test.parser.pas @@ -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; diff --git a/compiler/src/test/pascal/cp.test.units.pas b/compiler/src/test/pascal/cp.test.units.pas index ed5e07c..bd1e8e1 100644 --- a/compiler/src/test/pascal/cp.test.units.pas +++ b/compiler/src/test/pascal/cp.test.units.pas @@ -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 }