packr 0.5.0

A WebAssembly package runtime with extended WIT support for recursive types
Documentation
# RESOLVED: StaticComposer produces invalid WASM for complex modules

**Status: FIXED** - See fixes below

## Summary

`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.

## Reproduction

### Input modules

1. **spawn-repl-actor.wasm** (18,445 bytes)
   - 4 function imports
   - 13 defined functions
   - 1 memory
   - Valid per `wasm-validate`

2. **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)

### Composition code

```rust
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()?;
```

### Result

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)
```

## Analysis

The errors are all function call type mismatches - call instructions are targeting wrong functions after index remapping.

### Suspected issue location

In `src/compose/merger.rs`, the function index remapping at line 822:

```rust
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.

### Possible causes

1. **Incomplete remap population** - Not all function indices are being added to the remap HashMap

2. **Ordering issue** - When processing the second module, function indices might not account for the first module's functions correctly

3. **Import counting** - The `num_imported_functions` tracking might be off when one import is wired internally (resolved to another module's export)

### Expected behavior for this case

- 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.

## Test modules vs real modules

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.

## Files

- Composed output saved at: `examples/actors/composed-repl.wasm` (in wisp repo)
- Source modules: `examples/actors/spawn-repl-actor.wasm`, `examples/wisp-compiler.wasm`

## How to reproduce

```bash
# 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
```

## Debugging suggestions

1. 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

2. Create a minimal failing test with two modules that have:
   - Multiple imports (some wired, some external)
   - Many functions
   - Cross-module calls via wiring

3. Verify the remap contains entries for ALL function indices before `remap_function_body()` is called

## Environment

- Pack commit: (current main)
- wasmparser: 0.219.2
- wasm-encoder: (matching version)
- Rust: 1.85+
- OS: NixOS

## Resolution

Two issues were identified and fixed:

### Issue 1: Import/function processing order

**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:
1. First pass: Process ALL imports from ALL modules
2. 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)

### Issue 2: Unsupported WASM instructions silently dropped

**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` - Added `ReturnCall` and `ReturnCallIndirect` variants
- `src/compose/merger.rs` - Added conversion for tail-call instructions

### Issue 3: Host functions not wired to packages without cross-package imports

**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