From 61bb86f8967fc6bd6bd755e74e7a4ff0f4404471 Mon Sep 17 00:00:00 2001 From: Andrew Haines Date: Mon, 29 Jun 2026 12:19:13 -0400 Subject: [PATCH] feat(units): directed lookup for unit-qualified field-access references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A unit-qualified type-member reference written as a field access — `Unit.TEnum.Member` (a type-qualified enum member) or `Unit.TFoo.StaticVar` — resolved its base type through the flat uses-chain lookup, i.e. the cross-unit last-wins winner, ignoring the unit qualifier. With two used units exporting a same-named enum/type this bound to the wrong one: `ea.TPalette.paThree` failed "Enum 'TPalette' has no member 'paThree'" because it resolved TPalette to eb. TFieldAccessExpr now carries the parser-collapsed unit qualifier (like TIdentExpr and TMethodCallExpr already do), and AnalyseFieldAccess resolves the base via the directed ResolveQualified when it is set — so the reference binds to the named unit's own type, independent of `uses` order. Also completes the .bif round-trip for the qualifier fields: TFieldAccessExpr .QualifierUnit and the previously-unserialised TMethodCallExpr.QualifierUnit are now encoded/decoded, and bif-coverage.status records all the qualifier/owner fields added across the cross-unit work (serialise vs safe). Test: e2e cross-unit qualified enum member, order-independent. --- compiler/src/main/pascal/uAST.pas | 6 +++ compiler/src/main/pascal/uParser.pas | 1 + compiler/src/main/pascal/uSemantic.pas | 10 ++++- compiler/src/main/pascal/uUnitInterfaceIO.pas | 10 +++-- .../src/test/pascal/cp.test.e2e.useschain.pas | 44 +++++++++++++++++++ tools/bif-coverage/bif-coverage.status | 5 +++ 6 files changed, 72 insertions(+), 4 deletions(-) diff --git a/compiler/src/main/pascal/uAST.pas b/compiler/src/main/pascal/uAST.pas index 0a70b52..b941dce 100644 --- a/compiler/src/main/pascal/uAST.pas +++ b/compiler/src/main/pascal/uAST.pas @@ -136,6 +136,12 @@ type public RecordName: string; { used when Base = nil (leaf access) } FieldName: string; + QualifierUnit: string; { set by the parser — non-empty when the + base type was written unit-qualified, e.g. + 'Unit.TEnum.Member' or 'Unit.TFoo.StaticVar'. + uSemantic resolves RecordName against this + specific unit's exports (directed lookup) + instead of the flat last-wins winner. } Base: TASTExpr; { owned — when non-nil, chained access (e.g. A.B.C) } FieldInfo: TFieldInfo; { set by uSemantic — nil for constructor calls } IsConstant: Boolean; { set by uSemantic — TypeName.ConstName resolves to a class constant } diff --git a/compiler/src/main/pascal/uParser.pas b/compiler/src/main/pascal/uParser.pas index 8a31bac..9cb13c4 100644 --- a/compiler/src/main/pascal/uParser.pas +++ b/compiler/src/main/pascal/uParser.pas @@ -5115,6 +5115,7 @@ begin FldNode.Col := Col; FldNode.RecordName := Name; FldNode.FieldName := SecondName; + FldNode.QualifierUnit := QualUnit; Result := FldNode; { Indexed property read: Ident.Prop[idx] } if Check(tkLBracket) then diff --git a/compiler/src/main/pascal/uSemantic.pas b/compiler/src/main/pascal/uSemantic.pas index 5a77459..fa29101 100644 --- a/compiler/src/main/pascal/uSemantic.pas +++ b/compiler/src/main/pascal/uSemantic.pas @@ -12288,7 +12288,15 @@ begin Exit; end; - RecSym := FTable.Lookup(AAccess.RecordName); + { A unit-qualified base type 'Unit.TEnum.Member' / 'Unit.TFoo.StaticVar' + resolves the base against that specific unit's exports (directed lookup), so + it binds to the named unit rather than the flat cross-unit last-wins winner. + Falls back to the normal lookup on a miss. } + RecSym := nil; + if AAccess.QualifierUnit <> '' then + RecSym := ResolveQualified(AAccess.QualifierUnit, AAccess.RecordName); + if RecSym = nil then + RecSym := FTable.Lookup(AAccess.RecordName); { If the name contains '<' and wasn't found, resolve scope-bound type params (e.g. 'TGenEnum' → 'TGenEnum' when T=Integer is in scope) and update AAccess.RecordName so codegen sees the concrete instantiation. } diff --git a/compiler/src/main/pascal/uUnitInterfaceIO.pas b/compiler/src/main/pascal/uUnitInterfaceIO.pas index 8f1abfd..46a08df 100644 --- a/compiler/src/main/pascal/uUnitInterfaceIO.pas +++ b/compiler/src/main/pascal/uUnitInterfaceIO.pas @@ -820,6 +820,7 @@ begin Result := EncodeLpstr('mcall') + EncodeLpstr(TMethodCallExpr(AE).ObjectName) + EncodeLpstr(TMethodCallExpr(AE).Name) + + EncodeLpstr(TMethodCallExpr(AE).QualifierUnit) + EncodeExpr(TMethodCallExpr(AE).ObjExpr) + EncodeExprList(TMethodCallExpr(AE).Args) else if AE is TIndirectFuncCallExpr then @@ -830,6 +831,7 @@ begin Result := EncodeLpstr('field') + EncodeLpstr(TFieldAccessExpr(AE).RecordName) + EncodeLpstr(TFieldAccessExpr(AE).FieldName) + + EncodeLpstr(TFieldAccessExpr(AE).QualifierUnit) + EncodeExpr(TFieldAccessExpr(AE).Base) + EncodeExpr(TFieldAccessExpr(AE).PropIndexExpr) else if AE is TDerefExpr then @@ -1522,9 +1524,10 @@ begin else if Kind = 'mcall' then begin MCE := TMethodCallExpr.Create(); - MCE.ObjectName := ReadLpstrAt(AText, APos); - MCE.Name := ReadLpstrAt(AText, APos); - MCE.ObjExpr := ReadExpr(AText, APos); + MCE.ObjectName := ReadLpstrAt(AText, APos); + MCE.Name := ReadLpstrAt(AText, APos); + MCE.QualifierUnit := ReadLpstrAt(AText, APos); + MCE.ObjExpr := ReadExpr(AText, APos); ReadExprList(AText, APos, MCE.Args); Result := MCE; end @@ -1540,6 +1543,7 @@ begin FA := TFieldAccessExpr.Create(); FA.RecordName := ReadLpstrAt(AText, APos); FA.FieldName := ReadLpstrAt(AText, APos); + FA.QualifierUnit := ReadLpstrAt(AText, APos); FA.Base := ReadExpr(AText, APos); FA.PropIndexExpr := ReadExpr(AText, APos); Result := FA; diff --git a/compiler/src/test/pascal/cp.test.e2e.useschain.pas b/compiler/src/test/pascal/cp.test.e2e.useschain.pas index a893a32..ad6309b 100644 --- a/compiler/src/test/pascal/cp.test.e2e.useschain.pas +++ b/compiler/src/test/pascal/cp.test.e2e.useschain.pas @@ -68,6 +68,11 @@ type class (distinct vtable/dispatch), independent of the bare last-wins winner, so both behaviours are observable side by side. } procedure TestRun_CrossUnitType_QualifiedDisambig; + { Unit-qualified ENUM member 'Unit.TEnum.Member' (field-access form) binds + the enum type to that specific unit's exports via directed lookup, so it + reaches members the bare/last-wins enum type does not — independent of + `uses` order. } + procedure TestRun_CrossUnitEnum_QualifiedMember; end; implementation @@ -566,6 +571,45 @@ begin AssertEquals('tca.TShape then tcb.TShape', '3' + LE + '4' + LE, Output); end; +const + EA_Enum = ''' + unit ea; + interface + type TPalette = (paOne, paTwo, paThree); + implementation + end. + '''; + EB_Enum = ''' + unit eb; + interface + type TPalette = (paZero, paOne); + implementation + end. + '''; + +procedure TE2EUsesChainTests.TestRun_CrossUnitEnum_QualifiedMember; +const + { ea.TPalette.paThree exists only in ea (ordinal 2); eb.TPalette.paZero only + in eb (ordinal 0). A flat/last-wins enum-type lookup would resolve one of + them to the other unit's TPalette and fail "no member"; the qualifier picks + each unit's own enum. } + DrvSrc = ''' + program P; + uses ea, eb; + begin + WriteLn(Ord(ea.TPalette.paThree)); + WriteLn(Ord(eb.TPalette.paZero)) + end. + '''; +var Output: string; RCode: Integer; +begin + if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end; + AssertTrue('compile+link+run', + CompileAndRunWithUnits(EA_Enum, EB_Enum, DrvSrc, Output, RCode)); + AssertEquals('exit 0', 0, RCode); + AssertEquals('ea.paThree=2, eb.paZero=0', '2' + LE + '0' + LE, Output); +end; + initialization RegisterTest(TE2EUsesChainTests); diff --git a/tools/bif-coverage/bif-coverage.status b/tools/bif-coverage/bif-coverage.status index 3f272ed..0b8cfe7 100644 --- a/tools/bif-coverage/bif-coverage.status +++ b/tools/bif-coverage/bif-coverage.status @@ -42,10 +42,13 @@ TIdentExpr.IsImplicitSelf safe TIdentExpr.IsImplicitSelfMethod safe TIdentExpr.IsMetaclassRef safe TIdentExpr.ConstArraySymbol safe +TIdentExpr.QualifierUnit serialise +TIdentExpr.ResolvedOwnerUnit safe # TFieldAccessExpr (uAST.pas:118) TFieldAccessExpr.RecordName serialise TFieldAccessExpr.FieldName serialise +TFieldAccessExpr.QualifierUnit serialise TFieldAccessExpr.Base serialise TFieldAccessExpr.FieldInfo safe TFieldAccessExpr.IsConstant safe @@ -107,6 +110,7 @@ TAssignment.IsGlobal safe TAssignment.IsThreadVar safe TAssignment.IsWeakLhs safe TAssignment.ImplicitSelfField safe +TAssignment.ResolvedOwnerUnit safe # TIfStmt (uAST.pas:238) TIfStmt.Condition serialise @@ -265,6 +269,7 @@ TInheritedCallStmt.Args serialise # TMethodCallExpr (uAST.pas:881) TMethodCallExpr.ObjectName serialise TMethodCallExpr.Name serialise +TMethodCallExpr.QualifierUnit serialise TMethodCallExpr.Args serialise TMethodCallExpr.ObjExpr serialise TMethodCallExpr.IsConstructorCall safe