kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit c4eed6ef3afdbc721e74fd31d2448f0a4c875262
parent a30a842d4d00917c292efc5ae5c1c37cc7ed28cd
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat, 23 May 2026 13:30:58 -0700

lang: registry-driven frontend dispatch with extensions on the vtable

Move frontend registration into libcfree (src/api/lang_registry.c),
folding lang/c, lang/wasm, and lang/toy into libcfree.a gated by
CFREE_LANG_*_ENABLED. Each frontend's vtable carries its own NULL-term
extensions list; cfree_language_for_path now takes a CfreeCompiler and
walks c->frontends[] (case-insensitive, so .S still maps to asm).

The asm built-in fallback in frontend_for_language is gone — asm is
registered like any other slot (unconditionally), so embedders can
clear it via cfree_register_frontend(c, CFREE_LANG_ASM, NULL).

Diffstat:
MMakefile | 45+++++++++++++++++++++++++++------------------
Mdoc/REGISTRY.md | 58++++++++++++++++++++++++++++++++--------------------------
Mdriver/cc.c | 23++++++++++++++++-------
Mdriver/dbg.c | 10++++++----
Mdriver/env.c | 7-------
Mdriver/inputs.c | 2+-
Minclude/cfree/compile.h | 10+++++++++-
Mlang/c/c.c | 10+++++-----
Mlang/c/c.h | 9+++++----
Mlang/toy/compile.c | 3+++
Mlang/wasm/wasm.c | 9++++-----
Mlang/wasm/wasm.h | 2+-
Msrc/api/compile.c | 60+++++++++++++++++++++++++++++++++++++++++-------------------
Asrc/api/lang_registry.c | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/api/lang_registry.h | 10++++++++++
Msrc/core/core.c | 3+++
Mtest/parse/harness/parse_runner.c | 5-----
17 files changed, 215 insertions(+), 103 deletions(-)

diff --git a/Makefile b/Makefile @@ -18,7 +18,6 @@ LIB_CFLAGS = $(CFLAGS_COMMON) -ffreestanding -Iinclude -Isrc # Driver: hosted CLI binary. Sees only the public include/ tree — that's # what makes the driver the first consumer of libcfree. DRIVER_CFLAGS = $(CFLAGS_COMMON) -Iinclude -I. -LANG_CFLAGS = $(CFLAGS_COMMON) -Iinclude include mk/config.mk @@ -49,12 +48,27 @@ ifeq ($(CFREE_ARCH_C_TARGET_ENABLED),1) LIB_SRCS += $(LIB_SRCS_ARCH_C_TARGET) endif -LANG_C_SRCS = $(shell find lang/c -name '*.c' 2>/dev/null) +# Per-frontend source sets. Each is gated by its CFREE_LANG_*_ENABLED flag +# from mk/config.mk so the matching `#if` in src/api/lang_registry.c and +# the build agree on which frontends are compiled in. +LANG_C_SRCS = $(shell find lang/c -name '*.c' 2>/dev/null) LANG_WASM_SRCS = $(shell find lang/wasm -name '*.c' 2>/dev/null) +LANG_TOY_SRCS = $(wildcard lang/toy/*.c) + +LANG_OBJS = +ifeq ($(CFREE_LANG_C_ENABLED),1) +LANG_OBJS += $(patsubst lang/c/%.c,build/lang/c/%.o,$(LANG_C_SRCS)) +endif +ifeq ($(CFREE_LANG_WASM_ENABLED),1) +LANG_OBJS += $(patsubst lang/wasm/%.c,build/lang/wasm/%.o,$(LANG_WASM_SRCS)) +endif +ifeq ($(CFREE_LANG_TOY_ENABLED),1) +LANG_OBJS += $(patsubst lang/toy/%.c,build/lang/toy/%.o,$(LANG_TOY_SRCS)) +endif + LIB_ASMS = $(shell find src -name '*.S') LIB_OBJS = $(patsubst src/%.c,build/lib/%.o,$(LIB_SRCS)) \ - $(patsubst lang/c/%.c,build/lang/c/%.o,$(LANG_C_SRCS)) \ - $(patsubst lang/wasm/%.c,build/lang/wasm/%.o,$(LANG_WASM_SRCS)) \ + $(LANG_OBJS) \ $(patsubst src/%.S,build/lib/%.o,$(LIB_ASMS)) LIB_DEPS = $(LIB_OBJS:.o=.d) @@ -62,12 +76,7 @@ DRIVER_SRCS = $(wildcard driver/*.c) DRIVER_OBJS = $(patsubst driver/%.c,build/driver/%.o,$(DRIVER_SRCS)) DRIVER_DEPS = $(DRIVER_OBJS:.o=.d) -LANG_TOY_SRCS = $(wildcard lang/toy/*.c) -LANG_TOY_OBJS = $(patsubst lang/toy/%.c,build/lang/toy/%.o,$(LANG_TOY_SRCS)) -LANG_TOY_DEPS = $(LANG_TOY_OBJS:.o=.d) - LIB_AR = build/libcfree.a -LANG_TOY_AR = build/libcfree_toy.a BIN = build/cfree .PHONY: all lib bin format clean bootstrap bench-opt @@ -85,18 +94,19 @@ $(LIB_AR): $(LIB_OBJS) @rm -f $@ ar rcs $@ $(LIB_OBJS) -$(LANG_TOY_AR): $(LANG_TOY_OBJS) - @mkdir -p $(dir $@) - @rm -f $@ - ar rcs $@ $(LANG_TOY_OBJS) - -$(BIN): $(DRIVER_OBJS) $(LIB_AR) $(LANG_TOY_AR) - $(CC) $(HOST_SYSROOT_LDFLAGS) -o $@ $(DRIVER_OBJS) $(LIB_AR) $(LANG_TOY_AR) +$(BIN): $(DRIVER_OBJS) $(LIB_AR) + $(CC) $(HOST_SYSROOT_LDFLAGS) -o $@ $(DRIVER_OBJS) $(LIB_AR) build/lib/%.o: src/%.c Makefile @mkdir -p $(dir $@) $(CC) $(LIB_CFLAGS) $(DEPFLAGS) -c $< -o $@ +# lang_registry.c is the one libcfree source that crosses into lang/*; it +# uses -Ilang so the frontend headers can be reached as "c/c.h" etc. +build/lib/api/lang_registry.o: src/api/lang_registry.c Makefile + @mkdir -p $(dir $@) + $(CC) $(LIB_CFLAGS) -Ilang $(DEPFLAGS) -c $< -o $@ + build/lang/c/%.o: lang/c/%.c Makefile @mkdir -p $(dir $@) $(CC) $(CFLAGS_COMMON) -ffreestanding -Iinclude -Ilang/c $(DEPFLAGS) -c $< -o $@ @@ -115,7 +125,7 @@ build/driver/%.o: driver/%.c Makefile build/lang/toy/%.o: lang/toy/%.c Makefile @mkdir -p $(dir $@) - $(CC) $(LANG_CFLAGS) $(DEPFLAGS) -c $< -o $@ + $(CC) $(CFLAGS_COMMON) -ffreestanding -Iinclude -Ilang/toy $(DEPFLAGS) -c $< -o $@ include rt/Makefile @@ -154,6 +164,5 @@ clean: -include $(LIB_DEPS) -include $(DRIVER_DEPS) --include $(LANG_TOY_DEPS) include test/test.mk diff --git a/doc/REGISTRY.md b/doc/REGISTRY.md @@ -167,31 +167,35 @@ isn't compiled in. ## Axis 4: Language frontends -**Status: vtable already exists publicly; registration is host-side. -Smallest change.** +**Status: registry-driven, done.** - **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. + API. Per-frontend instances are exposed as externs: + `cfree_c_frontend_vtable`, `cfree_toy_frontend_vtable`, + `cfree_wasm_frontend_vtable`, and `cfree_asm_frontend_vtable` (the + asm vtable's declaration lives in `src/api/lang_registry.c` since + asm has no `lang/asm/` directory). +- **Extensions on the vtable**: each vtable carries a NULL-terminated + `extensions` list (lowercase, no leading dot). `cfree_language_for_path` + now takes a `CfreeCompiler*` and walks `c->frontends[]`, matching + case-insensitively so `.S` resolves to asm's `"s"` entry. C has no + extensions and serves as the fallback when nothing else matches. +- **Registry**: `src/api/lang_registry.c` is the sole place that checks + `CFREE_LANG_*_ENABLED`. `lang_registry_init()` is called from + `compiler_init` and populates `c->frontends[]` with each compiled-in + vtable plus the always-on asm frontend. +- **Build**: `lang/c`, `lang/wasm`, and `lang/toy` sources are folded + into `libcfree.a` and gated by the matching `_ENABLED` flag in the + Makefile. The standalone `libcfree_toy.a` archive is gone — the toy + frontend is now first-class alongside C and WASM. +- **No fallback**: `frontend_for_language()` returns whatever is in + `c->frontends[lang]` and nothing more. The asm frontend is registered + by the registry like any other; an embedder that doesn't want asm can + clear the slot with `cfree_register_frontend(c, CFREE_LANG_ASM, NULL)` + after construction. +- **Public override**: `cfree_register_frontend()` remains public, so + embedders can swap in a custom vtable for any `CfreeLanguage` slot + (or clear it) after `cfree_compiler_new`. ## Summary @@ -200,7 +204,7 @@ code that the public API was explicitly designed to let callers swap. | 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 | +| Frontend | `CfreeFrontendVTable` (exists) | `src/api/lang_registry.c` (new) | Per-frontend vtable extern + folded into `libcfree.a` | ## Implementation order @@ -212,8 +216,10 @@ code that the public API was explicitly designed to let callers swap. 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. +5. Add `src/api/lang_registry.c`, expose `cfree_<lang>_frontend_vtable` + externs, fold `lang/<name>/*.c` into `libcfree.a` gated by + `CFREE_LANG_<NAME>_ENABLED`, and drop host-side registration calls + from `driver/env.c`. Each step is independently testable and leaves the build green with the default all-on configuration. diff --git a/driver/cc.c b/driver/cc.c @@ -787,10 +787,15 @@ static int cc_record_stdin(CcOptions* o) { return 0; } -static CfreeLanguage cc_lang_for_path_or_forced(const char* path, - int forced_lang) { - if (forced_lang >= 0) return (CfreeLanguage)forced_lang; - return cfree_language_for_path(path); +/* Stored in source_langs[] during arg parsing to mean "no -x override — + * resolve from the path at compile time, once a compiler is around to + * consult its frontend extension registry." */ +#define CC_LANG_AUTO ((CfreeLanguage)CFREE_LANG_COUNT) + +static CfreeLanguage cc_resolve_lang(CfreeCompiler* c, const char* path, + CfreeLanguage stored) { + if (stored != CC_LANG_AUTO) return stored; + return cfree_language_for_path(c, path); } static int cc_classify_positional(CcOptions* o, const char* a, @@ -798,7 +803,7 @@ static int cc_classify_positional(CcOptions* o, const char* a, if (driver_streq(a, "-")) return cc_record_stdin(o); if (forced_lang >= 0 || cc_is_c_source(a)) { o->source_langs[o->nsource_files] = - cc_lang_for_path_or_forced(a, forced_lang); + forced_lang >= 0 ? (CfreeLanguage)forced_lang : CC_LANG_AUTO; o->source_files[o->nsource_files++] = a; cc_push_link_item(o, CC_LINK_SOURCE_FILE, o->nsource_files - 1u); return 0; @@ -2152,7 +2157,10 @@ static int cc_run_compile_one(DriverEnv* env, const CcOptions* o, } { CfreeLanguage lang = - is_memory ? o->source_memory[index].lang : o->source_langs[index]; + is_memory + ? o->source_memory[index].lang + : cc_resolve_lang(compiler, o->source_files[index], + o->source_langs[index]); CfreeStatus st; st = cc_compile_source_emit(compiler, lang, &copts, &input, obj_w); if (st != CFREE_OK) goto out; @@ -2324,7 +2332,8 @@ static int cc_run_link_exe(DriverEnv* env, const CcOptions* o, cc_fill_c_opts(o, pp, &copts); for (i = 0; i < o->nsource_files; ++i) { - CfreeLanguage lang = o->source_langs[i]; + CfreeLanguage lang = + cc_resolve_lang(compiler, o->source_files[i], o->source_langs[i]); CfreeStatus st; st = cc_compile_source_obj(compiler, lang, &copts, &src_bytes[i], &objs[i]); if (st != CFREE_OK) goto out; diff --git a/driver/dbg.c b/driver/dbg.c @@ -1619,7 +1619,7 @@ static CfreeLanguage dbg_jit_language_for_tag(DbgState* s, const char* tag, return CFREE_LANG_WASM; } if (name_out) *name_out = tag; - return cfree_language_for_path(tag); + return cfree_language_for_path(s->compiler, tag); } static CfreeStatus dbg_compile_session_for(DbgState* s, CfreeLanguage lang, @@ -2382,9 +2382,11 @@ static void dbg_cmd_help(void) { " info variables [PATTERN] list JIT globals matching PATTERN\n"); } -static CfreeLanguage dbg_default_language_from_inputs(const DbgOpts* o) { +static CfreeLanguage dbg_default_language_from_inputs(CfreeCompiler* c, + const DbgOpts* o) { if (o->has_default_lang) return o->default_lang; - if (o->inputs.nsources) return cfree_language_for_path(o->inputs.sources[0]); + if (o->inputs.nsources) + return cfree_language_for_path(c, o->inputs.sources[0]); if (o->inputs.nsource_memory) return o->inputs.source_memory[0].lang; return CFREE_LANG_C; } @@ -2787,7 +2789,7 @@ int driver_dbg(int argc, char** argv) { st.ctx = ctx; dbg_fill_compile_options(&o, &st.copts); st.jit = jit; - st.default_jit_lang = dbg_default_language_from_inputs(&o); + st.default_jit_lang = dbg_default_language_from_inputs(compiler, &o); st.default_jit_name = dbg_jit_default_name(st.default_jit_lang); st.prog_argc = (int)o.prog_argc; st.prog_argv = o.prog_argv; diff --git a/driver/env.c b/driver/env.c @@ -37,9 +37,6 @@ #endif #include "driver.h" -#include "lang/c/c.h" -#include "lang/toy/toy.h" -#include "lang/wasm/wasm.h" /* Dual-mapping back-ends for strict W^X. Picks per-platform: * @@ -169,10 +166,6 @@ CfreeStatus driver_compiler_new(CfreeTarget t, const CfreeContext *ctx, if (out) *out = NULL; return st; } - cfree_c_register(c); - (void)cfree_register_frontend(c, CFREE_LANG_TOY, - &cfree_toy_frontend_vtable); - cfree_wasm_register(c); driver_diag_set_compiler(c); if (out) *out = c; return CFREE_OK; diff --git a/driver/inputs.c b/driver/inputs.c @@ -166,7 +166,7 @@ int driver_inputs_compile_and_jit(DriverInputs* in, CfreeCompiler* compiler, cfree_frontend_metrics_scope_end(compiler, "driver.load_sources"); for (i = 0; i < in->nsources; ++i) { - CfreeLanguage lang = cfree_language_for_path(in->sources[i]); + CfreeLanguage lang = cfree_language_for_path(compiler, in->sources[i]); CfreeCompileSessionOptions sopts; CfreeCompileSession* session = NULL; CfreeSourceInput sin; diff --git a/include/cfree/compile.h b/include/cfree/compile.h @@ -90,9 +90,17 @@ typedef struct CfreeFrontendVTable { CfreeFrontendNewFn new_frontend; CfreeFrontendCompileFn compile; CfreeFrontendFreeFn free_frontend; + + /* NULL-terminated list of lowercase file extensions (no leading dot) + * that this frontend claims. cfree_language_for_path walks every + * registered frontend's list to map a path's extension back to a + * CfreeLanguage. May be NULL for frontends with no canonical + * extension. Matching is case-insensitive so `.S` and `.s` both map + * to the asm frontend's `"s"` entry. */ + const char* const* extensions; } CfreeFrontendVTable; -CfreeLanguage cfree_language_for_path(const char* path); +CfreeLanguage cfree_language_for_path(CfreeCompiler*, const char* path); CfreeStatus cfree_register_frontend(CfreeCompiler*, CfreeLanguage, const CfreeFrontendVTable*); diff --git a/lang/c/c.c b/lang/c/c.c @@ -272,12 +272,12 @@ static void c_frontend_free(CfreeFrontendState* frontend) { h->free(h, fe, sizeof(*fe)); } -static const CfreeFrontendVTable c_frontend_vtable = { +/* C claims no extensions — the language-for-path lookup falls through + * to CFREE_LANG_C as the default when nothing else matches, so listing + * `c`/`h` here would only duplicate that fallback. */ +const CfreeFrontendVTable cfree_c_frontend_vtable = { c_frontend_new, c_frontend_compile, c_frontend_free, + NULL, }; - -void cfree_c_register(CfreeCompiler* c) { - (void)cfree_register_frontend(c, CFREE_LANG_C, &c_frontend_vtable); -} diff --git a/lang/c/c.h b/lang/c/c.h @@ -3,9 +3,9 @@ /* Public surface for the cfree C frontend. * - * The C frontend is registered with the compiler via cfree_c_register, which - * installs a CfreeFrontendVTable under CFREE_LANG_C so the libcfree pipeline - * can create a frontend instance, compile through it, and free it. + * The C frontend's vtable, cfree_c_frontend_vtable, is exposed as an + * extern so libcfree's lang_registry can wire it into c->frontends[] + * during compiler construction when CFREE_LANG_C_ENABLED is set. * * The pipeline constructs its CfreeFrontendCompileOptions by copying * CfreeCCompileOptions.code and .diagnostics, then planting the original @@ -26,6 +26,7 @@ CfreeStatus cfree_c_preprocess(CfreeCompiler*, const CfreePreprocessOptions*, const CfreeBytes*, CfreeWriter*); CfreeStatus cfree_c_dump_tokens(CfreeCompiler*, const CfreeBytes*, CfreeWriter*); -void cfree_c_register(CfreeCompiler*); + +extern const CfreeFrontendVTable cfree_c_frontend_vtable; #endif diff --git a/lang/toy/compile.c b/lang/toy/compile.c @@ -215,8 +215,11 @@ static void toy_frontend_free(CfreeFrontendState* frontend) { h->free(h, fe, sizeof(*fe)); } +static const char* const toy_extensions[] = {"toy", NULL}; + const CfreeFrontendVTable cfree_toy_frontend_vtable = { toy_frontend_new, toy_frontend_compile, toy_frontend_free, + toy_extensions, }; diff --git a/lang/wasm/wasm.c b/lang/wasm/wasm.c @@ -48,16 +48,15 @@ static void wasm_frontend_free(CfreeFrontendState* frontend) { h->free(h, fe, sizeof(*fe)); } -static const CfreeFrontendVTable wasm_frontend_vtable = { +static const char* const wasm_extensions[] = {"wat", "wasm", NULL}; + +const CfreeFrontendVTable cfree_wasm_frontend_vtable = { wasm_frontend_new, wasm_frontend_compile, wasm_frontend_free, + wasm_extensions, }; -void cfree_wasm_register(CfreeCompiler* c) { - (void)cfree_register_frontend(c, CFREE_LANG_WASM, &wasm_frontend_vtable); -} - int cfree_wasm_wat_to_wasm(CfreeCompiler* c, const CfreeBytes* input, CfreeWriter* out) { WasmModule m; diff --git a/lang/wasm/wasm.h b/lang/wasm/wasm.h @@ -7,7 +7,7 @@ #include "runtime_abi.h" -void cfree_wasm_register(CfreeCompiler*); +extern const CfreeFrontendVTable cfree_wasm_frontend_vtable; /* Internal test/developer helper: parse accepted WAT and write equivalent * binary Wasm. This is intentionally not part of the installed public API. */ diff --git a/src/api/compile.c b/src/api/compile.c @@ -38,10 +38,13 @@ static CfreeStatus asm_frontend_compile(CfreeFrontendState* fe, CfreeObjBuilder* out); static void asm_frontend_free(CfreeFrontendState* fe); -static const CfreeFrontendVTable asm_frontend_vtable = { +static const char* const asm_extensions[] = {"s", NULL}; + +const CfreeFrontendVTable cfree_asm_frontend_vtable = { asm_frontend_new, asm_frontend_compile, asm_frontend_free, + asm_extensions, }; static SrcLoc no_loc(void) { @@ -56,27 +59,48 @@ static _Noreturn void panic_bad_options(Compiler* c, const char* msg) { compiler_panic(c, no_loc(), "bad cfree options: %s", msg); } -CfreeLanguage cfree_language_for_path(const char* path) { - size_t i, len; - if (!path) return CFREE_LANG_C; +/* Compare `ext` to `pat` letter-for-letter, lowercasing ASCII A-Z in + * the path side so `.S` matches asm's `"s"` entry. Both inputs are + * NUL-terminated; returns nonzero on full match. */ +static int ext_eq_ci(const char* ext, const char* pat) { + while (*ext && *pat) { + char a = *ext++; + char b = *pat++; + if (a >= 'A' && a <= 'Z') a = (char)(a - 'A' + 'a'); + if (a != b) return 0; + } + return *ext == 0 && *pat == 0; +} + +CfreeLanguage cfree_language_for_path(CfreeCompiler* c, const char* path) { + size_t len; + size_t i; + const char* ext; + unsigned lang; + + if (!c || !path) return CFREE_LANG_C; for (len = 0; path[len]; ++len) { } + /* Strip back to the last `.` after the final `/`. No dot → no + * extension → fall through to the C default. */ + ext = NULL; i = len; while (i > 0) { --i; - if (path[i] == '/') return CFREE_LANG_C; + if (path[i] == '/') break; if (path[i] == '.') { - const char* ext = path + i + 1; - if ((ext[0] == 's' || ext[0] == 'S') && ext[1] == '\0') - return CFREE_LANG_ASM; - if (ext[0] == 't' && ext[1] == 'o' && ext[2] == 'y' && ext[3] == '\0') - return CFREE_LANG_TOY; - if (ext[0] == 'w' && ext[1] == 'a' && ext[2] == 't' && ext[3] == '\0') - return CFREE_LANG_WASM; - if (ext[0] == 'w' && ext[1] == 'a' && ext[2] == 's' && ext[3] == 'm' && - ext[4] == '\0') - return CFREE_LANG_WASM; - return CFREE_LANG_C; + ext = path + i + 1; + break; + } + } + if (!ext) return CFREE_LANG_C; + + for (lang = 0; lang < CFREE_LANG_COUNT; ++lang) { + const CfreeFrontendVTable* v = c->frontends[lang]; + const char* const* exts; + if (!v || !v->extensions) continue; + for (exts = v->extensions; *exts; ++exts) { + if (ext_eq_ci(ext, *exts)) return (CfreeLanguage)lang; } } return CFREE_LANG_C; @@ -116,9 +140,7 @@ static void validate_bytes(Compiler* c, const CfreeBytes* in) { static const CfreeFrontendVTable* frontend_for_language(Compiler* c, CfreeLanguage lang) { if ((unsigned)lang >= CFREE_LANG_COUNT) return NULL; - if (c->frontends[lang]) return c->frontends[lang]; - if (lang == CFREE_LANG_ASM) return &asm_frontend_vtable; - return NULL; + return c->frontends[lang]; } static void validate_bytes(Compiler* c, const CfreeBytes* in); diff --git a/src/api/lang_registry.c b/src/api/lang_registry.c @@ -0,0 +1,52 @@ +/* Language frontend registry. + * + * This file is the *only* place in libcfree that checks + * CFREE_LANG_*_ENABLED. It runs during compiler construction and wires + * each compiled-in frontend's vtable into c->frontends[] so the public + * compile/pipeline paths can dispatch by CfreeLanguage without any + * host-side bootstrap. + * + * The asm frontend is unconditional and registered here too; there is + * no fallback in frontend_for_language(), so an embedder that doesn't + * want asm can drop the slot after compiler construction with + * cfree_register_frontend(c, CFREE_LANG_ASM, NULL). + * + * Third parties may still call cfree_register_frontend() to install or + * override any slot after construction; that public API is unchanged. + */ + +#include "api/lang_registry.h" + +#include <cfree/compile.h> + +#include "cfree/config.h" + +/* Defined in src/api/compile.c, alongside the asm frontend's + * new/compile/free functions. Treated as part of the codegen substrate + * (no per-frontend lang/ directory), so its declaration lives here + * rather than in a public header. */ +extern const CfreeFrontendVTable cfree_asm_frontend_vtable; + +#if CFREE_LANG_C_ENABLED +#include "c/c.h" +#endif +#if CFREE_LANG_TOY_ENABLED +#include "toy/toy.h" +#endif +#if CFREE_LANG_WASM_ENABLED +#include "wasm/wasm.h" +#endif + +void lang_registry_init(CfreeCompiler* c) { + (void)cfree_register_frontend(c, CFREE_LANG_ASM, &cfree_asm_frontend_vtable); +#if CFREE_LANG_C_ENABLED + (void)cfree_register_frontend(c, CFREE_LANG_C, &cfree_c_frontend_vtable); +#endif +#if CFREE_LANG_TOY_ENABLED + (void)cfree_register_frontend(c, CFREE_LANG_TOY, &cfree_toy_frontend_vtable); +#endif +#if CFREE_LANG_WASM_ENABLED + (void)cfree_register_frontend(c, CFREE_LANG_WASM, + &cfree_wasm_frontend_vtable); +#endif +} diff --git a/src/api/lang_registry.h b/src/api/lang_registry.h @@ -0,0 +1,10 @@ +#ifndef CFREE_INTERNAL_API_LANG_REGISTRY_H +#define CFREE_INTERNAL_API_LANG_REGISTRY_H + +#include <cfree/core.h> + +/* Wire every CFREE_LANG_*_ENABLED frontend into c->frontends[]. Called + * once during compiler construction; see src/api/lang_registry.c. */ +void lang_registry_init(CfreeCompiler* c); + +#endif diff --git a/src/core/core.c b/src/core/core.c @@ -7,6 +7,7 @@ #include <string.h> #include "abi/abi.h" +#include "api/lang_registry.h" #include "core/arena.h" #include "core/diag.h" #include "core/heap.h" @@ -42,6 +43,8 @@ void compiler_init(Compiler* c, Target target, const CfreeContext* ctx) { c->abi = abi_new(c); c->cleanup = NULL; + + lang_registry_init(c); } void compiler_fini(Compiler* c) { diff --git a/test/parse/harness/parse_runner.c b/test/parse/harness/parse_runner.c @@ -37,7 +37,6 @@ #include <sys/stat.h> #include <unistd.h> -#include "../../../lang/c/c.h" #include "lib/cfree_test_target.h" /* ---- env: heap, diag ---- */ @@ -409,8 +408,6 @@ static int mode_emit_impl(const char* src_path, const char* out_path, free(src); return 2; } - cfree_c_register(c); - memset(&in, 0, sizeof in); in.name = src_path; in.data = src; @@ -495,8 +492,6 @@ static int mode_jit(const char* src_path) { free(src); return 2; } - cfree_c_register(c); - memset(&in, 0, sizeof in); in.name = src_path; in.data = src;