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:
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;