feat(units): directed lookup for unit-qualified field-access references

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.
This commit is contained in:
Andrew Haines 2026-06-29 12:19:13 -04:00 committed by Graeme Geldenhuys
parent 9ec970393d
commit 61bb86f896
6 changed files with 72 additions and 4 deletions

View file

@ -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 }

View file

@ -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

View file

@ -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<T>' 'TGenEnum<Integer>' when T=Integer is in scope)
and update AAccess.RecordName so codegen sees the concrete instantiation. }

View file

@ -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;

View file

@ -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);

View file

@ -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