commit 45214fd7bc2683b9318bb9e6164251be7d74b11a
parent 35868a8761e3923ce2a866dc5bd07d79a44996d2
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Sat, 23 May 2026 10:06:05 -0700
doc: add REGISTRY.md — build-time component configuration plan
Plan for how cfree's optional components (backend arches, object
formats, language frontends) are gated via include/cfree/config.h and
registered at runtime through vtable + registry pairs, collapsing
dispatch sites to a single lookup.
Diffstat:
| A | doc/REGISTRY.md | | | 219 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
1 file changed, 219 insertions(+), 0 deletions(-)
diff --git a/doc/REGISTRY.md b/doc/REGISTRY.md
@@ -0,0 +1,219 @@
+# Build-time configuration and component registries
+
+This document specifies how cfree's optional components — backend
+architectures, object/image formats, and language frontends — are gated
+at build time and registered at runtime. The goal is a single
+`config.h` that selects which components are compiled into `libcfree.a`
+(and the `cfree` driver), with each axis flowing through a vtable +
+registry pair so dispatch sites collapse to a single lookup.
+
+## Configuration axes
+
+Three independent axes, plus one derived axis:
+
+1. **Backend architectures** — `aa64`, `rv64`, `x64`, `c_target`
+2. **Object/image formats** — `ELF`, `Mach-O`, `PE/COFF`
+3. **Language frontends** — `C`, `Toy`, `WASM` (`asm` is unconditional)
+4. **ABIs (derived)** — the (arch × format/OS) cross-product. Not user
+ configurable; pulled in automatically when both sides are enabled.
+
+The ABI table is the reason we don't expose ABIs as a fourth knob: every
+valid ABI maps 1:1 to an (arch, OS-family) pair, where OS family is
+implied by the object format (ELF→SysV-style, Mach-O→Apple,
+PE/COFF→Windows). Exposing ABIs separately would create mostly-invalid
+combinations and offer no real flexibility.
+
+## `include/cfree/config.h`
+
+A single header consumed by both `libcfree.a` sources and the driver.
+Hand-edited today; a future `configure` script can generate it.
+
+```c
+#ifndef CFREE_CONFIG_H
+#define CFREE_CONFIG_H
+
+/* Backend architectures. */
+#define CFREE_ARCH_AA64 1
+#define CFREE_ARCH_X64 1
+#define CFREE_ARCH_RV64 1
+#define CFREE_ARCH_C_TARGET 0
+
+/* Object/image formats. Each gates emit + read + link-image paths and
+ * the matching arch-side reloc tables. */
+#define CFREE_OBJ_ELF 1
+#define CFREE_OBJ_MACHO 1
+#define CFREE_OBJ_COFF 1
+
+/* Language frontends. CFREE_LANG_ASM is unconditional: the assembler
+ * lives inside libcfree as part of the codegen substrate. */
+#define CFREE_LANG_C_ENABLED 1
+#define CFREE_LANG_TOY_ENABLED 1
+#define CFREE_LANG_WASM_ENABLED 1
+
+#endif
+```
+
+A `config.c` adds `_Static_assert` checks that at least one arch and at
+least one obj format are enabled.
+
+## Axis 1: Backend architectures
+
+**Status: vtable and registry already exist; only gating is new.**
+
+- **Vtable**: `ArchImpl` (`src/arch/arch.h:1098`). Carries sub-pointers
+ for the format-specific reloc tables (`elf`, `macho`, `coff`),
+ `dwarf`/`dbg` hooks, the `link` arch descriptor, the per-OS
+ `abi_vtable` dispatcher, and the codegen/assembler/disassembler
+ factories.
+- **Registry**: `src/arch/registry.c` already holds a static
+ `arch_impls[]` array and exposes `arch_lookup`,
+ `arch_lookup_elf_machine`, `arch_lookup_macho_cputype`,
+ `arch_lookup_coff_machine`.
+- **Change**: wrap each entry (and its `extern const ArchImpl`
+ declaration) with `#if CFREE_ARCH_<NAME>`. The `Makefile` switches
+ from globbed arch sources to per-arch object groups gated by the same
+ flags, so disabled archs are neither compiled nor linked.
+
+No new vtable or registry code; this axis is the cheapest of the four.
+
+## Axis 2: Object/image formats
+
+**Status: no vtable exists today; switches in multiple call sites.
+Largest piece of new work.**
+
+Today `emit_elf` / `emit_macho` / `emit_coff` are bare functions
+(`src/obj/{elf,macho,coff}_emit.c`), the read paths are likewise bare
+(`*_read.c`), and link-image emission dispatches via a switch in
+`src/link/link.c:958` calling `link_emit_elf` / `link_emit_macho` /
+`link_emit_coff`. DSO header readers (`*_read_dso.c`) follow the same
+shape.
+
+**New vtable** (`src/obj/format.h`):
+
+```c
+typedef struct ObjFormatImpl {
+ CfreeObjFormat kind;
+ const char* name;
+
+ /* Relocatable object emit + read. */
+ void (*emit)(Compiler*, ObjBuilder*, Writer*);
+ CfreeStatus (*read)(Compiler*, const u8* data, size_t len,
+ ObjBuilder* out);
+
+ /* Link-image emit (executable / shared object). */
+ void (*link_emit)(LinkImage*, Writer*);
+
+ /* DSO header reader for `-lfoo` resolution against .so/.dylib/.dll. */
+ CfreeStatus (*read_dso)(Compiler*, const u8*, size_t, /* ... */);
+} ObjFormatImpl;
+
+const ObjFormatImpl* obj_format_lookup(CfreeObjFormat);
+```
+
+**New registry** (`src/obj/registry.c`), gated the same way as the arch
+registry:
+
+```c
+extern const ObjFormatImpl obj_format_impl_elf;
+extern const ObjFormatImpl obj_format_impl_macho;
+extern const ObjFormatImpl obj_format_impl_coff;
+
+static const ObjFormatImpl* const obj_format_impls[] = {
+#if CFREE_OBJ_ELF
+ &obj_format_impl_elf,
+#endif
+#if CFREE_OBJ_MACHO
+ &obj_format_impl_macho,
+#endif
+#if CFREE_OBJ_COFF
+ &obj_format_impl_coff,
+#endif
+};
+```
+
+**Call-site changes**: the switch in `src/link/link.c` and the obj
+emit/read entry points collapse to
+`obj_format_lookup(target.obj_format)->fn(...)`.
+
+The arch-side format reloc tables (`ArchElfOps`, `ArchMachoOps`,
+`ArchCoffOps` on `ArchImpl`) stay where they are — they're the
+(arch × format) intersection and naturally drop out when either side
+isn't compiled in.
+
+## Axis 3: ABIs (derived)
+
+**Status: vtable and dispatch already exist; only gating is new.**
+
+- **Vtable**: `ABIVtable`, selected by `arch->abi_vtable(c, os)`
+ (`src/abi/abi.c:177`).
+- **Selection**: each arch's dispatch function maps `CfreeOSKind` to one
+ of its supported `ABIVtable`s. Today every (arch × OS) cell that
+ cfree supports is wired in unconditionally.
+- **Change**: gate per-OS entries inside the dispatch by the
+ corresponding format flag — `#if CFREE_OBJ_COFF` drops the
+ `win64_x64` / `aapcs64_windows` entries; `#if CFREE_OBJ_MACHO` drops
+ `apple_arm64` / `apple_x64`; etc. The format flag is used as a proxy
+ for "the OS family that uses this format."
+- **Build**: per-ABI `.c` files in `src/abi/` opt in via the same
+ combined flags (e.g. `abi_win64_x64.c` only when
+ `CFREE_ARCH_X64 && CFREE_OBJ_COFF`).
+
+| Arch | ELF (SysV-ish) | Mach-O (Apple) | PE/COFF (Windows) |
+|----------|-------------------|-----------------|----------------------|
+| aa64 | aapcs64 | apple_arm64 | aapcs64_windows |
+| x64 | sysv_x64 | apple_x64 | win64_x64 |
+| rv64 | rv64 | — | — |
+| c_target | n/a (source-level emission, no machine ABI) |
+
+## Axis 4: Language frontends
+
+**Status: vtable already exists publicly; registration is host-side.
+Smallest change.**
+
+- **Vtable**: `CfreeFrontendVTable` (`include/cfree/compile.h`), public
+ API.
+- **Registration today**: `driver/env.c:172-175` explicitly calls
+ `cfree_c_register(c)`, `cfree_register_frontend(c, TOY, ...)`,
+ `cfree_wasm_register(c)`. The assembler frontend is built into
+ libcfree and registered by `cfree_compiler_new`. Other frontends are
+ library consumers of libcfree, not part of it.
+
+**Two options considered:**
+
+- **A (chosen)**: keep host registration. Gate the three calls in
+ `driver/env.c` with `#if CFREE_LANG_<NAME>_ENABLED` and gate the
+ matching `lang/<name>/*.c` sources in the Makefile.
+- **B (rejected)**: introduce `lang/registry.c` with auto-registration
+ from inside `cfree_compiler_new`. Pro: symmetric with arch/obj. Con:
+ pulls `lang/c` (~30 files) into `libcfree.a`, breaks the current
+ "libcfree is the substrate, lang/ is built on top" boundary, and
+ prevents embedders from swapping in a custom C frontend.
+
+The asymmetry is justified: arch and obj are codegen-internal — libcfree
+cannot function without picking one — while frontends are user-of-libcfree
+code that the public API was explicitly designed to let callers swap.
+
+## Summary
+
+| Axis | Vtable | Registry | Net new work |
+|-------------|-------------------------------------------------|----------------------------------------------|-----------------------------------------------------------|
+| Arch | `ArchImpl` (exists) | `src/arch/registry.c` (exists) | `#if CFREE_ARCH_*` gates |
+| Obj format | `ObjFormatImpl` (new) | `src/obj/registry.c` (new) | Extract `emit_*` / `read_*` / `link_emit_*` behind vtable |
+| ABI | `ABIVtable` (exists) | per-arch `abi_dispatch` (exists) | Gate per-OS dispatch entries by obj-format flag |
+| Frontend | `CfreeFrontendVTable` (exists) | none — host-side (`driver/env.c`) | `#if CFREE_LANG_*_ENABLED` around three driver calls |
+
+## Implementation order
+
+1. Land `include/cfree/config.h` with all flags set to `1` (no behavior
+ change). Add `_Static_assert` minimums in `config.c`.
+2. Gate arch registry entries and arch Makefile sources. Verify with a
+ build that flips one arch off.
+3. Extract `ObjFormatImpl` and the obj registry. Replace the switches
+ in `link.c` and the obj entry points. Verify a build with one obj
+ format off.
+4. Gate per-ABI sources and per-OS dispatch entries.
+5. Gate frontend registration in `driver/env.c` and frontend Makefile
+ sources.
+
+Each step is independently testable and leaves the build green with the
+default all-on configuration.