From c1f14b5c782a5b58f905c609d871ea74948e5fff Mon Sep 17 00:00:00 2001 From: Graeme Geldenhuys Date: Tue, 30 Jun 2026 17:44:23 +0100 Subject: [PATCH] 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 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. --- compiler/src/main/pascal/uAST.pas | 19 ++++++++++ compiler/src/main/pascal/uParser.pas | 38 +++++++++++++++++++ compiler/src/main/pascal/uSemanticExport.pas | 7 ++++ compiler/src/main/pascal/uUnitInterface.pas | 10 +++++ compiler/src/main/pascal/uUnitInterfaceIO.pas | 13 ++++++- .../src/test/pascal/cp.test.e2e.external.pas | 17 +++++++++ compiler/src/test/pascal/cp.test.external.pas | 33 ++++++++++++++++ .../src/test/pascal/cp.test.unitinterface.pas | 33 ++++++++++++++-- tools/bif-coverage/bif-coverage.status | 1 + 9 files changed, 166 insertions(+), 5 deletions(-) diff --git a/compiler/src/main/pascal/uAST.pas b/compiler/src/main/pascal/uAST.pas index 9272e43..dd5e094 100644 --- a/compiler/src/main/pascal/uAST.pas +++ b/compiler/src/main/pascal/uAST.pas @@ -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 on ELF, .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 diff --git a/compiler/src/main/pascal/uParser.pas b/compiler/src/main/pascal/uParser.pas index 982ac29..fe542fd 100644 --- a/compiler/src/main/pascal/uParser.pas +++ b/compiler/src/main/pascal/uParser.pas @@ -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; diff --git a/compiler/src/main/pascal/uSemanticExport.pas b/compiler/src/main/pascal/uSemanticExport.pas index 5c9297d..a02d366 100644 --- a/compiler/src/main/pascal/uSemanticExport.pas +++ b/compiler/src/main/pascal/uSemanticExport.pas @@ -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; diff --git a/compiler/src/main/pascal/uUnitInterface.pas b/compiler/src/main/pascal/uUnitInterface.pas index 7ac82e3..d7004d4 100644 --- a/compiler/src/main/pascal/uUnitInterface.pas +++ b/compiler/src/main/pascal/uUnitInterface.pas @@ -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 / + .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(); diff --git a/compiler/src/main/pascal/uUnitInterfaceIO.pas b/compiler/src/main/pascal/uUnitInterfaceIO.pas index 655846d..8366f39 100644 --- a/compiler/src/main/pascal/uUnitInterfaceIO.pas +++ b/compiler/src/main/pascal/uUnitInterfaceIO.pas @@ -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'); diff --git a/compiler/src/test/pascal/cp.test.e2e.external.pas b/compiler/src/test/pascal/cp.test.e2e.external.pas index 6e6c9ff..81b3ea9 100644 --- a/compiler/src/test/pascal/cp.test.e2e.external.pas +++ b/compiler/src/test/pascal/cp.test.e2e.external.pas @@ -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; diff --git a/compiler/src/test/pascal/cp.test.external.pas b/compiler/src/test/pascal/cp.test.external.pas index 449e2a0..58bbf2f 100644 --- a/compiler/src/test/pascal/cp.test.external.pas +++ b/compiler/src/test/pascal/cp.test.external.pas @@ -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. } + 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; diff --git a/compiler/src/test/pascal/cp.test.unitinterface.pas b/compiler/src/test/pascal/cp.test.unitinterface.pas index 80c6306..de0e05d 100644 --- a/compiler/src/test/pascal/cp.test.unitinterface.pas +++ b/compiler/src/test/pascal/cp.test.unitinterface.pas @@ -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; diff --git a/tools/bif-coverage/bif-coverage.status b/tools/bif-coverage/bif-coverage.status index a47dd9d..3f272ed 100644 --- a/tools/bif-coverage/bif-coverage.status +++ b/tools/bif-coverage/bif-coverage.status @@ -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)