commit 9102a9951e9c68f81a05ac78565d71a43c3a337d
parent 8812d246da197618158da26b55ec717632e8bcb7
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 9 Jun 2026 15:29:08 -0700
build: wasm build-exe via CG-merge with DCE from entry point
kit build-exe -target wasm32-none a.c b.c -e entry -o out.wasm now
produces a runnable .wasm. Uses KIT_CG_OUTPUT_EXECUTABLE so DCE and
symbol internalization fire (unreachable code stripped, non-exported
symbols go local), giving a leaner module than build-obj's RELOCATABLE
output.
The entry symbol (-e, defaulting to "main") is resolved after compilation
via a new kit_obj_builder_find_symbol API and passed as the preserved
symbol to driver_compile_pending_lto_finish, keeping the entry alive
through DCE. build_is_link_output returns 0 for wasm exe so the hosted
libc/sysroot/runtime-archive setup is skipped. Default output name is
a.wasm. Link inputs (.o/.a/-l) are rejected with a diagnostic.
Tests: structural (be-wasm-exe, default name, bad-entry negative) in
test-driver-build; execution (wasm-exe/D JIT, wasm-exe/N interp) in
test-wasm-front.
Diffstat:
5 files changed, 168 insertions(+), 2 deletions(-)
diff --git a/driver/cmd/build.c b/driver/cmd/build.c
@@ -953,6 +953,7 @@ static int build_parse(int argc, char** argv, BuildOptions* o) {
/* ===================================================================== */
static int build_is_link_output(const BuildOptions* o) {
+ if (o->kind == BUILD_OUT_EXE && o->target.obj == KIT_OBJ_WASM) return 0;
return o->kind == BUILD_OUT_EXE || (o->kind == BUILD_OUT_LIB && o->dynamic);
}
@@ -1813,6 +1814,93 @@ out:
return rc;
}
+/* build-exe wasm: merge all sources into one .wasm with DCE/internalization. */
+static int build_run_wasm_exe(BuildOptions* o, KitCompiler* compiler,
+ const KitContext* ctx,
+ const KitCodeOptions* code,
+ const KitDiagnosticOptions* diag) {
+ DriverEnv* env = o->env;
+ KitObjBuilder** objs = NULL;
+ uint32_t* source_obj_index = NULL;
+ uint8_t* source_order_keep = NULL;
+ DriverCompilePendingLto pending_lto = {0};
+ KitWriter* out_w = NULL;
+ KitCodeOptions code2 = *code;
+ uint32_t nobjs = 0;
+ uint32_t i;
+ int rc = 1;
+
+ code2.lto = true;
+
+ objs = driver_alloc_zeroed(env, o->nsources * sizeof(*objs));
+ source_obj_index =
+ driver_alloc_zeroed(env, o->nsources * sizeof(*source_obj_index));
+ source_order_keep =
+ driver_alloc_zeroed(env, o->nsources * sizeof(*source_order_keep));
+ if (!objs || !source_obj_index || !source_order_keep) {
+ driver_errf(o->tool, "out of memory");
+ goto out;
+ }
+ {
+ DriverCompileBatchOptions batch;
+ memset(&batch, 0, sizeof batch);
+ batch.output_kind = KIT_CG_OUTPUT_EXECUTABLE;
+ batch.interposition_policy = KIT_CG_INTERPOSITION_NONE;
+ batch.defer_lto_finish = 1;
+ if (build_compile_all(o, compiler, ctx, &code2, diag, objs,
+ source_obj_index, source_order_keep,
+ &batch, &pending_lto, &nobjs) != 0)
+ goto out;
+ }
+ if (nobjs != 1) {
+ driver_errf(o->tool, "build-exe: wasm expects a single merged module");
+ goto out;
+ }
+ {
+ const char* entry_name = o->link.entry ? o->link.entry : "main";
+ KitSym entry_interned =
+ kit_sym_intern(compiler, kit_slice_cstr(entry_name));
+ KitObjSymbol entry_sym = KIT_OBJ_SYMBOL_NONE;
+ DriverCompileBatchOptions batch2;
+ KitCgSym csym;
+
+ if (!entry_interned ||
+ kit_obj_builder_find_symbol(pending_lto.obj, entry_interned,
+ &entry_sym) != KIT_OK) {
+ driver_errf(o->tool,
+ "build-exe: entry symbol '%s' not found in module",
+ entry_name);
+ goto out;
+ }
+ csym = (KitCgSym)entry_sym;
+ memset(&batch2, 0, sizeof batch2);
+ batch2.output_kind = KIT_CG_OUTPUT_EXECUTABLE;
+ batch2.interposition_policy = KIT_CG_INTERPOSITION_NONE;
+ if (driver_compile_pending_lto_finish(&pending_lto, &batch2,
+ &csym, 1) != KIT_OK) {
+ driver_errf(o->tool, "build-exe: LTO finish failed");
+ goto out;
+ }
+ }
+ if (build_open_output(ctx, env, o->tool, o->output_path, &out_w) != 0)
+ goto out;
+ rc = (kit_obj_builder_emit(objs[0], out_w) == KIT_OK) ? 0 : 1;
+
+out:
+ if (out_w) kit_writer_close(out_w);
+ if (pending_lto.active) driver_compile_pending_lto_abort(&pending_lto);
+ if (objs) {
+ for (i = 0; i < nobjs; ++i) kit_obj_builder_free(objs[i]);
+ driver_free(env, objs, o->nsources * sizeof(*objs));
+ }
+ if (source_order_keep)
+ driver_free(env, source_order_keep,
+ o->nsources * sizeof(*source_order_keep));
+ if (source_obj_index)
+ driver_free(env, source_obj_index, o->nsources * sizeof(*source_obj_index));
+ return rc;
+}
+
/* build-lib static: archive every compiled source object. */
static int build_run_archive(BuildOptions* o, KitCompiler* compiler,
const KitContext* ctx, const KitCodeOptions* code,
@@ -1968,7 +2056,21 @@ static int build_validate(BuildOptions* o) {
driver_errf(o->tool, "--emit/-S/-fsyntax-only are build-obj options");
return 1;
}
- if (!o->output_path) o->output_path = driver_default_exe_name(o->target);
+ if (!o->output_path) {
+ o->output_path = (o->target.obj == KIT_OBJ_WASM)
+ ? "a.wasm"
+ : driver_default_exe_name(o->target);
+ }
+ if (o->target.obj == KIT_OBJ_WASM) {
+ uint32_t total_link =
+ o->nobject_files + o->narchives + o->ndsos + o->npending_libs;
+ if (total_link) {
+ driver_errf(o->tool,
+ "build-exe -target wasm32-none accepts only source files "
+ "(no .o / .a / -l inputs)");
+ return 1;
+ }
+ }
return 0;
}
@@ -2167,7 +2269,10 @@ static int build_main(int argc, char** argv, int kind, const char* tool) {
diag.max_errors = o.max_errors;
if (o.kind == BUILD_OUT_EXE) {
- rc = build_run_link(&o, compiler, &ctx, &code, &diag, KIT_LINK_OUTPUT_EXE);
+ if (o.target.obj == KIT_OBJ_WASM)
+ rc = build_run_wasm_exe(&o, compiler, &ctx, &code, &diag);
+ else
+ 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)
diff --git a/include/kit/object.h b/include/kit/object.h
@@ -214,6 +214,8 @@ KIT_API KitStatus kit_obj_builder_symbol_set_bind(KitObjBuilder*, KitObjSymbol,
KitSymBind);
KIT_API KitStatus kit_obj_builder_symbol_set_vis(KitObjBuilder*, KitObjSymbol,
KitSymVis);
+KIT_API KitStatus kit_obj_builder_find_symbol(const KitObjBuilder*, KitSym,
+ KitObjSymbol* out);
KIT_API KitStatus kit_obj_builder_section_replace_bytes(KitObjBuilder*,
KitObjSection,
const void* data,
diff --git a/src/api/object_builder.c b/src/api/object_builder.c
@@ -264,6 +264,15 @@ KitStatus kit_obj_builder_section_replace_bytes(KitObjBuilder* b,
return KIT_OK;
}
+KitStatus kit_obj_builder_find_symbol(const KitObjBuilder* b, KitSym name,
+ KitObjSymbol* out) {
+ ObjSymId id;
+ if (!b || !out) return KIT_INVALID;
+ id = obj_symbol_find((ObjBuilder*)b, (Sym)name);
+ *out = intern_to_pub_sym(id);
+ return id != OBJ_SYM_NONE ? KIT_OK : KIT_NOT_FOUND;
+}
+
KitStatus kit_obj_builder_emit(KitObjBuilder* b, KitWriter* w) {
Compiler* c;
if (!b || !w) return KIT_INVALID;
diff --git a/test/buildcmds/run.sh b/test/buildcmds/run.sh
@@ -42,6 +42,8 @@ cp "$(ls "$repo_root"/test/toy/cases/*.toy | head -1)" "$work/prog.toy"
cp "$repo_root/test/wasm/cases/if_return.wat" "$work/prog.wat"
printf 'int wasm_add(int a, int b);\nint wasm_main(void){return wasm_add(1,2)==3?0:1;}\n' > "$work/wasm_main.c"
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"
cd "$work"
@@ -229,5 +231,28 @@ printf 'int helper(void);\nint main(void){return helper();}\n' > usemix.c
run_ok be-link-archive "$KIT" build-exe -lc usemix.c -L. -lmix -o app2
run_ok be-run-archive ./app2
+# ===========================================================================
+# build-exe — wasm target
+# ===========================================================================
+
+# Multi-source: produces a .wasm with DCE/internalization, runnable via kit run.
+run_ok be-wasm-exe "$KIT" build-exe -target wasm32-none \
+ wexe_main.c wexe_add.c -e test_main -o wexe.wasm
+assert_file_exists be-wasm-exe-file wexe.wasm
+_wexe_hdr=$(od -A n -t x1 -N 4 wexe.wasm | tr -d ' \t\n')
+[ "$_wexe_hdr" = "0061736d" ] && ok be-wasm-exe-magic || not_ok be-wasm-exe-magic
+run_ok be-wasm-exe-run "$KIT" run -e test_main wexe.wasm
+
+# Default output name a.wasm when -o is omitted.
+run_ok be-wasm-exe-default-name "$KIT" build-exe -target wasm32-none \
+ wexe_main.c wexe_add.c -e test_main
+assert_file_exists be-wasm-exe-default-name-file a.wasm
+
+# Unknown entry symbol is rejected with a clear diagnostic.
+run_fail be-wasm-exe-bad-entry "$KIT" build-exe -target wasm32-none \
+ wexe_main.c wexe_add.c -e no_such_fn -o bad_entry.wasm
+contains be-wasm-exe-bad-entry-diag "$work/be-wasm-exe-bad-entry.err" \
+ "entry symbol"
+
kit_summary build-driver
kit_exit
diff --git a/test/wasm/run.sh b/test/wasm/run.sh
@@ -380,5 +380,30 @@ EOF
fi
fi
+# ---- wasm build-exe: DCE from entry point, runnable via kit run -------------
+if [ "$RUN_D" -eq 1 ] || [ "$RUN_N" -eq 1 ]; then
+ KIT_WORK="$BUILD_DIR/wasm-exe"; rm -rf "$KIT_WORK"; mkdir -p "$KIT_WORK"
+ cat > "$KIT_WORK/exe_add.c" <<'EOF'
+int add(int a, int b) { return a + b; }
+int dead_fn(void) { return 99; }
+EOF
+ cat > "$KIT_WORK/exe_main.c" <<'EOF'
+int add(int a, int b);
+int test_main(void) { return add(2, 3) == 5 ? 0 : 1; }
+EOF
+ _wasm_exe="$KIT_WORK/out.wasm"
+ if "$KIT_BIN" build-exe -target wasm32-none \
+ "$KIT_WORK/exe_main.c" "$KIT_WORK/exe_add.c" \
+ -e test_main -o "$_wasm_exe" \
+ >"$KIT_WORK/build.out" 2>"$KIT_WORK/build.err"; then
+ [ "$RUN_D" -eq 1 ] && wf_rc "wasm-exe/D" 0 \
+ "$KIT_BIN" run -e test_main "$_wasm_exe"
+ [ "$RUN_N" -eq 1 ] && wf_rc_interp "wasm-exe/N" 0 \
+ "$KIT_BIN" run --no-jit -e test_main "$_wasm_exe"
+ else
+ kit_fail "wasm-exe/build" "build-exe failed"
+ fi
+fi
+
kit_summary test-wasm-front
kit_exit