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. }
IsExternal: Boolean; { declared with 'external' directive — no body }
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
block that owns the whole frame; codegen
emits no compiler prologue/epilogue }
@ -1067,6 +1068,19 @@ type
destructor Destroy; override;
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 }
{ ------------------------------------------------------------------ }
@ -1082,6 +1096,7 @@ type
GenericMethodInstances: TObjectList; { owned TGenericMethodInstance — populated by uSemantic }
GenericIntfInstances: TObjectList; { owned TGenericInterfaceInstance — populated by uSemantic }
GenericRecordInstances: TObjectList; { owned TGenericRecordInstance — populated by uSemantic }
LinkLibs: TObjectList; { owned TLinkLibDecl — libraries to link, collected by the parser }
constructor Create;
destructor Destroy; override;
end;
@ -1105,6 +1120,7 @@ type
GenericMethodInstances: TObjectList;
GenericIntfInstances: TObjectList;
GenericRecordInstances: TObjectList;
LinkLibs: TObjectList; { owned TLinkLibDecl — libraries to link, collected by the parser }
constructor Create;
destructor Destroy; override;
end;
@ -1811,6 +1827,7 @@ begin
GenericMethodInstances := TObjectList.Create(True);
GenericIntfInstances := TObjectList.Create(True);
GenericRecordInstances := TObjectList.Create(True);
LinkLibs := TObjectList.Create(True);
end;
destructor TConstDecl.Destroy;
@ -1841,6 +1858,7 @@ begin
GenericMethodInstances := TObjectList.Create(True);
GenericIntfInstances := TObjectList.Create(True);
GenericRecordInstances := TObjectList.Create(True);
LinkLibs := TObjectList.Create(True);
end;
destructor TUnit.Destroy;
@ -2437,6 +2455,7 @@ begin
Result.IsPublished := ASrc.IsPublished;
Result.IsExternal := ASrc.IsExternal;
Result.ExternalName := ASrc.ExternalName;
Result.ExternalLib := ASrc.ExternalLib;
Result.CallingConv := ASrc.CallingConv;
Result.IsRecordMethod := ASrc.IsRecordMethod;
{ 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
'System'. Consulted purely as a syntactic name list
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 IsUnitPrefix(const AName: string): Boolean;
{ Unbounded lookahead, exposed as scalar accessors (0 = FCurrent). A
@ -261,6 +266,20 @@ begin
end;
{ 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;
var
I: Integer;
@ -754,6 +773,7 @@ end;
function TParser.ParseProgram: TProgram;
begin
Result := TProgram.Create();
FLinkLibs := Result.LinkLibs; { collect external 'lib' deps into the program }
try
Result.Line := FCurrent.Line;
Result.Col := FCurrent.Col;
@ -2505,6 +2525,15 @@ begin
begin
Result.IsExternal := True;
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' }
if Check(tkIdent) and SameText(FCurrent.Value, 'name') then
begin
@ -4471,6 +4500,14 @@ begin
begin
Result.IsExternal := True;
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
begin
Advance();
@ -4523,6 +4560,7 @@ var
InitStmt: TASTStmt;
begin
Result := TUnit.Create();
FLinkLibs := Result.LinkLibs; { collect external 'lib' deps into the unit }
try
Result.Line := FCurrent.Line;
Result.Col := FCurrent.Col;

View file

@ -481,6 +481,13 @@ begin
if AUnit.ImplUsedUnits <> nil then
for I := 0 to AUnit.ImplUsedUnits.Count - 1 do
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 :=
(AUnit.InitStmts <> nil) and (AUnit.InitStmts.Count > 0);
end;

View file

@ -204,6 +204,14 @@ type
that loads this unit from its cached .bif
still pulls in (and links) impl-only
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
section. An incremental rebuild loads this
unit from its cached .bif and must still
@ -402,6 +410,7 @@ begin
Name := AName;
UsedUnits := TStringList.Create();
ImplUsedUnits := TStringList.Create();
LinkLibs := TStringList.Create();
Types := TObjectList.Create(True);
Consts := TObjectList.Create(True);
@ -432,6 +441,7 @@ begin
Consts.Free();
Types.Free();
LinkLibs.Free();
ImplUsedUnits.Free();
UsedUnits.Free();
inherited Destroy();

View file

@ -53,7 +53,14 @@ uses
const
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/
ReadMethodSig (one extra byte after IsStatic) and
TMethodDecl.IsOverload added to EncodeMethodDecl/
@ -735,6 +742,7 @@ begin
EncodeInt64(AIface.SourceModTime) +
EncodeStringList(AIface.UsedUnits) +
EncodeStringList(AIface.ImplUsedUnits) +
EncodeStringList(AIface.LinkLibs) +
EncodeBool(AIface.HasInitialization));
SB.AppendLine('END');
Result := SB.ToString();
@ -2120,6 +2128,9 @@ begin
C := DecodeCount(AText, APos);
for I := 1 to C do
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';
if ReadTag(AText, APos) <> 'END' then
raise EIfaceFormatError.Create('META block: missing END marker');

View file

@ -26,6 +26,11 @@ type
{ Basic external call — strlen via external name }
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
as Byte/Word/SmallInt upper bits must be masked/sign-extended. }
procedure TestRun_ExternalByteReturn_MaskedCorrectly;
@ -70,6 +75,18 @@ begin
AssertRunsOnAll(Src, '5' + Chr(10), 0);
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;
const Src = '''
program T;

View file

@ -26,6 +26,9 @@ type
procedure TestParse_ExternalProc_IsExternal;
procedure TestParse_ExternalProc_ExternalNameEmpty;
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 }
procedure TestParse_ExternalFunc_IsExternal;
procedure TestParse_ExternalFuncNamed_ExternalName;
@ -199,6 +202,36 @@ begin
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;
var
Prog: TProgram;

View file

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

View file

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