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:
parent
9ec970393d
commit
61bb86f896
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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. }
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue