feat(units): unit-qualified var access and cross-unit var last-wins

Two used units could not export the same module-scope global var name:
the second Define raised "Duplicate identifier", and even where one slot
existed a qualified reference (Unit.V) was lowered by bare name, so it
could not pick a specific unit's storage. Bare references also resolved
through the flat table (analysis order) rather than the uses chain,
disagreeing with the last-in-uses rule that consts already follow.

Give vars the same cross-unit semantics as consts:

- DefineGlobalLastWins detaches the prior unit's var on a cross-unit
  collision (keeping it alive in the per-unit cache so Unit.V still
  reaches it) and installs the later unit as the flat winner; same-unit
  redeclaration and module-name markers stay hard errors. RegisterVars
  mirrors this on the prebuilt-import path, as RegisterConsts does.

- The analyser stamps the resolved owning unit onto each global-var
  reference and assignment target (the uses-chain winner for a bare ref,
  the named unit for a qualified one). Codegen mangles the storage
  symbol with that owner instead of re-looking-up the bare name, so the
  definition (keyed on the unit being compiled) and every reference
  agree. A bare reference now follows uses order like a const; a
  qualified reference always hits its own unit's slot.

Tests: e2e last-wins (+ reversed) and qualified disambiguation across two
units exporting `var V`, plus an IR-level check that qualified loads emit
distinct owner-prefixed symbols.
This commit is contained in:
Andrew Haines 2026-06-29 09:23:42 -04:00 committed by Graeme Geldenhuys
parent 9aa44911b0
commit cd9af9ac7b
7 changed files with 361 additions and 67 deletions

View file

@ -386,8 +386,19 @@ type
function EmitInstancePtr(AExpr: TASTExpr): string; function EmitInstancePtr(AExpr: TASTExpr): string;
function FieldPtr(const ARecordVar: string; AOffset: Integer; AIsGlobal: Boolean = False): string; function FieldPtr(const ARecordVar: string; AOffset: Integer; AIsGlobal: Boolean = False): string;
{ Returns the QBE address token for a variable: '$Name' for globals, { Returns the QBE address token for a variable: '$Name' for globals,
'%_var_Name' for locals. } '%_var_Name' for locals. The AOwner overload mangles a global's
function VarRef(const AName: string; AIsGlobal: Boolean): string; storage symbol with an explicit owning unit (from the resolved
symbol's OwningUnit) rather than re-looking-up the bare name, so a
reference to a cross-unit last-wins var reaches the correct slot.
AOwner = '' falls back to the bare-name resolution. }
function VarRef(const AName: string; AIsGlobal: Boolean): string; overload;
function VarRef(const AName: string; AIsGlobal: Boolean;
const AOwner: string): string; overload;
{ Owning unit to mangle a global-var reference with: the semantic-stamped
ResolvedOwnerUnit, falling back to the explicit QualifierUnit (the latter
survives .bif round-trips for an inlined qualified reference where the
former, deliberately not serialised, is absent). }
function IdentVarOwner(AIdent: TIdentExpr): string;
{ Returns a QBE temp (or address token) that holds the address to pass as a { Returns a QBE temp (or address token) that holds the address to pass as a
var-param actual argument. When the actual argument is itself a var param var-param actual argument. When the actual argument is itself a var param
its local slot contains a pointer — emit loadl to obtain the original its local slot contains a pointer — emit loadl to obtain the original
@ -533,6 +544,12 @@ type
allowlist semantics as uSemantic.MangleUnitPrefix. } allowlist semantics as uSemantic.MangleUnitPrefix. }
function ClassUnitPrefix(const AClassName: string): string; function ClassUnitPrefix(const AClassName: string): string;
function GlobalVarUnitPrefix(const AName: string): string; function GlobalVarUnitPrefix(const AName: string): string;
{ Map an owning unit to its global-var mangle prefix (program-scope and
the RTL allowlist stay bare). The shared core of GlobalVarUnitPrefix
(name-based owner resolution) and the AOwner VarRef overload, so a
definition site (keyed on the unit being compiled) and a reference
site (keyed on the resolved symbol's owner) always agree. }
function MangleGlobalOwner(const AOwner: string): string;
{ Compute the QBE call target for a property accessor. When AVSlot >= 0 { Compute the QBE call target for a property accessor. When AVSlot >= 0
the accessor is virtual: emit the vptr+slot loads and return the the accessor is virtual: emit the vptr+slot loads and return the
function-pointer temp. Otherwise return the static mangled symbol function-pointer temp. Otherwise return the static mangled symbol
@ -2011,11 +2028,14 @@ begin
if Decl.IsThreadVar then if Decl.IsThreadVar then
FThreadVarNames.Add(VarName); { keyed on the bare name — VarRef's FThreadVarNames.Add(VarName); { keyed on the bare name — VarRef's
threadvar test sees the bare ident } threadvar test sees the bare ident }
{ Mangle the emitted symbol with the owning unit (here always the unit { Mangle the emitted symbol with the unit currently being compiled so
being compiled) so same-named globals across units don't collide; VarRef same-named globals across units don't collide; VarRef applies the
applies the identical prefix at every reference. Bare name kept above identical prefix (via the resolved symbol's owner) at every reference.
for the threadvar lookup key. } Keyed on FCurrentUnitName, not a name re-lookup, so a cross-unit
VarName := GlobalVarUnitPrefix(VarName) + VarName; last-wins loser still defines its own slot (the flat table holds only
the winner after extraction). Bare name kept above for the threadvar
lookup key. }
VarName := MangleGlobalOwner(FCurrentUnitName) + VarName;
{ Initialised global: emit the folded value into the data section instead { Initialised global: emit the folded value into the data section instead
of a zero slot. threadvars cannot carry a non-zero static initialiser of a zero slot. threadvars cannot carry a non-zero static initialiser
(they live in .tbss), so the parser/semantic restrict initialisers to (they live in .tbss), so the parser/semantic restrict initialisers to
@ -4613,14 +4633,14 @@ begin
whole bitmap into the destination slot. } whole bitmap into the destination slot. }
ValTemp := EmitExpr(AAssign.Expr); ValTemp := EmitExpr(AAssign.Expr);
EmitLine(Format(' call $memcpy(l %s, l %s, l %d)', EmitLine(Format(' call $memcpy(l %s, l %s, l %d)',
[VarRef(AAssign.Name, AAssign.IsGlobal), ValTemp, [VarRef(AAssign.Name, AAssign.IsGlobal, AAssign.ResolvedOwnerUnit), ValTemp,
TSetTypeDesc(AAssign.ResolvedLhsType).RawSize()])); TSetTypeDesc(AAssign.ResolvedLhsType).RawSize()]));
end end
else if (AAssign.ResolvedLhsType <> nil) and else if (AAssign.ResolvedLhsType <> nil) and
(AAssign.ResolvedLhsType.Kind = tyStaticArray) and (AAssign.ResolvedLhsType.Kind = tyStaticArray) and
IsRecordCall(AAssign.Expr) then IsRecordCall(AAssign.Expr) then
begin begin
EmitRecordCallSret(AAssign.Expr, VarRef(AAssign.Name, AAssign.IsGlobal)); EmitRecordCallSret(AAssign.Expr, VarRef(AAssign.Name, AAssign.IsGlobal, AAssign.ResolvedOwnerUnit));
end end
else if (AAssign.ResolvedLhsType <> nil) and else if (AAssign.ResolvedLhsType <> nil) and
(AAssign.ResolvedLhsType.Kind = tyRecord) then (AAssign.ResolvedLhsType.Kind = tyRecord) then
@ -4642,21 +4662,21 @@ begin
EmitLine(Format(' call $memset(l %s, w 0, l %d)', EmitLine(Format(' call $memset(l %s, w 0, l %d)',
[SretBuf, ClassRT.TotalSize()])); [SretBuf, ClassRT.TotalSize()]));
EmitRecordCallSret(AAssign.Expr, SretBuf); EmitRecordCallSret(AAssign.Expr, SretBuf);
EmitRecordReleaseFields(ClassRT, VarRef(AAssign.Name, AAssign.IsGlobal)); EmitRecordReleaseFields(ClassRT, VarRef(AAssign.Name, AAssign.IsGlobal, AAssign.ResolvedOwnerUnit));
EmitLine(Format(' call $memcpy(l %s, l %s, l %d)', EmitLine(Format(' call $memcpy(l %s, l %s, l %d)',
[VarRef(AAssign.Name, AAssign.IsGlobal), SretBuf, ClassRT.TotalSize()])); [VarRef(AAssign.Name, AAssign.IsGlobal, AAssign.ResolvedOwnerUnit), SretBuf, ClassRT.TotalSize()]));
end end
else if IsRecordCall(AAssign.Expr) then else if IsRecordCall(AAssign.Expr) then
begin begin
EmitRecordReleaseFields(ClassRT, VarRef(AAssign.Name, AAssign.IsGlobal)); EmitRecordReleaseFields(ClassRT, VarRef(AAssign.Name, AAssign.IsGlobal, AAssign.ResolvedOwnerUnit));
EmitLine(Format(' call $memset(l %s, w 0, l %d)', EmitLine(Format(' call $memset(l %s, w 0, l %d)',
[VarRef(AAssign.Name, AAssign.IsGlobal), ClassRT.TotalSize()])); [VarRef(AAssign.Name, AAssign.IsGlobal, AAssign.ResolvedOwnerUnit), ClassRT.TotalSize()]));
EmitRecordCallSret(AAssign.Expr, VarRef(AAssign.Name, AAssign.IsGlobal)); EmitRecordCallSret(AAssign.Expr, VarRef(AAssign.Name, AAssign.IsGlobal, AAssign.ResolvedOwnerUnit));
end end
else else
begin begin
ValTemp := EmitExpr(AAssign.Expr); ValTemp := EmitExpr(AAssign.Expr);
EmitRecordCopy(ClassRT, VarRef(AAssign.Name, AAssign.IsGlobal), ValTemp); EmitRecordCopy(ClassRT, VarRef(AAssign.Name, AAssign.IsGlobal, AAssign.ResolvedOwnerUnit), ValTemp);
end; end;
end end
else if (AAssign.ResolvedLhsType <> nil) and else if (AAssign.ResolvedLhsType <> nil) and
@ -4668,7 +4688,7 @@ begin
another method-pointer var or a TMethod record (same layout). } another method-pointer var or a TMethod record (same layout). }
ValTemp := EmitExpr(AAssign.Expr); ValTemp := EmitExpr(AAssign.Expr);
EmitLine(Format(' call $memcpy(l %s, l %s, l 16)', EmitLine(Format(' call $memcpy(l %s, l %s, l 16)',
[VarRef(AAssign.Name, AAssign.IsGlobal), ValTemp])); [VarRef(AAssign.Name, AAssign.IsGlobal, AAssign.ResolvedOwnerUnit), ValTemp]));
end end
else if AAssign.Expr.ResolvedType.IsString() then else if AAssign.Expr.ResolvedType.IsString() then
begin begin
@ -4677,7 +4697,7 @@ begin
if not AAssign.IsGlobal and IsPromoted(AAssign.Name) then if not AAssign.IsGlobal and IsPromoted(AAssign.Name) then
EmitLine(Format(' %s =l copy %%_var_%s', [OldTemp, AAssign.Name])) EmitLine(Format(' %s =l copy %%_var_%s', [OldTemp, AAssign.Name]))
else else
EmitLine(Format(' %s =l loadl %s', [OldTemp, VarRef(AAssign.Name, AAssign.IsGlobal)])); EmitLine(Format(' %s =l loadl %s', [OldTemp, VarRef(AAssign.Name, AAssign.IsGlobal, AAssign.ResolvedOwnerUnit)]));
ValTemp := EmitExpr(AAssign.Expr); ValTemp := EmitExpr(AAssign.Expr);
if not ExprOwnsRef(AAssign.Expr) then if not ExprOwnsRef(AAssign.Expr) then
EmitLine(Format(' call $_StringAddRef(l %s)', [ValTemp])); EmitLine(Format(' call $_StringAddRef(l %s)', [ValTemp]));
@ -4685,7 +4705,7 @@ begin
if not AAssign.IsGlobal and IsPromoted(AAssign.Name) then if not AAssign.IsGlobal and IsPromoted(AAssign.Name) then
EmitLine(Format(' %%_var_%s =l copy %s', [AAssign.Name, ValTemp])) EmitLine(Format(' %%_var_%s =l copy %s', [AAssign.Name, ValTemp]))
else else
EmitLine(Format(' storel %s, %s', [ValTemp, VarRef(AAssign.Name, AAssign.IsGlobal)])); EmitLine(Format(' storel %s, %s', [ValTemp, VarRef(AAssign.Name, AAssign.IsGlobal, AAssign.ResolvedOwnerUnit)]));
end end
else if (AAssign.ResolvedLhsType <> nil) and else if (AAssign.ResolvedLhsType <> nil) and
(AAssign.ResolvedLhsType.Kind = tyDynArray) then (AAssign.ResolvedLhsType.Kind = tyDynArray) then
@ -4697,7 +4717,7 @@ begin
if not AAssign.IsGlobal and IsPromoted(AAssign.Name) then if not AAssign.IsGlobal and IsPromoted(AAssign.Name) then
EmitLine(Format(' %s =l copy %%_var_%s', [OldTemp, AAssign.Name])) EmitLine(Format(' %s =l copy %%_var_%s', [OldTemp, AAssign.Name]))
else else
EmitLine(Format(' %s =l loadl %s', [OldTemp, VarRef(AAssign.Name, AAssign.IsGlobal)])); EmitLine(Format(' %s =l loadl %s', [OldTemp, VarRef(AAssign.Name, AAssign.IsGlobal, AAssign.ResolvedOwnerUnit)]));
ValTemp := EmitExpr(AAssign.Expr); ValTemp := EmitExpr(AAssign.Expr);
if not ExprOwnsRef(AAssign.Expr) then if not ExprOwnsRef(AAssign.Expr) then
EmitLine(Format(' call $_DynArrayAddRef(l %s)', [ValTemp])); EmitLine(Format(' call $_DynArrayAddRef(l %s)', [ValTemp]));
@ -4705,7 +4725,7 @@ begin
if not AAssign.IsGlobal and IsPromoted(AAssign.Name) then if not AAssign.IsGlobal and IsPromoted(AAssign.Name) then
EmitLine(Format(' %%_var_%s =l copy %s', [AAssign.Name, ValTemp])) EmitLine(Format(' %%_var_%s =l copy %s', [AAssign.Name, ValTemp]))
else else
EmitLine(Format(' storel %s, %s', [ValTemp, VarRef(AAssign.Name, AAssign.IsGlobal)])); EmitLine(Format(' storel %s, %s', [ValTemp, VarRef(AAssign.Name, AAssign.IsGlobal, AAssign.ResolvedOwnerUnit)]));
end end
else if (AAssign.ResolvedLhsType <> nil) and else if (AAssign.ResolvedLhsType <> nil) and
(AAssign.ResolvedLhsType.Kind = tyClass) and (AAssign.ResolvedLhsType.Kind = tyClass) and
@ -4713,19 +4733,19 @@ begin
begin begin
if AAssign.IsWeakLhs then if AAssign.IsWeakLhs then
EmitLine(Format(' call $_WeakClear(l %s)', EmitLine(Format(' call $_WeakClear(l %s)',
[VarRef(AAssign.Name, AAssign.IsGlobal)])) [VarRef(AAssign.Name, AAssign.IsGlobal, AAssign.ResolvedOwnerUnit)]))
else else
begin begin
OldTemp := AllocTemp(); OldTemp := AllocTemp();
if not AAssign.IsGlobal and IsPromoted(AAssign.Name) then if not AAssign.IsGlobal and IsPromoted(AAssign.Name) then
EmitLine(Format(' %s =l copy %%_var_%s', [OldTemp, AAssign.Name])) EmitLine(Format(' %s =l copy %%_var_%s', [OldTemp, AAssign.Name]))
else else
EmitLine(Format(' %s =l loadl %s', [OldTemp, VarRef(AAssign.Name, AAssign.IsGlobal)])); EmitLine(Format(' %s =l loadl %s', [OldTemp, VarRef(AAssign.Name, AAssign.IsGlobal, AAssign.ResolvedOwnerUnit)]));
EmitLine(Format(' call $_ClassRelease(l %s)', [OldTemp])); EmitLine(Format(' call $_ClassRelease(l %s)', [OldTemp]));
if not AAssign.IsGlobal and IsPromoted(AAssign.Name) then if not AAssign.IsGlobal and IsPromoted(AAssign.Name) then
EmitLine(Format(' %%_var_%s =l copy 0', [AAssign.Name])) EmitLine(Format(' %%_var_%s =l copy 0', [AAssign.Name]))
else else
EmitLine(Format(' storel 0, %s', [VarRef(AAssign.Name, AAssign.IsGlobal)])); EmitLine(Format(' storel 0, %s', [VarRef(AAssign.Name, AAssign.IsGlobal, AAssign.ResolvedOwnerUnit)]));
end; end;
end end
else if AAssign.IsWeakLhs and (AAssign.Expr.ResolvedType.Kind = tyClass) then else if AAssign.IsWeakLhs and (AAssign.Expr.ResolvedType.Kind = tyClass) then
@ -4736,7 +4756,7 @@ begin
any prior registration for this slot. } any prior registration for this slot. }
ValTemp := EmitExpr(AAssign.Expr); ValTemp := EmitExpr(AAssign.Expr);
EmitLine(Format(' call $_WeakAssign(l %s, l %s)', EmitLine(Format(' call $_WeakAssign(l %s, l %s)',
[VarRef(AAssign.Name, AAssign.IsGlobal), ValTemp])); [VarRef(AAssign.Name, AAssign.IsGlobal, AAssign.ResolvedOwnerUnit), ValTemp]));
end end
else if AAssign.Expr.ResolvedType.Kind = tyClass then else if AAssign.Expr.ResolvedType.Kind = tyClass then
begin begin
@ -4758,7 +4778,7 @@ begin
if not ExprOwnsRef(AAssign.Expr) then if not ExprOwnsRef(AAssign.Expr) then
EmitLine(Format(' call $_ClassAddRef(l %s)', [ValTemp])); EmitLine(Format(' call $_ClassAddRef(l %s)', [ValTemp]));
EmitLine(Format(' storel %s, %s', EmitLine(Format(' storel %s, %s',
[ValTemp, VarRef(AAssign.Name, AAssign.IsGlobal)])); [ValTemp, VarRef(AAssign.Name, AAssign.IsGlobal, AAssign.ResolvedOwnerUnit)]));
MarkArcSlotWritten(AAssign.Name); MarkArcSlotWritten(AAssign.Name);
end end
else else
@ -4767,7 +4787,7 @@ begin
if not AAssign.IsGlobal and IsPromoted(AAssign.Name) then if not AAssign.IsGlobal and IsPromoted(AAssign.Name) then
EmitLine(Format(' %s =l copy %%_var_%s', [OldTemp, AAssign.Name])) EmitLine(Format(' %s =l copy %%_var_%s', [OldTemp, AAssign.Name]))
else else
EmitLine(Format(' %s =l loadl %s', [OldTemp, VarRef(AAssign.Name, AAssign.IsGlobal)])); EmitLine(Format(' %s =l loadl %s', [OldTemp, VarRef(AAssign.Name, AAssign.IsGlobal, AAssign.ResolvedOwnerUnit)]));
ValTemp := EmitExpr(AAssign.Expr); ValTemp := EmitExpr(AAssign.Expr);
if not ExprOwnsRef(AAssign.Expr) then if not ExprOwnsRef(AAssign.Expr) then
EmitLine(Format(' call $_ClassAddRef(l %s)', [ValTemp])); EmitLine(Format(' call $_ClassAddRef(l %s)', [ValTemp]));
@ -4775,7 +4795,7 @@ begin
if not AAssign.IsGlobal and IsPromoted(AAssign.Name) then if not AAssign.IsGlobal and IsPromoted(AAssign.Name) then
EmitLine(Format(' %%_var_%s =l copy %s', [AAssign.Name, ValTemp])) EmitLine(Format(' %%_var_%s =l copy %s', [AAssign.Name, ValTemp]))
else else
EmitLine(Format(' storel %s, %s', [ValTemp, VarRef(AAssign.Name, AAssign.IsGlobal)])); EmitLine(Format(' storel %s, %s', [ValTemp, VarRef(AAssign.Name, AAssign.IsGlobal, AAssign.ResolvedOwnerUnit)]));
MarkArcSlotWritten(AAssign.Name); MarkArcSlotWritten(AAssign.Name);
end; end;
end end
@ -4797,7 +4817,7 @@ begin
ValTemp := EmitExpr(AAssign.Expr); ValTemp := EmitExpr(AAssign.Expr);
EmitLine(Format(' call $_ClassAddRef(l %s)', [ValTemp])); EmitLine(Format(' call $_ClassAddRef(l %s)', [ValTemp]));
EmitLine(Format(' storel %s, %s', EmitLine(Format(' storel %s, %s',
[ValTemp, VarRef(AAssign.Name, AAssign.IsGlobal)])); [ValTemp, VarRef(AAssign.Name, AAssign.IsGlobal, AAssign.ResolvedOwnerUnit)]));
MarkArcSlotWritten(AAssign.Name); MarkArcSlotWritten(AAssign.Name);
end end
else else
@ -4806,14 +4826,14 @@ begin
if not AAssign.IsGlobal and IsPromoted(AAssign.Name) then if not AAssign.IsGlobal and IsPromoted(AAssign.Name) then
EmitLine(Format(' %s =l copy %%_var_%s', [OldTemp, AAssign.Name])) EmitLine(Format(' %s =l copy %%_var_%s', [OldTemp, AAssign.Name]))
else else
EmitLine(Format(' %s =l loadl %s', [OldTemp, VarRef(AAssign.Name, AAssign.IsGlobal)])); EmitLine(Format(' %s =l loadl %s', [OldTemp, VarRef(AAssign.Name, AAssign.IsGlobal, AAssign.ResolvedOwnerUnit)]));
ValTemp := EmitExpr(AAssign.Expr); ValTemp := EmitExpr(AAssign.Expr);
EmitLine(Format(' call $_ClassAddRef(l %s)', [ValTemp])); EmitLine(Format(' call $_ClassAddRef(l %s)', [ValTemp]));
EmitLine(Format(' call $_ClassRelease(l %s)', [OldTemp])); EmitLine(Format(' call $_ClassRelease(l %s)', [OldTemp]));
if not AAssign.IsGlobal and IsPromoted(AAssign.Name) then if not AAssign.IsGlobal and IsPromoted(AAssign.Name) then
EmitLine(Format(' %%_var_%s =l copy %s', [AAssign.Name, ValTemp])) EmitLine(Format(' %%_var_%s =l copy %s', [AAssign.Name, ValTemp]))
else else
EmitLine(Format(' storel %s, %s', [ValTemp, VarRef(AAssign.Name, AAssign.IsGlobal)])); EmitLine(Format(' storel %s, %s', [ValTemp, VarRef(AAssign.Name, AAssign.IsGlobal, AAssign.ResolvedOwnerUnit)]));
MarkArcSlotWritten(AAssign.Name); MarkArcSlotWritten(AAssign.Name);
end; end;
end end
@ -4851,7 +4871,7 @@ begin
if not AAssign.IsGlobal and IsPromoted(AAssign.Name) then if not AAssign.IsGlobal and IsPromoted(AAssign.Name) then
EmitLine(Format(' %%_var_%s =d copy %s', [AAssign.Name, ValTemp])) EmitLine(Format(' %%_var_%s =d copy %s', [AAssign.Name, ValTemp]))
else else
EmitLine(Format(' stored %s, %s', [ValTemp, VarRef(AAssign.Name, AAssign.IsGlobal)])); EmitLine(Format(' stored %s, %s', [ValTemp, VarRef(AAssign.Name, AAssign.IsGlobal, AAssign.ResolvedOwnerUnit)]));
end end
else if (AAssign.ResolvedLhsType <> nil) and else if (AAssign.ResolvedLhsType <> nil) and
(AAssign.ResolvedLhsType.Kind = tySingle) then (AAssign.ResolvedLhsType.Kind = tySingle) then
@ -4883,7 +4903,7 @@ begin
if not AAssign.IsGlobal and IsPromoted(AAssign.Name) then if not AAssign.IsGlobal and IsPromoted(AAssign.Name) then
EmitLine(Format(' %%_var_%s =s copy %s', [AAssign.Name, ValTemp])) EmitLine(Format(' %%_var_%s =s copy %s', [AAssign.Name, ValTemp]))
else else
EmitLine(Format(' stores %s, %s', [ValTemp, VarRef(AAssign.Name, AAssign.IsGlobal)])); EmitLine(Format(' stores %s, %s', [ValTemp, VarRef(AAssign.Name, AAssign.IsGlobal, AAssign.ResolvedOwnerUnit)]));
end end
else if (AAssign.ResolvedLhsType <> nil) and else if (AAssign.ResolvedLhsType <> nil) and
(QbeTypeOf(AAssign.ResolvedLhsType) = 'l') then (QbeTypeOf(AAssign.ResolvedLhsType) = 'l') then
@ -4898,7 +4918,7 @@ begin
if not AAssign.IsGlobal and IsPromoted(AAssign.Name) then if not AAssign.IsGlobal and IsPromoted(AAssign.Name) then
EmitLine(Format(' %%_var_%s =l copy %s', [AAssign.Name, ValTemp])) EmitLine(Format(' %%_var_%s =l copy %s', [AAssign.Name, ValTemp]))
else else
EmitLine(Format(' storel %s, %s', [ValTemp, VarRef(AAssign.Name, AAssign.IsGlobal)])); EmitLine(Format(' storel %s, %s', [ValTemp, VarRef(AAssign.Name, AAssign.IsGlobal, AAssign.ResolvedOwnerUnit)]));
end end
else else
begin begin
@ -4916,7 +4936,7 @@ begin
's': StoreInstr := 'stores'; 's': StoreInstr := 'stores';
else StoreInstr := 'storel'; else StoreInstr := 'storel';
end; end;
EmitLine(Format(' %s %s, %s', [StoreInstr, ValTemp, VarRef(AAssign.Name, AAssign.IsGlobal)])); EmitLine(Format(' %s %s, %s', [StoreInstr, ValTemp, VarRef(AAssign.Name, AAssign.IsGlobal, AAssign.ResolvedOwnerUnit)]));
end; end;
end; end;
end; end;
@ -4954,7 +4974,7 @@ begin
if (AExpr.ResolvedType <> nil) and (AExpr.ResolvedType.Kind = tyClass) then if (AExpr.ResolvedType <> nil) and (AExpr.ResolvedType.Kind = tyClass) then
begin begin
Loaded := AllocTemp(); Loaded := AllocTemp();
EmitLine(Format(' %s =l loadl %s', [Loaded, VarRef(Id.Name, Id.IsGlobal)])); EmitLine(Format(' %s =l loadl %s', [Loaded, VarRef(Id.Name, Id.IsGlobal, IdentVarOwner(Id))]));
if Id.ParamMode <> pmNone then if Id.ParamMode <> pmNone then
begin begin
{ var-param class ident: slot -> caller var -> instance. } { var-param class ident: slot -> caller var -> instance. }
@ -4969,11 +4989,11 @@ begin
{ Var-record param: dereference the param slot to get the actual record { Var-record param: dereference the param slot to get the actual record
address. } address. }
Loaded := AllocTemp(); Loaded := AllocTemp();
EmitLine(Format(' %s =l loadl %s', [Loaded, VarRef(Id.Name, Id.IsGlobal)])); EmitLine(Format(' %s =l loadl %s', [Loaded, VarRef(Id.Name, Id.IsGlobal, IdentVarOwner(Id))]));
Result := Loaded; Result := Loaded;
end end
else else
Result := VarRef(Id.Name, Id.IsGlobal); { inline record } Result := VarRef(Id.Name, Id.IsGlobal, IdentVarOwner(Id)); { inline record }
Exit; Exit;
end; end;
@ -5128,28 +5148,52 @@ begin
Owner := FInlineSrcUnits.Strings[FInlineDepth - 1] Owner := FInlineSrcUnits.Strings[FInlineDepth - 1]
else else
Owner := FCurrentUnitName; Owner := FCurrentUnitName;
if Owner = '' then Exit; Result := MangleGlobalOwner(Owner);
if (FProgramName <> '') and SameText(Owner, FProgramName) then Exit; end;
Result := MangleUnitPrefix(Owner);
{ Idempotence guard: a static class-var (e.g. RegModU.TReg.FCount) is emitted function TCodeGenQBE.MangleGlobalOwner(const AOwner: string): string;
under its already unit+class-qualified symbol RegModU_TReg_FCount. Such a begin
name is passed here verbatim and Lookup does not find it, so Owner falls back
to the current unit and the prefix would be applied a SECOND time
(RegModU_RegModU_TReg_FCount), breaking the def/ref match at link. If AName
already starts with the prefix, it is carried — don't re-prefix it. }
if (Result <> '') and (Length(AName) >= Length(Result)) and
SameText(Copy(AName, 0, Length(Result)), Result) then
Result := ''; Result := '';
if AOwner = '' then Exit;
{ Sentinel: the global's name is ALREADY a fully-mangled symbol (e.g. a static
class var's ClassVarEmitName, which master computes as
CurrentUnitPrefix()+Class+'_'+Field — its class qualifier is more
comprehensive than the module-var prefix, so use it verbatim and add no
further prefix). }
if AOwner = PreMangledGlobalOwner then Exit;
if (FProgramName <> '') and SameText(AOwner, FProgramName) then Exit;
Result := MangleUnitPrefix(AOwner);
end; end;
function TCodeGenQBE.VarRef(const AName: string; AIsGlobal: Boolean): string; function TCodeGenQBE.VarRef(const AName: string; AIsGlobal: Boolean): string;
overload;
begin
Result := VarRef(AName, AIsGlobal, '');
end;
function TCodeGenQBE.IdentVarOwner(AIdent: TIdentExpr): string;
begin
Result := AIdent.ResolvedOwnerUnit;
if Result = '' then Result := AIdent.QualifierUnit;
end;
function TCodeGenQBE.VarRef(const AName: string; AIsGlobal: Boolean;
const AOwner: string): string; overload;
var
Prefix: string;
begin begin
if AIsGlobal then if AIsGlobal then
begin begin
if FThreadVarNames.IndexOf(AName) >= 0 then { An explicit owner (the resolved symbol's OwningUnit) mangles directly;
Result := 'thread $' + GlobalVarUnitPrefix(AName) + AName otherwise fall back to resolving the owner from the bare name. }
if AOwner <> '' then
Prefix := MangleGlobalOwner(AOwner)
else else
Result := '$' + GlobalVarUnitPrefix(AName) + AName; Prefix := GlobalVarUnitPrefix(AName);
if FThreadVarNames.IndexOf(AName) >= 0 then
Result := 'thread $' + Prefix + AName
else
Result := '$' + Prefix + AName;
end end
{ A variable captured from an enclosing proc is reached through the hidden { A variable captured from an enclosing proc is reached through the hidden
%_cap_<Name> pointer parameter, which holds the address of the enclosing %_cap_<Name> pointer parameter, which holds the address of the enclosing
@ -5180,7 +5224,7 @@ begin
{ Interface variable: the fat pointer lives in one contiguous 16-byte { Interface variable: the fat pointer lives in one contiguous 16-byte
block (locals: one alloc; globals: one 16-byte data item) whose base block (locals: one alloc; globals: one 16-byte data item) whose base
is the _obj slot — that base IS the var-arg address. } is the _obj slot — that base IS the var-arg address. }
Result := VarRef(AIdent.Name, AIdent.IsGlobal) + '_obj'; Result := VarRef(AIdent.Name, AIdent.IsGlobal, IdentVarOwner(AIdent)) + '_obj';
end end
else if AIdent.IsImplicitSelf then else if AIdent.IsImplicitSelf then
begin begin
@ -5197,7 +5241,7 @@ begin
Result := SelfT; Result := SelfT;
end end
else else
Result := VarRef(AIdent.Name, AIdent.IsGlobal); Result := VarRef(AIdent.Name, AIdent.IsGlobal, IdentVarOwner(AIdent));
end; end;
function TCodeGenQBE.EmitLValueAddr(AExpr: TASTExpr): string; function TCodeGenQBE.EmitLValueAddr(AExpr: TASTExpr): string;
@ -6470,6 +6514,9 @@ begin
try try
CVStore.Name := AAssign.ClassVarEmitName; CVStore.Name := AAssign.ClassVarEmitName;
CVStore.IsGlobal := True; CVStore.IsGlobal := True;
{ ClassVarEmitName is already the fully-mangled static-var symbol; flag it
so VarRef's module-var unit-prefixing does not double-apply. }
CVStore.ResolvedOwnerUnit := PreMangledGlobalOwner;
CVStore.ResolvedLhsType := AAssign.ClassVarLhsType; CVStore.ResolvedLhsType := AAssign.ClassVarLhsType;
CVStore.Expr := AAssign.Expr; CVStore.Expr := AAssign.Expr;
EmitAssignment(CVStore); EmitAssignment(CVStore);
@ -13088,7 +13135,7 @@ begin
{ Jumbo set value param: the QBE backend spills it into a local inline { Jumbo set value param: the QBE backend spills it into a local inline
bitmap slot (value semantics), so %_var_X IS the bitmap address — bitmap slot (value semantics), so %_var_X IS the bitmap address —
return it directly without a load. } return it directly without a load. }
Exit(VarRef(TIdentExpr(AExpr).Name, TIdentExpr(AExpr).IsGlobal)); Exit(VarRef(TIdentExpr(AExpr).Name, TIdentExpr(AExpr).IsGlobal, IdentVarOwner(TIdentExpr(AExpr))));
end end
else if (TIdentExpr(AExpr).ParamMode <> pmNone) and else if (TIdentExpr(AExpr).ParamMode <> pmNone) and
(AExpr.ResolvedType <> nil) and (AExpr.ResolvedType <> nil) and
@ -13120,7 +13167,7 @@ begin
IsAggregateAddrType(AExpr.ResolvedType) then IsAggregateAddrType(AExpr.ResolvedType) then
begin begin
{ Aggregate variable — return its storage address directly (no load). } { Aggregate variable — return its storage address directly (no load). }
Exit(VarRef(TIdentExpr(AExpr).Name, TIdentExpr(AExpr).IsGlobal)); Exit(VarRef(TIdentExpr(AExpr).Name, TIdentExpr(AExpr).IsGlobal, IdentVarOwner(TIdentExpr(AExpr))));
end end
else if (AExpr.ResolvedType <> nil) and else if (AExpr.ResolvedType <> nil) and
(AExpr.ResolvedType.Kind = tyInterface) then (AExpr.ResolvedType.Kind = tyInterface) then
@ -13145,14 +13192,14 @@ begin
QType := QbeTypeOf(AExpr.ResolvedType); QType := QbeTypeOf(AExpr.ResolvedType);
case QType of case QType of
'w': EmitLine(Format(' %s =w loadw %s', 'w': EmitLine(Format(' %s =w loadw %s',
[T, VarRef(TIdentExpr(AExpr).Name, TIdentExpr(AExpr).IsGlobal)])); [T, VarRef(TIdentExpr(AExpr).Name, TIdentExpr(AExpr).IsGlobal, IdentVarOwner(TIdentExpr(AExpr)))]));
'd': EmitLine(Format(' %s =d loadd %s', 'd': EmitLine(Format(' %s =d loadd %s',
[T, VarRef(TIdentExpr(AExpr).Name, TIdentExpr(AExpr).IsGlobal)])); [T, VarRef(TIdentExpr(AExpr).Name, TIdentExpr(AExpr).IsGlobal, IdentVarOwner(TIdentExpr(AExpr)))]));
's': EmitLine(Format(' %s =s loads %s', 's': EmitLine(Format(' %s =s loads %s',
[T, VarRef(TIdentExpr(AExpr).Name, TIdentExpr(AExpr).IsGlobal)])); [T, VarRef(TIdentExpr(AExpr).Name, TIdentExpr(AExpr).IsGlobal, IdentVarOwner(TIdentExpr(AExpr)))]));
else else
EmitLine(Format(' %s =l loadl %s', EmitLine(Format(' %s =l loadl %s',
[T, VarRef(TIdentExpr(AExpr).Name, TIdentExpr(AExpr).IsGlobal)])); [T, VarRef(TIdentExpr(AExpr).Name, TIdentExpr(AExpr).IsGlobal, IdentVarOwner(TIdentExpr(AExpr)))]));
end; end;
end; end;
Result := T; Result := T;

View file

@ -122,6 +122,14 @@ type
resolves the name against this specific unit's resolves the name against this specific unit's
exports (per-unit cache) instead of the uses chain, exports (per-unit cache) instead of the uses chain,
so a same-named const in another unit can't shadow it. } so a same-named const in another unit can't shadow it. }
ResolvedOwnerUnit: string; { set by uSemantic — for a module-scope global var,
the owning unit of the symbol actually resolved (the
uses-chain last-wins winner for a bare ref, or the
named unit for a qualified ref). Codegen mangles the
global's storage symbol with this owner so two used
units exporting the same var name reference distinct
slots. Not serialised — recomputed at analysis time;
empty falls back to the bare-name resolution. }
end; end;
TFieldAccessExpr = class(TASTExpr) TFieldAccessExpr = class(TASTExpr)
@ -247,6 +255,10 @@ type
_WeakAssign in place of the strong _WeakAssign in place of the strong
addref/release pattern. } addref/release pattern. }
ImplicitSelfField: TObject; { TFieldInfo — non-nil when LHS is bare field (implicit Self) } ImplicitSelfField: TObject; { TFieldInfo — non-nil when LHS is bare field (implicit Self) }
ResolvedOwnerUnit: string; { set by uSemantic — owning unit of a module-scope global
var target; codegen mangles the store address with it so
same-named cross-unit globals write distinct slots. See
the matching field on TIdentExpr. }
destructor Destroy; override; destructor Destroy; override;
end; end;

View file

@ -504,6 +504,15 @@ type
harvested into the cache. } harvested into the cache. }
function ResolveQualified(const AUnit, AName: string): TSymbol; function ResolveQualified(const AUnit, AName: string): TSymbol;
{ Define a module-scope global (interface var) with cross-unit
last-in-uses-wins semantics, mirroring AnalyseConstDecls. On a
clean Define the symbol is registered in the per-unit cache; on a
cross-unit collision the prior unit's symbol is detached (kept alive
in the per-unit cache so a qualified Unit.Var reference still reaches
it) and this unit's symbol is installed as the flat winner. Same-unit
redeclaration and module-name markers stay hard errors. }
procedure DefineGlobalLastWins(ASym: TSymbol; ALine, ACol: Integer);
{ Record one enum member in the reverse index (FEnumMemberIndex). { Record one enum member in the reverse index (FEnumMemberIndex).
Called once per member as its enum type is analysed. Enum members Called once per member as its enum type is analysed. Enum members
are NOT registered as bare global symbols any more — a bare member are NOT registered as bare global symbols any more — a bare member
@ -1991,12 +2000,7 @@ begin
Sym := TSymbol.Create(VDecl.Names.Strings[J], skVariable, ParType); Sym := TSymbol.Create(VDecl.Names.Strings[J], skVariable, ParType);
Sym.IsGlobal := True; Sym.IsGlobal := True;
Sym.IsThreadVar := VDecl.IsThreadVar; Sym.IsThreadVar := VDecl.IsThreadVar;
if not FTable.Define(Sym) then DefineGlobalLastWins(Sym, VDecl.Line, VDecl.Col);
begin
Sym.Free();
SemanticError(Format('Duplicate identifier ''%s''',
[VDecl.Names.Strings[J]]), VDecl.Line, VDecl.Col);
end;
end; end;
end; end;
@ -5272,6 +5276,44 @@ begin
end; end;
end; end;
procedure TSemanticAnalyser.DefineGlobalLastWins(ASym: TSymbol;
ALine, ACol: Integer);
var
RefSym: TSymbol;
Prev: TSymbol;
begin
if not FTable.Define(ASym) then
begin
RefSym := FTable.CurrentScope.LookupLocal(ASym.Name);
{ A module-name marker (issue #84), a non-unit symbol (builtin /
program-scope, OwningUnit = ''), or a same-unit redeclaration
blocking the Define is a hard error — only a genuine cross-unit
collision (two used units exporting the same name) gets last-wins. }
if (RefSym = nil) or (RefSym.Kind = skModule) or
(RefSym.OwningUnit = '') or
SameText(RefSym.OwningUnit, ASym.OwningUnit) then
begin
ASym.Free();
SemanticError(Format('Duplicate identifier ''%s''', [ASym.Name]),
ALine, ACol);
Exit;
end;
{ Cross-unit collision: last-in-uses wins. Detach the prior unit's
var and stash it in the per-unit cache so a qualified reference
(Unit.Var) can still reach the shadowed slot; install this unit's
var as the flat winner. Mirrors AnalyseConstDecls and RegisterVars. }
Prev := FTable.ExtractLocal(ASym.Name);
if (Prev <> nil) and (Prev.OwningUnit <> '') then
RegisterUnitSymbol(Prev.OwningUnit, Prev);
FTable.Define(ASym);
end;
{ Register every interface var in the per-unit cache keyed by its owning
unit, so a qualified reference resolves against the declaring unit's own
slot regardless of which unit won the bare (flat) slot. }
if ASym.OwningUnit <> '' then
RegisterUnitSymbol(ASym.OwningUnit, ASym);
end;
function TSemanticAnalyser.NewArrayConstLabel(const AName: string): string; function TSemanticAnalyser.NewArrayConstLabel(const AName: string): string;
begin begin
Inc(FArrayConstCounter); Inc(FArrayConstCounter);
@ -8629,6 +8671,18 @@ begin
AAssign.IsWeakLhs := VarSym.IsWeak; AAssign.IsWeakLhs := VarSym.IsWeak;
AAssign.IsGlobal := VarSym.IsGlobal; AAssign.IsGlobal := VarSym.IsGlobal;
AAssign.IsThreadVar := VarSym.IsThreadVar; AAssign.IsThreadVar := VarSym.IsThreadVar;
{ Owning unit of a module-scope global target, so the store address is
mangled with the unit that won resolution — keeps a bare write to a
cross-unit last-wins var hitting the same slot a bare read sees. A static
class/record var's name is its already fully-mangled GlobalEmitName, so flag
it pre-mangled to keep codegen's module-var prefixing from double-applying. }
if VarSym.IsGlobal and (VarSym.Kind = skVariable) then
begin
if VarSym.IsClassVar then
AAssign.ResolvedOwnerUnit := PreMangledGlobalOwner
else
AAssign.ResolvedOwnerUnit := VarSym.OwningUnit;
end;
ResolveDiamond(AAssign.Expr, VarSym.TypeDesc); ResolveDiamond(AAssign.Expr, VarSym.TypeDesc);
@ -11742,6 +11796,22 @@ begin
TIdentExpr(AExpr).ParamMode := pmNone; TIdentExpr(AExpr).ParamMode := pmNone;
TIdentExpr(AExpr).IsGlobal := Sym.IsGlobal; TIdentExpr(AExpr).IsGlobal := Sym.IsGlobal;
TIdentExpr(AExpr).IsThreadVar := Sym.IsThreadVar; TIdentExpr(AExpr).IsThreadVar := Sym.IsThreadVar;
{ Record the owning unit of a module-scope global so codegen mangles
the storage symbol with the unit that actually won resolution (the
uses-chain last-wins winner for a bare ref, the named unit for a
qualified ref). Without this, two used units exporting the same var
name both resolve to one slot. }
if Sym.IsGlobal and (Sym.Kind = skVariable) then
begin
{ A static class/record var's Name was rewritten above to its already
fully-mangled GlobalEmitName (Unit_Class_Field); flag it so codegen's
module-var prefixing does not double-apply. Plain module globals carry
their owning unit for owner-based mangling. }
if Sym.IsClassVar then
TIdentExpr(AExpr).ResolvedOwnerUnit := PreMangledGlobalOwner
else
TIdentExpr(AExpr).ResolvedOwnerUnit := Sym.OwningUnit;
end;
if Sym.Kind = skConstant then if Sym.Kind = skConstant then
begin begin
TIdentExpr(AExpr).IsConstant := True; TIdentExpr(AExpr).IsConstant := True;

View file

@ -928,7 +928,14 @@ begin
Sym.IsThreadVar := Entry.IsThreadVar; Sym.IsThreadVar := Entry.IsThreadVar;
Sym.OwningUnit := AIface.Name; Sym.OwningUnit := AIface.Name;
if not ATable.Define(Sym) then if not ATable.Define(Sym) then
Sym.Free(); begin
{ Cross-unit collision (last-wins): detach the slot the table already
holds — kept alive via the per-unit cache (RegisterUnitIface) for
qualified access — and store this later unit's symbol instead.
Mirrors RegisterConsts and the source-path DefineGlobalLastWins. }
ATable.ExtractLocal(Entry.Name);
ATable.Define(Sym);
end;
end; end;
end; end;

View file

@ -711,6 +711,14 @@ function IsUnmangledUnit(const AUnitName: string): Boolean;
agree on the QBE global name. } agree on the QBE global name. }
function MangleUnitPrefix(const AUnitName: string): string; function MangleUnitPrefix(const AUnitName: string): string;
const
{ Sentinel owning-unit value meaning "this global's emit name is ALREADY a
fully-mangled symbol — codegen must add no further unit prefix". Used for
static class/record vars, whose label (Unit_Class_Field) is class-qualified
and complete; the module-var prefixing must not double-apply. The leading
control byte can never collide with a real unit name. }
PreMangledGlobalOwner = #1'premangled';
implementation implementation
uses uses

View file

@ -49,6 +49,15 @@ type
unit-prefix mangling on module-scope globals they no longer collide at unit-prefix mangling on module-scope globals they no longer collide at
link, so a program using both links and runs. } link, so a program using both links and runs. }
procedure TestRun_SameNamedGlobals_NoLinkCollision; procedure TestRun_SameNamedGlobals_NoLinkCollision;
{ Cross-unit interface VAR shadowing: two used units export the same var
name; the unit later in `uses` wins a bare reference (last-in-uses), and
reversing the order flips the winner — mirrors the const last-wins rule. }
procedure TestRun_CrossUnitVar_LastWins;
procedure TestRun_CrossUnitVar_LastWins_Reversed;
{ Unit-qualified VAR disambiguation: 'Unit.V' references that specific
unit's own slot (distinct storage), independent of the bare last-wins
winner, so both values are readable side by side. }
procedure TestRun_CrossUnitVar_QualifiedDisambig;
end; end;
implementation implementation
@ -370,6 +379,82 @@ begin
AssertEquals('GetOne + GetTwo', '33' + LE, Output); AssertEquals('GetOne + GetTwo', '33' + LE, Output);
end; end;
const
UVA_Var = '''
unit uva;
interface
var V: Integer = 7;
implementation
end.
''';
UVB_Var = '''
unit uvb;
interface
var V: Integer = 9;
implementation
end.
''';
procedure TE2EUsesChainTests.TestRun_CrossUnitVar_LastWins;
const
DrvSrc = '''
program P;
uses uva, uvb;
begin
WriteLn(V)
end.
''';
var Output: string; RCode: Integer;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
{ Both uva and uvb export `V`; uvb is later in `uses`, so bare V = 9.
The shadowed uva.V keeps its own slot (no link collision). }
AssertTrue('compile+link+run',
CompileAndRunWithUnits(UVA_Var, UVB_Var, DrvSrc, Output, RCode));
AssertEquals('exit 0', 0, RCode);
AssertEquals('last-in-uses (uvb) wins', '9' + LE, Output);
end;
procedure TE2EUsesChainTests.TestRun_CrossUnitVar_LastWins_Reversed;
const
DrvSrc = '''
program P;
uses uvb, uva;
begin
WriteLn(V)
end.
''';
var Output: string; RCode: Integer;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
{ Reversed `uses` order: uva is now later, so bare V = 7. }
AssertTrue('compile+link+run',
CompileAndRunWithUnits(UVA_Var, UVB_Var, DrvSrc, Output, RCode));
AssertEquals('exit 0', 0, RCode);
AssertEquals('last-in-uses (uva) wins', '7' + LE, Output);
end;
procedure TE2EUsesChainTests.TestRun_CrossUnitVar_QualifiedDisambig;
const
DrvSrc = '''
program P;
uses uva, uvb;
begin
WriteLn(uva.V);
WriteLn(uvb.V)
end.
''';
var Output: string; RCode: Integer;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
{ Qualified references pick each unit's own slot (uva.V = 7, uvb.V = 9)
regardless of the bare last-wins rule — distinct storage per unit. }
AssertTrue('compile+link+run',
CompileAndRunWithUnits(UVA_Var, UVB_Var, DrvSrc, Output, RCode));
AssertEquals('exit 0', 0, RCode);
AssertEquals('uva.V then uvb.V', '7' + LE + '9' + LE, Output);
end;
initialization initialization
RegisterTest(TE2EUsesChainTests); RegisterTest(TE2EUsesChainTests);

View file

@ -56,6 +56,10 @@ type
{ ------------------------------------------------------------------ } { ------------------------------------------------------------------ }
procedure TestCodegen_TwoFileCompile_UnitFuncExported; procedure TestCodegen_TwoFileCompile_UnitFuncExported;
procedure TestCodegen_TwoFileCompile_MainPresent; procedure TestCodegen_TwoFileCompile_MainPresent;
{ Two used units export the same global var name; a qualified reference
'Unit.V' must emit each unit's own owner-prefixed storage symbol so the
two refer to distinct slots rather than colliding on a bare '$V'. }
procedure TestCodegen_CrossUnitQualifiedVar_DistinctSymbols;
end; end;
implementation implementation
@ -733,6 +737,67 @@ begin
end; end;
end; end;
procedure TMultifileTests.TestCodegen_CrossUnitQualifiedVar_DistinctSymbols;
const
UnitA =
'''
unit uva;
interface
var V: Integer = 7;
implementation
end.
''';
UnitB =
'''
unit uvb;
interface
var V: Integer = 9;
implementation
end.
''';
ProgSrc =
'''
program TestP;
uses uva, uvb;
var r: Integer;
begin
r := uva.V;
r := uvb.V
end.
''';
var
UA, UB: TUnit;
Prog: TProgram;
SA: TSemanticAnalyser;
CG: TCodeGenQBE;
IR: string;
begin
UA := ParseUnitSrc(UnitA);
UB := ParseUnitSrc(UnitB);
Prog := ParseProg(ProgSrc);
SA := TSemanticAnalyser.Create();
CG := TCodeGenQBE.Create();
try
SA.AnalyseUnitForExport(UA);
SA.AnalyseUnitForExport(UB);
SA.Analyse(Prog);
CG.AppendUnit(UA);
CG.AppendUnit(UB);
CG.AppendProgram(Prog);
IR := CG.GetOutput();
{ Each unit defines its own owner-prefixed slot, and the qualified loads
reference both — proving the two same-named vars do not collapse. }
AssertTrue('uva.V slot referenced', Pos('$uva_V', IR) > 0);
AssertTrue('uvb.V slot referenced', Pos('$uvb_V', IR) > 0);
finally
CG.Free();
SA.Free();
Prog.Free();
UB.Free();
UA.Free();
end;
end;
initialization initialization
RegisterTest(TMultifileTests); RegisterTest(TMultifileTests);
end. end.