Status: FIXED - See fixes below
StaticComposer produces invalid WASM when composing larger/complex modules. The composed output has many function call type mismatches, suggesting the function index remapping is incorrect.
-
spawn-repl-actor.wasm (18,445 bytes)
- 4 function imports
- 13 defined functions
- 1 memory
- Valid per
wasm-validate
-
wisp-compiler.wasm (149,766 bytes)
- 0 function imports
- 133 defined functions
- 1 memory
- Valid per wasmtime (wabt reports opcode 0x12 issue but wasmtime loads it fine)
let composed_wasm = StaticComposer::new()
.add_module("compiler", compiler_wasm)?
.add_module("repl", actor_wasm)?
.wire(
"repl",
"wisp:compiler/compiler",
"compile-source",
"compiler",
"compile-source",
)
.export("theater:simple/actor.init", "repl", "theater:simple/actor.init")
.export("theater:simple/message-server-client.handle-send", "repl", "theater:simple/message-server-client.handle-send")
.export("theater:simple/message-server-client.handle-request", "repl", "theater:simple/message-server-client.handle-request")
.export("memory", "repl", "memory")
.compose()?;Composed WASM is 156,883 bytes but invalid:
$ wasm-validate --enable-multi-memory composed-repl.wasm
0000272: error: type mismatch in call, expected [i32, i32, i32, i32] but got [i32]
0000374: error: type mismatch in `if true` branch, expected [i32] but got [... i64]
...
(hundreds of similar errors)
The errors are all function call type mismatches - call instructions are targeting wrong functions after index remapping.
In src/compose/merger.rs, the function index remapping at line 822:
StoredOperator::Call(idx) => {
let new_idx = remap.functions.get(idx).copied().unwrap_or(*idx);
Instruction::Call(new_idx)
}The .unwrap_or(*idx) fallback uses the original index if not found in remap, which would be wrong in a merged module context.
-
Incomplete remap population - Not all function indices are being added to the remap HashMap
-
Ordering issue - When processing the second module, function indices might not account for the first module's functions correctly
-
Import counting - The
num_imported_functionstracking might be off when one import is wired internally (resolved to another module's export)
-
Compiler (added first, 0 imports, 133 functions):
- Functions get merged indices 0-132 (assuming no external imports from compiler)
-
Repl-actor (added second, 4 imports where 1 is wired, 13 functions):
- 3 external imports get merged import indices 0-2
- 1 wired import (compile-source) maps to compiler's export function
- 13 defined functions get indices after all imports and compiler functions
When repl-actor code has call 5 (to its function index 5), it should be remapped to the correct merged index, not left as 5.
The 75 existing tests pass. The test modules are small and simple:
- Few functions
- Simple wirings
- Single-digit indices
The real modules are much larger, which likely exposes edge cases in the remapping logic.
- Composed output saved at:
examples/actors/composed-repl.wasm(in wisp repo) - Source modules:
examples/actors/spawn-repl-actor.wasm,examples/wisp-compiler.wasm
# From wisp repo root
cd /home/colin/work/wisp
# Compile the actor (if not already compiled)
cargo run -- compile examples/actors/spawn-repl-actor.lisp examples/actors/spawn-repl-actor
# The compiler WASM should already exist at examples/wisp-compiler.wasm
# Run theater-repl with --static flag to trigger composition
cargo run -p theater-repl -- --static
# This will fail with "WASM execution error: WebAssembly translation error"
# The composed WASM is saved to examples/actors/composed-repl.wasm for inspection-
Add debug logging to
merge_module()showing:- Each module's import/function counts
- Each remap entry as it's created
- Final remap state before processing function bodies
-
Create a minimal failing test with two modules that have:
- Multiple imports (some wired, some external)
- Many functions
- Cross-module calls via wiring
-
Verify the remap contains entries for ALL function indices before
remap_function_body()is called
- Pack commit: (current main)
- wasmparser: 0.219.2
- wasm-encoder: (matching version)
- Rust: 1.85+
- OS: NixOS
Two issues were identified and fixed:
Problem: Imports and defined functions were processed per-module sequentially, causing function index collisions. When module A (no imports, 133 functions) was processed first, its functions got indices 0-132. When module B (4 imports) was processed second, its imports got indices 0-2, overlapping with module A's functions.
Fix: Split processing into two passes:
- First pass: Process ALL imports from ALL modules
- Second pass: Process ALL defined functions (which now start after all imports)
Location: src/compose/merger.rs - Step 5 split into Step 5 (imports) and Step 5b (defined functions)
Problem: The convert_operator() function returns None for unsupported instructions, and the calling code silently skips them. This corrupts function bodies when instructions like return_call (tail-call proposal) are used.
Fix: Added support for return_call and return_call_indirect instructions in both parser and merger.
Location:
src/compose/parser.rs- AddedReturnCallandReturnCallIndirectvariantssrc/compose/merger.rs- Added conversion for tail-call instructions
Problem: CompositionBuilder treated packages without wire() calls as "providers" and instantiated them without host functions, even if host functions were registered.
Fix: Modified the provider/consumer classification to treat packages as consumers when host functions are defined.
Location: src/runtime/composition.rs - Added has_host_functions check to consumer filter