docs: kernel leaf is a link-time swap, not a runtime TKernelABI port

Records the Step 0c outcome: a runtime TKernelABI virtual port (30 abstract
methods + a GKernel global, routing all rtl.platform.posix kernel calls through
it with a libc-delegating Linux adapter) was implemented and reverted. It
destabilised self-hosting — binaries built against the modified RTL segfaulted
at startup with an unwindable/overflowed stack, and the failure compounded
across bootstrap stages rather than converging. The Linux adapter was pure libc
delegation, so it carried real self-hosting risk for zero present benefit.

Replacement design: the kernel leaf is a LINK-TIME symbol swap (the pattern
already used for blaise_setjmp/atomic .s objects). The RTL keeps its
'external name' bindings; the FreeBSD archive links a raw-syscall stub object
exporting the same symbols. No runtime virtual dispatch, no new global, no
change to the layout-sensitive RTL units. The kernel mechanism is a build-time
property of a target (libc-linked vs freestanding, never switched at runtime),
so a link-time swap models it exactly while a runtime port does not.

Both docs updated throughout: ports table, units-that-change table, migration
sequence (0a/0b done, kernel-leaf revised, ARC-walker lift deferred as arm64
groundwork), toolkit sketch, summaries. Adds a [#kernel-leaf] rationale note.
This commit is contained in:
Graeme Geldenhuys 2026-06-25 01:02:35 +01:00
parent de27984647
commit 1000fe7686
2 changed files with 207 additions and 121 deletions

View file

@ -251,33 +251,34 @@ subclassing. This backend completes that idiom.
The full structural design lives in
`docs/native-target-architecture.adoc`. In brief, each target is a *family of
adapter classes* produced by an Abstract Factory (`TTargetToolkit`) resolved by
name from a `TTargetRegistry`, with two new runtime ports introduced for
Strategy B:
name from a `TTargetRegistry`. The two per-target seams Strategy B needs:
* `TKernelABI` — the raw syscall + `_start` entry leaf (adapters
`TKernelLinuxX86_64`, `TKernelFreeBSDX86_64`).
* `TPlatformLayout` — `struct stat`/`struct tm` layouts and the OS constants
(`O_*`, signals, `CLOCK_*`, `WNOHANG`, `SEEK_*`), so the Linux values
currently hard-coded inside `TRtlPlatformPosix` move behind a per-target
port instead of leaking into a "shared" adapter.
* *The kernel leaf* — the raw syscalls + `_start` entry stub. This is a
*link-time symbol swap*, NOT a runtime port: the FreeBSD RTL archive links a
syscall-stub object that resolves the same `external name` symbols (`open`,
`read`, …) that libc resolves on Linux. (A runtime `TKernelABI` virtual port
was tried and reverted — it destabilised self-hosting for zero present
benefit; see the kernel-leaf note in the architecture doc.)
* `TPlatformLayout` — `struct stat` layout and the OS constants (`O_*`,
`CLOCK_*`, `WNOHANG`, `SEEK_*`), moved behind a per-target runtime port
(Step 0b, done) instead of leaking into a "shared" adapter.
Target divergence therefore lives in *which adapter object is instantiated*,
decided once at startup — and the test runner can select the FreeBSD adapter
at run time to test it natively on a FreeBSD host, or structurally from Linux.
The implementation steps below assume the structural refactor in the
architecture doc (introduce toolkit/registry, extract `TPlatformLayout`,
introduce `TKernelABI`) lands first as behaviour-preserving refactors against
the existing Linux suite.
Target divergence therefore lives in *which adapter object is instantiated and
which RTL archive is linked* — and the test runner can select the FreeBSD
adapter to test it natively on a FreeBSD host, or structurally from Linux.
The implementation steps below assume the structural refactors in the
architecture doc (toolkit/registry — Step 0a done; `TPlatformLayout` — Step 0b
done) are already in place.
== Implementation plan
Each step states a verification check, per the project's goal-driven
convention. Steps 0a–0c are the behaviour-preserving structural refactor from
convention. Steps 0a–0b are the behaviour-preserving structural refactor from
`docs/native-target-architecture.adoc` (no FreeBSD code, no behaviour change —
verified against the existing Linux suite/fixpoints). Steps 1–9 add the
FreeBSD target as a new adapter family on those clean seams. Strategy B
(direct syscalls, static `ET_EXEC`) is assumed throughout; there is no
dynamic-link / sysroot work on this path.
verified against the existing Linux suite/fixpoints; *both are done*). Steps
1–9 add the FreeBSD target as a new adapter family on those clean seams.
Strategy B (direct syscalls, static `ET_EXEC`) is assumed throughout; there is
no dynamic-link / sysroot work on this path.
=== Step 0a — Toolkit + registry (refactor, no behaviour change)
@ -296,15 +297,21 @@ dynamic-link / sysroot work on this path.
No `{$IFDEF}`; the layout object is selected at startup.
* *Verify:* full suite unchanged (the Linux layout reproduces current values).
=== Step 0c — Introduce `TKernelABI` (refactor, no behaviour change)
=== Step 0c — Kernel leaf: link-time swap, not a runtime port (revised)
* Add the `TKernelABI` port (syscall + entry-stub leaf). The first Linux
adapter may still *delegate to the existing libc bindings* so the seam lands
with zero behaviour change before any syscall rewrite.
* Lift the ARC field-kind walkers and record-return classifier into
`TNativeBackend` as template methods with abstract per-CPU leaves, as the
backend base class already plans.
* *Verify:* full suite + native fixpoint unchanged.
* *No refactor in this step.* A runtime `TKernelABI` virtual port was attempted
and reverted: routing the RTL's kernel calls through a 30-method virtual class
+ a new global destabilised self-hosting (segfaulting binaries, non-converging
across bootstrap stages) for zero present benefit — the Linux adapter was pure
libc delegation. See the kernel-leaf note in
`docs/native-target-architecture.adoc`.
* The RTL keeps its `external name 'open'`/`'read'`/… bindings unchanged. The
FreeBSD kernel leaf is introduced as a *link-time symbol-stub object* when the
FreeBSD RTL archive is built (Step 4), resolving those same symbols with raw
syscalls. Nothing in the Linux RTL changes.
* The ARC-walker lift into `TNativeBackend` template methods is *deferred* —
it is arm64 groundwork (FreeBSD reuses the x86_64 backend unchanged) and does
not block this target.
=== Step 1 — FreeBSD link target + toolkit skeleton
@ -330,11 +337,11 @@ dynamic-link / sysroot work on this path.
=== Step 3 — Freestanding `_start` entry stub
* The FreeBSD `TKernelABI` adapter's entry contract emits `_start` (not
`__libc_start_main`): read `argc` at `[%rsp]`, `argv` at `[%rsp+8]`, align
the stack, call `_SetArgs`, run the body, then `exit` via syscall. The
linker's static `ET_EXEC` / `_start` path (Phase B) already supports this —
no GOT/PLT, no interp, no CRT.
* The FreeBSD kernel-stub object provides `_start` (not `__libc_start_main`):
read `argc` at `[%rsp]`, `argv` at `[%rsp+8]`, align the stack, call
`_SetArgs`, run the body, then `exit` via syscall. The linker's static
`ET_EXEC` / `_start` path (Phase B) already supports this — no GOT/PLT, no
interp, no CRT.
* *Verify:* `freebsd-x86_64` link produces an `ET_EXEC` with entry `_start`
and no `PT_INTERP`; linker unit test asserts the ELF shape.
@ -348,11 +355,12 @@ dynamic-link / sysroot work on this path.
* Add a FreeBSD syscall-number table (e.g. open=5, read=3, write=4, close=6,
exit=1, plus `fstat`/`lseek`/`mmap`/`munmap`/… — FreeBSD numbers, *not*
Linux's).
* Implement the `TKernelABI` leaves against the trampoline, then the harder
leaves per the work breakdown above (`memcpy/memset/memcmp` freestanding;
pure-Pascal `localtime_r/gmtime_r/timegm`; `mmap`-based allocator with a
no-`mremap` grow fallback; `thr_new`/`_umtx_op` threads last; verify the
FreeBSD `jmp_buf` for setjmp/longjmp; an `atexit` registry).
* Implement the syscall-stub leaves (exporting `open`/`read`/`write`/… as raw
syscalls) against the trampoline, then the harder leaves per the work
breakdown above (`memcpy/memset/memcmp` freestanding; pure-Pascal
`localtime_r/gmtime_r/timegm`; `mmap`-based allocator with a no-`mremap` grow
fallback; `thr_new`/`_umtx_op` threads last; verify the FreeBSD `jmp_buf` for
setjmp/longjmp; an `atexit` registry).
* *Verify:* a FreeBSD hello-world + file-I/O smoke test produce correct output
under emulation.
@ -418,8 +426,8 @@ here so the FreeBSD work is shaped to extend rather than be re-done:
exists, but Windows is materially more backend work than FreeBSD.
* *Syscall/startup abstraction.* Each OS gets its own entry stub and either
a syscall leaf (Strategy B) or an import-library leaf (Win32: `kernel32`).
Building the FreeBSD direct-syscall leaf as a clean, target-keyed
`TKernelABI` adapter (Step 4) is the template every later OS follows.
Building the FreeBSD direct-syscall leaf as a clean, link-time-swapped
syscall-stub object (Step 4) is the template every later OS follows.
* *Host independence.* Strategy B's "no host CRT, no host libc" property is
what makes Linux-hosted cross-compilation tool-free. Every target that
can be driven by direct syscalls (Linux, FreeBSD) should support it;
@ -468,18 +476,19 @@ A `freebsd-x86_64` native backend is feasible and the codebase is already
staged for it: the instruction selector, register allocator, System V ABI
lowering, ELF relocation core, and OPDF debug sections are all reused
unchanged. The entire FreeBSD-specific surface is (1) a FreeBSD `TLinkTarget`
(OSABI, base, page size), (2) a FreeBSD `TPlatformLayout` (`struct stat`/
`struct tm` layouts and OS constants), and (3) a FreeBSD `TKernelABI` (direct
syscalls + `_start` entry stub).
(OSABI, base, page size), (2) a FreeBSD `TPlatformLayout` (`struct stat` layout
and OS constants), and (3) a FreeBSD kernel-stub object (raw syscalls + `_start`
entry stub) that the FreeBSD RTL archive links in place of libc.
The chosen kernel-access strategy is *direct syscalls into a static `ET_EXEC`*
(Strategy B): the linker's static path already supports it, and it is the only
route that delivers the stated goal — cross-compiling a runnable FreeBSD binary
from a Linux host with no external tools and nothing staged. Its cost is the
libc-replacement leaves (threads, allocator, libc string ops, date math),
which become methods on the FreeBSD `TKernelABI` adapter, done hardest-last.
A conventional libc-linked path (Strategy A) is documented only as a fallback
shape and is not on this implementation path.
which are implemented in that per-target syscall-stub object (a link-time swap,
not a runtime port), done hardest-last. A conventional libc-linked path
(Strategy A) is documented only as a fallback shape and is not on this
implementation path.
Structurally, FreeBSD is *one new adapter family* — composed by an Abstract
Factory toolkit, resolved from a registry, with zero `{$IFDEF}` and no target

View file

@ -90,29 +90,74 @@ startup. Callers depend on the port, never on a concrete target.
| `TRtlPlatform` (exists)
| High-level OS operations (file/dir/process/time/console). Unchanged.
| `TKernelABI` (new)
| The *syscall + startup leaf*: raw `Open/Read/Write/Close/Mmap/Munmap/Exit/
…` for the target kernel, and the entry-stub contract (how `_start` reads
`argc/argv`, how `exit` is issued). Adapters: `TKernelLinuxX86_64`,
`TKernelFreeBSDX86_64`. In Strategy B, `TRtlPlatformPosix` calls
`GKernel.Open(...)` instead of `external name 'open'`.
| Kernel leaf (NOT a runtime port — see below)
| The *syscall + startup leaf*: `open/read/write/close/mmap/exit/…` and the
`_start` entry stub. This is a *link-time symbol swap*, not a runtime
virtual port: each target's RTL archive resolves the same `external name`
symbols differently. See <<kernel-leaf>> for the rationale and the design
that was tried and rejected.
| `TPlatformLayout` (new)
| The target's *struct layouts and OS constants*: `struct stat` field
offsets, `struct tm` layout, `O_*` flag bits, signal numbers, `SEEK_*`,
`CLOCK_*`. This is where the Linux constants leaking into
`TRtlPlatformPosix` go to live properly, per target.
`TRtlPlatformPosix` go to live properly, per target. Implemented in Step 0b.
|===
[#kernel-leaf]
[NOTE]
.The kernel leaf is a link-time swap, not a runtime port
====
`TKernelABI` and `TPlatformLayout` are kept *separate* deliberately. The
syscall ABI and the struct/constant layout vary on *different axes*: a future
`freebsd-arm64` may reuse FreeBSD's syscall numbers (`TKernelABI`) while
needing different struct packing (`TPlatformLayout`), and vice-versa. Two
ports let those vary independently instead of forcing a four-way adapter
matrix. The Abstract Factory (below) composes the correct pair so callers
never assemble an inconsistent combination by hand.
An earlier revision of this design specified a runtime `TKernelABI` virtual
class (≈30 abstract methods) with a `GKernel` global, through which every
`TRtlPlatformPosix` kernel call was routed; the Linux adapter forwarded each
method to the existing libc binding, and a FreeBSD adapter would later issue
raw syscalls.
*That approach was implemented and reverted (2026-06-25).* Introducing the
class and the third managed RTL global into `rtl.platform`/`rtl.platform.posix`
— the most layout-sensitive units in the self-hosting RTL — destabilised the
bootstrap. Binaries built against the modified RTL segfaulted at startup with
a corrupted/overflowed stack that no debugger (gdb or pdr) could unwind. The
failure was bootstrap-stage-dependent and non-converging: the release binary
built a *working* hello-world from the modified RTL source, but the
release-built stage-1 emitted a *crashing* one, and stage-2 was worse. The
precise cause was not isolated (candidates: a `TimeNow` method-name collision
with `TRtlPlatform.TimeNow` in the vtable, the compiler baking layout for a new
managed global, or the large abstract vtable), but the verdict is independent
of the cause: *the abstraction carried real self-hosting risk for zero present
benefit* — the Linux adapter was pure libc delegation, so nothing functional
changed until a FreeBSD syscall adapter existed.
The replacement design is a *link-time symbol swap*, the pattern Blaise already
uses for `blaise_setjmp_x86_64.s` and `blaise_atomic_x86_64.s`:
* The RTL keeps its `external name 'open'` / `'read'` / `'write'` / … bindings
exactly as today.
* On Linux (and any libc-linked build), those symbols resolve to libc.
* For FreeBSD Strategy B, the FreeBSD RTL archive instead links a small
syscall-stub object that *exports the same symbol names* (`open`, `read`,
…) implemented as raw `syscall` instructions with the FreeBSD numbers and
carry-flag error translation, plus a `_start`.
* The kernel leaf therefore varies by *which object the per-target archive
composes* — selected at link time by the toolkit/RTL-archive choice — with
*no new runtime virtual dispatch, no new global, and no change to the
layout-sensitive RTL units*.
Why this is better here: the kernel mechanism is genuinely a build-time
property of a target (a binary is libc-linked *or* freestanding; it never
switches at run time), so a link-time swap models it exactly, while a runtime
port adds vtable/global surface to the RTL for a choice that is never made at
run time. `TPlatformLayout` stays a runtime object because its values
(struct offsets, flag bits) are read by shared method bodies and genuinely
differ per target instance; the kernel leaf does not need that — it needs a
different *symbol implementation*, which the linker already selects.
`TPlatformLayout` and the kernel leaf still vary on *different axes* (a future
`freebsd-arm64` may reuse FreeBSD syscall numbers but need different struct
packing), so they remain separate concerns; they are simply realised by
*different mechanisms* — a runtime object for layout, a link-time symbol set
for the kernel leaf.
====
Compiler-side ports (already present, kept as ports):
@ -142,16 +187,19 @@ call vary by target"*:
getenv/exit/nanosleep/pipe/dup2/fork/execvp/waitpid`
| *Yes* — different syscall numbers; FreeBSD reports errors via the carry
flag, not a negative return.
| Route through `TKernelABI`.
| Link-time symbol swap: the FreeBSD archive links a syscall-stub object
exporting these names; the RTL's `external name` bindings are unchanged
(see <<kernel-leaf>>).
| `mmap/munmap/mremap` (`blaise_mem.pas`)
| *Yes* — and FreeBSD has no `mremap`.
| Route through `TKernelABI` (memory leaf, with a grow-path fallback).
| Link-time symbol swap (memory leaf, with a grow-path fallback for the
missing `mremap`).
| `pthread_create/join/mutex_*` (`blaise_thread.pas`, and the mutexes used by
`blaise_exc.pas`/`blaise_weak.pas`)
| *Yes* — no pthreads without libc; FreeBSD uses `thr_new`/`_umtx_op`.
| Route through `TKernelABI` (threads leaf — the hardest, done last).
| Link-time symbol swap (threads leaf — the hardest, done last).
| `localtime_r/gmtime_r/timegm` (`rtl.platform.posix.pas`)
| *Yes* — libc-dependent, no syscall equivalent.
@ -176,27 +224,31 @@ call vary by target"*:
| Leave as-is.
|===
So the refactor is *narrow and funnelled*, not unit-by-unit:
So the refactor is *narrow and link-time*, not unit-by-unit:
. *One unit changes shape* — `rtl.platform.posix.pas`: its `external name
'open'` bindings become `GKernel.Open(...)` calls. This is the bulk of the
kernel-facing surface and the natural caller of the port.
. *A few units lose only their kernel-touching leaves* — `blaise_mem`,
`blaise_thread`, `blaise_exc`, `blaise_weak` keep all their logic; only the
handful of `mmap`/`pthread_*` lines move behind `TKernelABI`. The units are
*not* re-architected.
. *No RTL unit changes shape for the kernel leaf* — `rtl.platform.posix.pas`
keeps its `external name 'open'` bindings verbatim. The FreeBSD difference is
*which object resolves those symbols* (libc vs a raw-syscall stub), chosen
when the per-target RTL archive is composed — not a code change in the unit.
. *Units keep all their logic* — `blaise_mem`, `blaise_thread`, `blaise_exc`,
`blaise_weak` are untouched; their `mmap`/`pthread_*` `external name`
bindings are satisfied by the target's chosen kernel-stub object. The units
are *not* re-architected.
. *Most units are left alone* — pure-computation and CPU-keyed-asm units
(`blaise_str`, `blaise_float`, the atomic/setjmp stubs) do not touch a port.
(`blaise_str`, `blaise_float`, the atomic/setjmp stubs) need nothing.
`localtime_r`/etc. become one target-independent Pascal unit.
The principle: *every kernel-varying leaf funnels into the single `TKernelABI`
port; everything that does not vary by target stays where it is.* One port is
the one place "what does FreeBSD do differently" lives — adding a port per unit
would defeat that. Forcing target-invariant code (memcpy, string formatting,
atomics) through a port would be abstraction for its own sake — the
over-engineering this design explicitly rejects.
The principle: *the kernel-varying leaves are satisfied by one per-target
symbol set chosen at link time; everything that does not vary by target stays
where it is.* That single stub object is the one place "what does FreeBSD do
differently at the syscall level" lives. Routing these through a runtime
virtual port instead was tried and reverted (<<kernel-leaf>>): it added
vtable/global surface to the layout-sensitive RTL for a choice that is never
made at run time. Forcing target-invariant code (memcpy, string formatting,
atomics) through any indirection would likewise be abstraction for its own sake
— the over-engineering this design rejects.
The migration sequence below exploits this: Step 0c introduces `TKernelABI`
The migration sequence below exploits this: Step 0c introduces the kernel leaf
with a Linux adapter that *still delegates to the existing libc bindings*, so
every caller is repointed at the port with *zero behaviour change* (verified
against the current Linux suite) before a single syscall is rewritten. No unit
@ -212,18 +264,25 @@ and produces the *consistent family* of adapters for that target:
type
TTargetToolkit = class
public
function Name: string; virtual; abstract; { "linux-x86_64" }
function MakeBackend: TNativeBackend; virtual; abstract;
function MakeLinkTarget: TLinkTarget; virtual; abstract;
function MakeKernelABI: TKernelABI; virtual; abstract; { runtime side }
function MakeLayout: TPlatformLayout; virtual; abstract; { runtime side }
function TargetName: string; virtual; abstract;
end;
----
The compiler-side toolkit (Step 0a, as built) makes the backend and the link
target. It does *not* have a `MakeKernelABI` — the kernel leaf is a link-time
symbol set in the per-target RTL archive, not a runtime object the compiler
constructs (<<kernel-leaf>>). `TPlatformLayout` is also not made here: it is a
runtime RTL object selected at the produced binary's startup, in that binary's
own RTL archive, not in the compiler. (The method is named `Name`, not
`TargetName`, to avoid colliding with the unit-level `TargetName` free
function.)
Why Abstract Factory and not free functions: the produced objects must be
*coherent as a set*. A FreeBSD backend paired with a Linux link target, or a
FreeBSD syscall ABI paired with Linux struct offsets, is a silent bug. A
factory guarantees the family is internally consistent — this is the textbook
*coherent as a set*. A FreeBSD backend paired with a Linux link target is a
silent bug. A factory guarantees the family is internally consistent — this is
the textbook
Abstract Factory motivation (families of related products that must be used
together).
@ -255,9 +314,10 @@ hard-wiring with a lookup that the CLI *and* the test runner share.
| GoF pattern | Applied to | Why it earns its place
| Strategy
| `TNativeBackend`, `TKernelABI`, `TRtlPlatform`, `TPlatformLayout`
| `TNativeBackend`, `TRtlPlatform`, `TPlatformLayout`
| Interchangeable target behaviour chosen at run time. Already in use
(`TProfilerStrategy`, `TNativeBackend`).
(`TProfilerStrategy`, `TNativeBackend`). Note the kernel leaf is *not* a
Strategy object — it is a link-time symbol swap (<<kernel-leaf>>).
| Abstract Factory
| `TTargetToolkit`
@ -299,11 +359,13 @@ target's runtime into every produced binary.
resolves one toolkit from the registry.
| A *produced* binary (e.g. a FreeBSD app)
| Must contain *only its own* runtime adapter (`TKernelABI` /
`TPlatformLayout` / `TRtlPlatform`), selected at that binary's own startup.
The unused targets' runtime code must NOT be linked in. This falls out of
per-target RTL archives: a FreeBSD binary links
`blaise_rtl-freebsd-x86_64.a`, which contains only the FreeBSD adapters.
| Must contain *only its own* target's RTL: the `TRtlPlatform`/`TPlatformLayout`
runtime objects and the kernel-leaf symbol implementation for that target.
This falls out of per-target RTL archives — a FreeBSD binary links
`blaise_rtl-freebsd-x86_64.a`, which contains the FreeBSD layout object and
the FreeBSD syscall-stub object; the Linux archive contains the libc-backed
equivalents. The kernel leaf in particular is fixed purely by *which archive
is linked*, so no run-time selection is needed for it.
|===
So: the compiler chooses a target *to emit for*; the produced binary chooses a
@ -319,7 +381,7 @@ Because a toolkit is resolved from a `TTargetDesc` rather than frozen by
. *Native self-target.* On a FreeBSD host, `HostTarget()` resolves the
FreeBSD toolkit, so running the compiler's own test suite there exercises
the FreeBSD `TKernelABI`/`TPlatformLayout`/`TNativeBackend` adapters
the FreeBSD kernel-stub object, `TPlatformLayout`, and `TNativeBackend`
natively — no rebuild, no special flag.
. *Cross-target static coverage.* On any host, the e2e harness can loop over
@ -341,37 +403,52 @@ This is the *structural* refactor; the FreeBSD-specific content (syscall
numbers, struct layouts, `_start` shape) lives in
`freebsd-x86_64-backend-design.adoc`.
. Introduce `TTargetToolkit` + `TTargetRegistry`; register the existing Linux
target through them. Re-express `--target` resolution via the registry.
*No behaviour change* — verify the full suite still passes (pure refactor).
. Extract `TPlatformLayout` from `TRtlPlatformPosix`: move `TStatBuf`/`TTm`
layouts and the `O_*`/`S_*`/`CLOCK_*`/`WNOHANG`/`SEEK_*` constants behind the
port; the Linux adapter supplies today's values. Verify suite unchanged.
. Introduce `TKernelABI`; for Linux, the first adapter may still delegate to
the libc bindings (no behaviour change) so the seam lands before the
syscall rewrite. Verify suite unchanged.
. Lift the ARC field-kind walkers and record classifier into `TNativeBackend`
as template methods with abstract per-CPU leaves (as the base class already
plans), so a second CPU/OS adapter inherits them.
. Only then add the `TFreeBSDX86_64Toolkit` adapters (per the FreeBSD design
doc). Adding the target touches *no* existing target's code — it is new
files plus one registry line.
. *(DONE — Step 0a)* Introduce `TTargetToolkit` + `TTargetRegistry`; register
the existing Linux target through them. Re-express `--target` resolution via
the registry. *No behaviour change* — full suite + all fixpoints verified.
. *(DONE — Step 0b)* Extract `TPlatformLayout` from `TRtlPlatformPosix`: move the
`struct stat` layout (now an opaque buffer + offset accessors) and the
`O_*`/`S_*`/`CLOCK_*`/`WNOHANG`/`SEEK_*` constants behind the port; the Linux
adapter (`rtl.platform.layout.linux`) supplies today's values. Verified.
. *(REVISED — kernel leaf)* The kernel leaf is a *link-time symbol swap*, NOT a
runtime port. A runtime `TKernelABI` virtual port was attempted here and
reverted because it destabilised self-hosting for zero present benefit
(<<kernel-leaf>>). No refactor is needed in this step: the RTL keeps its
`external name` bindings, and the FreeBSD syscall-stub object is introduced
when the FreeBSD RTL archive is built (FreeBSD design doc, Step 4).
. *(DEFERRED)* Lift the ARC field-kind walkers and record classifier into
`TNativeBackend` as template methods with abstract per-CPU leaves. This is
*arm64 groundwork* — FreeBSD reuses the x86_64 backend unchanged, so it does
not block the FreeBSD target and is deferred until a second CPU lands.
. Add the `TFreeBSDX86_64Toolkit` adapters + FreeBSD RTL archive (per the
FreeBSD design doc). Adding the target touches *no* existing target's code —
new files plus one registry line.
Steps 1–4 are behaviour-preserving refactors verifiable against the existing
Linux suite and fixpoints; step 5 is the actual FreeBSD work. Sequencing the
refactor first means the FreeBSD adapter slots into clean seams rather than
forcing them open mid-implementation.
The completed steps are behaviour-preserving refactors verified against the
existing Linux suite and all four fixpoints; the last step is the actual
FreeBSD work. Sequencing the refactors first means the FreeBSD adapter slots
into clean seams rather than forcing them open mid-implementation.
== Summary
Hexagonal architecture is not just viable in Blaise — it is already the chosen
idiom for the runtime platform layer and the codegen backend, explicitly
`{$IFDEF}`-free. The path to many targets is to *complete* that idiom:
promote the kernel leaf and the struct/constant layout to first-class ports
(`TKernelABI`, `TPlatformLayout`), compose each target's adapter family with an
Abstract Factory (`TTargetToolkit`), resolve toolkits by name through a
Registry, keep the linker's container writer behind a Bridge, and reuse the
Template-Method backend base. Target divergence then lives entirely in *which
object is instantiated*, decided once at startup — the compiler picks a target
to emit for, a produced binary picks a platform to run on — and the test runner
can select adapters at run time to test any target, natively or structurally.
`{$IFDEF}`-free. The path to many targets is to *complete* that idiom, using
the right mechanism for each axis of divergence:
* the struct/constant layout becomes a runtime port (`TPlatformLayout`) —
its per-target values are read by shared method bodies;
* the kernel leaf becomes a *link-time symbol swap* — a per-target syscall/
startup object resolving the RTL's `external name` bindings (a runtime
`TKernelABI` port was tried and reverted, <<kernel-leaf>>);
* each target's compiler-side adapter family (backend + link target) is
composed by an Abstract Factory (`TTargetToolkit`) and resolved by name
through a Registry;
* the linker's container writer stays behind a Bridge and the backend reuses
the Template-Method base.
Target divergence then lives in *which object is instantiated or which archive
is linked*, decided once — the compiler picks a target to emit for, a produced
binary picks a platform to run on — with no `{$IFDEF}` and no scattered
per-target code. The test runner can select adapters at run time to test any
target, natively or structurally.