kit

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

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:
Mdoc/CBACKEND.md | 17+++++++++++++++++
Mdoc/DRIVER.md | 2+-
Mdriver/cmd/build.c | 227++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Minclude/kit/build.h | 17+++++++++++++++++
Msrc/api/build.c | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/arch/c_target/c_emit.c | 51+++++++++++++++++++++++++++++++++++++--------------
Mtest/buildcmds/run.sh | 24+++++++++++++++++++++++-
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.