feat(parser): external 'lib' name directive + link-library propagation

`external 'c' name 'strlen'` now records the bare library name on the
declaration (TMethodDecl.ExternalLib) and hoists it into the owning
unit/program's LinkLibs set, so the link layer can expand it to a
-l<name> dependency.  The library clause is optional and parsed before
the existing `name '...'` clause, so `external name 'x'` and bare
`external 'c'` both still parse.

TUnitInterface.LinkLibs is serialised in the .bif META block (one
EncodeStringList after ImplUsedUnits, before HasInitialization) so a
unit's external-library dependencies round-trip across separate
compilation.  IFACE_VERSION is bumped 6 -> 7: the META layout grew, so
a v6 reader must reject these .bif and recompile (without the bump a v6
binary would misparse the extra field and read past the END marker).

Tests: a parser test asserting `external 'c' name 'strlen'` populates
ExternalLib + the program's LinkLibs; a .bif round-trip test for
TUnitInterface.LinkLibs; the magic/version assertion updated to v7; and
the existing e2e strlen case.  bif-coverage.status records
TUnitInterface.LinkLibs as serialised.
This commit is contained in:
Graeme Geldenhuys 2026-06-30 17:44:23 +01:00
parent e2ee936663
commit c1f14b5c78
9 changed files with 166 additions and 5 deletions

View file

@ -863,6 +863,7 @@ type
consumed by codegen for cross-unit references. } consumed by codegen for cross-unit references. }
IsExternal: Boolean; { declared with 'external' directive — no body } IsExternal: Boolean; { declared with 'external' directive — no body }
ExternalName: string; { C symbol name from 'external name ''c_foo'''; empty = use Pascal name } ExternalName: string; { C symbol name from 'external name ''c_foo'''; empty = use Pascal name }
ExternalLib: string; { library from 'external ''c'' name ''malloc'''; bare name, link layer expands per platform. Also recorded unit-level in LinkLibs. }
NoStackFrame: Boolean; { declared 'nostackframe' — body is an asm NoStackFrame: Boolean; { declared 'nostackframe' — body is an asm
block that owns the whole frame; codegen block that owns the whole frame; codegen
emits no compiler prologue/epilogue } emits no compiler prologue/epilogue }
@ -1067,6 +1068,19 @@ type
destructor Destroy; override; destructor Destroy; override;
end; end;
{ A library this compilation unit must be linked against — collected by the
parser from every 'external ''lib''' declaration (interface OR
implementation). Hoisted to the unit level so an implementation-private
import still records its link dependency: the private routine stays out of
the .bif, but the library it needs is serialised via
TUnitInterface.LinkLibs and propagates to any program that uses the unit.
LibName is the bare name ('c', 'kernel32') — the link layer expands it per
platform (-l<name> on ELF, <name>.dll on PE). }
TLinkLibDecl = class(TASTNode)
public
LibName: string;
end;
{ ------------------------------------------------------------------ } { ------------------------------------------------------------------ }
{ Block and Program } { Block and Program }
{ ------------------------------------------------------------------ } { ------------------------------------------------------------------ }
@ -1082,6 +1096,7 @@ type
GenericMethodInstances: TObjectList; { owned TGenericMethodInstance — populated by uSemantic } GenericMethodInstances: TObjectList; { owned TGenericMethodInstance — populated by uSemantic }
GenericIntfInstances: TObjectList; { owned TGenericInterfaceInstance — populated by uSemantic } GenericIntfInstances: TObjectList; { owned TGenericInterfaceInstance — populated by uSemantic }
GenericRecordInstances: TObjectList; { owned TGenericRecordInstance — populated by uSemantic } GenericRecordInstances: TObjectList; { owned TGenericRecordInstance — populated by uSemantic }
LinkLibs: TObjectList; { owned TLinkLibDecl — libraries to link, collected by the parser }
constructor Create; constructor Create;
destructor Destroy; override; destructor Destroy; override;
end; end;
@ -1105,6 +1120,7 @@ type
GenericMethodInstances: TObjectList; GenericMethodInstances: TObjectList;
GenericIntfInstances: TObjectList; GenericIntfInstances: TObjectList;
GenericRecordInstances: TObjectList; GenericRecordInstances: TObjectList;
LinkLibs: TObjectList; { owned TLinkLibDecl — libraries to link, collected by the parser }
constructor Create; constructor Create;
destructor Destroy; override; destructor Destroy; override;
end; end;
@ -1811,6 +1827,7 @@ begin
GenericMethodInstances := TObjectList.Create(True); GenericMethodInstances := TObjectList.Create(True);
GenericIntfInstances := TObjectList.Create(True); GenericIntfInstances := TObjectList.Create(True);
GenericRecordInstances := TObjectList.Create(True); GenericRecordInstances := TObjectList.Create(True);
LinkLibs := TObjectList.Create(True);
end; end;
destructor TConstDecl.Destroy; destructor TConstDecl.Destroy;
@ -1841,6 +1858,7 @@ begin
GenericMethodInstances := TObjectList.Create(True); GenericMethodInstances := TObjectList.Create(True);
GenericIntfInstances := TObjectList.Create(True); GenericIntfInstances := TObjectList.Create(True);
GenericRecordInstances := TObjectList.Create(True); GenericRecordInstances := TObjectList.Create(True);
LinkLibs := TObjectList.Create(True);
end; end;
destructor TUnit.Destroy; destructor TUnit.Destroy;
@ -2437,6 +2455,7 @@ begin
Result.IsPublished := ASrc.IsPublished; Result.IsPublished := ASrc.IsPublished;
Result.IsExternal := ASrc.IsExternal; Result.IsExternal := ASrc.IsExternal;
Result.ExternalName := ASrc.ExternalName; Result.ExternalName := ASrc.ExternalName;
Result.ExternalLib := ASrc.ExternalLib;
Result.CallingConv := ASrc.CallingConv; Result.CallingConv := ASrc.CallingConv;
Result.IsRecordMethod := ASrc.IsRecordMethod; Result.IsRecordMethod := ASrc.IsRecordMethod;
{ Static (class-level) methods take no implicit Self. Dropping this in the { Static (class-level) methods take no implicit Self. Dropping this in the

View file

@ -55,7 +55,12 @@ type
FUsedUnits: TStringList; { unit names from every 'uses' clause, plus implicit FUsedUnits: TStringList; { unit names from every 'uses' clause, plus implicit
'System'. Consulted purely as a syntactic name list 'System'. Consulted purely as a syntactic name list
to recognise 'UnitName.Symbol' — no symbol lookup. } to recognise 'UnitName.Symbol' — no symbol lookup. }
FLinkLibs: TObjectList; { non-owning ref to the current TUnit/TProgram.LinkLibs;
external 'lib' decls append a TLinkLibDecl here as they
are parsed (any section). nil outside a unit/program. }
{ Record a 'link library X' dependency on the unit/program being parsed. }
procedure AddLinkLib(const ALibName: string; ALine, ACol: Integer);
function IsUsedUnit(const AName: string): Boolean; function IsUsedUnit(const AName: string): Boolean;
function IsUnitPrefix(const AName: string): Boolean; function IsUnitPrefix(const AName: string): Boolean;
{ Unbounded lookahead, exposed as scalar accessors (0 = FCurrent). A { Unbounded lookahead, exposed as scalar accessors (0 = FCurrent). A
@ -261,6 +266,20 @@ begin
end; end;
{ Case-insensitive membership test against the parsed 'uses' names. } { Case-insensitive membership test against the parsed 'uses' names. }
{ Append a 'link library ALibName' dependency to the unit/program being parsed.
Duplicates are tolerated — the exporter dedupes when building the .bif set. }
procedure TParser.AddLinkLib(const ALibName: string; ALine, ACol: Integer);
var
LL: TLinkLibDecl;
begin
if (FLinkLibs = nil) or (ALibName = '') then Exit;
LL := TLinkLibDecl.Create();
LL.LibName := ALibName;
LL.Line := ALine;
LL.Col := ACol;
FLinkLibs.Add(LL);
end;
function TParser.IsUsedUnit(const AName: string): Boolean; function TParser.IsUsedUnit(const AName: string): Boolean;
var var
I: Integer; I: Integer;
@ -754,6 +773,7 @@ end;
function TParser.ParseProgram: TProgram; function TParser.ParseProgram: TProgram;
begin begin
Result := TProgram.Create(); Result := TProgram.Create();
FLinkLibs := Result.LinkLibs; { collect external 'lib' deps into the program }
try try
Result.Line := FCurrent.Line; Result.Line := FCurrent.Line;
Result.Col := FCurrent.Col; Result.Col := FCurrent.Col;
@ -2505,6 +2525,15 @@ begin
begin begin
Result.IsExternal := True; Result.IsExternal := True;
Advance(); Advance();
{ optional: a library name string — external 'c' name 'malloc'.
Records a unit-level link dependency (works for implementation
imports too, so the private routine's library still propagates). }
if Check(tkStringLit) then
begin
Result.ExternalLib := FCurrent.Value;
AddLinkLib(FCurrent.Value, FCurrent.Line, FCurrent.Col);
Advance();
end;
{ optional: name 'c_symbol' } { optional: name 'c_symbol' }
if Check(tkIdent) and SameText(FCurrent.Value, 'name') then if Check(tkIdent) and SameText(FCurrent.Value, 'name') then
begin begin
@ -4471,6 +4500,14 @@ begin
begin begin
Result.IsExternal := True; Result.IsExternal := True;
Advance(); Advance();
{ optional library name — external 'c' name 'malloc' (records a
unit-level link dependency). }
if Check(tkStringLit) then
begin
Result.ExternalLib := FCurrent.Value;
AddLinkLib(FCurrent.Value, FCurrent.Line, FCurrent.Col);
Advance();
end;
if Check(tkIdent) and SameText(FCurrent.Value, 'name') then if Check(tkIdent) and SameText(FCurrent.Value, 'name') then
begin begin
Advance(); Advance();
@ -4523,6 +4560,7 @@ var
InitStmt: TASTStmt; InitStmt: TASTStmt;
begin begin
Result := TUnit.Create(); Result := TUnit.Create();
FLinkLibs := Result.LinkLibs; { collect external 'lib' deps into the unit }
try try
Result.Line := FCurrent.Line; Result.Line := FCurrent.Line;
Result.Col := FCurrent.Col; Result.Col := FCurrent.Col;

View file

@ -481,6 +481,13 @@ begin
if AUnit.ImplUsedUnits <> nil then if AUnit.ImplUsedUnits <> nil then
for I := 0 to AUnit.ImplUsedUnits.Count - 1 do for I := 0 to AUnit.ImplUsedUnits.Count - 1 do
AIface.ImplUsedUnits.Add(AUnit.ImplUsedUnits.Strings[I]); AIface.ImplUsedUnits.Add(AUnit.ImplUsedUnits.Strings[I]);
{ Link libraries — hoisted from every 'external ''lib''' decl (interface or
implementation), deduped. Carries the unit's link dependencies into the
.bif even when the importing routine is implementation-private. }
if AUnit.LinkLibs <> nil then
for I := 0 to AUnit.LinkLibs.Count - 1 do
if AIface.LinkLibs.IndexOf(TLinkLibDecl(AUnit.LinkLibs.Items[I]).LibName) < 0 then
AIface.LinkLibs.Add(TLinkLibDecl(AUnit.LinkLibs.Items[I]).LibName);
AIface.HasInitialization := AIface.HasInitialization :=
(AUnit.InitStmts <> nil) and (AUnit.InitStmts.Count > 0); (AUnit.InitStmts <> nil) and (AUnit.InitStmts.Count > 0);
end; end;

View file

@ -204,6 +204,14 @@ type
that loads this unit from its cached .bif that loads this unit from its cached .bif
still pulls in (and links) impl-only still pulls in (and links) impl-only
dependencies. } dependencies. }
LinkLibs: TStringList; { owned — bare library names this unit must be
linked against (deduped), hoisted from every
'external ''lib''' decl, interface OR
implementation. An implementation-private
import keeps its symbol out of the .bif but
its library still propagates here, so a
downstream program links it (-l<name> /
<name>.dll). }
HasInitialization: Boolean; { unit has a non-empty initialization HasInitialization: Boolean; { unit has a non-empty initialization
section. An incremental rebuild loads this section. An incremental rebuild loads this
unit from its cached .bif and must still unit from its cached .bif and must still
@ -402,6 +410,7 @@ begin
Name := AName; Name := AName;
UsedUnits := TStringList.Create(); UsedUnits := TStringList.Create();
ImplUsedUnits := TStringList.Create(); ImplUsedUnits := TStringList.Create();
LinkLibs := TStringList.Create();
Types := TObjectList.Create(True); Types := TObjectList.Create(True);
Consts := TObjectList.Create(True); Consts := TObjectList.Create(True);
@ -432,6 +441,7 @@ begin
Consts.Free(); Consts.Free();
Types.Free(); Types.Free();
LinkLibs.Free();
ImplUsedUnits.Free(); ImplUsedUnits.Free();
UsedUnits.Free(); UsedUnits.Free();
inherited Destroy(); inherited Destroy();

View file

@ -53,7 +53,14 @@ uses
const const
IFACE_MAGIC = 'BLAISE-IFACE'; IFACE_MAGIC = 'BLAISE-IFACE';
IFACE_VERSION = 6; { v6 (this cycle): the `overload` directive now round-trips. IFACE_VERSION = 7; { v7 (this cycle): external-library link dependencies now
round-trip. TUnitInterface.LinkLibs is serialised in
the META block (one EncodeStringList after
ImplUsedUnits, before HasInitialization) so a unit's
`external 'lib' name '...'` declarations propagate the
libraries to link. The META layout grew, so v6 readers
must reject these .bif and recompile.
v6: the `overload` directive now round-trips.
TRoutineSig.IsOverload added to EncodeMethodSig/ TRoutineSig.IsOverload added to EncodeMethodSig/
ReadMethodSig (one extra byte after IsStatic) and ReadMethodSig (one extra byte after IsStatic) and
TMethodDecl.IsOverload added to EncodeMethodDecl/ TMethodDecl.IsOverload added to EncodeMethodDecl/
@ -735,6 +742,7 @@ begin
EncodeInt64(AIface.SourceModTime) + EncodeInt64(AIface.SourceModTime) +
EncodeStringList(AIface.UsedUnits) + EncodeStringList(AIface.UsedUnits) +
EncodeStringList(AIface.ImplUsedUnits) + EncodeStringList(AIface.ImplUsedUnits) +
EncodeStringList(AIface.LinkLibs) +
EncodeBool(AIface.HasInitialization)); EncodeBool(AIface.HasInitialization));
SB.AppendLine('END'); SB.AppendLine('END');
Result := SB.ToString(); Result := SB.ToString();
@ -2120,6 +2128,9 @@ begin
C := DecodeCount(AText, APos); C := DecodeCount(AText, APos);
for I := 1 to C do for I := 1 to C do
AIface.ImplUsedUnits.Add(ReadLpstrAt(AText, APos)); AIface.ImplUsedUnits.Add(ReadLpstrAt(AText, APos));
C := DecodeCount(AText, APos);
for I := 1 to C do
AIface.LinkLibs.Add(ReadLpstrAt(AText, APos));
AIface.HasInitialization := ReadLpstrAt(AText, APos) = '1'; AIface.HasInitialization := ReadLpstrAt(AText, APos) = '1';
if ReadTag(AText, APos) <> 'END' then if ReadTag(AText, APos) <> 'END' then
raise EIfaceFormatError.Create('META block: missing END marker'); raise EIfaceFormatError.Create('META block: missing END marker');

View file

@ -26,6 +26,11 @@ type
{ Basic external call — strlen via external name } { Basic external call — strlen via external name }
procedure TestRun_ExternalName_Strlen; procedure TestRun_ExternalName_Strlen;
{ Library-qualified external — external 'c' name 'strlen': the bare library
name is parsed and recorded (propagated via the unit's LinkLibs) while the
symbol resolves as usual. }
procedure TestRun_ExternalLibName_Strlen;
{ Narrow-int return masking: strlen returns size_t (64-bit) but declared { Narrow-int return masking: strlen returns size_t (64-bit) but declared
as Byte/Word/SmallInt — upper bits must be masked/sign-extended. } as Byte/Word/SmallInt — upper bits must be masked/sign-extended. }
procedure TestRun_ExternalByteReturn_MaskedCorrectly; procedure TestRun_ExternalByteReturn_MaskedCorrectly;
@ -70,6 +75,18 @@ begin
AssertRunsOnAll(Src, '5' + Chr(10), 0); AssertRunsOnAll(Src, '5' + Chr(10), 0);
end; end;
procedure TE2EExternalTests.TestRun_ExternalLibName_Strlen;
const Src = '''
program T;
function c_strlen(S: PChar): Integer; external 'c' name 'strlen';
begin
WriteLn(c_strlen(PChar('hello')))
end.
''';
begin
AssertRunsOnAll(Src, '5' + Chr(10), 0);
end;
procedure TE2EExternalTests.TestRun_ExternalByteReturn_MaskedCorrectly; procedure TE2EExternalTests.TestRun_ExternalByteReturn_MaskedCorrectly;
const Src = ''' const Src = '''
program T; program T;

View file

@ -26,6 +26,9 @@ type
procedure TestParse_ExternalProc_IsExternal; procedure TestParse_ExternalProc_IsExternal;
procedure TestParse_ExternalProc_ExternalNameEmpty; procedure TestParse_ExternalProc_ExternalNameEmpty;
procedure TestParse_ExternalProcNamed_ExternalName; procedure TestParse_ExternalProcNamed_ExternalName;
{ Parser — library-qualified external records ExternalLib and hoists
the bare library name into the program's LinkLibs set. }
procedure TestParse_ExternalLibName_RecordsLibAndLinkLib;
{ Parser — standalone function } { Parser — standalone function }
procedure TestParse_ExternalFunc_IsExternal; procedure TestParse_ExternalFunc_IsExternal;
procedure TestParse_ExternalFuncNamed_ExternalName; procedure TestParse_ExternalFuncNamed_ExternalName;
@ -199,6 +202,36 @@ begin
end; end;
end; end;
procedure TExternalTests.TestParse_ExternalLibName_RecordsLibAndLinkLib;
var
Prog: TProgram;
Decl: TMethodDecl;
Lib: TLinkLibDecl;
begin
Prog := ParseSrc(
'''
program Test;
function c_strlen(S: PChar): Integer; external 'c' name 'strlen';
begin
end.
'''
);
try
Decl := TMethodDecl(Prog.Block.ProcDecls.Items[0]);
AssertTrue('IsExternal should be True', Decl.IsExternal);
AssertEquals('ExternalName should be strlen', 'strlen', Decl.ExternalName);
AssertEquals('ExternalLib should be c', 'c', Decl.ExternalLib);
{ The bare library name is hoisted into the program's LinkLibs set so
the link layer can expand it to -l<name>. }
AssertEquals('one link lib collected', 1, Prog.LinkLibs.Count);
Lib := TLinkLibDecl(Prog.LinkLibs.Items[0]);
AssertEquals('link lib name', 'c', Lib.LibName);
finally
Prog.Free();
end;
end;
procedure TExternalTests.TestParse_ExternalFunc_IsExternal; procedure TExternalTests.TestParse_ExternalFunc_IsExternal;
var var
Prog: TProgram; Prog: TProgram;

View file

@ -230,6 +230,8 @@ type
procedure TestWrite_StartsWithMagicAndVersion; procedure TestWrite_StartsWithMagicAndVersion;
{ Unit name round-trips. } { Unit name round-trips. }
procedure TestRoundTrip_UnitNamePreserved; procedure TestRoundTrip_UnitNamePreserved;
{ External-library link deps (LinkLibs) round-trip via the META block. }
procedure TestRoundTrip_LinkLibsPreserved;
{ Int const round-trips. } { Int const round-trips. }
procedure TestRoundTrip_IntConstPreserved; procedure TestRoundTrip_IntConstPreserved;
{ String const round-trips, even with newlines + colons in the value. } { String const round-trips, even with newlines + colons in the value. }
@ -2693,12 +2695,12 @@ begin
Iface := TUnitInterface.Create('U'); Iface := TUnitInterface.Create('U');
try try
Buf := WriteUnitInterface(Iface); Buf := WriteUnitInterface(Iface);
{ Blaise Pos is 0-based; match-at-start returns 0. Version is 6 since the { Blaise Pos is 0-based; match-at-start returns 0. Version is 7 since
`overload` directive (TRoutineSig.IsOverload + TMethodDecl.IsOverload) was TUnitInterface.LinkLibs (external-library link deps) was added to the
added to the method encoded layouts (on top of v5's member Visibility, META block (on top of v6's `overload` directive, v5's member Visibility,
v4's TRoutineSig.IsStatic, and v3's static-member facts). } v4's TRoutineSig.IsStatic, and v3's static-member facts). }
AssertTrue('starts with magic', AssertTrue('starts with magic',
Pos('BLAISE-IFACE 6', Buf) = 0); Pos('BLAISE-IFACE 7', Buf) = 0);
finally finally
Iface.Free(); Iface.Free();
end; end;
@ -2723,6 +2725,29 @@ begin
end; end;
end; end;
procedure TIfaceIOTests.TestRoundTrip_LinkLibsPreserved;
var
Src, Dst: TUnitInterface;
Buf: string;
begin
Src := TUnitInterface.Create('MyUnit');
try
Src.LinkLibs.Add('c');
Src.LinkLibs.Add('m');
Buf := WriteUnitInterface(Src);
Dst := ReadUnitInterface(Buf);
try
AssertEquals('link-lib count', 2, Dst.LinkLibs.Count);
AssertEquals('first lib', 'c', Dst.LinkLibs.Strings[0]);
AssertEquals('second lib', 'm', Dst.LinkLibs.Strings[1]);
finally
Dst.Free();
end;
finally
Src.Free();
end;
end;
procedure TIfaceIOTests.TestRoundTrip_IntConstPreserved; procedure TIfaceIOTests.TestRoundTrip_IntConstPreserved;
var var
Src, Dst: TUnitInterface; Src, Dst: TUnitInterface;

View file

@ -314,6 +314,7 @@ TUnitInterface.SourceModTime serialise
TUnitInterface.CompilerId serialise TUnitInterface.CompilerId serialise
TUnitInterface.UsedUnits serialise TUnitInterface.UsedUnits serialise
TUnitInterface.ImplUsedUnits serialise TUnitInterface.ImplUsedUnits serialise
TUnitInterface.LinkLibs serialise
TUnitInterface.HasInitialization serialise TUnitInterface.HasInitialization serialise
# TMethodParam (uAST.pas:798) # TMethodParam (uAST.pas:798)