commit 8812d246da197618158da26b55ec717632e8bcb7
parent f8a6d1101582182af525f54ad681e7467a83014b
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 9 Jun 2026 15:17:45 -0700
build: wasm multi-source via CG-merge in build-obj
build-obj -target wasm32-none a.c b.c -o out.wasm now merges all inputs
into one .wasm via the LTO CG-merge path, bypassing the linker (which has
no wasm support). The linker's relocatable writer dropped the OBJ_EXT_WASM
payload, producing an empty 8-byte stub; the new build_run_wasm_module
function finishes the merged ObjBuilder inline and emits directly.
Cross-TU calls and cross-TU data references work at all opt levels.
Same-name statics stay local per TU (LOCAL binding → distinct ObjSymIds).
Duplicate strong defs are rejected at -O1 via opt_resolve_duplicate_funcs.
Tests: structural (bo-wasm-multi, bo-wasm-polyglot, bo-wasm-dup) in
test-driver-build; execution (multi-tu/N interp, multi-tu/D JIT) in
test-wasm-front.
Diffstat:
3 files changed, 125 insertions(+), 0 deletions(-)
diff --git a/driver/cmd/build.c b/driver/cmd/build.c
@@ -1755,6 +1755,64 @@ out:
return rc;
}
+/* build-obj wasm multi-source: merge all inputs into one .wasm via CG merge. */
+static int build_run_wasm_module(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;
+ 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_RELOCATABLE;
+ batch.interposition_policy = KIT_CG_INTERPOSITION_DEFAULT;
+ if (build_compile_all(o, compiler, ctx, &code2, diag, objs, source_obj_index,
+ source_order_keep, &batch, NULL, &nobjs) != 0)
+ goto out;
+ }
+ if (nobjs != 1) {
+ driver_errf(o->tool,
+ "build-obj: wasm multi-input expects a single merged module");
+ 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 (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,
@@ -2116,6 +2174,9 @@ static int build_main(int argc, char** argv, int kind, const char* tool) {
: 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 &&
+ o.target.obj == KIT_OBJ_WASM) {
+ rc = build_run_wasm_module(&o, compiler, &ctx, &code, &diag);
} else if (o.emit == BUILD_EMIT_OBJ && o.nsources > 1) {
rc = build_run_relocatable(&o, compiler, &ctx, &code, &diag);
} else {
diff --git a/test/buildcmds/run.sh b/test/buildcmds/run.sh
@@ -40,6 +40,8 @@ printf 'int helper(void){return 0;}\n' > "$work/helper.c"
printf '#include <stdint.h>\nint32_t f(void){return 1;}\n' > "$work/std.c"
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"
cd "$work"
@@ -110,6 +112,36 @@ same_file bo-reloc-ld-r-parity ldr.sorted combine.sorted
# Polyglot combine: C + toy in one relocatable object.
run_ok bo-polyglot-combine "$KIT" build-obj -Iinc helper.c prog.toy -o poly.o
+# ---- wasm multi-source combine (CG-merge path) -----------------------------
+# Two C files merged into one .wasm module (magic \0asm + non-empty content).
+run_ok bo-wasm-multi "$KIT" build-obj -target wasm32-none \
+ wasm_main.c wasm_helper.c -o multi.wasm
+assert_file_exists bo-wasm-multi-file multi.wasm
+# Wasm magic (\0asm = 00 61 73 6d): check first 4 bytes via od (avoids null-byte
+# issues with cmp on BSD).
+_wasm_hdr=$(od -A n -t x1 -N 4 multi.wasm | tr -d ' \t\n')
+[ "$_wasm_hdr" = "0061736d" ] \
+ && ok bo-wasm-multi-magic || not_ok bo-wasm-multi-magic
+# Non-trivial module: size must exceed the 8-byte magic+version stub.
+_wasm_size=$(wc -c < multi.wasm | tr -d ' ')
+[ "$_wasm_size" -gt 8 ] \
+ && ok bo-wasm-multi-nontrivial || not_ok bo-wasm-multi-nontrivial
+
+# Polyglot wasm: C + toy in one .wasm module.
+run_ok bo-wasm-polyglot "$KIT" build-obj -target wasm32-none \
+ wasm_helper.c prog.toy -o poly.wasm
+assert_file_exists bo-wasm-polyglot-file poly.wasm
+_poly_hdr=$(od -A n -t x1 -N 4 poly.wasm | tr -d ' \t\n')
+[ "$_poly_hdr" = "0061736d" ] \
+ && ok bo-wasm-polyglot-magic || not_ok bo-wasm-polyglot-magic
+
+# Negative: duplicate strong symbol definition must be rejected (at -O1 the
+# optimizer runs opt_resolve_duplicate_funcs which fires the ODR check).
+printf 'int wasm_add(int a, int b){return a-b;}\n' > wasm_dup.c
+run_fail bo-wasm-dup "$KIT" build-obj -O1 -target wasm32-none \
+ wasm_helper.c wasm_dup.c -o dup.wasm
+contains bo-wasm-dup-diag "$work/bo-wasm-dup.err" "duplicate definition"
+
# ---- negative paths --------------------------------------------------------
# A frontend with an unknown -X flag is rejected by that frontend's parser.
run_fail bo-neg-bad-Xflag "$KIT" build-obj -c -Xtoy --bogus prog.toy -o x.o
diff --git a/test/wasm/run.sh b/test/wasm/run.sh
@@ -348,5 +348,37 @@ if [ "$RUN_DIAG" -eq 1 ]; then
wf_fail "err/malformed-leb/wasm" "$KIT_BIN" cc -target "$target_triple" -c "$KIT_WORK/malformed-leb.wasm" -o "$KIT_WORK/bad-leb.o"
fi
+# ---- multi-source wasm (cross-TU call + cross-TU data reference) -----------
+if [ "$RUN_D" -eq 1 ] || [ "$RUN_N" -eq 1 ]; then
+ KIT_WORK="$BUILD_DIR/multi-tu"; rm -rf "$KIT_WORK"; mkdir -p "$KIT_WORK"
+ cat > "$KIT_WORK/mtu_helper.c" <<'EOF'
+int g_count = 0;
+int add_and_count(int a, int b) { g_count++; return a + b; }
+EOF
+ cat > "$KIT_WORK/mtu_main.c" <<'EOF'
+extern int g_count;
+int add_and_count(int a, int b);
+int test_main(void) {
+ int r = add_and_count(3, 4);
+ return (r == 7 && g_count == 1) ? 0 : 1;
+}
+EOF
+ _mtu_wasm="$KIT_WORK/multi_tu.wasm"
+ if "$KIT_BIN" build-obj -target wasm32-none \
+ "$KIT_WORK/mtu_main.c" "$KIT_WORK/mtu_helper.c" \
+ -o "$_mtu_wasm" >"$KIT_WORK/build.out" 2>"$KIT_WORK/build.err"; then
+ if [ "$RUN_N" -eq 1 ]; then
+ wf_rc_interp "multi-tu/N" 0 \
+ "$KIT_BIN" run --no-jit -e test_main "$_mtu_wasm"
+ fi
+ if [ "$RUN_D" -eq 1 ]; then
+ wf_rc "multi-tu/D" 0 \
+ "$KIT_BIN" run -e test_main "$_mtu_wasm"
+ fi
+ else
+ kit_fail "multi-tu/build" "build-obj failed"
+ fi
+fi
+
kit_summary test-wasm-front
kit_exit