blaise/compiler/src/main/pascal/uUnitInterface.pas
Graeme Geldenhuys 915077d7c8 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.
2026-06-22 12:23:28 +01:00

545 lines
18 KiB
ObjectPascal

{
Blaise - An Object Pascal Compiler
Copyright (c) 2026 Andrew Haines
SPDX-License-Identifier: Apache-2.0 WITH Swift-exception
Licensed under the Apache License v2.0 with Runtime Library Exception.
See LICENSE file in the project root for full license terms.
}
{ TUnitInterface — self-contained, post-semantic description of a unit's
interface section.
Purpose: separate compilation. After AnalyseUnitForExport returns a
TUnitInterface for unit U, downstream compilation (semantic + codegen
of units that 'uses U') must be able to proceed without keeping U's
source TUnit alive. That property is what later lets us serialise
TUnitInterface to disk as a .bpu and skip parsing U entirely.
Lifecycle:
1. Source TUnit parsed by uUnitLoader.
2. uSemantic.AnalyseUnitForExport(Source) -> TUnitInterface.
3. Source TUnit may be freed; the interface remains valid.
4. Held in TUnitLoader's cache, keyed by unit name.
5. Consumed read-only by downstream semantic + codegen.
6. Freed at compiler exit.
Discipline:
* No member of TUnitInterface holds a pointer into the source TUnit.
AST subtrees stored here (inline bodies, generic bodies, type
defs) are deep clones via CloneBlock / CloneTypeDecl.
* All cross-unit type references travel as TQualTypeRef (unit name
+ type name), never as raw TTypeDesc pointers.
* Immutable after AnalyseUnitForExport returns. Threading later
assumes reader-side concurrency over a frozen interface.
Not in scope yet (Phase 5+):
* SourceHash / CompilerVersion are reserved fields, populated as
empty strings for now. Used for .bpu staleness + schema checks
once binary serialisation lands. }
unit uUnitInterface;
interface
uses
Classes, Contnrs, uAST;
const
{ Sentinel unit-name values for TQualTypeRef.UnitName. }
QUALREF_THIS_UNIT = '';
QUALREF_BUILTIN = '$builtin';
type
{ Name-qualified reference to a type. Used wherever a TUnitInterface
member would otherwise need a raw TTypeDesc pointer. Resolution
back to a concrete TTypeDesc happens in the consumer's symbol
table, after the consumer's deps have been loaded. }
TQualTypeRef = record
UnitName: string;
TypeName: string;
end;
{ Free routine signature (no body). Bodies, when needed by the
consumer, live in TUnitInterface.InlineBodies / GenericBodies. }
TRoutineSig = class
public
Name: string;
IsFunction: Boolean;
Params: TObjectList; { owned TMethodParam — cloned from source }
ReturnType: TQualTypeRef; { TypeName = '' for procedures }
IsInline: Boolean; { → InlineBodies has matching entry }
IsPublished: Boolean; { set by parser on class methods inside
a 'published' section; carries through
for RTL MethodAddress lookups }
IsExternal: Boolean;
ExternalName: string;
CallingConv: string; { 'cdecl', 'stdcall', '' }
VTableSlot: Integer; { -1 = static; >= 0 = vtable index.
Populated by ExportUnitInterface
only when AnalyseUnitForExport has
run first. Pre-semantic stays -1. }
ResolvedQbeName: string; { mangled symbol label. For class
methods: 'ClassName_MethodName'
(with optional '$sig' suffix for
overloads). For free routines:
'Name' (or 'Name$sig'). Used by
importers to construct vtable
ImplName ('$' + ResolvedQbeName)
without re-running mangling. }
IsVirtual: Boolean; { virtual method that allocates a new
vtable slot }
IsOverride: Boolean; { override that replaces a parent slot }
constructor Create;
destructor Destroy; override;
end;
{ Const entry — wraps the cloned TConstDecl plus the resolved type. }
TConstEntry = class
public
Decl: TConstDecl; { owned, cloned from source }
TypeRef: TQualTypeRef;
destructor Destroy; override;
end;
{ Interface-section var (rare). }
TVarEntry = class
public
Name: string;
TypeRef: TQualTypeRef;
IsThreadVar: Boolean;
end;
{ Type entry. Def carries the structural shape (TRecordTypeDef,
TClassTypeDef, TEnumTypeDef, …) cloned from source. The semantic
metadata that downstream units actually need (parent class,
instance size, vtable layout) is duplicated here in
name-qualified form. }
TTypeEntry = class
public
Name: string;
Def: TASTTypeDef; { owned, cloned from source }
IsClass: Boolean;
IsGeneric: Boolean; { → GenericBodies has matching entry }
ParentClass: TQualTypeRef; { for classes; both fields '' if none }
Implements: TStringList; { owned — qualified 'Unit.Name' strings,
or just 'Name' when in same unit }
InstanceSize: Int64; { 0 until set by AnalyseUnitForExport }
Methods: TObjectList; { owned TRoutineSig — class/record methods }
VTableLayout: TObjectList; { non-owning refs to entries in Methods,
indexed by VTableSlot }
Attributes: TStringList; { custom attribute class names }
constructor Create;
destructor Destroy; override;
end;
{ Inline routine body — kept as AST so consumer semantic can
re-typecheck and codegen can inline at the call site. }
TInlineBody = class
public
RoutineName: string;
Block: TBlock; { owned, cloned from source }
destructor Destroy; override;
end;
{ Generic type or routine body — instantiated per concrete type-arg
set by the consumer. }
TGenericBody = class
public
Name: string;
TypeParams: TStringList; { owned — 'T', 'K', 'V', … }
Constraints: TStringList; { owned — parallel to TypeParams,
'' = unconstrained }
IsType: Boolean; { True = generic type, False = generic routine }
TypeDef: TASTTypeDef; { owned; non-nil when IsType }
RoutineSig: TRoutineSig; { owned; non-nil when not IsType }
Body: TBlock; { owned; non-nil when not IsType }
MethodDecl: TMethodDecl; { owned; non-nil when not IsType. Carries
the cloned TMethodDecl that
uSemantic.FGenericFuncTemplates uses as
its template — lets uSemanticImport
plumb generic free routines through
TSymbolTable.RegisterGenericRoutine
without synthesising an AST node at
import time. }
constructor Create;
destructor Destroy; override;
end;
{ The interface itself. See unit header for lifecycle + discipline. }
TUnitInterface = class
private
FTypeIndex: TStringList; { Name → TTypeEntry, case-insensitive }
FConstIndex: TStringList; { Name → TConstEntry }
FRoutineIndex: TStringList; { Name → TRoutineSig (free routines) }
FInlineIndex: TStringList; { RoutineName → TInlineBody }
FGenericIndex: TStringList; { Name → TGenericBody }
public
Name: string;
SourceFile: string; { absolute path the iface was compiled from }
SourceHash: string; { content hash of SourceFile at compile time;
used to detect "source edited since iface
written". Empty => not populated. }
SourceModTime: Int64; { mtime of SourceFile (Unix epoch seconds) at
compile time. 0 => not populated. }
CompilerId: string; { identifier of the compiler that wrote this
iface. When source is unavailable, an
exact CompilerId match is the only signal
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 }
Vars: TObjectList; { owned TVarEntry }
Routines: TObjectList; { owned TRoutineSig — free routines }
InlineBodies: TObjectList; { owned TInlineBody }
GenericBodies: TObjectList; { owned TGenericBody }
{ ACaseSensitive controls all Find* lookups. Default False
matches Object Pascal semantics; set True for cases that need
exact-match (e.g. external symbols, future .bpu name tables). }
constructor Create(const AName: string;
ACaseSensitive: Boolean = False);
destructor Destroy; override;
{ Mutators — used by AnalyseUnitForExport during construction.
Each takes ownership of the passed-in entry and registers it in
the index. After AnalyseUnitForExport returns the caller MUST
NOT invoke these again; the interface is intended to be frozen.
Discipline only — not enforced (yet). }
procedure AddType(AEntry: TTypeEntry);
procedure AddConst(AEntry: TConstEntry);
procedure AddVar(AEntry: TVarEntry);
procedure AddRoutine(ASig: TRoutineSig);
procedure AddInlineBody(ABody: TInlineBody);
procedure AddGenericBody(ABody: TGenericBody);
{ Lookups — case-insensitive, O(log n) via sorted TStringList.
Return nil when the name is not present. }
function FindType(const AName: string): TTypeEntry;
function FindConst(const AName: string): TConstEntry;
function FindRoutine(const AName: string): TRoutineSig;
function FindInlineBody(const ARoutineName: string): TInlineBody;
function FindGeneric(const AName: string): TGenericBody;
{ True iff the interface exports a publicly-visible symbol of
this name. Probes types, consts, vars, routines, generic
bodies, and enum-member names (each member of every enum
type counts as its own top-level symbol — matches what
uSemanticImport.RegisterEnum publishes). Used by task #44
uses-chain lookup. }
function HasSymbol(const AName: string): Boolean;
end;
{ Helpers for TQualTypeRef. }
function MakeQualRef(const AUnit, AType: string): TQualTypeRef;
function MakeBuiltinRef(const AType: string): TQualTypeRef;
function MakeLocalRef(const AType: string): TQualTypeRef;
function IsBuiltinRef(const ARef: TQualTypeRef): Boolean;
function IsLocalRef(const ARef: TQualTypeRef): Boolean;
function QualRefIsNil(const ARef: TQualTypeRef): Boolean;
function QualRefEqual(const A, B: TQualTypeRef): Boolean;
implementation
{ ----- TQualTypeRef helpers -------------------------------------- }
function MakeQualRef(const AUnit, AType: string): TQualTypeRef;
begin
Result.UnitName := AUnit;
Result.TypeName := AType;
end;
function MakeBuiltinRef(const AType: string): TQualTypeRef;
begin
Result.UnitName := QUALREF_BUILTIN;
Result.TypeName := AType;
end;
function MakeLocalRef(const AType: string): TQualTypeRef;
begin
Result.UnitName := QUALREF_THIS_UNIT;
Result.TypeName := AType;
end;
function IsBuiltinRef(const ARef: TQualTypeRef): Boolean;
begin
Result := ARef.UnitName = QUALREF_BUILTIN;
end;
function IsLocalRef(const ARef: TQualTypeRef): Boolean;
begin
Result := ARef.UnitName = QUALREF_THIS_UNIT;
end;
function QualRefIsNil(const ARef: TQualTypeRef): Boolean;
begin
Result := ARef.TypeName = '';
end;
function QualRefEqual(const A, B: TQualTypeRef): Boolean;
begin
Result := (A.UnitName = B.UnitName) and (A.TypeName = B.TypeName);
end;
{ ----- TRoutineSig ----------------------------------------------- }
constructor TRoutineSig.Create;
begin
inherited Create();
Params := TObjectList.Create(True);
ReturnType := MakeQualRef('', '');
VTableSlot := -1;
end;
destructor TRoutineSig.Destroy;
begin
Params.Free();
inherited Destroy();
end;
{ ----- TConstEntry ----------------------------------------------- }
destructor TConstEntry.Destroy;
begin
Decl.Free();
inherited Destroy();
end;
{ ----- TTypeEntry ------------------------------------------------ }
constructor TTypeEntry.Create;
begin
inherited Create();
Implements := TStringList.Create();
Methods := TObjectList.Create(True);
VTableLayout := TObjectList.Create(False); { non-owning }
Attributes := TStringList.Create();
end;
destructor TTypeEntry.Destroy;
begin
Attributes.Free();
VTableLayout.Free(); { non-owning — just refs into Methods }
Methods.Free();
Implements.Free();
Def.Free();
inherited Destroy();
end;
{ ----- TInlineBody ----------------------------------------------- }
destructor TInlineBody.Destroy;
begin
Block.Free();
inherited Destroy();
end;
{ ----- TGenericBody ---------------------------------------------- }
constructor TGenericBody.Create;
begin
inherited Create();
TypeParams := TStringList.Create();
Constraints := TStringList.Create();
end;
destructor TGenericBody.Destroy;
begin
MethodDecl.Free();
Body.Free();
RoutineSig.Free();
TypeDef.Free();
Constraints.Free();
TypeParams.Free();
inherited Destroy();
end;
{ ----- TUnitInterface -------------------------------------------- }
{ Build a sorted TStringList with the requested case sensitivity. }
function MakeIndex(ACaseSensitive: Boolean): TStringList;
begin
Result := TStringList.Create();
Result.CaseSensitive := ACaseSensitive;
Result.Sorted := True;
end;
constructor TUnitInterface.Create(const AName: string;
ACaseSensitive: Boolean = False);
begin
inherited Create();
Name := AName;
UsedUnits := TStringList.Create();
ImplUsedUnits := TStringList.Create();
Types := TObjectList.Create(True);
Consts := TObjectList.Create(True);
Vars := TObjectList.Create(True);
Routines := TObjectList.Create(True);
InlineBodies := TObjectList.Create(True);
GenericBodies := TObjectList.Create(True);
FTypeIndex := MakeIndex(ACaseSensitive);
FConstIndex := MakeIndex(ACaseSensitive);
FRoutineIndex := MakeIndex(ACaseSensitive);
FInlineIndex := MakeIndex(ACaseSensitive);
FGenericIndex := MakeIndex(ACaseSensitive);
end;
destructor TUnitInterface.Destroy;
begin
FGenericIndex.Free();
FInlineIndex.Free();
FRoutineIndex.Free();
FConstIndex.Free();
FTypeIndex.Free();
GenericBodies.Free();
InlineBodies.Free();
Routines.Free();
Vars.Free();
Consts.Free();
Types.Free();
ImplUsedUnits.Free();
UsedUnits.Free();
inherited Destroy();
end;
procedure TUnitInterface.AddType(AEntry: TTypeEntry);
begin
Types.Add(AEntry);
FTypeIndex.AddObject(AEntry.Name, AEntry);
end;
procedure TUnitInterface.AddConst(AEntry: TConstEntry);
begin
Consts.Add(AEntry);
FConstIndex.AddObject(AEntry.Decl.Name, AEntry);
end;
procedure TUnitInterface.AddVar(AEntry: TVarEntry);
begin
Vars.Add(AEntry);
end;
procedure TUnitInterface.AddRoutine(ASig: TRoutineSig);
begin
Routines.Add(ASig);
FRoutineIndex.AddObject(ASig.Name, ASig);
end;
procedure TUnitInterface.AddInlineBody(ABody: TInlineBody);
begin
InlineBodies.Add(ABody);
FInlineIndex.AddObject(ABody.RoutineName, ABody);
end;
procedure TUnitInterface.AddGenericBody(ABody: TGenericBody);
begin
GenericBodies.Add(ABody);
FGenericIndex.AddObject(ABody.Name, ABody);
end;
function TUnitInterface.FindType(const AName: string): TTypeEntry;
var
Idx: Integer;
begin
Idx := FTypeIndex.IndexOf(AName);
if Idx >= 0 then Result := TTypeEntry(FTypeIndex.Objects[Idx])
else Result := nil;
end;
function TUnitInterface.FindConst(const AName: string): TConstEntry;
var
Idx: Integer;
begin
Idx := FConstIndex.IndexOf(AName);
if Idx >= 0 then Result := TConstEntry(FConstIndex.Objects[Idx])
else Result := nil;
end;
function TUnitInterface.FindRoutine(const AName: string): TRoutineSig;
var
Idx: Integer;
begin
Idx := FRoutineIndex.IndexOf(AName);
if Idx >= 0 then Result := TRoutineSig(FRoutineIndex.Objects[Idx])
else Result := nil;
end;
function TUnitInterface.FindInlineBody(const ARoutineName: string): TInlineBody;
var
Idx: Integer;
begin
Idx := FInlineIndex.IndexOf(ARoutineName);
if Idx >= 0 then Result := TInlineBody(FInlineIndex.Objects[Idx])
else Result := nil;
end;
function TUnitInterface.FindGeneric(const AName: string): TGenericBody;
var
Idx: Integer;
begin
Idx := FGenericIndex.IndexOf(AName);
if Idx >= 0 then Result := TGenericBody(FGenericIndex.Objects[Idx])
else Result := nil;
end;
function TUnitInterface.HasSymbol(const AName: string): Boolean;
var
I, J: Integer;
T: TTypeEntry;
V: TVarEntry;
EnumDef: TEnumTypeDef;
begin
if FindType(AName) <> nil then begin Result := True; Exit; end;
if FindConst(AName) <> nil then begin Result := True; Exit; end;
if FindRoutine(AName) <> nil then begin Result := True; Exit; end;
if FindGeneric(AName) <> nil then begin Result := True; Exit; end;
{ Vars lack an index — linear walk. }
for I := 0 to Vars.Count - 1 do
begin
V := TVarEntry(Vars.Items[I]);
if SameText(V.Name, AName) then
begin
Result := True;
Exit;
end;
end;
{ Enum members are top-level identifiers (uSemanticImport.RegisterEnum
Define's each as skConstant). Walk types, look inside enum defs. }
for I := 0 to Types.Count - 1 do
begin
T := TTypeEntry(Types.Items[I]);
if T.Def is TEnumTypeDef then
begin
EnumDef := TEnumTypeDef(T.Def);
for J := 0 to EnumDef.Members.Count - 1 do
if SameText(EnumDef.Members.Strings[J], AName) then
begin
Result := True;
Exit;
end;
end;
end;
Result := False;
end;
end.