feat(codegen): target toolkit + registry; register freebsd-x86_64

Introduce TTargetToolkit (Abstract Factory) and TTargetRegistry so per-target
compiler-side objects (native backend + ELF link target) are resolved by name
rather than hard-coded case statements. CreateNativeBackend now delegates to
the registry; adding a target is a new toolkit + one registration with no edits
to existing dispatch.

Registers linux-x86_64 (unchanged behaviour) and freebsd-x86_64 (link target
with EI_OSABI=9; reuses TX86_64Backend since FreeBSD shares the System V AMD64
ABI). FreeBSD codegen/runtime lands in later steps.

The registry uses TOrderedDictionary<string, TTargetToolkit> (name -> toolkit)
per the Generics.Collections policy. It is built publish-after-populate: the
--incremental compile worker pool is threads, so a half-built global would let
a worker wrongly conclude a target has no backend.

Step 0a of docs/native-target-architecture.adoc. Behaviour-preserving:
FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK, WARMCACHE_FIXPOINT_OK;
full suite 3761 tests green (QBE- and native-built runners).
This commit is contained in:
Graeme Geldenhuys 2026-06-24 19:15:20 +01:00
parent 4384b272c3
commit e723e80063
5 changed files with 396 additions and 4 deletions

View file

@ -76,19 +76,24 @@ function CreateNativeBackend(const ATarget: TTargetDesc): TNativeBackend;
implementation implementation
uses uses
blaise.codegen.native.x86_64; blaise.codegen.toolkit;
{ ------------------------------------------------------------------ } { ------------------------------------------------------------------ }
{ Backend factory } { Backend factory }
{ ------------------------------------------------------------------ } { ------------------------------------------------------------------ }
function CreateNativeBackend(const ATarget: TTargetDesc): TNativeBackend; function CreateNativeBackend(const ATarget: TTargetDesc): TNativeBackend;
var
Toolkit: TTargetToolkit;
begin begin
if (ATarget.OS = osLinux) and (ATarget.CPU = cpuX86_64) then { Resolve the per-target adapter family from the registry (Abstract
Result := TX86_64Backend.Create(ATarget) Factory). Adding a target is a new toolkit + one registration — this
else dispatch never changes. See docs/native-target-architecture.adoc. }
Toolkit := ResolveToolkit(ATarget);
if Toolkit = nil then
raise ENativeCodeGenError.Create( raise ENativeCodeGenError.Create(
'native backend not yet implemented for target ' + TargetName(ATarget)); 'native backend not yet implemented for target ' + TargetName(ATarget));
Result := Toolkit.MakeBackend();
end; end;
{ ------------------------------------------------------------------ } { ------------------------------------------------------------------ }

View file

@ -0,0 +1,234 @@
{
Blaise - An Object Pascal Compiler
Copyright (c) 2026 Graeme Geldenhuys
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.
}
unit blaise.codegen.toolkit;
{ Multi-target toolkit + registry (docs/native-target-architecture.adoc).
A TTargetToolkit is the Abstract Factory that produces the consistent
*family* of per-target compiler-side objects: the native backend and the
link target. Keeping them behind one factory guarantees the family is
coherent — a FreeBSD backend paired with a Linux link target is a bug a
registry-resolved toolkit cannot produce.
TTargetRegistry maps a TTargetDesc to its toolkit. Adding a target is a new
TTargetToolkit subclass plus one registration call — no existing dispatch is
edited (Open/Closed). --target resolution and "does this target have a
native backend?" both go through the registry, replacing the previous
hard-coded case statements in blaise.codegen.native and
blaise.codegen.target.
This is the compiler-side half of the architecture. The runtime-side ports
(TKernelABI / TPlatformLayout) live in the RTL and are introduced in later
steps; they are not part of this unit. }
interface
uses
SysUtils, Generics.Collections,
blaise.codegen.target, blaise.codegen.native.backend, blaise.linker.elf;
type
{ Abstract Factory for one target's compiler-side object family. }
TTargetToolkit = class
public
{ Canonical "<os>-<cpu>" name (matches blaise.codegen.target.TargetName). }
function Name: string; virtual; abstract;
{ Construct the native backend for this target. Caller owns the result. }
function MakeBackend: TNativeBackend; virtual; abstract;
{ Construct the link target (ELF header facts) for this target. Caller
owns the result. }
function MakeLinkTarget: TLinkTarget; virtual; abstract;
end;
{ Linux x86_64 — the original (and, before FreeBSD, only) target. }
TLinuxX86_64Toolkit = class(TTargetToolkit)
public
function Name: string; override;
function MakeBackend: TNativeBackend; override;
function MakeLinkTarget: TLinkTarget; override;
end;
{ FreeBSD x86_64 — shares the System V AMD64 ABI and the x86_64 instruction
selector with Linux, so MakeBackend reuses TX86_64Backend; only the link
target's OS/ABI byte differs. The runtime/syscall divergence is handled
in the RTL (TKernelABI / TPlatformLayout), not here. }
TFreeBSDX86_64Toolkit = class(TTargetToolkit)
public
function Name: string; override;
function MakeBackend: TNativeBackend; override;
function MakeLinkTarget: TLinkTarget; override;
end;
{ Toolkit registry. Owns the registered toolkit instances. A linear list
is correct here: the target count is tiny (one per supported OS/CPU), so a
name scan is cheaper than a hash map and avoids instantiating a generic
map over a class type. }
TTargetRegistry = class
private
FByName: TOrderedDictionary<string, TTargetToolkit>;
public
constructor Create;
destructor Destroy; override;
{ Register a toolkit. Registry takes ownership. }
procedure Register(AToolkit: TTargetToolkit);
{ Resolve the toolkit for ATarget, or nil if none is registered. }
function Resolve(const ATarget: TTargetDesc): TTargetToolkit;
end;
{ The process-wide registry, populated in this unit's initialization. }
function GTargetRegistry: TTargetRegistry;
{ Resolve the toolkit for ATarget via the global registry (nil if unknown). }
function ResolveToolkit(const ATarget: TTargetDesc): TTargetToolkit;
{ True when ATarget has a registered native backend toolkit. Replacement for
blaise.codegen.target.TargetHasNativeBackend, driven by the registry. }
function RegisteredHasNativeBackend(const ATarget: TTargetDesc): Boolean;
implementation
uses
blaise.codegen.native.x86_64;
{ ---- TLinuxX86_64Toolkit --------------------------------------------- }
function TLinuxX86_64Toolkit.Name: string;
begin
Result := 'linux-x86_64';
end;
function TLinuxX86_64Toolkit.MakeBackend: TNativeBackend;
var
T: TTargetDesc;
begin
MakeTarget(osLinux, cpuX86_64, T);
Result := TX86_64Backend.Create(T);
end;
function TLinuxX86_64Toolkit.MakeLinkTarget: TLinkTarget;
begin
Result := LinuxX86_64Target();
end;
{ ---- TFreeBSDX86_64Toolkit ------------------------------------------- }
function TFreeBSDX86_64Toolkit.Name: string;
begin
Result := 'freebsd-x86_64';
end;
function TFreeBSDX86_64Toolkit.MakeBackend: TNativeBackend;
var
T: TTargetDesc;
begin
MakeTarget(osFreeBSD, cpuX86_64, T);
Result := TX86_64Backend.Create(T);
end;
function TFreeBSDX86_64Toolkit.MakeLinkTarget: TLinkTarget;
begin
Result := FreeBSDX86_64Target();
end;
{ ---- TTargetRegistry ------------------------------------------------- }
constructor TTargetRegistry.Create;
begin
inherited Create();
FByName := TOrderedDictionary<string, TTargetToolkit>.Create();
end;
destructor TTargetRegistry.Destroy;
var
I: Integer;
Tk: TTargetToolkit;
begin
if FByName <> nil then
begin
for I := 0 to FByName.Count - 1 do
begin
Tk := FByName.Values[I];
Tk.Free();
end;
FByName.Free();
end;
inherited Destroy();
end;
procedure TTargetRegistry.Register(AToolkit: TTargetToolkit);
begin
FByName.Items[AToolkit.Name()] := AToolkit;
end;
function TTargetRegistry.Resolve(const ATarget: TTargetDesc): TTargetToolkit;
var
Key: string;
Tk: TTargetToolkit;
begin
Key := TargetName(ATarget);
if FByName.TryGetValue(Key, Tk) then
Result := Tk
else
Result := nil;
end;
{ ---- Global registry -------------------------------------------------- }
var
GRegistry: TTargetRegistry;
{ Build the registry on first use. This unit's initialization section calls
EnsureRegistry once at startup (single-threaded, before any worker thread is
spawned), so the common path is a no-op nil check. The lazy guard also
covers the case where a codegen unit's own initialization constructs a
backend before this unit's initialization runs (link-order independence).
Thread-safety note: the incremental compile worker pool is THREADS, not
processes. The registry must be FULLY built before it is published to the
global, or a second thread could observe a non-nil GRegistry whose toolkit
list is still empty and wrongly conclude a target has no backend. Build
into a local and assign the global last (publish-after-populate). }
procedure EnsureRegistry;
var
R: TTargetRegistry;
begin
if GRegistry = nil then
begin
R := TTargetRegistry.Create();
R.Register(TLinuxX86_64Toolkit.Create());
R.Register(TFreeBSDX86_64Toolkit.Create());
GRegistry := R;
end;
end;
function GTargetRegistry: TTargetRegistry;
begin
EnsureRegistry();
Result := GRegistry;
end;
function ResolveToolkit(const ATarget: TTargetDesc): TTargetToolkit;
begin
EnsureRegistry();
Result := GRegistry.Resolve(ATarget);
end;
function RegisteredHasNativeBackend(const ATarget: TTargetDesc): Boolean;
begin
EnsureRegistry();
Result := GRegistry.Resolve(ATarget) <> nil;
end;
initialization
EnsureRegistry();
finalization
GRegistry.Free();
end.

View file

@ -84,6 +84,10 @@ type
{ Linux x86-64 ELF, non-PIE ET_EXEC. Caller frees. } { Linux x86-64 ELF, non-PIE ET_EXEC. Caller frees. }
function LinuxX86_64Target: TLinkTarget; function LinuxX86_64Target: TLinkTarget;
{ FreeBSD x86-64 ELF. Same System V AMD64 machine/arch as Linux; differs
only in EI_OSABI (9 = FreeBSD). Caller frees. }
function FreeBSDX86_64Target: TLinkTarget;
type type
{ One output section accumulating contributions from input objects. } { One output section accumulating contributions from input objects. }
TMergedSection = class TMergedSection = class
@ -611,6 +615,13 @@ begin
{ defaults already describe Linux x86-64 } { defaults already describe Linux x86-64 }
end; end;
function FreeBSDX86_64Target: TLinkTarget;
begin
Result := TLinkTarget.Create();
{ Identical to Linux x86-64 except the OS/ABI byte in the ELF header. }
Result.OSABI := ELFOSABI_FREEBSD;
end;
{ ---- TLinker ----------------------------------------------------------- } { ---- TLinker ----------------------------------------------------------- }
constructor TLinker.Create; constructor TLinker.Create;

View file

@ -130,6 +130,7 @@ uses
cp.test.strutils, cp.test.strutils,
cp.test.e2e.strutils, cp.test.e2e.strutils,
cp.test.toolchain, cp.test.toolchain,
cp.test.targettoolkit,
cp.test.dynarray, cp.test.dynarray,
cp.test.math, cp.test.math,
cp.test.e2e.math, cp.test.e2e.math,

View file

@ -0,0 +1,141 @@
{
Blaise - An Object Pascal Compiler
Copyright (c) 2026 Graeme Geldenhuys
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.
}
unit cp.test.targettoolkit;
{ Tests for the multi-target toolkit + registry (docs/native-target-
architecture.adoc). A TTargetToolkit is the Abstract Factory that produces
the consistent family of per-target objects (backend, link target); the
registry resolves a toolkit by target.
These tests pin the behaviour-preserving Step 0a refactor: the existing
Linux x86_64 target must resolve through the registry, and the FreeBSD
x86_64 target must be registered (its codegen lands in later steps, so we
only assert it is known to the registry here, not that it emits code). }
interface
uses
blaise.testing,
blaise.codegen.target, blaise.codegen.toolkit,
blaise.codegen.native.backend, blaise.linker.elf;
type
TTargetToolkitTests = class(TTestCase)
private
function LinuxTarget: TTargetDesc;
function FreeBSDTarget: TTargetDesc;
published
{ Linux x86_64 resolves to a toolkit, and that toolkit reports its name. }
procedure TestResolve_LinuxX86_64_ReturnsToolkit;
procedure TestResolve_LinuxX86_64_ToolkitName;
{ The Linux toolkit makes a real backend (the existing TX86_64Backend). }
procedure TestLinuxToolkit_MakeBackend_NotNil;
{ The Linux toolkit makes a SysV/Linux link target (OSABI 0). }
procedure TestLinuxToolkit_MakeLinkTarget_IsLinuxOSABI;
{ RegisteredHasNativeBackend mirrors the old TargetHasNativeBackend for
Linux x86_64. }
procedure TestRegistered_LinuxX86_64_HasBackend;
{ FreeBSD x86_64 is registered (resolves to a toolkit). }
procedure TestResolve_FreeBSDX86_64_ReturnsToolkit;
{ FreeBSD toolkit produces a FreeBSD-OSABI link target (OSABI 9). }
procedure TestFreeBSDToolkit_MakeLinkTarget_IsFreeBSDOSABI;
end;
implementation
const
ELFOSABI_SYSV = 0;
ELFOSABI_FREEBSD = 9;
function TTargetToolkitTests.LinuxTarget: TTargetDesc;
begin
MakeTarget(osLinux, cpuX86_64, Result);
end;
function TTargetToolkitTests.FreeBSDTarget: TTargetDesc;
begin
MakeTarget(osFreeBSD, cpuX86_64, Result);
end;
procedure TTargetToolkitTests.TestResolve_LinuxX86_64_ReturnsToolkit;
var
Tk: TTargetToolkit;
begin
Tk := ResolveToolkit(Self.LinuxTarget());
AssertTrue('linux-x86_64 must resolve to a toolkit', Tk <> nil);
end;
procedure TTargetToolkitTests.TestResolve_LinuxX86_64_ToolkitName;
var
Tk: TTargetToolkit;
begin
Tk := ResolveToolkit(Self.LinuxTarget());
AssertEquals('linux-x86_64', Tk.Name());
end;
procedure TTargetToolkitTests.TestLinuxToolkit_MakeBackend_NotNil;
var
Tk: TTargetToolkit;
B: TNativeBackend;
begin
Tk := ResolveToolkit(Self.LinuxTarget());
B := Tk.MakeBackend();
try
AssertTrue('Linux toolkit must produce a backend', B <> nil);
finally
B.Free();
end;
end;
procedure TTargetToolkitTests.TestLinuxToolkit_MakeLinkTarget_IsLinuxOSABI;
var
Tk: TTargetToolkit;
Lt: TLinkTarget;
begin
Tk := ResolveToolkit(Self.LinuxTarget());
Lt := Tk.MakeLinkTarget();
try
AssertEquals(ELFOSABI_SYSV, Lt.OSABI);
finally
Lt.Free();
end;
end;
procedure TTargetToolkitTests.TestRegistered_LinuxX86_64_HasBackend;
begin
AssertTrue('linux-x86_64 must have a native backend',
RegisteredHasNativeBackend(Self.LinuxTarget()));
end;
procedure TTargetToolkitTests.TestResolve_FreeBSDX86_64_ReturnsToolkit;
var
Tk: TTargetToolkit;
begin
Tk := ResolveToolkit(Self.FreeBSDTarget());
AssertTrue('freebsd-x86_64 must resolve to a toolkit', Tk <> nil);
end;
procedure TTargetToolkitTests.TestFreeBSDToolkit_MakeLinkTarget_IsFreeBSDOSABI;
var
Tk: TTargetToolkit;
Lt: TLinkTarget;
begin
Tk := ResolveToolkit(Self.FreeBSDTarget());
Lt := Tk.MakeLinkTarget();
try
AssertEquals(ELFOSABI_FREEBSD, Lt.OSABI);
finally
Lt.Free();
end;
end;
initialization
RegisterTest(TTargetToolkitTests);
end.