fix(incremental): make cached-interface rebuild lossless and coherent
Incremental compilation (per-unit .o + embedded .bif, the default) produced silently corrupt binaries when rebuilding into a populated --unit-cache: the clean build worked, but any rebuild that loaded a unit from its cached .bif instead of source dropped or mishandled state the in-memory path carried. Root cause is uniform across every symptom: the .bif round-trip was not a complete projection of the unit interface, and the cached-load path in the loader/driver diverged from the source-load path. .bif format bumped to version 3 (IFACE_VERSION; CompilerId +bif tag) so stale caches are cleanly rejected and recompiled from source. Fixes (each was a field/decision present on the source path, absent on the cached path): - Loader: a unit's implementation-section `uses` were never recorded in the .bif, so on rebuild the impl-only dependency's object was dropped from the link (undefined symbol). Record ImplUsedUnits in the .bif and collect those objects for LINK ONLY (CollectLinkOnlyObject) — not semantic import, which would break the leaf-first iface import on interface/impl-use cycles. - Native codegen: a global var owned by a cached unit was re-defined by the recompiled top program (`multiple definition of GTarget`). The program now references imported-unit globals as external (FImportedUnits / IsImportedGlobal gate in EmitDataSection); NoteDepInitUnit records every imported unit. - Unit initialization: <Unit>_init calls for cached deps were never emitted, so initialization sections silently never ran. Record HasInitialization in the .bif and note prebuilt-iface inits in dependency order at program codegen. - SynthesiseMethodDecl dropped VTableSlot/IsVirtual/IsOverride from cached method imports, degrading virtual dispatch to direct calls into the abstract base (SIGABRT at run time). Propagate them. - Generic-instance field/param types now resolve via the analyser's generic- aware resolver on import (ResolveImportedTypeName); generic instantiation during import no longer nil-derefs FProg/FCurrentUnit (pending-instance flush). - .bif round-trip completeness: serialise block-local var decls, Exit(value) return values, parameter default values, free-routine ResolvedQbeName, generic-class template properties, and fix implements-interface name stripping (strip to the last dot, not the first). Adds TestIncrementalRebuild_ImplOnlyUses_LinksDependency (e2e) and updates the magic/version assertion. Full suite OK (3722 tests); QBE, native, and internal-assembler fixpoints pass; clean and warm-cache rebuilds both produce correct binaries.
This commit is contained in:
parent
baf5079d6d
commit
915077d7c8
|
|
@ -860,24 +860,45 @@ begin
|
|||
CG.AppendUnit(TUnit(Units.Items[I]));
|
||||
CG.AppendUnit(TopUnit);
|
||||
end
|
||||
else if (Units <> nil) and (Units.Count > 0) then
|
||||
else if ((Units <> nil) and (Units.Count > 0)) or
|
||||
((Loader <> nil) and (Loader.PrebuiltIfaces.Count > 0)) then
|
||||
begin
|
||||
CG.SetSymbolTable(Prog.SymbolTable);
|
||||
{ Prebuilt (cached) ifaces are ALWAYS compiled into their own objects —
|
||||
their bodies are never appended here, regardless of SkipDepCodegen.
|
||||
So they must be noted as imported units unconditionally: the program
|
||||
startup must call each one's <Unit>_init (if it has an initialization
|
||||
section), and the backend must treat globals owned by these units as
|
||||
external references rather than re-defining them (otherwise the link
|
||||
step reports a multiple definition — the cached .o owns the symbol).
|
||||
|
||||
They are loaded leaf-first and any source units depend on them, so
|
||||
their inits must run before the source deps' inits. On a full rebuild
|
||||
every dep is cached, so this is the only loop that fires. }
|
||||
if Loader <> nil then
|
||||
for I := 0 to Loader.PrebuiltIfaces.Count - 1 do
|
||||
CG.NoteDepInitUnit(
|
||||
TUnitInterface(Loader.PrebuiltIfaces.Items[I]).Name,
|
||||
TUnitInterface(Loader.PrebuiltIfaces.Items[I]).HasInitialization);
|
||||
if not SkipDepCodegen then
|
||||
for I := 0 to Units.Count - 1 do
|
||||
CG.AppendUnit(TUnit(Units.Items[I]))
|
||||
begin
|
||||
{ Source-loaded deps are compiled inline into this object — append
|
||||
their bodies; they are not imported, so do not note them. }
|
||||
if Units <> nil then
|
||||
for I := 0 to Units.Count - 1 do
|
||||
CG.AppendUnit(TUnit(Units.Items[I]));
|
||||
end
|
||||
else
|
||||
{ Incremental / separate-compilation: dep bodies are compiled into
|
||||
their own objects, so AppendUnit is skipped here. But the program
|
||||
startup must still call each dep unit's <Unit>_init if it has an
|
||||
initialization section, in dependency order — otherwise unit
|
||||
initialization silently never runs. Register the init-unit names
|
||||
so AppendProgram's $main emits the calls (they resolve against the
|
||||
<Unit>_init symbols exported by the per-unit objects). }
|
||||
for I := 0 to Units.Count - 1 do
|
||||
CG.NoteDepInitUnit(TUnit(Units.Items[I]).Name,
|
||||
(TUnit(Units.Items[I]).InitStmts <> nil) and
|
||||
(TUnit(Units.Items[I]).InitStmts.Count > 0));
|
||||
begin
|
||||
{ Incremental / separate-compilation: source dep bodies are compiled
|
||||
into their own objects (skipped here). Note each so $main calls its
|
||||
<Unit>_init and the backend references its globals externally. }
|
||||
if Units <> nil then
|
||||
for I := 0 to Units.Count - 1 do
|
||||
CG.NoteDepInitUnit(TUnit(Units.Items[I]).Name,
|
||||
(TUnit(Units.Items[I]).InitStmts <> nil) and
|
||||
(TUnit(Units.Items[I]).InitStmts.Count > 0));
|
||||
end;
|
||||
CG.AppendProgram(Prog);
|
||||
end
|
||||
else
|
||||
|
|
@ -934,8 +955,14 @@ begin
|
|||
Loader.Free wipes them — needed by the link-step dispatch
|
||||
below the finally block. }
|
||||
if Loader <> nil then
|
||||
begin
|
||||
for I := 0 to Loader.PrebuiltObjectPaths.Count - 1 do
|
||||
PrebuiltObjPaths.Add(Loader.PrebuiltObjectPaths.Strings[I]);
|
||||
{ Impl-only dependency objects: linked but not semantically imported. }
|
||||
for I := 0 to Loader.LinkOnlyObjects.Count - 1 do
|
||||
if PrebuiltObjPaths.IndexOf(Loader.LinkOnlyObjects.Strings[I]) < 0 then
|
||||
PrebuiltObjPaths.Add(Loader.LinkOnlyObjects.Strings[I]);
|
||||
end;
|
||||
Units.Free();
|
||||
Loader.Free();
|
||||
SearchPaths.Free();
|
||||
|
|
|
|||
|
|
@ -173,6 +173,14 @@ type
|
|||
AppendUnit order. $main calls <Unit>_init for each, in order, after
|
||||
_SetArgs — mirrors the QBE backend's EmitMainHeader loop. }
|
||||
FUnitInitNames: TStringList;
|
||||
{ Names of dependency units imported from cached .bif/.o in incremental /
|
||||
separate-compilation mode (SkipDepCodegen). A global whose owning unit
|
||||
is in this set is DEFINED by that unit's own object — this program/unit
|
||||
object must only REFERENCE it (no .globl/label definition), otherwise the
|
||||
linker reports a multiple definition. Recorded by NoteDepInitUnit for
|
||||
every imported unit regardless of whether it has an initialization
|
||||
section. Stores raw (unmangled) unit names, matching TSymbol.OwningUnit. }
|
||||
FImportedUnits: TStringList;
|
||||
FCurrentUnitName: string;
|
||||
FProgramName: string; { set by EmitProgram — program-scope classes keep
|
||||
bare symbol names (no unit prefix), matching
|
||||
|
|
@ -192,6 +200,10 @@ type
|
|||
procedure MarkWeakGlobal(const AName: string);
|
||||
function IsThreadVarGlobal(const AName: string): Boolean;
|
||||
function IsWeakGlobal(const AName: string): Boolean;
|
||||
{ True when the global's owning unit was imported from a cached .bif/.o
|
||||
(SkipDepCodegen): the cached object DEFINES the global, so this object
|
||||
must reference it only — emitting a definition here would clash at link. }
|
||||
function IsImportedGlobal(const AName: string): Boolean;
|
||||
{ The static type of a frame-local slot, or nil if AName is not a local. }
|
||||
function LocalType(const AName: string): TTypeDesc;
|
||||
{ The static type of a program global, or nil if AName is not registered. }
|
||||
|
|
@ -794,6 +806,10 @@ begin
|
|||
FProgExcFrameCount := 0;
|
||||
FSystemDefsEmitted := False;
|
||||
FUnitInitNames := TStringList.Create();
|
||||
FImportedUnits := TStringList.Create();
|
||||
FImportedUnits.CaseSensitive := False;
|
||||
FImportedUnits.Sorted := True;
|
||||
FImportedUnits.Duplicates := dupIgnore;
|
||||
FConstArgUnsafe := TStringList.Create();
|
||||
FConstArgUnsafe.CaseSensitive := True;
|
||||
FConstArgUnsafe.Sorted := True;
|
||||
|
|
@ -804,6 +820,7 @@ destructor TX86_64Backend.Destroy;
|
|||
begin
|
||||
Self.ClearFrame();
|
||||
FConstArgUnsafe.Free();
|
||||
FImportedUnits.Free();
|
||||
FUnitInitNames.Free();
|
||||
FOALFrames.Free();
|
||||
FFinallyStack.Free();
|
||||
|
|
@ -869,6 +886,24 @@ begin
|
|||
Result := nil;
|
||||
end;
|
||||
|
||||
function TX86_64Backend.IsImportedGlobal(const AName: string): Boolean;
|
||||
var
|
||||
Sym: TSymbol;
|
||||
begin
|
||||
{ A global whose owning unit was imported from a cached .bif/.o (incremental /
|
||||
separate compilation) is DEFINED by that unit's own object. This object must
|
||||
only reference it (no .globl/label here), or the linker reports a multiple
|
||||
definition. References (leaq Name(%rip)) resolve against the cached object's
|
||||
exported symbol. }
|
||||
Result := False;
|
||||
if FImportedUnits.Count = 0 then Exit;
|
||||
if FSymTable = nil then Exit;
|
||||
Sym := FSymTable.Lookup(AName);
|
||||
if Sym = nil then Exit;
|
||||
if Sym.OwningUnit = '' then Exit;
|
||||
Result := FImportedUnits.IndexOf(Sym.OwningUnit) >= 0;
|
||||
end;
|
||||
|
||||
procedure TX86_64Backend.EmitDataSection;
|
||||
var
|
||||
I, Sz: Integer;
|
||||
|
|
@ -888,6 +923,8 @@ begin
|
|||
HasTbss := False;
|
||||
for I := 0 to FDataGlobals.Count - 1 do
|
||||
begin
|
||||
if Self.IsImportedGlobal(FDataGlobals.Keys[I]) then
|
||||
Continue;
|
||||
if Self.IsThreadVarGlobal(FDataGlobals.Keys[I]) then
|
||||
HasTbss := True
|
||||
else
|
||||
|
|
@ -901,6 +938,7 @@ begin
|
|||
for I := 0 to FDataGlobals.Count - 1 do
|
||||
begin
|
||||
Name := FDataGlobals.Keys[I];
|
||||
if Self.IsImportedGlobal(Name) then Continue;
|
||||
IsTls := Self.IsThreadVarGlobal(Name);
|
||||
if IsTls then Continue;
|
||||
{ Initialised global (var G: T = value): emit the folded value. threadvars
|
||||
|
|
@ -1008,6 +1046,7 @@ begin
|
|||
for I := 0 to FDataGlobals.Count - 1 do
|
||||
begin
|
||||
Name := FDataGlobals.Keys[I];
|
||||
if Self.IsImportedGlobal(Name) then Continue;
|
||||
if not Self.IsThreadVarGlobal(Name) then Continue;
|
||||
if (Self.GlobalType(Name) <> nil) and
|
||||
(IsJumboSet(Self.GlobalType(Name)) or
|
||||
|
|
@ -15963,6 +16002,10 @@ begin
|
|||
matches EmitUnit's FUnitInitNames.Add(NativeMangle(AUnit.Name)). }
|
||||
if AHasInit then
|
||||
FUnitInitNames.Add(NativeMangle(AUnitName));
|
||||
{ Record the imported unit (raw name, matching TSymbol.OwningUnit) so
|
||||
EmitDataSection emits globals owned by it as references, not definitions —
|
||||
the cached object owns the definition. }
|
||||
FImportedUnits.Add(AUnitName);
|
||||
end;
|
||||
|
||||
procedure TX86_64Backend.FinalizeEmit;
|
||||
|
|
|
|||
|
|
@ -773,6 +773,11 @@ type
|
|||
DefaultValue: TASTExpr; { owned — non-nil when the param has a default value
|
||||
('= expr' after the type); restricted to literal forms
|
||||
(int/float/string/nil) and named-constant idents. }
|
||||
HasDefault: Boolean; { set when this param has a default value but the value
|
||||
AST itself is unavailable — e.g. a method imported
|
||||
from a cached .bif, which records only that a default
|
||||
exists (so MinArity/overload resolution accept fewer
|
||||
args) without serialising the default expression. }
|
||||
destructor Destroy; override;
|
||||
end;
|
||||
|
||||
|
|
@ -2313,6 +2318,7 @@ begin
|
|||
Result.IsOutParam := ASrc.IsOutParam;
|
||||
Result.IsOpenArray := ASrc.IsOpenArray;
|
||||
Result.DefaultValue := CloneExpr(ASrc.DefaultValue);
|
||||
Result.HasDefault := ASrc.HasDefault;
|
||||
end;
|
||||
|
||||
function CloneMethodDecl(ASrc: TMethodDecl): TMethodDecl;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ unit uCompilerId;
|
|||
interface
|
||||
|
||||
const
|
||||
COMPILER_ID = 'blaise-0.11.0-SNAPSHOT+bif1';
|
||||
COMPILER_ID = 'blaise-0.12.0-SNAPSHOT+bif2';
|
||||
|
||||
implementation
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,16 @@ type
|
|||
AddGroupEntry is also added here, which retains it for the analyser's
|
||||
lifetime; the destructor frees this list, releasing the groups. }
|
||||
FGroupKeepAlive: TObjectList;
|
||||
{ Generic instances created while no program/unit is being analysed —
|
||||
i.e. when ImportUnitInterface triggers FindTypeOrInstantiate to resolve
|
||||
a cached iface's generic-instance field type (e.g. 'TList<TFoo>') before
|
||||
Analyse/AnalyseUnit has set FProg/FCurrentUnit. Holding them here (non-
|
||||
owning) avoids a nil-deref on FProg.GenericInstances.Add; FlushPending
|
||||
GenericInstances moves them into the real owner once analysis begins so
|
||||
codegen still emits the instance's methods. }
|
||||
FPendingGenericInstances: TObjectList; { TGenericInstance — not owned }
|
||||
FPendingGenericRecordInstances: TObjectList; { TGenericRecordInstance — not owned }
|
||||
FPendingGenericIntfInstances: TObjectList; { TGenericInterfaceInstance — not owned }
|
||||
FGenericFuncTemplates: TStringList; { base name → TMethodDecl template (not owned) }
|
||||
FGenericMethodTemplates: TStringList; { 'OwnerType.Method' → TMethodDecl template (not owned) — generic methods with method-level <T> }
|
||||
FLoopDepth: Integer; { depth of enclosing while/for — Break only legal if > 0 }
|
||||
|
|
@ -186,6 +196,10 @@ type
|
|||
procedure LinkClassMethodImpls(ABlock: TBlock);
|
||||
procedure LinkGenericClassMethodImpls(ABlock: TBlock);
|
||||
procedure RepairEarlyGenericInstances;
|
||||
{ Move any generic instances parked at import time (FProg/FCurrentUnit
|
||||
both nil) into the now-current program or unit, so codegen emits their
|
||||
method bodies. Called at the start of Analyse / AnalyseUnit. }
|
||||
procedure FlushPendingGenericInstances;
|
||||
procedure AnalyseMethodBodies(ABlock: TBlock);
|
||||
procedure AnalyseMethodDecl(AMethod: TMethodDecl; AClassType: TRecordTypeDesc);
|
||||
procedure AnalyseStandaloneDecls(ABlock: TBlock);
|
||||
|
|
@ -375,6 +389,15 @@ type
|
|||
procedure RegisterImportedMethod(const ATypeName: string;
|
||||
ADecl: TMethodDecl);
|
||||
|
||||
{ Public entry to the on-demand type resolver used by the source
|
||||
compile path. uSemanticImport calls this to resolve a cached
|
||||
iface's type-name string (e.g. a field type 'TList<TFoo>' or an
|
||||
'array of TFoo') that is not a plain registered symbol — the
|
||||
import path otherwise has no way to instantiate generics, so a
|
||||
generic-instance field type from a .bif fails to resolve. Mirrors
|
||||
what AnalyseUnitForExport does for the same field in source form. }
|
||||
function ResolveImportedTypeName(const AName: string): TTypeDesc;
|
||||
|
||||
{ Register a TUnitInterface in FUnitIfaces, keyed by AIface.Name.
|
||||
AIface is NOT owned — caller (Blaise.pas) retains lifetime.
|
||||
Subsequent registrations of the same name replace the entry
|
||||
|
|
@ -524,11 +547,17 @@ begin
|
|||
FUnitSymbols.CaseSensitive := False;
|
||||
FActiveTypeParams := TStringList.Create();
|
||||
FActiveTypeParams.CaseSensitive := False;
|
||||
FPendingGenericInstances := TObjectList.Create(False);
|
||||
FPendingGenericRecordInstances := TObjectList.Create(False);
|
||||
FPendingGenericIntfInstances := TObjectList.Create(False);
|
||||
FLoopDepth := 0;
|
||||
end;
|
||||
|
||||
destructor TSemanticAnalyser.Destroy;
|
||||
begin
|
||||
FPendingGenericIntfInstances.Free();
|
||||
FPendingGenericRecordInstances.Free();
|
||||
FPendingGenericInstances.Free();
|
||||
FActiveTypeParams.Free();
|
||||
FUnitSymbols.Free();
|
||||
FUnitIfaces.Free();
|
||||
|
|
@ -1039,6 +1068,7 @@ var
|
|||
I: Integer;
|
||||
begin
|
||||
FProg := AProg;
|
||||
FlushPendingGenericInstances;
|
||||
FCurrentUnitName := AProg.Name;
|
||||
BuildUsesChain(AProg.UsedUnits);
|
||||
FTable.UsesChainProvider := Self;
|
||||
|
|
@ -1072,6 +1102,7 @@ var
|
|||
begin
|
||||
FCurrentUnitName := AUnit.Name;
|
||||
FCurrentUnit := AUnit;
|
||||
FlushPendingGenericInstances;
|
||||
FTable.PushScope();
|
||||
try
|
||||
{ The unit's own name and the names of directly used units are
|
||||
|
|
@ -1396,6 +1427,7 @@ var
|
|||
begin
|
||||
FCurrentUnitName := AUnit.Name;
|
||||
FCurrentUnit := AUnit;
|
||||
FlushPendingGenericInstances;
|
||||
BuildUsesChain(AUnit.UsedUnits);
|
||||
FTable.UsesChainProvider := Self;
|
||||
{ Auto-tag every global Define within this unit's analysis with the
|
||||
|
|
@ -1704,6 +1736,11 @@ begin
|
|||
AddGroupEntry(FMethodGroups, ATypeName + '.' + ADecl.Name, ADecl);
|
||||
end;
|
||||
|
||||
function TSemanticAnalyser.ResolveImportedTypeName(const AName: string): TTypeDesc;
|
||||
begin
|
||||
Result := Self.FindTypeOrInstantiate(AName);
|
||||
end;
|
||||
|
||||
procedure TSemanticAnalyser.RegisterUnitIface(AIface: TUnitInterface);
|
||||
var
|
||||
Idx, I: Integer;
|
||||
|
|
@ -2005,6 +2042,39 @@ begin
|
|||
end;
|
||||
end;
|
||||
|
||||
procedure TSemanticAnalyser.FlushPendingGenericInstances;
|
||||
var
|
||||
I: Integer;
|
||||
begin
|
||||
if FCurrentUnit <> nil then
|
||||
begin
|
||||
for I := 0 to FPendingGenericInstances.Count - 1 do
|
||||
FCurrentUnit.GenericInstances.Add(FPendingGenericInstances.Items[I]);
|
||||
for I := 0 to FPendingGenericRecordInstances.Count - 1 do
|
||||
FCurrentUnit.GenericRecordInstances.Add(
|
||||
FPendingGenericRecordInstances.Items[I]);
|
||||
for I := 0 to FPendingGenericIntfInstances.Count - 1 do
|
||||
FCurrentUnit.GenericIntfInstances.Add(
|
||||
FPendingGenericIntfInstances.Items[I]);
|
||||
end
|
||||
else if FProg <> nil then
|
||||
begin
|
||||
for I := 0 to FPendingGenericInstances.Count - 1 do
|
||||
FProg.GenericInstances.Add(FPendingGenericInstances.Items[I]);
|
||||
for I := 0 to FPendingGenericRecordInstances.Count - 1 do
|
||||
FProg.GenericRecordInstances.Add(
|
||||
FPendingGenericRecordInstances.Items[I]);
|
||||
for I := 0 to FPendingGenericIntfInstances.Count - 1 do
|
||||
FProg.GenericIntfInstances.Add(
|
||||
FPendingGenericIntfInstances.Items[I]);
|
||||
end
|
||||
else
|
||||
Exit;
|
||||
FPendingGenericInstances.Clear();
|
||||
FPendingGenericRecordInstances.Clear();
|
||||
FPendingGenericIntfInstances.Clear();
|
||||
end;
|
||||
|
||||
procedure TSemanticAnalyser.RepairEarlyGenericInstances;
|
||||
var
|
||||
Instances: TObjectList;
|
||||
|
|
@ -2685,8 +2755,16 @@ var
|
|||
ParentSym: TSymbol;
|
||||
ParentRT: TRecordTypeDesc;
|
||||
FldInfo: TFieldInfo;
|
||||
DeferBodies: Boolean;
|
||||
begin
|
||||
Result := nil;
|
||||
{ No program/unit context yet (ImportUnitInterface resolving a cached
|
||||
iface's generic-instance field type). Build the type descriptor +
|
||||
fields now, but leave method bodies nil and park the instance — once
|
||||
Analyse/AnalyseUnit sets a context, RepairEarlyGenericInstances clones
|
||||
and analyses the bodies. Analysing them here would deref nil FProg/
|
||||
FCurrentUnit deep in the body walk. }
|
||||
DeferBodies := (FProg = nil) and (FCurrentUnit = nil);
|
||||
|
||||
{ Parse 'BaseName<Arg1,Arg2>' }
|
||||
BracPos := StrPos('<', ATypeName);
|
||||
|
|
@ -2777,13 +2855,16 @@ begin
|
|||
NewMDecl.OwnerTypeName := ATypeName;
|
||||
NewMDecl.IsVirtual := MDecl.IsVirtual;
|
||||
NewMDecl.IsOverride := MDecl.IsOverride;
|
||||
if MDecl.Body <> nil then
|
||||
if (MDecl.Body <> nil) and (not DeferBodies) then
|
||||
begin
|
||||
NewMDecl.Body := CloneBlock(MDecl.Body);
|
||||
NewMDecl.OwnBody := True;
|
||||
end
|
||||
else
|
||||
begin
|
||||
{ Body left nil — either the template is signature-only, or we are
|
||||
deferring body analysis to RepairEarlyGenericInstances (which only
|
||||
repairs methods whose Body is still nil). }
|
||||
NewMDecl.Body := nil;
|
||||
NewMDecl.OwnBody := False;
|
||||
end;
|
||||
|
|
@ -3028,8 +3109,13 @@ begin
|
|||
GI.DefUnitName := Templ.DefUnitName;
|
||||
if FCurrentUnit <> nil then
|
||||
FCurrentUnit.GenericInstances.Add(GI)
|
||||
else if FProg <> nil then
|
||||
FProg.GenericInstances.Add(GI)
|
||||
else
|
||||
FProg.GenericInstances.Add(GI);
|
||||
{ Instantiated during ImportUnitInterface, before Analyse/AnalyseUnit
|
||||
has set a program/unit owner. Park it; FlushPendingGenericInstances
|
||||
hands it to the real owner once analysis starts. }
|
||||
FPendingGenericInstances.Add(GI);
|
||||
|
||||
Result := RT;
|
||||
finally
|
||||
|
|
@ -3060,8 +3146,13 @@ var
|
|||
RT: TRecordTypeDesc;
|
||||
GRI: TGenericRecordInstance;
|
||||
ConcrType: TTypeDesc;
|
||||
DeferBodies: Boolean;
|
||||
begin
|
||||
Result := nil;
|
||||
{ See InstantiateGeneric — defer body clone+analysis when there is no
|
||||
program/unit context (import-time resolution of a cached field type).
|
||||
RepairEarlyGenericInstances re-clones and analyses the bodies later. }
|
||||
DeferBodies := (FProg = nil) and (FCurrentUnit = nil);
|
||||
|
||||
BracPos := StrPos('<', ATypeName);
|
||||
if BracPos < 0 then Exit;
|
||||
|
|
@ -3120,13 +3211,15 @@ begin
|
|||
NewMDecl.Name := MDecl.Name;
|
||||
NewMDecl.OwnerTypeName := ATypeName;
|
||||
NewMDecl.IsRecordMethod := True;
|
||||
if MDecl.Body <> nil then
|
||||
if (MDecl.Body <> nil) and (not DeferBodies) then
|
||||
begin
|
||||
NewMDecl.Body := CloneBlock(MDecl.Body);
|
||||
NewMDecl.OwnBody := True;
|
||||
end
|
||||
else
|
||||
begin
|
||||
{ Body left nil — signature-only template, or deferred to
|
||||
RepairEarlyGenericInstances (import-time, no program context). }
|
||||
NewMDecl.Body := nil;
|
||||
NewMDecl.OwnBody := False;
|
||||
end;
|
||||
|
|
@ -3228,8 +3321,10 @@ begin
|
|||
GRI.TypeDesc := RT;
|
||||
if FCurrentUnit <> nil then
|
||||
FCurrentUnit.GenericRecordInstances.Add(GRI)
|
||||
else if FProg <> nil then
|
||||
FProg.GenericRecordInstances.Add(GRI)
|
||||
else
|
||||
FProg.GenericRecordInstances.Add(GRI);
|
||||
FPendingGenericRecordInstances.Add(GRI);
|
||||
|
||||
Result := RT;
|
||||
finally
|
||||
|
|
@ -3329,8 +3424,10 @@ begin
|
|||
GII.TypeDesc := Result;
|
||||
if FCurrentUnit <> nil then
|
||||
FCurrentUnit.GenericIntfInstances.Add(GII)
|
||||
else if FProg <> nil then
|
||||
FProg.GenericIntfInstances.Add(GII)
|
||||
else
|
||||
FProg.GenericIntfInstances.Add(GII);
|
||||
FPendingGenericIntfInstances.Add(GII);
|
||||
finally
|
||||
Args.Free();
|
||||
end;
|
||||
|
|
@ -7935,7 +8032,8 @@ var
|
|||
I: Integer;
|
||||
begin
|
||||
for I := 0 to ADecl.Params.Count - 1 do
|
||||
if TMethodParam(ADecl.Params.Items[I]).DefaultValue <> nil then
|
||||
if (TMethodParam(ADecl.Params.Items[I]).DefaultValue <> nil) or
|
||||
(TMethodParam(ADecl.Params.Items[I]).HasDefault) then
|
||||
begin
|
||||
Exit(I);
|
||||
end;
|
||||
|
|
@ -7955,10 +8053,13 @@ begin
|
|||
PSrc := TMethodParam(AFrom.Params.Items[I]);
|
||||
PDst := TMethodParam(AInto.Params.Items[I]);
|
||||
if (PSrc.DefaultValue <> nil) and (PDst.DefaultValue = nil) then
|
||||
begin
|
||||
PDst.DefaultValue := PSrc.DefaultValue;
|
||||
PSrc.DefaultValue := nil; { ownership transferred }
|
||||
end;
|
||||
{ Clone rather than move: the source is the interface forward decl,
|
||||
which uSemanticExport later serialises into the unit's .bif. Moving
|
||||
(nil-ing PSrc) stripped the default from the exported signature, so a
|
||||
caller in a SEPARATE compilation that loaded the unit from its cached
|
||||
.bif could not omit the defaulted trailing arguments. Both decls now
|
||||
keep their own copy. }
|
||||
PDst.DefaultValue := CloneExpr(PSrc.DefaultValue);
|
||||
end;
|
||||
end;
|
||||
|
||||
|
|
|
|||
|
|
@ -465,9 +465,14 @@ procedure CopyUsedUnits(AUnit: TUnit; AIface: TUnitInterface);
|
|||
var
|
||||
I: Integer;
|
||||
begin
|
||||
if AUnit.UsedUnits = nil then Exit;
|
||||
for I := 0 to AUnit.UsedUnits.Count - 1 do
|
||||
AIface.UsedUnits.Add(AUnit.UsedUnits.Strings[I]);
|
||||
if AUnit.UsedUnits <> nil then
|
||||
for I := 0 to AUnit.UsedUnits.Count - 1 do
|
||||
AIface.UsedUnits.Add(AUnit.UsedUnits.Strings[I]);
|
||||
if AUnit.ImplUsedUnits <> nil then
|
||||
for I := 0 to AUnit.ImplUsedUnits.Count - 1 do
|
||||
AIface.ImplUsedUnits.Add(AUnit.ImplUsedUnits.Strings[I]);
|
||||
AIface.HasInitialization :=
|
||||
(AUnit.InitStmts <> nil) and (AUnit.InitStmts.Count > 0);
|
||||
end;
|
||||
|
||||
{ ----- Top-level ------------------------------------------------- }
|
||||
|
|
|
|||
|
|
@ -62,6 +62,24 @@ implementation
|
|||
|
||||
{ ----- Type-ref resolution -------------------------------------- }
|
||||
|
||||
{ Return the bare type name from a possibly unit-qualified name. The
|
||||
unit qualifier may itself be dotted (e.g. 'blaise.codegen.ICodeGen'),
|
||||
so everything up to and including the LAST '.' is removed. Names with
|
||||
no '.' are returned unchanged. Uses StrAt (0-based) per house style. }
|
||||
function StripUnitQualifier(const AName: string): string;
|
||||
var
|
||||
I, LastDot: Integer;
|
||||
begin
|
||||
LastDot := -1;
|
||||
for I := 0 to Length(AName) - 1 do
|
||||
if StrAt(AName, I) = Ord('.') then
|
||||
LastDot := I;
|
||||
if LastDot < 0 then
|
||||
Result := AName
|
||||
else
|
||||
Result := StrCopyTail(AName, LastDot + 1);
|
||||
end;
|
||||
|
||||
{ Resolve a TQualTypeRef to a TTypeDesc by name lookup. After topo-
|
||||
order import, every referenced symbol is already in ATable. }
|
||||
function ResolveTypeName(const ATypeName: string; ATable: TSymbolTable): TTypeDesc;
|
||||
|
|
@ -135,6 +153,34 @@ begin
|
|||
end;
|
||||
end;
|
||||
|
||||
{ Resolve a type-name string from a cached iface entry to a TTypeDesc,
|
||||
trying every avenue the source-compile path has: a registered symbol,
|
||||
an inline type pattern ('^T', 'array of T', etc.), and finally the
|
||||
semantic analyser's on-demand resolver, which instantiates generics
|
||||
('TList<TFoo>') and synthesises anonymous array/set types. The last
|
||||
step is only available when ASemantic is non-nil (the Blaise.pas
|
||||
prebuilt-iface path always passes it); without it generic-instance
|
||||
field types from a .bif cannot be resolved. }
|
||||
function ResolveImportTypeName(const ATypeName: string;
|
||||
ATable: TSymbolTable;
|
||||
ASemantic: TSemanticAnalyser): TTypeDesc;
|
||||
var
|
||||
Sym: TSymbol;
|
||||
begin
|
||||
Result := nil;
|
||||
if ATypeName = '' then Exit;
|
||||
Sym := ATable.Lookup(ATypeName);
|
||||
if (Sym <> nil) and (Sym.Kind = skType) then
|
||||
begin
|
||||
Result := Sym.TypeDesc;
|
||||
Exit;
|
||||
end;
|
||||
Result := ResolveInlineTypeName(ATypeName, ATable);
|
||||
if Result <> nil then Exit;
|
||||
if ASemantic <> nil then
|
||||
Result := ASemantic.ResolveImportedTypeName(ATypeName);
|
||||
end;
|
||||
|
||||
{ ----- Type-entry registration ---------------------------------- }
|
||||
|
||||
procedure RegisterEnum(AEntry: TTypeEntry; ATable: TSymbolTable;
|
||||
|
|
@ -415,12 +461,7 @@ begin
|
|||
for I := 0 to ClassDef.Fields.Count - 1 do
|
||||
begin
|
||||
FldDecl := TFieldDecl(ClassDef.Fields.Items[I]);
|
||||
FldType := nil;
|
||||
FldSym := ATable.Lookup(FldDecl.TypeName);
|
||||
if (FldSym <> nil) and (FldSym.Kind = skType) then
|
||||
FldType := FldSym.TypeDesc
|
||||
else
|
||||
FldType := ResolveInlineTypeName(FldDecl.TypeName, ATable);
|
||||
FldType := ResolveImportTypeName(FldDecl.TypeName, ATable, ASemantic);
|
||||
if FldType = nil then
|
||||
raise EImportError.CreateFmt(
|
||||
'Class %s field type %s unresolved',
|
||||
|
|
@ -442,7 +483,7 @@ begin
|
|||
if ASemantic <> nil then
|
||||
begin
|
||||
MDecl := SynthesiseMethodDecl(
|
||||
TRoutineSig(AEntry.Methods.Items[I]), AUnitName, ATable);
|
||||
TRoutineSig(AEntry.Methods.Items[I]), AUnitName, ATable, ASemantic);
|
||||
ATable.OwnImportedDecl(MDecl);
|
||||
ASemantic.RegisterImportedMethod(AEntry.Name, MDecl);
|
||||
end;
|
||||
|
|
@ -454,8 +495,11 @@ begin
|
|||
for I := 0 to AEntry.Implements.Count - 1 do
|
||||
begin
|
||||
ParentName := AEntry.Implements.Strings[I];
|
||||
J := Pos('.', ParentName);
|
||||
if J > 0 then ParentName := Copy(ParentName, J + 1, Length(ParentName) - J);
|
||||
{ Strip any 'Unit.' qualifier — the unit name may itself be dotted
|
||||
(e.g. 'blaise.codegen.ICodeGen'), so take the segment after the
|
||||
LAST '.' as the type name. The flat symbol-table namespace carries
|
||||
no unit qualification. }
|
||||
ParentName := StripUnitQualifier(ParentName);
|
||||
Sym := ATable.Lookup(ParentName);
|
||||
if (Sym <> nil) and (Sym.TypeDesc is TInterfaceTypeDesc) then
|
||||
RT.AddImplements(TInterfaceTypeDesc(Sym.TypeDesc));
|
||||
|
|
@ -522,11 +566,12 @@ begin
|
|||
end;
|
||||
|
||||
procedure RegisterRecord(AEntry: TTypeEntry; ATable: TSymbolTable;
|
||||
const AUnitName: string);
|
||||
const AUnitName: string;
|
||||
ASemantic: TSemanticAnalyser = nil);
|
||||
var
|
||||
RecDef: TRecordTypeDef;
|
||||
RecDesc: TRecordTypeDesc;
|
||||
Sym, FldSym: TSymbol;
|
||||
Sym: TSymbol;
|
||||
I, J: Integer;
|
||||
FldDecl: TFieldDecl;
|
||||
FldType: TTypeDesc;
|
||||
|
|
@ -552,12 +597,7 @@ begin
|
|||
for I := 0 to RecDef.Fields.Count - 1 do
|
||||
begin
|
||||
FldDecl := TFieldDecl(RecDef.Fields.Items[I]);
|
||||
FldType := nil;
|
||||
FldSym := ATable.Lookup(FldDecl.TypeName);
|
||||
if (FldSym <> nil) and (FldSym.Kind = skType) then
|
||||
FldType := FldSym.TypeDesc
|
||||
else
|
||||
FldType := ResolveInlineTypeName(FldDecl.TypeName, ATable);
|
||||
FldType := ResolveImportTypeName(FldDecl.TypeName, ATable, ASemantic);
|
||||
if FldType = nil then
|
||||
raise EImportError.CreateFmt(
|
||||
'Record %s field type %s unresolved',
|
||||
|
|
@ -696,7 +736,7 @@ begin
|
|||
else if Entry.Def is TClassTypeDef then
|
||||
RegisterClass(Entry, ATable, AIface.Name, ASemantic)
|
||||
else if Entry.Def is TRecordTypeDef then
|
||||
RegisterRecord(Entry, ATable, AIface.Name)
|
||||
RegisterRecord(Entry, ATable, AIface.Name, ASemantic)
|
||||
else if Entry.Def is TProceduralTypeDef then
|
||||
RegisterProcType(Entry, ATable, AIface.Name)
|
||||
else
|
||||
|
|
@ -772,20 +812,16 @@ end;
|
|||
{ Build a TParamDesc for the symbol table from a cloned TMethodParam.
|
||||
Param types must already be registered in ATable (caller imported
|
||||
the dep, or it's a builtin). }
|
||||
function BuildParamDesc(AParam: TMethodParam; ATable: TSymbolTable): TParamDesc;
|
||||
var
|
||||
Sym: TSymbol;
|
||||
function BuildParamDesc(AParam: TMethodParam; ATable: TSymbolTable;
|
||||
ASemantic: TSemanticAnalyser = nil): TParamDesc;
|
||||
begin
|
||||
Result := TParamDesc.Create();
|
||||
Result.Name := AParam.ParamName;
|
||||
Result.IsConst := AParam.IsConstParam;
|
||||
Result.IsVar := AParam.IsVarParam;
|
||||
Sym := ATable.Lookup(AParam.TypeName);
|
||||
if (Sym <> nil) and (Sym.Kind = skType) then
|
||||
Result.TypeDesc := Sym.TypeDesc;
|
||||
{ Unresolved param type leaves TypeDesc nil; downstream call-site
|
||||
resolution would catch this — but for the cases in 6c-A scope we
|
||||
expect all param types to resolve. }
|
||||
{ Generic-aware resolution so an instance param type (e.g.
|
||||
'TList<TArchiveMember>') resolves rather than leaving TypeDesc nil. }
|
||||
Result.TypeDesc := ResolveImportTypeName(AParam.TypeName, ATable, ASemantic);
|
||||
end;
|
||||
|
||||
{ Synthesise a TMethodDecl from a TRoutineSig + its return-type
|
||||
|
|
@ -797,20 +833,32 @@ end;
|
|||
symbol the exporting unit defines. }
|
||||
function SynthesiseMethodDecl(ASig: TRoutineSig;
|
||||
const AOwningUnit: string;
|
||||
ATable: TSymbolTable): TMethodDecl;
|
||||
ATable: TSymbolTable;
|
||||
ASemantic: TSemanticAnalyser = nil): TMethodDecl;
|
||||
var
|
||||
J: Integer;
|
||||
Param: TMethodParam;
|
||||
PSyn: TMethodParam;
|
||||
Sym: TSymbol;
|
||||
begin
|
||||
Result := TMethodDecl.Create();
|
||||
Result.Name := ASig.Name;
|
||||
Result.OwningUnit := AOwningUnit;
|
||||
Result.ReturnTypeName := ASig.ReturnType.TypeName;
|
||||
Result.ResolvedReturnType :=
|
||||
ResolveTypeName(ASig.ReturnType.TypeName, ATable);
|
||||
Result.ResolvedQbeName := MangleUnitPrefix(AOwningUnit) + ASig.Name;
|
||||
ResolveImportTypeName(ASig.ReturnType.TypeName, ATable, ASemantic);
|
||||
if ASig.ResolvedQbeName <> '' then
|
||||
Result.ResolvedQbeName := ASig.ResolvedQbeName
|
||||
else
|
||||
Result.ResolvedQbeName := MangleUnitPrefix(AOwningUnit) + ASig.Name;
|
||||
{ Propagate the vtable-dispatch facts. Without these, a call to a virtual
|
||||
method resolved from a cached .bif keeps VTableSlot = -1 (the TMethodDecl
|
||||
default) and codegen emits a DIRECT call to the base/abstract symbol
|
||||
instead of a vtable dispatch — e.g. an incremental rebuild miscompiles
|
||||
Driver.CreateUnitCodeGen into a call to the abstract TBackendDriver stub,
|
||||
aborting at run time. The .bif already carries these (EncodeMethodSig). }
|
||||
Result.VTableSlot := ASig.VTableSlot;
|
||||
Result.IsVirtual := ASig.IsVirtual;
|
||||
Result.IsOverride := ASig.IsOverride;
|
||||
for J := 0 to ASig.Params.Count - 1 do
|
||||
begin
|
||||
Param := TMethodParam(ASig.Params.Items[J]);
|
||||
|
|
@ -820,9 +868,18 @@ begin
|
|||
PSyn.IsVarParam := Param.IsVarParam;
|
||||
PSyn.IsConstParam := Param.IsConstParam;
|
||||
PSyn.IsOpenArray := Param.IsOpenArray;
|
||||
Sym := ATable.Lookup(Param.TypeName);
|
||||
if (Sym <> nil) and (Sym.Kind = skType) then
|
||||
PSyn.ResolvedType := Sym.TypeDesc;
|
||||
PSyn.HasDefault := Param.HasDefault;
|
||||
{ Carry the default-value expression through so call sites that omit
|
||||
the trailing arguments can synthesise them (AppendDefaultArgs). The
|
||||
.bif round-trips the literal/ident default expression; ownership of
|
||||
the cloned node passes to PSyn. }
|
||||
PSyn.DefaultValue := Param.DefaultValue;
|
||||
Param.DefaultValue := nil;
|
||||
{ Resolve the param type, including generic instances such as
|
||||
'TList<TArchiveMember>' — a plain symbol Lookup cannot, so without the
|
||||
generic-aware resolver the param's ResolvedType stays nil and the
|
||||
call-site overload scorer rejects every candidate. }
|
||||
PSyn.ResolvedType := ResolveImportTypeName(Param.TypeName, ATable, ASemantic);
|
||||
Result.Params.Add(PSyn);
|
||||
end;
|
||||
end;
|
||||
|
|
@ -844,7 +901,7 @@ begin
|
|||
|
||||
if Sig.IsFunction then
|
||||
begin
|
||||
RetType := ResolveTypeName(Sig.ReturnType.TypeName, ATable);
|
||||
RetType := ResolveImportTypeName(Sig.ReturnType.TypeName, ATable, ASemantic);
|
||||
Sym := TSymbol.Create(Sig.Name, skFunction, RetType);
|
||||
end
|
||||
else
|
||||
|
|
@ -855,7 +912,7 @@ begin
|
|||
for J := 0 to Sig.Params.Count - 1 do
|
||||
begin
|
||||
Param := TMethodParam(Sig.Params.Items[J]);
|
||||
PDesc := BuildParamDesc(Param, ATable);
|
||||
PDesc := BuildParamDesc(Param, ATable, ASemantic);
|
||||
Sym.Params.Add(PDesc);
|
||||
end;
|
||||
|
||||
|
|
@ -863,7 +920,7 @@ begin
|
|||
via FProcIndex. Owned by the symbol table to outlive imports. }
|
||||
if ASemantic <> nil then
|
||||
begin
|
||||
MDecl := SynthesiseMethodDecl(Sig, AIface.Name, ATable);
|
||||
MDecl := SynthesiseMethodDecl(Sig, AIface.Name, ATable, ASemantic);
|
||||
ATable.OwnImportedDecl(MDecl);
|
||||
Sym.Decl := MDecl;
|
||||
ASemantic.RegisterImportedRoutine(Sig.Name, MDecl);
|
||||
|
|
|
|||
|
|
@ -187,6 +187,16 @@ type
|
|||
that the iface is safe to trust. }
|
||||
|
||||
UsedUnits: TStringList; { owned — interface-section uses, in order }
|
||||
ImplUsedUnits: TStringList; { owned — implementation-section uses, in
|
||||
order. Needed so an incremental rebuild
|
||||
that loads this unit from its cached .bif
|
||||
still pulls in (and links) impl-only
|
||||
dependencies. }
|
||||
HasInitialization: Boolean; { unit has a non-empty initialization
|
||||
section. An incremental rebuild loads this
|
||||
unit from its cached .bif and must still
|
||||
emit a call to <Unit>_init at program
|
||||
startup, or the init section never runs. }
|
||||
|
||||
Types: TObjectList; { owned TTypeEntry }
|
||||
Consts: TObjectList; { owned TConstEntry }
|
||||
|
|
@ -372,6 +382,7 @@ begin
|
|||
inherited Create();
|
||||
Name := AName;
|
||||
UsedUnits := TStringList.Create();
|
||||
ImplUsedUnits := TStringList.Create();
|
||||
|
||||
Types := TObjectList.Create(True);
|
||||
Consts := TObjectList.Create(True);
|
||||
|
|
@ -402,6 +413,7 @@ begin
|
|||
Consts.Free();
|
||||
Types.Free();
|
||||
|
||||
ImplUsedUnits.Free();
|
||||
UsedUnits.Free();
|
||||
inherited Destroy();
|
||||
end;
|
||||
|
|
|
|||
|
|
@ -53,7 +53,11 @@ uses
|
|||
|
||||
const
|
||||
IFACE_MAGIC = 'BLAISE-IFACE';
|
||||
IFACE_VERSION = 1;
|
||||
IFACE_VERSION = 3; { v2: META block carries ImplUsedUnits + HasInitialization
|
||||
v3: EncodeBlock carries local var declarations so generic
|
||||
template method bodies round-trip with their locals
|
||||
(e.g. 'var Ptr: ^T'); without this an instantiation
|
||||
from a cached .bif fails with 'Undeclared variable'. }
|
||||
|
||||
type
|
||||
EIfaceFormatError = class(Exception);
|
||||
|
|
@ -77,6 +81,11 @@ function ContentHashFnv1a64(const AContent: string): string;
|
|||
|
||||
implementation
|
||||
|
||||
{ Forward — EncodeExpr is defined far below (with the statement/expression
|
||||
serialiser) but the method-signature writers need it to emit parameter
|
||||
default values. }
|
||||
function EncodeExpr(AE: TASTExpr): string; forward;
|
||||
|
||||
{ ----- Writer ----------------------------------------------------- }
|
||||
|
||||
{ Length-prefixed string: "<len>:bytes". Lets the reader consume
|
||||
|
|
@ -274,7 +283,8 @@ begin
|
|||
Result := Result +
|
||||
EncodeLpstr(P.ParamName) +
|
||||
EncodeLpstr(P.TypeName) +
|
||||
EncodeParamFlags(P);
|
||||
EncodeParamFlags(P) +
|
||||
EncodeExpr(P.DefaultValue);
|
||||
end;
|
||||
end;
|
||||
|
||||
|
|
@ -357,7 +367,8 @@ begin
|
|||
Result := Result +
|
||||
EncodeLpstr(P.ParamName) +
|
||||
EncodeLpstr(P.TypeName) +
|
||||
EncodeParamFlags(P);
|
||||
EncodeParamFlags(P) +
|
||||
EncodeExpr(P.DefaultValue);
|
||||
end;
|
||||
end;
|
||||
|
||||
|
|
@ -422,6 +433,10 @@ begin
|
|||
M := TMethodDecl(ClassDef.Methods.Items[J]);
|
||||
Result := Result + EncodeBlock(M.Body);
|
||||
end;
|
||||
{ Property declarations — needed so a cached template instantiates with
|
||||
its properties (e.g. TList<T>.Count, TDictionary<K,V>.Count). Without
|
||||
these the instance has no properties and 'D.Count' fails to resolve. }
|
||||
Result := Result + EncodePropertyList(ClassDef.Properties);
|
||||
end
|
||||
else
|
||||
{ Defensive: encode "has-body" flag = false so the reader can
|
||||
|
|
@ -430,7 +445,8 @@ begin
|
|||
EncodeQualRefParts('', '') +
|
||||
EncodeLpstr('0') +
|
||||
EncodeCount(0) + EncodeCount(0) +
|
||||
EncodeCount(0) + EncodeCount(0);
|
||||
EncodeCount(0) + EncodeCount(0) +
|
||||
EncodeCount(0);
|
||||
end;
|
||||
|
||||
function WriteGenericInterfacePayload(AEntry: TTypeEntry): string;
|
||||
|
|
@ -562,6 +578,10 @@ begin
|
|||
if AParam.IsConstParam then B := B or 2;
|
||||
if AParam.IsOpenArray then B := B or 4;
|
||||
if AParam.IsOutParam then B := B or 8;
|
||||
{ Bit 16: this param has a default value. The default expression itself
|
||||
is not serialised; the flag is enough for MinArity / overload resolution
|
||||
to accept calls that omit the trailing defaulted arguments. }
|
||||
if (AParam.DefaultValue <> nil) or AParam.HasDefault then B := B or 16;
|
||||
Result := EncodeLpstr(IntToStr(B));
|
||||
end;
|
||||
|
||||
|
|
@ -590,9 +610,16 @@ begin
|
|||
Line := Line +
|
||||
EncodeLpstr(P.ParamName) +
|
||||
EncodeLpstr(P.TypeName) +
|
||||
EncodeParamFlags(P);
|
||||
EncodeParamFlags(P) +
|
||||
EncodeExpr(P.DefaultValue);
|
||||
end;
|
||||
Line := Line + EncodeLpstr(R.CallingConv);
|
||||
{ ResolvedQbeName — the mangled link symbol. Required for overloaded
|
||||
free routines (e.g. GCHashOf): each overload has a distinct mangled
|
||||
name ('..._GCHashOf_D_S' etc.). Without it the importer falls back
|
||||
to the unmangled 'Unit_Name' for every overload, so a call site emits
|
||||
a reference to a symbol that does not exist in the cached .o. }
|
||||
Line := Line + EncodeLpstr(R.ResolvedQbeName);
|
||||
SB.AppendLine(Line);
|
||||
end;
|
||||
SB.AppendLine('END');
|
||||
|
|
@ -613,7 +640,9 @@ begin
|
|||
EncodeLpstr(AIface.SourceHash) +
|
||||
EncodeLpstr(AIface.CompilerId) +
|
||||
EncodeInt64(AIface.SourceModTime) +
|
||||
EncodeStringList(AIface.UsedUnits));
|
||||
EncodeStringList(AIface.UsedUnits) +
|
||||
EncodeStringList(AIface.ImplUsedUnits) +
|
||||
EncodeBool(AIface.HasInitialization));
|
||||
SB.AppendLine('END');
|
||||
Result := SB.ToString();
|
||||
finally
|
||||
|
|
@ -634,7 +663,6 @@ end;
|
|||
format-error fallback at read time — a structural change in the
|
||||
AST needs a matching version bump in the wire format. }
|
||||
|
||||
function EncodeExpr(AE: TASTExpr): string; forward;
|
||||
function EncodeStmt(AStmt: TASTStmt): string; forward;
|
||||
function EncodeBlock(AB: TBlock): string; forward;
|
||||
|
||||
|
|
@ -814,7 +842,12 @@ begin
|
|||
Result := EncodeLpstr('raise') +
|
||||
EncodeExpr(TRaiseStmt(AStmt).Expr)
|
||||
else if AStmt is TExitStmt then
|
||||
Result := EncodeLpstr('exit')
|
||||
{ Carry the optional return value — 'Exit(expr)'. Without it a cached
|
||||
generic-template body's 'Exit(-1)' round-trips as a bare 'Exit',
|
||||
leaving Result uninitialised (e.g. TDictionary.FindKey returns 0 not
|
||||
-1, so every key appears found at slot 0). }
|
||||
Result := EncodeLpstr('exit') +
|
||||
EncodeExpr(TExitStmt(AStmt).Value)
|
||||
else if AStmt is TBreakStmt then
|
||||
Result := EncodeLpstr('brk')
|
||||
else if AStmt is TContinueStmt then
|
||||
|
|
@ -871,11 +904,36 @@ end;
|
|||
nested types, and the importer doesn't currently re-emit nested
|
||||
proc decls. Add as needed; nil ABlock encodes as a single
|
||||
'nil' lpstr. }
|
||||
{ Encode a block's local var declarations (names + type name). Only
|
||||
the facts a downstream instantiation needs to declare the local into
|
||||
scope are emitted — initialisers and attributes are not required for
|
||||
generic template bodies (e.g. 'var Ptr: ^T') and are omitted. Each
|
||||
TVarDecl is emitted flattened: one record per name. }
|
||||
function EncodeBlockVarDecls(AB: TBlock): string;
|
||||
var
|
||||
I, J: Integer;
|
||||
VD: TVarDecl;
|
||||
Total: Integer;
|
||||
begin
|
||||
Total := 0;
|
||||
for I := 0 to AB.Decls.Count - 1 do
|
||||
Total := Total + TVarDecl(AB.Decls.Items[I]).Names.Count;
|
||||
Result := EncodeCount(Total);
|
||||
for I := 0 to AB.Decls.Count - 1 do
|
||||
begin
|
||||
VD := TVarDecl(AB.Decls.Items[I]);
|
||||
for J := 0 to VD.Names.Count - 1 do
|
||||
Result := Result + EncodeLpstr(VD.Names.Strings[J]) +
|
||||
EncodeLpstr(VD.TypeName);
|
||||
end;
|
||||
end;
|
||||
|
||||
function EncodeBlock(AB: TBlock): string;
|
||||
begin
|
||||
if AB = nil then
|
||||
begin Result := EncodeLpstr('nil'); Exit; end;
|
||||
Result := EncodeLpstr('block') + EncodeStmtList(AB.Stmts);
|
||||
Result := EncodeLpstr('block') + EncodeStmtList(AB.Stmts) +
|
||||
EncodeBlockVarDecls(AB);
|
||||
end;
|
||||
|
||||
{ Emit only generic free routines. Generic class/interface
|
||||
|
|
@ -1450,6 +1508,7 @@ var
|
|||
TFSn: TTryFinallyStmt;
|
||||
TESn: TTryExceptStmt;
|
||||
RaSn: TRaiseStmt;
|
||||
ESn: TExitStmt;
|
||||
CSSn: TCaseStmt;
|
||||
FASn: TFieldAssignment;
|
||||
SSAn: TStaticSubscriptAssign;
|
||||
|
|
@ -1557,7 +1616,12 @@ begin
|
|||
RaSn.Expr := ReadExpr(AText, APos);
|
||||
Result := RaSn;
|
||||
end
|
||||
else if Kind = 'exit' then Result := TExitStmt.Create()
|
||||
else if Kind = 'exit' then
|
||||
begin
|
||||
ESn := TExitStmt.Create();
|
||||
ESn.Value := ReadExpr(AText, APos); { nil for bare 'Exit' }
|
||||
Result := ESn;
|
||||
end
|
||||
else if Kind = 'brk' then Result := TBreakStmt.Create()
|
||||
else if Kind = 'cont' then Result := TContinueStmt.Create()
|
||||
else if Kind = 'case' then
|
||||
|
|
@ -1626,7 +1690,11 @@ end;
|
|||
|
||||
function ReadBlock(const AText: string; var APos: Integer): TBlock;
|
||||
var
|
||||
Kind: string;
|
||||
Kind: string;
|
||||
C, I: Integer;
|
||||
VarName: string;
|
||||
VarType: string;
|
||||
VD: TVarDecl;
|
||||
begin
|
||||
Kind := ReadLpstrAt(AText, APos);
|
||||
if Kind = 'nil' then begin Result := nil; Exit; end;
|
||||
|
|
@ -1634,6 +1702,17 @@ begin
|
|||
raise EIfaceFormatError.Create('ReadBlock: expected ''block'' got ''' + Kind + '''');
|
||||
Result := TBlock.Create();
|
||||
ReadStmtList(AText, APos, Result.Stmts);
|
||||
{ Local var declarations (v3+): one flattened record per name. }
|
||||
C := DecodeCount(AText, APos);
|
||||
for I := 1 to C do
|
||||
begin
|
||||
VarName := ReadLpstrAt(AText, APos);
|
||||
VarType := ReadLpstrAt(AText, APos);
|
||||
VD := TVarDecl.Create();
|
||||
VD.Names.Add(VarName);
|
||||
VD.TypeName := VarType;
|
||||
Result.Decls.Add(VD);
|
||||
end;
|
||||
end;
|
||||
|
||||
function DecodeCount(const AText: string; var APos: Integer): Integer;
|
||||
|
|
@ -1710,6 +1789,7 @@ begin
|
|||
Param.TypeName := ReadLpstrAt(AText, APos);
|
||||
FlagsStr := ReadLpstrAt(AText, APos);
|
||||
DecodeParamFlags(StrToInt(FlagsStr), Param);
|
||||
Param.DefaultValue := ReadExpr(AText, APos);
|
||||
Result.Params.Add(Param);
|
||||
end;
|
||||
end;
|
||||
|
|
@ -1779,6 +1859,7 @@ begin
|
|||
Param.TypeName := ReadLpstrAt(AText, APos);
|
||||
FlagsStr := ReadLpstrAt(AText, APos);
|
||||
DecodeParamFlags(StrToInt(FlagsStr), Param);
|
||||
Param.DefaultValue := ReadExpr(AText, APos);
|
||||
Result.Params.Add(Param);
|
||||
end;
|
||||
end;
|
||||
|
|
@ -1876,6 +1957,10 @@ begin
|
|||
C := DecodeCount(AText, APos);
|
||||
for I := 1 to C do
|
||||
AIface.UsedUnits.Add(ReadLpstrAt(AText, APos));
|
||||
C := DecodeCount(AText, APos);
|
||||
for I := 1 to C do
|
||||
AIface.ImplUsedUnits.Add(ReadLpstrAt(AText, APos));
|
||||
AIface.HasInitialization := ReadLpstrAt(AText, APos) = '1';
|
||||
if ReadTag(AText, APos) <> 'END' then
|
||||
raise EIfaceFormatError.Create('META block: missing END marker');
|
||||
end;
|
||||
|
|
@ -1954,6 +2039,9 @@ begin
|
|||
ClassDef.Methods.Add(MD);
|
||||
end;
|
||||
|
||||
{ Property declarations (parallel to WriteGenericClassPayload). }
|
||||
ReadPropertyList(AText, APos, ClassDef.Properties);
|
||||
|
||||
AEntry.IsGeneric := True;
|
||||
AEntry.Def := Def;
|
||||
end;
|
||||
|
|
@ -2085,6 +2173,7 @@ begin
|
|||
AParam.IsConstParam := (AFlags and 2) <> 0;
|
||||
AParam.IsOpenArray := (AFlags and 4) <> 0;
|
||||
AParam.IsOutParam := (AFlags and 8) <> 0;
|
||||
AParam.HasDefault := (AFlags and 16) <> 0;
|
||||
end;
|
||||
|
||||
procedure ReadRoutines(const AText: string; var APos: Integer;
|
||||
|
|
@ -2119,9 +2208,11 @@ begin
|
|||
Param.TypeName := ReadLpstrAt(AText, APos);
|
||||
FlagsStr := ReadLpstrAt(AText, APos);
|
||||
DecodeParamFlags(StrToInt(FlagsStr), Param);
|
||||
Param.DefaultValue := ReadExpr(AText, APos);
|
||||
R.Params.Add(Param);
|
||||
end;
|
||||
R.CallingConv := ReadLpstrAt(AText, APos);
|
||||
R.ResolvedQbeName := ReadLpstrAt(AText, APos);
|
||||
AIface.AddRoutine(R);
|
||||
end;
|
||||
if ReadTag(AText, APos) <> 'END' then
|
||||
|
|
|
|||
|
|
@ -53,6 +53,10 @@ type
|
|||
ifaces. Caller links against these alongside the main
|
||||
program's object. }
|
||||
property PrebuiltObjectPaths: TStringList read FPrebuiltObjectPaths;
|
||||
{ Object paths for impl-only dependencies that must be linked but were
|
||||
not semantically imported (and thus are absent from PrebuiltObjectPaths
|
||||
and the source-compiled unit set). Caller links these too. }
|
||||
property LinkOnlyObjects: TStringList read FLinkOnlyObjects;
|
||||
private
|
||||
FSearchPaths: TStringList; { not owned }
|
||||
FDefines: TStringList; { not owned — conditional symbols for each unit's lexer }
|
||||
|
|
@ -61,6 +65,11 @@ type
|
|||
FResult: TObjectList; { the in-progress output list (not owned here) }
|
||||
FPrebuiltIfaces: TObjectList; { owned TUnitInterface }
|
||||
FPrebuiltObjectPaths: TStringList;
|
||||
FLinkOnlyObjects: TStringList; { .o paths for impl-only deps that
|
||||
must be linked but NOT semantically
|
||||
imported (see CollectLinkOnlyObject) }
|
||||
FLinkOnlySeen: TStringList; { unit names already visited by the
|
||||
link-only collector — cycle guard }
|
||||
function IsBuiltin(const AName: string): Boolean;
|
||||
function Locate(const AName: string): string;
|
||||
{ Look for '<AName>.o' on the search paths (lowercase or as-cased).
|
||||
|
|
@ -71,6 +80,11 @@ type
|
|||
function LoadIfaceFromObject(const APath: string): TUnitInterface;
|
||||
function LoadOne(const APath: string): TUnit;
|
||||
procedure LoadTransitive(const AName: string);
|
||||
{ Collect the .o for an impl-only dependency (and its transitive
|
||||
dependencies) for linking, without parsing source or importing its
|
||||
iface. Units already loaded normally are skipped — they are linked
|
||||
via FPrebuiltObjectPaths / the source-compiled worker objects. }
|
||||
procedure CollectLinkOnlyObject(const AName: string);
|
||||
{ Decide whether to trust a freshly-loaded .bif. Hash-compares
|
||||
against the source .pas if present on the search path; falls
|
||||
back to a CompilerId match when source is unavailable. }
|
||||
|
|
@ -243,6 +257,37 @@ begin
|
|||
end;
|
||||
end;
|
||||
|
||||
procedure TUnitLoader.CollectLinkOnlyObject(const AName: string);
|
||||
var
|
||||
ObjPath: string;
|
||||
Iface: TUnitInterface;
|
||||
I: Integer;
|
||||
begin
|
||||
if IsBuiltin(AName) then Exit;
|
||||
{ Already loaded the normal way → its object is already linked. }
|
||||
if FLoadedNames.IndexOf(AName) >= 0 then Exit;
|
||||
if FLinkOnlySeen.IndexOf(AName) >= 0 then Exit;
|
||||
FLinkOnlySeen.Add(AName);
|
||||
|
||||
ObjPath := LocateObject(AName);
|
||||
if ObjPath = '' then Exit; { no cached object — nothing to link }
|
||||
Iface := LoadIfaceFromObject(ObjPath);
|
||||
if Iface = nil then Exit; { unreadable iface — the source-load path
|
||||
elsewhere will have handled this unit }
|
||||
try
|
||||
if not ValidateIface(Iface, AName) then Exit;
|
||||
if FLinkOnlyObjects.IndexOf(ObjPath) < 0 then
|
||||
FLinkOnlyObjects.Add(ObjPath);
|
||||
{ Recurse so this dep's own dependencies are linked too. }
|
||||
for I := 0 to Iface.UsedUnits.Count - 1 do
|
||||
CollectLinkOnlyObject(Iface.UsedUnits.Strings[I]);
|
||||
for I := 0 to Iface.ImplUsedUnits.Count - 1 do
|
||||
CollectLinkOnlyObject(Iface.ImplUsedUnits.Strings[I]);
|
||||
finally
|
||||
Iface.Free();
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TUnitLoader.LoadTransitive(const AName: string);
|
||||
var
|
||||
Path: string;
|
||||
|
|
@ -290,6 +335,17 @@ begin
|
|||
FPrebuiltIfaces.Add(Iface);
|
||||
FPrebuiltObjectPaths.Add(ObjPath);
|
||||
FLoadedNames.Add(AName);
|
||||
{ Impl-only dependencies are not reachable via the interface uses,
|
||||
but their objects must still be linked or the program loses their
|
||||
code (an incremental rebuild that loads this unit from its cached
|
||||
.bif would otherwise drop them). They are collected for LINK ONLY
|
||||
— not semantically imported — because the consuming unit never
|
||||
references their symbols, and importing their ifaces early would
|
||||
break the dependency-ordered import (impl/interface-use cycles, e.g.
|
||||
a backend unit whose interface a peer impl-uses, cannot be resolved
|
||||
by the leaf-first iface import). }
|
||||
for I := 0 to Iface.ImplUsedUnits.Count - 1 do
|
||||
CollectLinkOnlyObject(Iface.ImplUsedUnits.Strings[I]);
|
||||
finally
|
||||
FLoading.Delete(FLoading.IndexOf(AName));
|
||||
end;
|
||||
|
|
@ -337,10 +393,16 @@ begin
|
|||
FPrebuiltIfaces := TObjectList.Create(True);
|
||||
FPrebuiltObjectPaths := TStringList.Create();
|
||||
FPrebuiltObjectPaths.CaseSensitive := False;
|
||||
FLinkOnlyObjects := TStringList.Create();
|
||||
FLinkOnlyObjects.CaseSensitive := False;
|
||||
FLinkOnlySeen := TStringList.Create();
|
||||
FLinkOnlySeen.CaseSensitive := False;
|
||||
end;
|
||||
|
||||
destructor TUnitLoader.Destroy;
|
||||
begin
|
||||
FLinkOnlySeen.Free();
|
||||
FLinkOnlyObjects.Free();
|
||||
FPrebuiltObjectPaths.Free();
|
||||
FPrebuiltIfaces.Free();
|
||||
FLoadedNames.Free();
|
||||
|
|
|
|||
|
|
@ -56,6 +56,16 @@ type
|
|||
runtime Makefile drives (every RTL unit is compiled `--source X.pas
|
||||
--output X.o`), so it broke `make` in runtime/. }
|
||||
procedure TestNativeIncremental_UnitUsesUnit_Compiles;
|
||||
{ Regression: a unit that USES another unit only in its IMPLEMENTATION
|
||||
section. On a clean build all units are parsed, so the impl-only
|
||||
dependency's object is linked. But on an incremental REBUILD, the
|
||||
consuming unit is loaded from its cached .o/.bif — and the .bif only
|
||||
recorded the INTERFACE uses, so the impl-only dependency was never
|
||||
loaded and its .o was dropped from the link → undefined symbol at
|
||||
run time. This is exactly what broke a second `pasbuild test-compile`
|
||||
(the compiler rebuilt itself incrementally into a populated cache and
|
||||
lost ~880 KB of impl-only-dependency code). }
|
||||
procedure TestIncrementalRebuild_ImplOnlyUses_LinksDependency;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
|
@ -670,6 +680,93 @@ begin
|
|||
AssertEquals('use_derived stdout', '42' + #10, Captured)
|
||||
end;
|
||||
|
||||
procedure TSepCompileTests.TestIncrementalRebuild_ImplOnlyUses_LinksDependency;
|
||||
const
|
||||
HelperSrc =
|
||||
'''
|
||||
unit HelperU;
|
||||
interface
|
||||
function HelperVal: Integer;
|
||||
implementation
|
||||
function HelperVal: Integer;
|
||||
begin Result := 42 end;
|
||||
end.
|
||||
''';
|
||||
{ MidU uses HelperU only in its IMPLEMENTATION section — HelperU does not
|
||||
appear in MidU's interface, so MidU's .bif must still record it as an
|
||||
implementation-section dependency. }
|
||||
MidSrc =
|
||||
'''
|
||||
unit MidU;
|
||||
interface
|
||||
function MidVal: Integer;
|
||||
implementation
|
||||
uses HelperU;
|
||||
function MidVal: Integer;
|
||||
begin Result := HelperVal() + 1 end;
|
||||
end.
|
||||
''';
|
||||
ProgSrc =
|
||||
'''
|
||||
program UseMid;
|
||||
uses MidU;
|
||||
begin
|
||||
WriteLn(MidVal())
|
||||
end.
|
||||
''';
|
||||
var
|
||||
HelperPas, MidPas, ProgPas, ProgBin, CacheDir: string;
|
||||
Captured: string;
|
||||
Rc: Integer;
|
||||
begin
|
||||
if not ToolchainAvailable() then
|
||||
begin
|
||||
Fail('toolchain missing — qbe or RTL not found');
|
||||
Exit
|
||||
end;
|
||||
if not FileExists(BlaisePath()) then
|
||||
begin
|
||||
Fail('blaise binary missing at ' + BlaisePath());
|
||||
Exit
|
||||
end;
|
||||
|
||||
HelperPas := FScratch + '/HelperU.pas';
|
||||
MidPas := FScratch + '/MidU.pas';
|
||||
ProgPas := FScratch + '/use_mid.pas';
|
||||
ProgBin := FScratch + '/use_mid';
|
||||
CacheDir := FScratch + '/units-impl';
|
||||
|
||||
WriteFile(HelperPas, HelperSrc);
|
||||
WriteFile(MidPas, MidSrc);
|
||||
WriteFile(ProgPas, ProgSrc);
|
||||
ForceDirectories(CacheDir);
|
||||
|
||||
{ Build 1 (clean cache): every unit is parsed from source, so HelperU's
|
||||
object is linked. This populates the cache with .o/.bif for MidU and
|
||||
HelperU. }
|
||||
Rc := RunBlaise(['--source', ProgPas, '--output', ProgBin,
|
||||
'--unit-cache', CacheDir,
|
||||
'--unit-path', FScratch], Captured);
|
||||
AssertEquals('build1 exit code (out: ' + Captured + ')', 0, Rc);
|
||||
Rc := RunBinary(ProgBin, Captured);
|
||||
AssertEquals('build1 run exit code', 0, Rc);
|
||||
AssertEquals('build1 stdout', '43' + #10, Captured);
|
||||
|
||||
{ Build 2 (populated cache): MidU is now loaded from its cached iface.
|
||||
The loader must still pull in HelperU (MidU's impl-only dependency) and
|
||||
link helperu.o — otherwise the link drops it and the program aborts at
|
||||
run time with an undefined symbol. }
|
||||
Rc := RunBlaise(['--source', ProgPas, '--output', ProgBin,
|
||||
'--unit-cache', CacheDir,
|
||||
'--unit-path', FScratch], Captured);
|
||||
AssertEquals('build2 exit code (out: ' + Captured + ')', 0, Rc);
|
||||
AssertTrue('use_mid exists after rebuild', FileExists(ProgBin));
|
||||
|
||||
Rc := RunBinary(ProgBin, Captured);
|
||||
AssertEquals('build2 run exit code (impl-only dep must be linked)', 0, Rc);
|
||||
AssertEquals('build2 stdout', '43' + #10, Captured)
|
||||
end;
|
||||
|
||||
initialization
|
||||
RegisterTest(TSepCompileTests);
|
||||
|
||||
|
|
|
|||
|
|
@ -2683,7 +2683,7 @@ begin
|
|||
Buf := WriteUnitInterface(Iface);
|
||||
{ Blaise Pos is 0-based; match-at-start returns 0. }
|
||||
AssertTrue('starts with magic',
|
||||
Pos('BLAISE-IFACE 1', Buf) = 0);
|
||||
Pos('BLAISE-IFACE 3', Buf) = 0);
|
||||
finally
|
||||
Iface.Free();
|
||||
end;
|
||||
|
|
|
|||
Loading…
Reference in a new issue