From e723e80063c439f82aa8a552729c47bf02c0e2bc Mon Sep 17 00:00:00 2001 From: Graeme Geldenhuys Date: Wed, 24 Jun 2026 19:15:20 +0100 Subject: [PATCH] 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 (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). --- .../src/main/pascal/blaise.codegen.native.pas | 13 +- .../main/pascal/blaise.codegen.toolkit.pas | 234 ++++++++++++++++++ .../src/main/pascal/blaise.linker.elf.pas | 11 + compiler/src/test/pascal/TestRunner.pas | 1 + .../src/test/pascal/cp.test.targettoolkit.pas | 141 +++++++++++ 5 files changed, 396 insertions(+), 4 deletions(-) create mode 100644 compiler/src/main/pascal/blaise.codegen.toolkit.pas create mode 100644 compiler/src/test/pascal/cp.test.targettoolkit.pas diff --git a/compiler/src/main/pascal/blaise.codegen.native.pas b/compiler/src/main/pascal/blaise.codegen.native.pas index f19371c..1218751 100644 --- a/compiler/src/main/pascal/blaise.codegen.native.pas +++ b/compiler/src/main/pascal/blaise.codegen.native.pas @@ -76,19 +76,24 @@ function CreateNativeBackend(const ATarget: TTargetDesc): TNativeBackend; implementation uses - blaise.codegen.native.x86_64; + blaise.codegen.toolkit; { ------------------------------------------------------------------ } { Backend factory } { ------------------------------------------------------------------ } function CreateNativeBackend(const ATarget: TTargetDesc): TNativeBackend; +var + Toolkit: TTargetToolkit; begin - if (ATarget.OS = osLinux) and (ATarget.CPU = cpuX86_64) then - Result := TX86_64Backend.Create(ATarget) - else + { Resolve the per-target adapter family from the registry (Abstract + Factory). Adding a target is a new toolkit + one registration — this + dispatch never changes. See docs/native-target-architecture.adoc. } + Toolkit := ResolveToolkit(ATarget); + if Toolkit = nil then raise ENativeCodeGenError.Create( 'native backend not yet implemented for target ' + TargetName(ATarget)); + Result := Toolkit.MakeBackend(); end; { ------------------------------------------------------------------ } diff --git a/compiler/src/main/pascal/blaise.codegen.toolkit.pas b/compiler/src/main/pascal/blaise.codegen.toolkit.pas new file mode 100644 index 0000000..afc3284 --- /dev/null +++ b/compiler/src/main/pascal/blaise.codegen.toolkit.pas @@ -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 "-" 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; + 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.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. diff --git a/compiler/src/main/pascal/blaise.linker.elf.pas b/compiler/src/main/pascal/blaise.linker.elf.pas index fd19ac3..5741996 100644 --- a/compiler/src/main/pascal/blaise.linker.elf.pas +++ b/compiler/src/main/pascal/blaise.linker.elf.pas @@ -84,6 +84,10 @@ type { Linux x86-64 ELF, non-PIE ET_EXEC. Caller frees. } 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 { One output section accumulating contributions from input objects. } TMergedSection = class @@ -611,6 +615,13 @@ begin { defaults already describe Linux x86-64 } 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 ----------------------------------------------------------- } constructor TLinker.Create; diff --git a/compiler/src/test/pascal/TestRunner.pas b/compiler/src/test/pascal/TestRunner.pas index 2899a63..10cb725 100644 --- a/compiler/src/test/pascal/TestRunner.pas +++ b/compiler/src/test/pascal/TestRunner.pas @@ -130,6 +130,7 @@ uses cp.test.strutils, cp.test.e2e.strutils, cp.test.toolchain, + cp.test.targettoolkit, cp.test.dynarray, cp.test.math, cp.test.e2e.math, diff --git a/compiler/src/test/pascal/cp.test.targettoolkit.pas b/compiler/src/test/pascal/cp.test.targettoolkit.pas new file mode 100644 index 0000000..4f0845a --- /dev/null +++ b/compiler/src/test/pascal/cp.test.targettoolkit.pas @@ -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.