commit 3178f9665c093c0bc131b9fef38ae47cbb94f684
parent 618d04fce07df20407a1a5f54c3cd41415e8ee37
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Sat, 18 Jul 2026 08:56:14 -0700
build: add single-TU C library emission
Diffstat:
7 files changed, 309 insertions(+), 81 deletions(-)
diff --git a/doc/CBACKEND.md b/doc/CBACKEND.md
@@ -78,6 +78,23 @@ compiler assumes a different layout, the access is wrong. This is the same
trade LLVM IR makes (datalayout-locked), and it does not limit the stated goal,
since the user already fixed the triple at kit invocation.
+## Single-TU library emission
+
+`kit build-lib --emit=c -o library.c sources...` feeds every semantic source
+into one shared CG session and emits one C translation unit. This is the
+distribution-oriented counterpart to `build-obj --emit=c`, which emits one
+source at a time. Each input retains its own preprocessor/group and frontend
+options, while external declarations resolve across inputs in the shared
+semantic unit. Internal-linkage symbols receive generated TU-unique C names so
+two original source files may both define (for example) `static helper` without
+colliding in the amalgamated output.
+
+Only frontends with `KIT_FRONTEND_LTO_CG` capability can participate. C, Toy,
+and Wasm are semantic today; standalone assembly is object-only and is rejected
+because an opaque object has no C-source representation. As with every C-backend
+output, this is a generated semantic amalgamation, not textual source
+concatenation, and it remains locked to the selected target's data layout.
+
## Semantic temporaries become C locals
CG mints fresh, unbounded local ids (`CGLocal`); each one becomes a single
diff --git a/doc/DRIVER.md b/doc/DRIVER.md
@@ -88,7 +88,7 @@ tool reaches into compiler internals.
| `cc` | C compiler driver: compile, optionally link; preprocess (`-E`), dep-emit (`-M*`), ELF `-shared`. GCC flag subset. Resolves `-l`/`-L` to concrete archive paths. |
| `check` | Run the C frontend checks with no code emission. |
| `build-exe` | Kit-native build verb: compile a polyglot source set (C / asm / wasm, per file) in memory and link it — with any `.o`/`.a`/`.so` inputs — into an executable. No intermediate files. |
-| `build-lib` | Compile a polyglot source set in memory into a static `.a` (default) or, with `-dynamic`, an ELF shared library. |
+| `build-lib` | Compile a polyglot source set in memory into a static `.a` (default), one target-locked C TU (`--emit=c`), or, with `-dynamic`, an ELF shared library. |
| `build-obj` | Compile sources to one object (or `--emit=asm\|c\|ir`, or `-fsyntax-only` check); multiple sources combine into one relocatable object (`ld -r`). The kit-native replacement for the retired `compile` tool. |
| `build` | Resolve workspace targets with the content-addressed build coordinator; run test targets; manage workspace repositories/packages; and serve the recipe-side source/glob/fetch/config/need/export protocol. |
| `install` | Lay down per-tool links (symlinks; hard links on Windows) in a target dir so the toolchain works under bare names (`cc`, `ld`, `nm`, …). Default set is the toolchain + standard-named byte utils; `--all` / explicit names override. |
diff --git a/driver/cmd/build.c b/driver/cmd/build.c
@@ -23,8 +23,8 @@
* parse+run parameterized by output kind:
*
* build-exe link an executable (link session, OUTPUT_EXE)
- * build-lib static .a (archive of compiled objects;
- * dynamic/shared not yet supported)
+ * build-lib static .a / single C TU (archive of compiled objects,
+ * semantic C amalgamation, or DSO)
* build-obj one object; or a relocatable (compile each source / link
* combine of N sources; or session, OUTPUT_RELOCATABLE)
* --emit=asm|c|ir; -fsyntax-only
@@ -1416,32 +1416,57 @@ static int build_open_output(const KitContext* ctx, DriverEnv* env,
return 0;
}
-/* Compile every source to an in-memory builder; objs[] is caller-owned. */
-static int build_compile_all(BuildOptions* o, KitCompiler* compiler,
- const KitContext* ctx, const KitCodeOptions* code,
- const KitDiagnosticOptions* diag,
- KitObjBuilder** objs, uint32_t* source_obj_index,
- uint8_t* source_order_keep,
- const KitBuildBatchOptions* batch,
- KitBuildPendingLto* pending_lto,
- uint32_t* nobjs_out) {
- DriverLoad* loads = NULL;
- KitBuildSource* sources = NULL;
- void** lang_extras = NULL;
- KitBuildObjects out;
+typedef struct BuildSourceBatch {
+ DriverLoad* loads;
+ KitBuildSource* sources;
+ void** lang_extras;
+} BuildSourceBatch;
+
+static void build_source_batch_fini(BuildOptions* o, KitCompiler* compiler,
+ const KitContext* ctx,
+ BuildSourceBatch* batch) {
uint32_t i;
- KitStatus st;
- int rc = 1;
+ if (batch->lang_extras && batch->sources) {
+ for (i = 0; i < o->nsources; ++i)
+ if (batch->lang_extras[i])
+ kit_frontend_free_options(compiler, batch->sources[i].lang,
+ batch->lang_extras[i]);
+ }
+ if (batch->loads)
+ for (i = 0; i < o->nsources; ++i)
+ driver_release_bytes(ctx->file_io, &batch->loads[i]);
+ if (batch->lang_extras)
+ driver_free(o->env, batch->lang_extras,
+ o->nsources * sizeof(*batch->lang_extras));
+ if (batch->sources)
+ driver_free(o->env, batch->sources,
+ o->nsources * sizeof(*batch->sources));
+ if (batch->loads)
+ driver_free(o->env, batch->loads, o->nsources * sizeof(*batch->loads));
+ memset(batch, 0, sizeof *batch);
+}
- if (nobjs_out) *nobjs_out = 0;
+/* Load and resolve every source while retaining its independent preprocessing
+ * and frontend options. Both object batching and single-TU C emission consume
+ * this shape. */
+static int build_source_batch_init(BuildOptions* o, KitCompiler* compiler,
+ const KitContext* ctx,
+ BuildSourceBatch* batch) {
+ uint32_t i;
+
+ memset(batch, 0, sizeof *batch);
if (o->nsources == 0) return 0;
- loads = driver_alloc_zeroed(o->env, o->nsources * sizeof(*loads));
- sources = driver_alloc_zeroed(o->env, o->nsources * sizeof(*sources));
- lang_extras = driver_alloc_zeroed(o->env, o->nsources * sizeof(*lang_extras));
- if (!loads || !sources || !lang_extras) {
+ batch->loads =
+ driver_alloc_zeroed(o->env, o->nsources * sizeof(*batch->loads));
+ batch->sources =
+ driver_alloc_zeroed(o->env, o->nsources * sizeof(*batch->sources));
+ batch->lang_extras = driver_alloc_zeroed(
+ o->env, o->nsources * sizeof(*batch->lang_extras));
+ if (!batch->loads || !batch->sources || !batch->lang_extras) {
driver_errf(o->tool, "out of memory");
- goto out;
+ build_source_batch_fini(o, compiler, ctx, batch);
+ return 1;
}
for (i = 0; i < o->nsources; ++i) {
@@ -1451,62 +1476,71 @@ static int build_compile_all(BuildOptions* o, KitCompiler* compiler,
char** fe_argv = NULL;
uint32_t fe_n = 0;
- if (driver_load_bytes(ctx->file_io, o->tool, path, &loads[i],
- &sources[i].bytes) != 0)
- goto out;
+ if (driver_load_bytes(ctx->file_io, o->tool, path, &batch->loads[i],
+ &batch->sources[i].bytes) != 0) {
+ build_source_batch_fini(o, compiler, ctx, batch);
+ return 1;
+ }
lang = build_resolve_lang(o, compiler, i);
if (lang == KIT_LANG_UNKNOWN) {
driver_errf(o->tool, "cannot determine language for %.*s (use -x LANG)",
KIT_SLICE_ARG(kit_slice_cstr(path)));
- goto out;
+ build_source_batch_fini(o, compiler, ctx, batch);
+ return 1;
}
+ batch->sources[i].lang = lang;
if (build_collect_fe_argv(o, i, lang, &fe_argv, &fe_n) != 0) {
if (fe_argv) driver_free(o->env, fe_argv, fe_n * sizeof(*fe_argv));
- goto out;
+ build_source_batch_fini(o, compiler, ctx, batch);
+ return 1;
}
if (fe_n) {
if (kit_frontend_parse_options(compiler, lang, (int)fe_n, fe_argv,
- &lang_extras[i]) != KIT_OK) {
+ &batch->lang_extras[i]) != KIT_OK) {
driver_errf(
o->tool, "unsupported -X%.*s frontend flag: %.*s",
KIT_SLICE_ARG(kit_slice_cstr(kit_language_name(compiler, lang))),
KIT_SLICE_ARG(kit_slice_cstr(fe_argv[0])));
if (fe_argv) driver_free(o->env, fe_argv, fe_n * sizeof(*fe_argv));
- goto out;
+ build_source_batch_fini(o, compiler, ctx, batch);
+ return 1;
}
}
if (fe_argv) driver_free(o->env, fe_argv, fe_n * sizeof(*fe_argv));
- sources[i].lang = lang;
- sources[i].name = kit_slice_cstr(path);
- sources[i].pp = &o->groups[gi].pp;
- sources[i].lang_extra = lang_extras[i];
+ batch->sources[i].name = kit_slice_cstr(path);
+ batch->sources[i].pp = &o->groups[gi].pp;
+ batch->sources[i].lang_extra = batch->lang_extras[i];
}
+ return 0;
+}
+
+/* Compile every source to an in-memory builder; objs[] is caller-owned. */
+static int build_compile_all(BuildOptions* o, KitCompiler* compiler,
+ const KitContext* ctx, const KitCodeOptions* code,
+ const KitDiagnosticOptions* diag,
+ KitObjBuilder** objs, uint32_t* source_obj_index,
+ uint8_t* source_order_keep,
+ const KitBuildBatchOptions* batch,
+ KitBuildPendingLto* pending_lto,
+ uint32_t* nobjs_out) {
+ BuildSourceBatch sources;
+ KitBuildObjects out;
+ KitStatus st;
+
+ if (nobjs_out) *nobjs_out = 0;
+ if (o->nsources == 0) return 0;
+ if (build_source_batch_init(o, compiler, ctx, &sources) != 0) return 1;
memset(&out, 0, sizeof out);
out.objs = objs;
out.source_obj_index = source_obj_index;
out.source_order_keep = source_order_keep;
out.pending_lto = pending_lto;
- st = kit_build_compile(compiler, code, diag, sources, o->nsources, batch,
- &out);
+ st = kit_build_compile(compiler, code, diag, sources.sources, o->nsources,
+ batch, &out);
if (nobjs_out) *nobjs_out = out.nobjs;
- if (st != KIT_OK) goto out;
- rc = 0;
-
-out:
- if (lang_extras && sources) {
- for (i = 0; i < o->nsources; ++i)
- if (lang_extras[i])
- kit_frontend_free_options(compiler, sources[i].lang, lang_extras[i]);
- }
- if (loads)
- for (i = 0; i < o->nsources; ++i)
- driver_release_bytes(ctx->file_io, &loads[i]);
- if (lang_extras)
- driver_free(o->env, lang_extras, o->nsources * sizeof(*lang_extras));
- if (sources) driver_free(o->env, sources, o->nsources * sizeof(*sources));
- if (loads) driver_free(o->env, loads, o->nsources * sizeof(*loads));
- return rc;
+ build_source_batch_fini(o, compiler, ctx, &sources);
+ return st == KIT_OK ? 0 : 1;
}
typedef struct BuildPreservedVec {
@@ -2205,6 +2239,49 @@ out:
return rc;
}
+/* build-lib --emit=c: merge semantic sources into one C translation unit. */
+static int build_run_c_tu(BuildOptions* o, KitCompiler* compiler,
+ const KitContext* ctx, const KitCodeOptions* code,
+ const KitDiagnosticOptions* diag) {
+ BuildSourceBatch sources;
+ KitBuildBatchOptions batch;
+ KitWriter* out_w = NULL;
+ uint32_t i;
+ int rc = 1;
+
+ if (build_source_batch_init(o, compiler, ctx, &sources) != 0) return 1;
+ for (i = 0; i < o->nsources; ++i) {
+ KitFrontendCaps caps;
+ memset(&caps, 0, sizeof caps);
+ if (kit_frontend_caps(compiler, sources.sources[i].lang, &caps) != KIT_OK ||
+ caps.lto_mode != KIT_FRONTEND_LTO_CG) {
+ driver_errf(o->tool,
+ "cannot emit C for %.*s: the %.*s frontend is object-only",
+ KIT_SLICE_ARG(sources.sources[i].name),
+ KIT_SLICE_ARG(kit_slice_cstr(kit_language_name(
+ compiler, sources.sources[i].lang))));
+ goto out;
+ }
+ }
+ if (build_open_output(ctx, o->env, o->tool, o->output_path, &out_w) != 0)
+ goto out;
+
+ memset(&batch, 0, sizeof batch);
+ batch.output_kind = KIT_CG_OUTPUT_ARCHIVE_MEMBER;
+ batch.interposition_policy = KIT_CG_INTERPOSITION_DEFAULT;
+ if (kit_build_emit_c(compiler, code, diag, sources.sources, o->nsources,
+ &batch, out_w) != KIT_OK) {
+ driver_errf(o->tool, "failed to emit C translation unit");
+ goto out;
+ }
+ rc = 0;
+
+out:
+ if (out_w) kit_writer_close(out_w);
+ build_source_batch_fini(o, compiler, ctx, &sources);
+ return rc;
+}
+
/* build-obj per-source: check-only, or one output per source (obj/asm/c/ir). */
static int build_run_per_source(BuildOptions* o, KitCompiler* compiler,
const KitContext* ctx,
@@ -2310,8 +2387,15 @@ static int build_validate(BuildOptions* o) {
}
if (o->kind == BUILD_OUT_LIB) {
- if (o->emit != BUILD_EMIT_OBJ || o->syntax_only) {
- driver_errf(o->tool, "--emit/-S/-fsyntax-only are build-obj options");
+ if ((o->emit != BUILD_EMIT_OBJ && o->emit != BUILD_EMIT_C) ||
+ o->syntax_only) {
+ driver_errf(o->tool,
+ "build-lib supports object output or --emit=c; "
+ "-S/--emit=ir/-fsyntax-only are build-obj options");
+ return 1;
+ }
+ if (o->emit == BUILD_EMIT_C && o->dynamic) {
+ driver_errf(o->tool, "--emit=c is incompatible with -dynamic/-shared");
return 1;
}
if (total_link != 0) {
@@ -2442,7 +2526,8 @@ static int build_main(int argc, char** argv, int kind, const char* tool,
if (build_parse(argc, argv, &o) != 0) goto done;
if (build_apply_env(&o) != 0) goto done;
- o.shared = (o.kind == BUILD_OUT_LIB && o.dynamic);
+ o.shared = (o.kind == BUILD_OUT_LIB && o.dynamic &&
+ o.emit == BUILD_EMIT_OBJ);
if (o.shared && !o.pic_explicit) o.target.pic = KIT_PIC_PIC;
if (o.shared && o.target.obj != KIT_OBJ_ELF) {
driver_errf(tool, "-shared output is supported only for ELF targets in v1");
@@ -2567,9 +2652,13 @@ static int build_main(int argc, char** argv, int kind, const char* tool,
rc =
build_run_link(&o, compiler, &ctx, &code, &diag, KIT_LINK_OUTPUT_EXE);
} else if (o.kind == BUILD_OUT_LIB) {
- rc = o.shared ? build_run_link(&o, compiler, &ctx, &code, &diag,
- KIT_LINK_OUTPUT_SHARED)
- : build_run_archive(&o, compiler, &ctx, &code, &diag);
+ if (o.emit == BUILD_EMIT_C)
+ rc = build_run_c_tu(&o, compiler, &ctx, &code, &diag);
+ else if (o.shared)
+ rc = build_run_link(&o, compiler, &ctx, &code, &diag,
+ KIT_LINK_OUTPUT_SHARED);
+ else
+ rc = build_run_archive(&o, compiler, &ctx, &code, &diag);
} else if (o.syntax_only) {
rc = build_run_per_source(&o, compiler, &ctx, &code, &diag);
} else if (o.emit == BUILD_EMIT_OBJ && o.nsources > 1 &&
@@ -2668,25 +2757,30 @@ void driver_help_build_lib(void) {
driver_printf(
"%.*s",
KIT_SLICE_ARG(KIT_SLICE_LIT(
- "kit build-lib — build a static library (.a) or ELF shared library\n"
+ "kit build-lib — build a library or one C translation unit\n"
"\n"
"USAGE\n"
" kit build-lib -o LIB.a [options] sources...\n"
+ " kit build-lib --emit=c -o LIB.c [options] sources...\n"
" kit build-lib -dynamic -o LIB.so [options] sources...\n"
"\n"
"DESCRIPTION\n"
" Compiles a polyglot source set in memory and archives the "
"objects\n"
" into a static library (.a), or links an ELF shared library with\n"
- " -dynamic/-shared. Non-ELF shared-library output is rejected.\n"
+ " -dynamic/-shared. --emit=c merges semantic sources into one\n"
+ " target-locked C translation unit. Non-ELF shared-library output\n"
+ " is rejected.\n"
"\n"
"INPUTS\n"
" Registered sources are C (.c), assembly (.s/.S), and WebAssembly\n"
- " (.wat/.wasm), selected by suffix or -x. Native ELF dynamic links\n"
- " may also consume compatible object/archive/library inputs.\n"
+ " (.wat/.wasm), selected by suffix or -x. --emit=c accepts semantic\n"
+ " frontends (C, Toy, Wasm), not standalone assembly. Native ELF\n"
+ " dynamic links may also consume compatible link inputs.\n"
"\n"
"OPTIONS\n"
- " -o PATH Output archive (required)\n"
+ " -o PATH Output path (required)\n"
+ " --emit=c Emit one C translation unit\n"
" -dynamic, -shared Build an ELF shared library\n"
" -fPIC Position-independent code\n"
" -O0 -O1 -O2 -g Optimization / debug info (-O2 aliases "
@@ -2706,12 +2800,15 @@ void driver_help_build_lib(void) {
" Relocated distributions discover sibling support automatically.\n"
" Native macOS discovers its SDK; hosted cross targets require an\n"
" explicit sysroot. Static .a output is the portable library shape;\n"
- " -dynamic/-shared is ELF-only.\n"
+ " -dynamic/-shared is ELF-only. Generated C must be compiled for the\n"
+ " same target selected here because its layouts are target-locked.\n"
"\n"
"EXAMPLES\n"
" kit build-lib \\\n"
" -o libanswer.a answer.c helper.s\n"
"\n"
+ " kit build-lib --emit=c -o answer_amalgam.c answer.c helper.c\n"
+ "\n"
" # Replace SYSROOT with a supplied x86-64 Linux sysroot.\n"
" SYSROOT=/replace/with/x86_64-linux-sysroot\n"
" kit build-lib \\\n"
diff --git a/include/kit/build.h b/include/kit/build.h
@@ -95,6 +95,23 @@ KIT_API KitStatus kit_build_compile(KitCompiler* compiler,
const KitBuildBatchOptions* batch,
KitBuildObjects* out);
+/* Compile a semantic source batch into one target-locked C translation unit.
+ * Every source must use a KIT_FRONTEND_LTO_CG frontend; an object-only
+ * frontend (standalone assembly, for example) returns KIT_UNSUPPORTED because
+ * its object bytes have no C-source representation. Per-source preprocessor
+ * and language options remain independent while declarations and references
+ * share one semantic CG session. `code` supplies the usual code policy; this
+ * entry selects emit_c_source and wires `out` itself. `batch` controls the CG
+ * finish/output policy; defer_lto_finish must be zero because emission finishes
+ * before this call returns. */
+KIT_API KitStatus kit_build_emit_c(KitCompiler* compiler,
+ const KitCodeOptions* code,
+ const KitDiagnosticOptions* diagnostics,
+ const KitBuildSource* sources,
+ uint32_t nsources,
+ const KitBuildBatchOptions* batch,
+ KitWriter* out);
+
KIT_API KitStatus kit_build_lto_finish(KitBuildPendingLto* pending,
const KitBuildBatchOptions* batch,
const KitCgSym* preserved_symbols,
diff --git a/src/api/build.c b/src/api/build.c
@@ -245,6 +245,58 @@ out:
return st;
}
+KitStatus kit_build_emit_c(KitCompiler* compiler, const KitCodeOptions* code,
+ const KitDiagnosticOptions* diagnostics,
+ const KitBuildSource* sources, uint32_t nsources,
+ const KitBuildBatchOptions* batch, KitWriter* out) {
+ KitCodeOptions emit_code;
+ KitObjBuilder* ob = NULL;
+ KitCg* cg = NULL;
+ KitCgFinishOptions finish;
+ KitStatus st = KIT_OK;
+
+ if (!compiler || !code || !diagnostics || !sources || nsources == 0 ||
+ !out || (batch && batch->defer_lto_finish))
+ return KIT_INVALID;
+
+ emit_code = *code;
+ emit_code.check_only = false;
+ emit_code.emit_asm_source = false;
+ emit_code.emit_c_source = true;
+ emit_code.emit_ir = false;
+ emit_code.c_source_writer = out;
+ emit_code.ir_dump_writer = NULL;
+
+ for (uint32_t i = 0; i < nsources; ++i) {
+ KitFrontendCaps caps;
+ memset(&caps, 0, sizeof caps);
+ st = kit_frontend_caps(compiler, sources[i].lang, &caps);
+ if (st != KIT_OK) return st;
+ if (caps.lto_mode != KIT_FRONTEND_LTO_CG) return KIT_UNSUPPORTED;
+ }
+
+ st = kit_obj_builder_new(compiler, &ob);
+ if (st == KIT_OK) st = kit_cg_new(compiler, &cg);
+ if (st == KIT_OK) st = kit_cg_begin(cg, ob, &emit_code);
+
+ for (uint32_t i = 0; st == KIT_OK && i < nsources; ++i)
+ st = build_compile_cg_run(compiler, &emit_code, diagnostics, &sources[i],
+ cg);
+
+ if (st == KIT_OK) {
+ memset(&finish, 0, sizeof finish);
+ finish.output_kind =
+ batch ? batch->output_kind : KIT_CG_OUTPUT_RELOCATABLE;
+ finish.interposition_policy =
+ batch ? batch->interposition_policy : KIT_CG_INTERPOSITION_DEFAULT;
+ st = kit_cg_finish(cg, &finish);
+ }
+
+ kit_cg_free(cg);
+ kit_obj_builder_free(ob);
+ return st;
+}
+
typedef struct BuildPreservedVec {
KitHeap* heap;
KitCgSym* syms;
diff --git a/src/arch/c_target/c_emit.c b/src/arch/c_target/c_emit.c
@@ -891,19 +891,22 @@ const char* c_sym_name(CTarget* t, ObjSymId sym) {
obj_format_demangle_c(t->c, &s, &n);
/* Sanitize for C identifier rules: assemblers accept '.', '$', etc. in
* symbol names; C does not. Replace each illegal byte with '_' and prepend
- * '_' if the first char isn't alpha/underscore. Local syms (SB_LOCAL) get
- * renamed freely since they have no cross-TU contract. Globals are assumed
- * to come in with C-safe names; if they don't, we still rewrite — the
- * resulting symbol won't link against other TUs that use the asm spelling,
- * but kit-produced code uses the rewritten spelling consistently. */
- int needs_rewrite = 0;
- if (n == 0) {
+ * '_' if the first char isn't alpha/underscore. Local syms (SB_LOCAL) also
+ * get an ObjSymId prefix: one C emit session may contain several original
+ * source units, each of which may legally define the same internal-linkage
+ * name. Globals are assumed to come in with C-safe names; if they don't, we
+ * still rewrite — the resulting symbol won't link against other TUs that use
+ * the asm spelling, but kit-produced code uses it consistently. */
+ int is_local = os->bind == SB_LOCAL;
+ int needs_rewrite = is_local;
+ if (n == 0 && !is_local) {
return s;
}
- if (!((s[0] >= 'a' && s[0] <= 'z') || (s[0] >= 'A' && s[0] <= 'Z') ||
- s[0] == '_')) {
- needs_rewrite = 1;
- } else {
+ if (n != 0 && !is_local) {
+ if (!((s[0] >= 'a' && s[0] <= 'z') || (s[0] >= 'A' && s[0] <= 'Z') ||
+ s[0] == '_')) {
+ needs_rewrite = 1;
+ }
for (size_t i = 0; i < n; ++i) {
char ch = s[i];
if (!((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
@@ -917,9 +920,24 @@ const char* c_sym_name(CTarget* t, ObjSymId sym) {
char buf[256];
size_t cap = sizeof(buf) - 1u;
size_t out = 0;
- int first_alpha = (s[0] >= 'a' && s[0] <= 'z') ||
- (s[0] >= 'A' && s[0] <= 'Z') || s[0] == '_';
- if (!first_alpha && out < cap) buf[out++] = '_';
+ if (is_local) {
+ static const char prefix[] = "__kit_local_";
+ char digits[16];
+ size_t ndigits = 0;
+ u32 value = (u32)sym;
+ for (size_t i = 0; i + 1u < sizeof prefix && out < cap; ++i)
+ buf[out++] = prefix[i];
+ do {
+ digits[ndigits++] = (char)('0' + value % 10u);
+ value /= 10u;
+ } while (value != 0 && ndigits < sizeof digits);
+ while (ndigits != 0 && out < cap) buf[out++] = digits[--ndigits];
+ if (out < cap) buf[out++] = '_';
+ } else {
+ int first_alpha = (s[0] >= 'a' && s[0] <= 'z') ||
+ (s[0] >= 'A' && s[0] <= 'Z') || s[0] == '_';
+ if (!first_alpha && out < cap) buf[out++] = '_';
+ }
for (size_t i = 0; i < n && out < cap; ++i) {
char ch = s[i];
int ok = (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
@@ -1006,6 +1024,10 @@ void c_emit_func_begin(CTarget* t, const CGFuncDesc* fd) {
* prototype regardless of definition order. */
c_ensure_forward_decl(t, fd->sym, fd->fn_type);
+ {
+ const ObjSym* os = obj_symbol_get(t->obj, fd->sym);
+ if (os && os->bind == SB_LOCAL) cbuf_puts(&t->body, "static ");
+ }
c_emit_func_signature(t, &t->body, name, fd->fn_type);
cbuf_puts(&t->body, " {\n");
t->fn_body_start = t->body.len;
@@ -1027,6 +1049,7 @@ void c_ensure_forward_decl(CTarget* t, ObjSymId sym, KitCgTypeId fn_type) {
const char* name = c_sym_name(t, sym);
const ObjSym* os = obj_symbol_get(t->obj, sym);
if ((os && (os->kind == SK_FUNC || os->kind == SK_IFUNC)) || fn_type != 0) {
+ if (os && os->bind == SB_LOCAL) cbuf_puts(&t->forwards, "static ");
c_emit_func_signature(t, &t->forwards, name, fn_type);
cbuf_puts(&t->forwards, ";\n");
} else {
diff --git a/test/buildcmds/run.sh b/test/buildcmds/run.sh
@@ -9,7 +9,8 @@
# -fsyntax-only, default output naming, -o - to stdout, the
# -X<lang> frontend-flag router, target features, the multi-source
# relocatable combine (with ld -r parity), and the negative paths.
-# build-lib : static .a from mixed sources, then link against it.
+# build-lib : static .a from mixed sources, then link against it; single-TU
+# C-source emission with cross-TU references and local names.
# build-exe : polyglot relocatable combine, native link+exec, --group scoping
# (verified through the produced exit code), and -L/-l.
@@ -45,6 +46,10 @@ printf 'int wasm_add(int a, int b);\nint wasm_main(void){return wasm_add(1,2)==3
printf 'int wasm_add(int a, int b){return a+b;}\n' > "$work/wasm_helper.c"
printf 'int wexe_add(int a, int b);\nint test_main(void){return wexe_add(2,3)==5?0:1;}\n' > "$work/wexe_main.c"
printf 'int wexe_add(int a, int b){return a+b;}\nint dead_fn(void){return 99;}\n' > "$work/wexe_add.c"
+printf '#ifndef AMAL_VALUE\n#error missing AMAL_VALUE\n#endif\nstatic int slot=AMAL_VALUE; static int local(void){return slot;}\nint amal_left(void){return local();}\n' > "$work/amal_left.c"
+printf '#ifndef AMAL_VALUE\n#error missing AMAL_VALUE\n#endif\nstatic int slot=AMAL_VALUE; static int local(void){return slot;}\nint amal_right(void){return local();}\n' > "$work/amal_right.c"
+printf 'int amal_left(void); int amal_right(void);\nint main(void){return amal_left()+amal_right()==42?0:1;}\n' > "$work/amal_main.c"
+printf '.text\n' > "$work/opaque.s"
cd "$work"
@@ -207,6 +212,23 @@ run_ok bl-static "$KIT" build-lib -Iinc -o libmix.a helper.c std.c prog.toy
assert_file_exists bl-static-file libmix.a
"$KIT" nm libmix.a > libmix.nm 2>/dev/null
contains bl-static-has-helper libmix.nm helper
+
+# One semantic batch becomes one C translation unit. Reusing `local` and
+# `slot` in both inputs verifies that source-level internal linkage remains
+# isolated after the original translation-unit boundary disappears.
+run_ok bl-emit-c "$KIT" build-lib --emit=c -o libamal.c \
+ --group -DAMAL_VALUE=20 -- amal_left.c \
+ --group -DAMAL_VALUE=22 -- amal_right.c
+assert_file_exists bl-emit-c-file libamal.c
+run_ok bl-emit-c-host-compile "${CC:-cc}" -std=c11 -Wall -Wextra -Werror \
+ libamal.c amal_main.c -o amal-app
+run_ok bl-emit-c-run ./amal-app
+
+# Standalone assembly is an object-only frontend and cannot be represented in
+# the generated C translation unit.
+run_fail bl-emit-c-opaque "$KIT" build-lib --emit=c -o opaque.c opaque.s
+contains bl-emit-c-opaque-diag "$work/bl-emit-c-opaque.err" \
+ "cannot emit C"
# -o is required (no obvious base name across N sources).
run_fail bl-neg-needs-o "$KIT" build-lib helper.c std.c
# build-lib takes only sources.