diff --git a/Cargo.lock b/Cargo.lock
index 3efe762..d277289 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -8,7 +8,7 @@ version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
dependencies = [
- "gimli",
+ "gimli 0.31.1",
]
[[package]]
@@ -111,6 +111,12 @@ dependencies = [
"syn",
]
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
[[package]]
name = "base64"
version = "0.21.7"
@@ -268,7 +274,7 @@ dependencies = [
"cranelift-control",
"cranelift-entity",
"cranelift-isle",
- "gimli",
+ "gimli 0.31.1",
"hashbrown 0.14.5",
"log",
"regalloc2",
@@ -469,6 +475,12 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "fallible-iterator"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
+
[[package]]
name = "fallible-iterator"
version = "0.3.0"
@@ -542,17 +554,34 @@ dependencies = [
"wasip2",
]
+[[package]]
+name = "gimli"
+version = "0.26.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d"
+dependencies = [
+ "fallible-iterator 0.2.0",
+ "indexmap 1.9.3",
+ "stable_deref_trait",
+]
+
[[package]]
name = "gimli"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
dependencies = [
- "fallible-iterator",
- "indexmap",
+ "fallible-iterator 0.3.0",
+ "indexmap 2.13.0",
"stable_deref_trait",
]
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
[[package]]
name = "hashbrown"
version = "0.14.5"
@@ -591,6 +620,16 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.12.3",
+]
+
[[package]]
name = "indexmap"
version = "2.13.0"
@@ -757,7 +796,7 @@ checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
dependencies = [
"crc32fast",
"hashbrown 0.15.5",
- "indexmap",
+ "indexmap 2.13.0",
"memchr",
]
@@ -786,6 +825,8 @@ dependencies = [
"sha2",
"thiserror 1.0.69",
"tokio",
+ "toml",
+ "walrus",
"wasm-encoder 0.219.2",
"wasmparser 0.219.2",
"wasmtime",
@@ -1219,7 +1260,7 @@ version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
- "indexmap",
+ "indexmap 2.13.0",
"serde",
"serde_spanned",
"toml_datetime",
@@ -1279,6 +1320,34 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+[[package]]
+name = "walrus"
+version = "0.23.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6481311b98508f4bc2d0abbfa5d42172e7a54b4b24d8f15e28b0dc650be0c59f"
+dependencies = [
+ "anyhow",
+ "gimli 0.26.2",
+ "id-arena",
+ "leb128",
+ "log",
+ "walrus-macro",
+ "wasm-encoder 0.214.0",
+ "wasmparser 0.214.0",
+]
+
+[[package]]
+name = "walrus-macro"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ad39ff894c43c9649fa724cdde9a6fc50b855d517ef071a93e5df82fe51d3"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
@@ -1339,6 +1408,15 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "wasm-encoder"
+version = "0.214.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff694f02a8d7a50b6922b197ae03883fbf18cdb2ae9fbee7b6148456f5f44041"
+dependencies = [
+ "leb128",
+]
+
[[package]]
name = "wasm-encoder"
version = "0.219.2"
@@ -1359,6 +1437,20 @@ dependencies = [
"wasmparser 0.244.0",
]
+[[package]]
+name = "wasmparser"
+version = "0.214.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5309c1090e3e84dad0d382f42064e9933fdaedb87e468cc239f0eabea73ddcb6"
+dependencies = [
+ "ahash",
+ "bitflags",
+ "hashbrown 0.14.5",
+ "indexmap 2.13.0",
+ "semver",
+ "serde",
+]
+
[[package]]
name = "wasmparser"
version = "0.219.2"
@@ -1368,7 +1460,7 @@ dependencies = [
"ahash",
"bitflags",
"hashbrown 0.14.5",
- "indexmap",
+ "indexmap 2.13.0",
"semver",
"serde",
]
@@ -1380,7 +1472,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
"bitflags",
- "indexmap",
+ "indexmap 2.13.0",
"semver",
]
@@ -1410,9 +1502,9 @@ dependencies = [
"cfg-if",
"encoding_rs",
"fxprof-processed-profile",
- "gimli",
+ "gimli 0.31.1",
"hashbrown 0.14.5",
- "indexmap",
+ "indexmap 2.13.0",
"ittapi",
"libc",
"libm",
@@ -1515,7 +1607,7 @@ dependencies = [
"cranelift-entity",
"cranelift-frontend",
"cranelift-native",
- "gimli",
+ "gimli 0.31.1",
"itertools",
"log",
"object 0.36.7",
@@ -1537,8 +1629,8 @@ dependencies = [
"cpp_demangle",
"cranelift-bitset",
"cranelift-entity",
- "gimli",
- "indexmap",
+ "gimli 0.31.1",
+ "indexmap 2.13.0",
"log",
"object 0.36.7",
"postcard",
@@ -1617,7 +1709,7 @@ checksum = "89d6b5297bea14d8387c3974b2b011de628cc9b188f135cec752b74fd368964b"
dependencies = [
"anyhow",
"cranelift-codegen",
- "gimli",
+ "gimli 0.31.1",
"object 0.36.7",
"target-lexicon",
"wasmparser 0.219.2",
@@ -1634,7 +1726,7 @@ checksum = "bf3963c9c29df91564d8bd181eb00d0dbaeafa1b2a01e15952bb7391166b704e"
dependencies = [
"anyhow",
"heck",
- "indexmap",
+ "indexmap 2.13.0",
"wit-parser",
]
@@ -1699,7 +1791,7 @@ checksum = "8b42b678c8651ec4900d7600037d235429fc985c31cbc33515885ec0d2a9e158"
dependencies = [
"anyhow",
"cranelift-codegen",
- "gimli",
+ "gimli 0.31.1",
"regalloc2",
"smallvec",
"target-lexicon",
@@ -1819,7 +1911,7 @@ checksum = "ca004bb251010fe956f4a5b9d4bf86b4e415064160dd6669569939e8cbf2504f"
dependencies = [
"anyhow",
"id-arena",
- "indexmap",
+ "indexmap 2.13.0",
"log",
"semver",
"serde",
diff --git a/Cargo.toml b/Cargo.toml
index 3eb9d2a..e38e934 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -66,6 +66,8 @@ anyhow = "1.0"
# Static composition - parse and emit WASM modules
wasmparser = "0.219"
wasm-encoder = "0.219"
+walrus = "0.23" # pack compose: internalize memory/globals into one self-contained module
+toml = "0.8" # pack compose: manifest parsing
# Hashing for interface compatibility
sha2 = "0.10"
diff --git a/assets/adder_fixedbase.wasm b/assets/adder_fixedbase.wasm
new file mode 100755
index 0000000..d641e7b
Binary files /dev/null and b/assets/adder_fixedbase.wasm differ
diff --git a/assets/doubler_fixedbase.wasm b/assets/doubler_fixedbase.wasm
new file mode 100755
index 0000000..8ae27a2
Binary files /dev/null and b/assets/doubler_fixedbase.wasm differ
diff --git a/docs/pic-composition.md b/docs/pic-composition.md
index b5ad6cd..32ec3ad 100644
--- a/docs/pic-composition.md
+++ b/docs/pic-composition.md
@@ -190,18 +190,47 @@ substrate, and the smallest useful validation of §3.
---
-## 6. Level 3 (future): static merge → one `.wasm`
+## 6. Level 3 (shipped): static merge → one `.wasm` — `pack compose`
+
+Implemented as the **`pack compose`** command. It merges the packages (allocator
++ A + B + …) into a **single self-contained `.wasm` with zero imports** —
+the original "package it up and pass it around" goal — that runs on any stock
+runtime with no harness (`wasm-tools validate` clean).
+
+**Pipeline** (`src/compose/static_compose.rs`):
+1. **`wasm-merge`** (binaryen) fuses the modules and internalizes the
+ cross-package imports: `(import "math" "double")` → a direct call to the
+ provider's `double`; `(import "pack:alloc" …)` → a call to the merged-in
+ allocator. Each package is named by the import-module-name its consumers use.
+2. A **`walrus`** pass does what `wasm-merge` can't: unify the several
+ `env.memory` imports into one internal memory, bake the allocator's
+ `__memory_base` / `__heap_base` / `__heap_end` imports into constants, and
+ export the memory.
+
+**Build recipe** — packages are compiled at disjoint **fixed bases** (non-PIC, so
+data lands at absolute addresses needing no relocation):
-The PIC side modules (allocator + A + B) are relocatable objects. A wasm linker
-step (`wasm-ld -r`, or a purpose-built merge) can combine them into a single
-relocatable module with one fixed memory layout, finalized into **one passable
-`.wasm`** — the original "package it up and pass it around" goal. This is a
-larger, separate effort; §3 (runtime PIC composition) is its prerequisite and its
-input.
+```
+RUSTFLAGS="-Clink-arg=--import-memory -Clink-arg=--initial-memory=8388608 \
+ -Clink-arg=--stack-first -Clink-arg=-zstack-size=262144 \
+ -Clink-arg=--global-base= -Clink-arg=--no-entry"
+```
+
+**CLI** — `packr compose [-o out.wasm]`, where the manifest lists
+`[[package]]` entries (providers before consumers) with `name` = the consumer's
+import module-name, plus an optional `[layout]`. Requires `wasm-merge` (binaryen)
+on `PATH`.
+
+> **Packaging, not performance.** The merge internalizes the *call*, but the packr
+> ABI marshalling (encode/decode/alloc per cross-call) is fossilized into the
+> compiled packages, so it remains. `pack compose` gives one portable artifact; it
+> does not make the boundary cheaper. A by-reference shared-memory ABI would be the
+> separate perf lever.
Keep **both**: runtime PIC composition (level 2) stays valuable for loading /
-swapping packages at runtime; the static merge (level 3) is for shipping a frozen
-bundle. They share the same PIC side-module format.
+swapping packages at runtime; the static merge (`pack compose`) is for shipping a
+frozen, self-contained bundle. They share the same PIC/fixed-base side-module
+substrate.
---
diff --git a/flake.nix b/flake.nix
index 967fd7f..f7ea22a 100644
--- a/flake.nix
+++ b/flake.nix
@@ -49,6 +49,7 @@
cargo-expand
wasm-pack
wasmtime
+ binaryen # wasm-merge, used by `pack compose`
gh
];
diff --git a/src/compose/merger.rs b/src/compose/merger.rs
deleted file mode 100644
index e318038..0000000
--- a/src/compose/merger.rs
+++ /dev/null
@@ -1,1387 +0,0 @@
-//! Module merging logic.
-//!
-//! Handles merging multiple WASM modules into one, including:
-//! - Type deduplication
-//! - Index remapping for functions, types, globals, etc.
-//! - Resolving imports to internal calls
-//! - Memory and data segment merging
-
-use std::borrow::Cow;
-use std::collections::{HashMap, HashSet};
-
-use wasm_encoder::{
- CodeSection, ConstExpr as EncConstExpr, DataSection, ElementSection, Elements,
- ExportKind as EncExportKind, ExportSection, Function, FunctionSection, GlobalSection,
- GlobalType, HeapType, ImportSection, Instruction, MemorySection, MemoryType, Module, RefType,
- TableSection, TableType, TypeSection, ValType,
-};
-
-use super::error::ComposeError;
-use super::parser::{
- BlockType, ConstExpr, DataSegment, DataSegmentKind, Element, ElementItems, ElementKind,
- ExportKind, FunctionCode, Global, ImportKind, MemArg, ParsedModule, RefTypeKind,
- StoredOperator,
-};
-
-/// Wiring specification: how to resolve an import.
-#[derive(Debug, Clone)]
-pub struct Wiring {
- /// The module that has the import.
- pub consumer: String,
- /// The import module name (as declared in the WASM import).
- pub import_module: String,
- /// The import function name.
- pub import_fn: String,
- /// The module providing the implementation.
- pub provider: String,
- /// The export name in the provider module.
- pub provider_export: String,
-}
-
-/// Export specification: what to export from the merged module.
-#[derive(Debug, Clone)]
-pub struct ExportSpec {
- /// Name of the export in the merged module.
- pub name: String,
- /// Source module name.
- pub source_module: String,
- /// Export name in the source module.
- pub source_export: String,
-}
-
-/// The merger combines multiple parsed modules into a single WASM module.
-pub struct Merger {
- modules: Vec,
- wirings: Vec,
- exports: Vec,
-}
-
-/// Index remapping for a single module when merged.
-#[derive(Debug, Default)]
-struct IndexRemap {
- /// Old type index -> new type index.
- types: HashMap,
- /// Old function index -> new function index.
- functions: HashMap,
- /// Old table index -> new table index.
- tables: HashMap,
- /// Old memory index -> new memory index.
- memories: HashMap,
- /// Old global index -> new global index.
- globals: HashMap,
-}
-
-impl Merger {
- /// Create a new merger.
- pub fn new(modules: Vec, wirings: Vec, exports: Vec) -> Self {
- Self {
- modules,
- wirings,
- exports,
- }
- }
-
- /// Merge the modules and produce a combined WASM binary.
- pub fn merge(self) -> Result, ComposeError> {
- if self.modules.is_empty() {
- return Err(ComposeError::NoModules);
- }
-
- // Step 1: Build dependency graph and topologically sort modules
- let sorted_modules = self.topological_sort()?;
-
- // Step 2: Build combined structures with remapping
- let mut merged = MergedModule::new();
-
- // Maps to track where things end up
- let mut module_remaps: HashMap = HashMap::new();
- let mut resolved_imports: HashMap<(String, String, String), u32> = HashMap::new();
-
- // Step 3: Collect all types and deduplicate
- for module in &sorted_modules {
- let mut remap = IndexRemap::default();
- for (old_idx, func_type) in module.types.iter().enumerate() {
- let new_idx = merged.add_type(func_type);
- remap.types.insert(old_idx as u32, new_idx);
- }
- module_remaps.insert(module.name.clone(), remap);
- }
-
- // Step 4: Process imports and figure out which become internal calls
- // First pass: collect all wiring info
- let mut import_resolutions: HashMap<(String, String, String), (String, String)> =
- HashMap::new();
- for wiring in &self.wirings {
- import_resolutions.insert(
- (
- wiring.consumer.clone(),
- wiring.import_module.clone(),
- wiring.import_fn.clone(),
- ),
- (wiring.provider.clone(), wiring.provider_export.clone()),
- );
- }
-
- // Step 5: Process imports from ALL modules first
- // This is critical: WASM requires all imports before all defined functions,
- // so we must collect all imports first to know the total import count
- // before assigning indices to defined functions.
- for module in &sorted_modules {
- let remap = module_remaps.get_mut(&module.name).unwrap();
-
- // Process imports
- let mut import_func_idx = 0u32;
- let mut import_table_idx = 0u32;
- let mut import_memory_idx = 0u32;
- let mut import_global_idx = 0u32;
-
- for import in &module.imports {
- match &import.kind {
- ImportKind::Function(type_idx) => {
- let old_func_idx = import_func_idx;
- import_func_idx += 1;
-
- let key = (
- module.name.clone(),
- import.module.clone(),
- import.name.clone(),
- );
-
- if import_resolutions.contains_key(&key) {
- // This import is resolved internally - we'll fix the index later
- // after we've processed the provider
- let placeholder = u32::MAX; // Placeholder
- remap.functions.insert(old_func_idx, placeholder);
- resolved_imports.insert(key, old_func_idx);
- } else {
- // Keep as external import
- let new_type_idx = remap.types[type_idx];
- let new_func_idx = merged.add_import_function(
- &import.module,
- &import.name,
- new_type_idx,
- );
- remap.functions.insert(old_func_idx, new_func_idx);
- }
- }
- ImportKind::Table(table_ty) => {
- let old_idx = import_table_idx;
- import_table_idx += 1;
- let new_idx =
- merged.add_import_table(&import.module, &import.name, table_ty);
- remap.tables.insert(old_idx, new_idx);
- }
- ImportKind::Memory(mem_ty) => {
- let old_idx = import_memory_idx;
- import_memory_idx += 1;
- let new_idx =
- merged.add_import_memory(&import.module, &import.name, mem_ty);
- remap.memories.insert(old_idx, new_idx);
- }
- ImportKind::Global(global_ty) => {
- let old_idx = import_global_idx;
- import_global_idx += 1;
- let new_idx =
- merged.add_import_global(&import.module, &import.name, global_ty);
- remap.globals.insert(old_idx, new_idx);
- }
- }
- }
- }
-
- // Step 5b: Process defined entities (tables, memories, globals, functions)
- // Now that all imports are processed, we know the correct starting indices.
- for module in &sorted_modules {
- let remap = module_remaps.get_mut(&module.name).unwrap();
-
- // Add defined tables
- for (i, table_ty) in module.tables.iter().enumerate() {
- let old_idx = module.num_imported_tables + i as u32;
- let new_idx = merged.add_table(table_ty);
- remap.tables.insert(old_idx, new_idx);
- }
-
- // Add defined memories - SHARE memory across all modules
- // When modules are wired together, they need to share memory for
- // cross-module function calls to work (strings/data passed by pointer).
- // We map all module-defined memories to a single shared memory (index 0).
- for (i, mem_ty) in module.memories.iter().enumerate() {
- let old_idx = module.num_imported_memories + i as u32;
- // Add memory only if this is the first one we've seen
- let new_idx = if merged.memories.is_empty() {
- merged.add_memory(mem_ty)
- } else {
- // Expand the shared memory if this module needs more
- merged.expand_memory(mem_ty);
- 0 // All modules share memory 0
- };
- remap.memories.insert(old_idx, new_idx);
- }
-
- // Add defined globals
- for (i, global) in module.globals.iter().enumerate() {
- let old_idx = module.num_imported_globals + i as u32;
- let (new_idx, _unified) = merged.add_global(global, remap);
- remap.globals.insert(old_idx, new_idx);
- }
-
- // Add defined functions (just declare types for now, code comes later)
- for (i, &type_idx) in module.functions.iter().enumerate() {
- let old_func_idx = module.num_imported_functions + i as u32;
- let new_type_idx = remap.types[&type_idx];
- let new_func_idx = merged.add_function(new_type_idx);
- remap.functions.insert(old_func_idx, new_func_idx);
- }
- }
-
- // Step 6: Resolve internal wirings now that we know all function indices
- for wiring in &self.wirings {
- let provider_module = sorted_modules
- .iter()
- .find(|m| m.name == wiring.provider)
- .ok_or_else(|| ComposeError::ModuleNotFound(wiring.provider.clone()))?;
-
- let export = provider_module
- .get_export(&wiring.provider_export)
- .ok_or_else(|| ComposeError::FunctionNotFound {
- module: wiring.provider.clone(),
- function: wiring.provider_export.clone(),
- })?;
-
- if export.kind != ExportKind::Function {
- return Err(ComposeError::FunctionNotFound {
- module: wiring.provider.clone(),
- function: wiring.provider_export.clone(),
- });
- }
-
- let provider_remap = &module_remaps[&wiring.provider];
- let new_func_idx = provider_remap.functions[&export.index];
-
- // Update the consumer's remap for this import
- let consumer_remap = module_remaps.get_mut(&wiring.consumer).unwrap();
- let key = (
- wiring.consumer.clone(),
- wiring.import_module.clone(),
- wiring.import_fn.clone(),
- );
- if let Some(&old_import_idx) = resolved_imports.get(&key) {
- consumer_remap
- .functions
- .insert(old_import_idx, new_func_idx);
- }
- }
-
- // Step 7: Add function bodies with remapped indices
- for module in &sorted_modules {
- let remap = &module_remaps[&module.name];
- for code in &module.code {
- let func = remap_function_body(code, remap);
- merged.add_code(func);
- }
- }
-
- // Step 8: Add data segments with relocation to avoid overlap
- // First module's data stays at original offsets, subsequent modules are relocated
- let mut cumulative_data_offset = 0u32;
- for module in &sorted_modules {
- let remap = &module_remaps[&module.name];
- for data in &module.data {
- merged.add_data(data, remap, cumulative_data_offset);
- }
- // After adding this module's data, update the offset for the next module
- // Align to 8-byte boundary for safety
- cumulative_data_offset = (merged.highest_data_addr + 7) & !7;
- }
-
- // Step 9: Add element segments
- for module in &sorted_modules {
- let remap = &module_remaps[&module.name];
- for elem in &module.elements {
- merged.add_element(elem, remap);
- }
- }
-
- // Step 10: Add exports
- for export_spec in &self.exports {
- let source_module = sorted_modules
- .iter()
- .find(|m| m.name == export_spec.source_module)
- .ok_or_else(|| ComposeError::ModuleNotFound(export_spec.source_module.clone()))?;
-
- let export = source_module
- .get_export(&export_spec.source_export)
- .ok_or_else(|| ComposeError::FunctionNotFound {
- module: export_spec.source_module.clone(),
- function: export_spec.source_export.clone(),
- })?;
-
- let remap = &module_remaps[&export_spec.source_module];
- let new_idx = match export.kind {
- ExportKind::Function => remap.functions[&export.index],
- ExportKind::Table => remap.tables[&export.index],
- ExportKind::Memory => remap.memories[&export.index],
- ExportKind::Global => remap.globals[&export.index],
- };
-
- merged.add_export(&export_spec.name, export.kind, new_idx);
- }
-
- // Step 11: Encode the merged module
- merged.encode()
- }
-
- /// Topologically sort modules so providers come before consumers.
- fn topological_sort(&self) -> Result, ComposeError> {
- let module_map: HashMap<&str, &ParsedModule> =
- self.modules.iter().map(|m| (m.name.as_str(), m)).collect();
-
- // Build dependency graph: consumer -> providers it depends on
- let mut deps: HashMap<&str, HashSet<&str>> = HashMap::new();
- for module in &self.modules {
- deps.insert(&module.name, HashSet::new());
- }
-
- for wiring in &self.wirings {
- if let Some(dep_set) = deps.get_mut(wiring.consumer.as_str()) {
- if module_map.contains_key(wiring.provider.as_str()) {
- dep_set.insert(wiring.provider.as_str());
- }
- }
- }
-
- // Kahn's algorithm for topological sort
- let mut in_degree: HashMap<&str, usize> = HashMap::new();
- for module in &self.modules {
- in_degree.insert(&module.name, 0);
- }
- for providers in deps.values() {
- for provider in providers {
- *in_degree.get_mut(provider).unwrap() += 1;
- }
- }
-
- let mut queue: Vec<&str> = in_degree
- .iter()
- .filter(|(_, °)| deg == 0)
- .map(|(&name, _)| name)
- .collect();
-
- let mut result = Vec::new();
- while let Some(name) = queue.pop() {
- result.push(module_map[name]);
- if let Some(providers) = deps.get(name) {
- for provider in providers {
- let deg = in_degree.get_mut(provider).unwrap();
- *deg -= 1;
- if *deg == 0 {
- queue.push(provider);
- }
- }
- }
- }
-
- if result.len() != self.modules.len() {
- return Err(ComposeError::CircularDependency {
- cycle: self.modules.iter().map(|m| m.name.clone()).collect(),
- });
- }
-
- // Reverse so providers come first
- result.reverse();
- Ok(result)
- }
-}
-
-/// The merged module being built.
-struct MergedModule {
- types: Vec,
- type_dedup: HashMap,
-
- imports: Vec,
- num_imported_functions: u32,
- num_imported_tables: u32,
- num_imported_memories: u32,
- num_imported_globals: u32,
-
- functions: Vec, // type indices
- tables: Vec,
- memories: Vec,
- globals: Vec<(wasmparser::GlobalType, ConstExpr)>,
- exports: Vec<(String, ExportKind, u32)>,
- code: Vec,
- data: Vec<(DataSegmentKind, Vec)>,
- elements: Vec,
-
- /// Highest data segment end address.
- /// Used to relocate subsequent modules' data to avoid overlap.
- highest_data_addr: u32,
-
- /// Index of the unified heap pointer global (mut i32, initial 0xC000).
- /// When modules share memory, they must share this global so allocations
- /// from different modules don't overlap.
- unified_heap_ptr: Option,
-
- /// Index of the unified encoding stack pointer global (mut i32, initial 0xB000).
- /// This is used for CGRF tuple encoding and must be shared when modules
- /// share memory.
- unified_enc_tuple_sp: Option,
-}
-
-struct MergedImport {
- module: String,
- name: String,
- kind: ImportKind,
-}
-
-struct MergedElement {
- kind: ElementKind,
- items: Vec, // Remapped function indices
-}
-
-/// Key for type deduplication.
-#[derive(Hash, Eq, PartialEq)]
-struct TypeKey {
- params: Vec,
- results: Vec,
-}
-
-impl TypeKey {
- fn from_func_type(ty: &wasmparser::FuncType) -> Self {
- Self {
- params: ty.params().iter().map(val_type_byte).collect(),
- results: ty.results().iter().map(val_type_byte).collect(),
- }
- }
-}
-
-fn val_type_byte(ty: &wasmparser::ValType) -> u8 {
- match ty {
- wasmparser::ValType::I32 => 0x7F,
- wasmparser::ValType::I64 => 0x7E,
- wasmparser::ValType::F32 => 0x7D,
- wasmparser::ValType::F64 => 0x7C,
- wasmparser::ValType::V128 => 0x7B,
- wasmparser::ValType::Ref(r) => {
- if r.is_func_ref() {
- 0x70
- } else {
- 0x6F
- }
- }
- }
-}
-
-impl MergedModule {
- fn new() -> Self {
- Self {
- types: Vec::new(),
- type_dedup: HashMap::new(),
- imports: Vec::new(),
- num_imported_functions: 0,
- num_imported_tables: 0,
- num_imported_memories: 0,
- num_imported_globals: 0,
- functions: Vec::new(),
- tables: Vec::new(),
- memories: Vec::new(),
- globals: Vec::new(),
- exports: Vec::new(),
- code: Vec::new(),
- data: Vec::new(),
- elements: Vec::new(),
- highest_data_addr: 0,
- unified_heap_ptr: None,
- unified_enc_tuple_sp: None,
- }
- }
-
- fn add_type(&mut self, func_type: &wasmparser::FuncType) -> u32 {
- let key = TypeKey::from_func_type(func_type);
- if let Some(&idx) = self.type_dedup.get(&key) {
- return idx;
- }
- let idx = self.types.len() as u32;
- self.types.push(func_type.clone());
- self.type_dedup.insert(key, idx);
- idx
- }
-
- fn next_function_index(&self) -> u32 {
- self.num_imported_functions + self.functions.len() as u32
- }
-
- fn next_table_index(&self) -> u32 {
- self.num_imported_tables + self.tables.len() as u32
- }
-
- fn next_memory_index(&self) -> u32 {
- self.num_imported_memories + self.memories.len() as u32
- }
-
- fn next_global_index(&self) -> u32 {
- self.num_imported_globals + self.globals.len() as u32
- }
-
- fn add_import_function(&mut self, module: &str, name: &str, type_idx: u32) -> u32 {
- let idx = self.num_imported_functions;
- self.num_imported_functions += 1;
- self.imports.push(MergedImport {
- module: module.to_string(),
- name: name.to_string(),
- kind: ImportKind::Function(type_idx),
- });
- idx
- }
-
- fn add_import_table(
- &mut self,
- module: &str,
- name: &str,
- table_ty: &wasmparser::TableType,
- ) -> u32 {
- let idx = self.num_imported_tables;
- self.num_imported_tables += 1;
- self.imports.push(MergedImport {
- module: module.to_string(),
- name: name.to_string(),
- kind: ImportKind::Table(*table_ty),
- });
- idx
- }
-
- fn add_import_memory(
- &mut self,
- module: &str,
- name: &str,
- mem_ty: &wasmparser::MemoryType,
- ) -> u32 {
- let idx = self.num_imported_memories;
- self.num_imported_memories += 1;
- self.imports.push(MergedImport {
- module: module.to_string(),
- name: name.to_string(),
- kind: ImportKind::Memory(*mem_ty),
- });
- idx
- }
-
- fn add_import_global(
- &mut self,
- module: &str,
- name: &str,
- global_ty: &wasmparser::GlobalType,
- ) -> u32 {
- let idx = self.num_imported_globals;
- self.num_imported_globals += 1;
- self.imports.push(MergedImport {
- module: module.to_string(),
- name: name.to_string(),
- kind: ImportKind::Global(*global_ty),
- });
- idx
- }
-
- fn add_table(&mut self, table_ty: &wasmparser::TableType) -> u32 {
- let idx = self.next_table_index();
- self.tables.push(*table_ty);
- idx
- }
-
- fn add_memory(&mut self, mem_ty: &wasmparser::MemoryType) -> u32 {
- let idx = self.next_memory_index();
- self.memories.push(*mem_ty);
- idx
- }
-
- /// Expand the shared memory (index 0) to accommodate a larger requirement.
- /// Takes the maximum of the current and new memory limits.
- fn expand_memory(&mut self, mem_ty: &wasmparser::MemoryType) {
- if self.memories.is_empty() {
- return;
- }
- let shared = &mut self.memories[0];
- // Take the maximum initial and maximum sizes
- shared.initial = shared.initial.max(mem_ty.initial);
- shared.maximum = match (shared.maximum, mem_ty.maximum) {
- (Some(a), Some(b)) => Some(a.max(b)),
- (Some(a), None) | (None, Some(a)) => Some(a),
- (None, None) => None,
- };
- }
-
- /// Add a global, unifying heap-related globals when modules share memory.
- ///
- /// Wisp-compiled modules have two critical globals that must be shared
- /// when modules share memory:
- /// - Heap pointer ($__heap_ptr): mut i32, initial value 0xC000 (49152)
- /// - Encoding stack pointer ($enc_tuple_sp): mut i32, initial value 0xB000 (45056)
- ///
- /// Without unification, each module would allocate from the same starting
- /// address, causing memory corruption.
- fn add_global(&mut self, global: &Global, _remap: &IndexRemap) -> (u32, bool) {
- // Check if this is a heap pointer global (mut i32, value 49152)
- if self.is_heap_ptr_global(global) {
- if let Some(unified_idx) = self.unified_heap_ptr {
- // Return the existing unified heap pointer
- return (unified_idx, true);
- } else {
- // First heap pointer - add it and track
- let idx = self.next_global_index();
- self.globals.push((global.ty, global.init_expr.clone()));
- self.unified_heap_ptr = Some(idx);
- return (idx, false);
- }
- }
-
- // Check if this is an enc_tuple_sp global (mut i32, value 45056)
- if self.is_enc_tuple_sp_global(global) {
- if let Some(unified_idx) = self.unified_enc_tuple_sp {
- // Return the existing unified enc_tuple_sp
- return (unified_idx, true);
- } else {
- // First enc_tuple_sp - add it and track
- let idx = self.next_global_index();
- self.globals.push((global.ty, global.init_expr.clone()));
- self.unified_enc_tuple_sp = Some(idx);
- return (idx, false);
- }
- }
-
- // Regular global - add as new
- let idx = self.next_global_index();
- self.globals.push((global.ty, global.init_expr.clone()));
- (idx, false)
- }
-
- /// Check if a global is the heap pointer (mut i32, value 0xC000).
- fn is_heap_ptr_global(&self, global: &Global) -> bool {
- if !global.ty.mutable {
- return false;
- }
- if global.ty.content_type != wasmparser::ValType::I32 {
- return false;
- }
- matches!(global.init_expr, ConstExpr::I32Const(49152))
- }
-
- /// Check if a global is the encoding tuple stack pointer (mut i32, value 0xB000).
- fn is_enc_tuple_sp_global(&self, global: &Global) -> bool {
- if !global.ty.mutable {
- return false;
- }
- if global.ty.content_type != wasmparser::ValType::I32 {
- return false;
- }
- matches!(global.init_expr, ConstExpr::I32Const(45056))
- }
-
- fn add_function(&mut self, type_idx: u32) -> u32 {
- let idx = self.next_function_index();
- self.functions.push(type_idx);
- idx
- }
-
- fn add_code(&mut self, func: Function) {
- self.code.push(func);
- }
-
- /// Add a data segment with relocation support.
- /// The `data_offset` parameter specifies how much to offset this segment
- /// to avoid overlap with other modules' data.
- fn add_data(&mut self, data: &DataSegment, remap: &IndexRemap, data_offset: u32) {
- let kind = match &data.kind {
- DataSegmentKind::Active {
- memory_index,
- offset_expr,
- } => {
- let new_mem_idx = remap
- .memories
- .get(memory_index)
- .copied()
- .unwrap_or(*memory_index);
- // Apply data offset if this is a constant offset
- let new_offset = match offset_expr {
- ConstExpr::I32Const(orig_offset) => {
- let relocated = (*orig_offset as u32).saturating_add(data_offset);
- // Track highest data address
- let data_end = relocated + data.data.len() as u32;
- if data_end > self.highest_data_addr {
- self.highest_data_addr = data_end;
- }
- ConstExpr::I32Const(relocated as i32)
- }
- _ => {
- // For non-constant offsets, we can't easily relocate
- // Just track the data size for highest_data_addr
- self.highest_data_addr = self.highest_data_addr.max(data.data.len() as u32);
- offset_expr.clone()
- }
- };
- DataSegmentKind::Active {
- memory_index: new_mem_idx,
- offset_expr: new_offset,
- }
- }
- DataSegmentKind::Passive => DataSegmentKind::Passive,
- };
- self.data.push((kind, data.data.clone()));
- }
-
- fn add_element(&mut self, elem: &Element, remap: &IndexRemap) {
- let items = match &elem.items {
- ElementItems::Functions(funcs) => funcs
- .iter()
- .map(|&idx| remap.functions.get(&idx).copied().unwrap_or(idx))
- .collect(),
- ElementItems::Expressions(_) => {
- // For expression-based elements, we'd need more complex handling
- // For now, skip these
- return;
- }
- };
-
- let kind = match &elem.kind {
- ElementKind::Active {
- table_index,
- offset_expr,
- } => {
- let new_table_idx = remap
- .tables
- .get(table_index)
- .copied()
- .unwrap_or(*table_index);
- ElementKind::Active {
- table_index: new_table_idx,
- offset_expr: offset_expr.clone(),
- }
- }
- ElementKind::Passive => ElementKind::Passive,
- ElementKind::Declared => ElementKind::Declared,
- };
-
- self.elements.push(MergedElement { kind, items });
- }
-
- fn add_export(&mut self, name: &str, kind: ExportKind, index: u32) {
- self.exports.push((name.to_string(), kind, index));
- }
-
- fn encode(self) -> Result, ComposeError> {
- let mut module = Module::new();
-
- // Type section
- if !self.types.is_empty() {
- let mut types = TypeSection::new();
- for func_type in &self.types {
- let params: Vec =
- func_type.params().iter().map(convert_val_type).collect();
- let results: Vec =
- func_type.results().iter().map(convert_val_type).collect();
- types.ty().function(params, results);
- }
- module.section(&types);
- }
-
- // Import section
- if !self.imports.is_empty() {
- let mut imports = ImportSection::new();
- for import in &self.imports {
- match &import.kind {
- ImportKind::Function(type_idx) => {
- imports.import(
- &import.module,
- &import.name,
- wasm_encoder::EntityType::Function(*type_idx),
- );
- }
- ImportKind::Table(table_ty) => {
- let enc_ty = convert_table_type(table_ty);
- imports.import(&import.module, &import.name, enc_ty);
- }
- ImportKind::Memory(mem_ty) => {
- let enc_ty = convert_memory_type(mem_ty);
- imports.import(&import.module, &import.name, enc_ty);
- }
- ImportKind::Global(global_ty) => {
- let enc_ty = convert_global_type(global_ty);
- imports.import(&import.module, &import.name, enc_ty);
- }
- }
- }
- module.section(&imports);
- }
-
- // Function section
- if !self.functions.is_empty() {
- let mut functions = FunctionSection::new();
- for &type_idx in &self.functions {
- functions.function(type_idx);
- }
- module.section(&functions);
- }
-
- // Table section
- if !self.tables.is_empty() {
- let mut tables = TableSection::new();
- for table_ty in &self.tables {
- let enc_ty = convert_table_type(table_ty);
- tables.table(enc_ty);
- }
- module.section(&tables);
- }
-
- // Memory section
- if !self.memories.is_empty() {
- let mut memories = MemorySection::new();
- for mem_ty in &self.memories {
- let enc_ty = convert_memory_type(mem_ty);
- memories.memory(enc_ty);
- }
- module.section(&memories);
- }
-
- // Global section
- if !self.globals.is_empty() {
- let mut globals = GlobalSection::new();
- for (global_ty, init_expr) in &self.globals {
- let enc_ty = convert_global_type(global_ty);
- let enc_expr = convert_const_expr(init_expr);
- globals.global(enc_ty, &enc_expr);
- }
- module.section(&globals);
- }
-
- // Export section
- if !self.exports.is_empty() {
- let mut exports = ExportSection::new();
- for (name, kind, index) in &self.exports {
- let enc_kind = match kind {
- ExportKind::Function => EncExportKind::Func,
- ExportKind::Table => EncExportKind::Table,
- ExportKind::Memory => EncExportKind::Memory,
- ExportKind::Global => EncExportKind::Global,
- };
- exports.export(name, enc_kind, *index);
- }
- module.section(&exports);
- }
-
- // Element section
- if !self.elements.is_empty() {
- let mut elements = ElementSection::new();
- for elem in &self.elements {
- match &elem.kind {
- ElementKind::Active {
- table_index,
- offset_expr,
- } => {
- let offset = convert_const_expr(offset_expr);
- elements.active(
- Some(*table_index),
- &offset,
- Elements::Functions(Cow::Borrowed(&elem.items)),
- );
- }
- ElementKind::Passive => {
- elements.passive(Elements::Functions(Cow::Borrowed(&elem.items)));
- }
- ElementKind::Declared => {
- elements.declared(Elements::Functions(Cow::Borrowed(&elem.items)));
- }
- }
- }
- module.section(&elements);
- }
-
- // Code section
- if !self.code.is_empty() {
- let mut code = CodeSection::new();
- for func in &self.code {
- code.function(func);
- }
- module.section(&code);
- }
-
- // Data section
- if !self.data.is_empty() {
- let mut data = DataSection::new();
- for (kind, bytes) in &self.data {
- match kind {
- DataSegmentKind::Active {
- memory_index,
- offset_expr,
- } => {
- let offset = convert_const_expr(offset_expr);
- data.active(*memory_index, &offset, bytes.iter().copied());
- }
- DataSegmentKind::Passive => {
- data.passive(bytes.iter().copied());
- }
- }
- }
- module.section(&data);
- }
-
- Ok(module.finish())
- }
-}
-
-/// Remap indices in a function body.
-fn remap_function_body(code: &FunctionCode, remap: &IndexRemap) -> Function {
- let mut func = Function::new(
- code.locals
- .iter()
- .map(|(count, ty)| (*count, convert_val_type(ty))),
- );
-
- for op in &code.operators {
- let inst = convert_stored_operator(op, remap);
- func.instruction(&inst);
- }
-
- func
-}
-
-/// Convert a stored operator to a wasm-encoder instruction with index remapping.
-fn convert_stored_operator(op: &StoredOperator, remap: &IndexRemap) -> Instruction<'static> {
- match op {
- // Control flow
- StoredOperator::Unreachable => Instruction::Unreachable,
- StoredOperator::Nop => Instruction::Nop,
- StoredOperator::Block(bt) => Instruction::Block(convert_block_type(bt, remap)),
- StoredOperator::Loop(bt) => Instruction::Loop(convert_block_type(bt, remap)),
- StoredOperator::If(bt) => Instruction::If(convert_block_type(bt, remap)),
- StoredOperator::Else => Instruction::Else,
- StoredOperator::End => Instruction::End,
- StoredOperator::Br(depth) => Instruction::Br(*depth),
- StoredOperator::BrIf(depth) => Instruction::BrIf(*depth),
- StoredOperator::BrTable { targets, default } => {
- Instruction::BrTable(Cow::Owned(targets.clone()), *default)
- }
- StoredOperator::Return => Instruction::Return,
- StoredOperator::Call(idx) => {
- let new_idx = remap.functions.get(idx).copied().unwrap_or(*idx);
- Instruction::Call(new_idx)
- }
- StoredOperator::CallIndirect {
- type_index,
- table_index,
- } => {
- let new_type = remap.types.get(type_index).copied().unwrap_or(*type_index);
- let new_table = remap
- .tables
- .get(table_index)
- .copied()
- .unwrap_or(*table_index);
- Instruction::CallIndirect {
- type_index: new_type,
- table_index: new_table,
- }
- }
- StoredOperator::ReturnCall(idx) => {
- let new_idx = remap.functions.get(idx).copied().unwrap_or(*idx);
- Instruction::ReturnCall(new_idx)
- }
- StoredOperator::ReturnCallIndirect {
- type_index,
- table_index,
- } => {
- let new_type = remap.types.get(type_index).copied().unwrap_or(*type_index);
- let new_table = remap
- .tables
- .get(table_index)
- .copied()
- .unwrap_or(*table_index);
- Instruction::ReturnCallIndirect {
- type_index: new_type,
- table_index: new_table,
- }
- }
-
- // Parametric
- StoredOperator::Drop => Instruction::Drop,
- StoredOperator::Select => Instruction::Select,
- StoredOperator::TypedSelect(ty) => Instruction::TypedSelect(convert_val_type(ty)),
-
- // Variable access
- StoredOperator::LocalGet(idx) => Instruction::LocalGet(*idx),
- StoredOperator::LocalSet(idx) => Instruction::LocalSet(*idx),
- StoredOperator::LocalTee(idx) => Instruction::LocalTee(*idx),
- StoredOperator::GlobalGet(idx) => {
- let new_idx = remap.globals.get(idx).copied().unwrap_or(*idx);
- Instruction::GlobalGet(new_idx)
- }
- StoredOperator::GlobalSet(idx) => {
- let new_idx = remap.globals.get(idx).copied().unwrap_or(*idx);
- Instruction::GlobalSet(new_idx)
- }
-
- // Table operations
- StoredOperator::TableGet(idx) => {
- let new_idx = remap.tables.get(idx).copied().unwrap_or(*idx);
- Instruction::TableGet(new_idx)
- }
- StoredOperator::TableSet(idx) => {
- let new_idx = remap.tables.get(idx).copied().unwrap_or(*idx);
- Instruction::TableSet(new_idx)
- }
- StoredOperator::TableGrow(idx) => {
- let new_idx = remap.tables.get(idx).copied().unwrap_or(*idx);
- Instruction::TableGrow(new_idx)
- }
- StoredOperator::TableSize(idx) => {
- let new_idx = remap.tables.get(idx).copied().unwrap_or(*idx);
- Instruction::TableSize(new_idx)
- }
- StoredOperator::TableFill(idx) => {
- let new_idx = remap.tables.get(idx).copied().unwrap_or(*idx);
- Instruction::TableFill(new_idx)
- }
- StoredOperator::TableCopy { dst, src } => {
- let new_dst = remap.tables.get(dst).copied().unwrap_or(*dst);
- let new_src = remap.tables.get(src).copied().unwrap_or(*src);
- Instruction::TableCopy {
- src_table: new_src,
- dst_table: new_dst,
- }
- }
- StoredOperator::TableInit { elem, table } => {
- let new_table = remap.tables.get(table).copied().unwrap_or(*table);
- Instruction::TableInit {
- elem_index: *elem,
- table: new_table,
- }
- }
- StoredOperator::ElemDrop(idx) => Instruction::ElemDrop(*idx),
-
- // Memory load operations
- StoredOperator::I32Load(m) => Instruction::I32Load(convert_memarg(m, remap)),
- StoredOperator::I64Load(m) => Instruction::I64Load(convert_memarg(m, remap)),
- StoredOperator::F32Load(m) => Instruction::F32Load(convert_memarg(m, remap)),
- StoredOperator::F64Load(m) => Instruction::F64Load(convert_memarg(m, remap)),
- StoredOperator::I32Load8S(m) => Instruction::I32Load8S(convert_memarg(m, remap)),
- StoredOperator::I32Load8U(m) => Instruction::I32Load8U(convert_memarg(m, remap)),
- StoredOperator::I32Load16S(m) => Instruction::I32Load16S(convert_memarg(m, remap)),
- StoredOperator::I32Load16U(m) => Instruction::I32Load16U(convert_memarg(m, remap)),
- StoredOperator::I64Load8S(m) => Instruction::I64Load8S(convert_memarg(m, remap)),
- StoredOperator::I64Load8U(m) => Instruction::I64Load8U(convert_memarg(m, remap)),
- StoredOperator::I64Load16S(m) => Instruction::I64Load16S(convert_memarg(m, remap)),
- StoredOperator::I64Load16U(m) => Instruction::I64Load16U(convert_memarg(m, remap)),
- StoredOperator::I64Load32S(m) => Instruction::I64Load32S(convert_memarg(m, remap)),
- StoredOperator::I64Load32U(m) => Instruction::I64Load32U(convert_memarg(m, remap)),
-
- // Memory store operations
- StoredOperator::I32Store(m) => Instruction::I32Store(convert_memarg(m, remap)),
- StoredOperator::I64Store(m) => Instruction::I64Store(convert_memarg(m, remap)),
- StoredOperator::F32Store(m) => Instruction::F32Store(convert_memarg(m, remap)),
- StoredOperator::F64Store(m) => Instruction::F64Store(convert_memarg(m, remap)),
- StoredOperator::I32Store8(m) => Instruction::I32Store8(convert_memarg(m, remap)),
- StoredOperator::I32Store16(m) => Instruction::I32Store16(convert_memarg(m, remap)),
- StoredOperator::I64Store8(m) => Instruction::I64Store8(convert_memarg(m, remap)),
- StoredOperator::I64Store16(m) => Instruction::I64Store16(convert_memarg(m, remap)),
- StoredOperator::I64Store32(m) => Instruction::I64Store32(convert_memarg(m, remap)),
-
- // Memory operations
- StoredOperator::MemorySize(idx) => {
- let new_idx = remap.memories.get(idx).copied().unwrap_or(*idx);
- Instruction::MemorySize(new_idx)
- }
- StoredOperator::MemoryGrow(idx) => {
- let new_idx = remap.memories.get(idx).copied().unwrap_or(*idx);
- Instruction::MemoryGrow(new_idx)
- }
- StoredOperator::MemoryInit { data, mem } => {
- let new_mem = remap.memories.get(mem).copied().unwrap_or(*mem);
- Instruction::MemoryInit {
- data_index: *data,
- mem: new_mem,
- }
- }
- StoredOperator::DataDrop(idx) => Instruction::DataDrop(*idx),
- StoredOperator::MemoryCopy { dst, src } => {
- let new_dst = remap.memories.get(dst).copied().unwrap_or(*dst);
- let new_src = remap.memories.get(src).copied().unwrap_or(*src);
- Instruction::MemoryCopy {
- src_mem: new_src,
- dst_mem: new_dst,
- }
- }
- StoredOperator::MemoryFill(idx) => {
- let new_idx = remap.memories.get(idx).copied().unwrap_or(*idx);
- Instruction::MemoryFill(new_idx)
- }
-
- // Constants
- StoredOperator::I32Const(v) => Instruction::I32Const(*v),
- StoredOperator::I64Const(v) => Instruction::I64Const(*v),
- StoredOperator::F32Const(v) => Instruction::F32Const(*v),
- StoredOperator::F64Const(v) => Instruction::F64Const(*v),
-
- // Comparison operators
- StoredOperator::I32Eqz => Instruction::I32Eqz,
- StoredOperator::I32Eq => Instruction::I32Eq,
- StoredOperator::I32Ne => Instruction::I32Ne,
- StoredOperator::I32LtS => Instruction::I32LtS,
- StoredOperator::I32LtU => Instruction::I32LtU,
- StoredOperator::I32GtS => Instruction::I32GtS,
- StoredOperator::I32GtU => Instruction::I32GtU,
- StoredOperator::I32LeS => Instruction::I32LeS,
- StoredOperator::I32LeU => Instruction::I32LeU,
- StoredOperator::I32GeS => Instruction::I32GeS,
- StoredOperator::I32GeU => Instruction::I32GeU,
-
- StoredOperator::I64Eqz => Instruction::I64Eqz,
- StoredOperator::I64Eq => Instruction::I64Eq,
- StoredOperator::I64Ne => Instruction::I64Ne,
- StoredOperator::I64LtS => Instruction::I64LtS,
- StoredOperator::I64LtU => Instruction::I64LtU,
- StoredOperator::I64GtS => Instruction::I64GtS,
- StoredOperator::I64GtU => Instruction::I64GtU,
- StoredOperator::I64LeS => Instruction::I64LeS,
- StoredOperator::I64LeU => Instruction::I64LeU,
- StoredOperator::I64GeS => Instruction::I64GeS,
- StoredOperator::I64GeU => Instruction::I64GeU,
-
- StoredOperator::F32Eq => Instruction::F32Eq,
- StoredOperator::F32Ne => Instruction::F32Ne,
- StoredOperator::F32Lt => Instruction::F32Lt,
- StoredOperator::F32Gt => Instruction::F32Gt,
- StoredOperator::F32Le => Instruction::F32Le,
- StoredOperator::F32Ge => Instruction::F32Ge,
-
- StoredOperator::F64Eq => Instruction::F64Eq,
- StoredOperator::F64Ne => Instruction::F64Ne,
- StoredOperator::F64Lt => Instruction::F64Lt,
- StoredOperator::F64Gt => Instruction::F64Gt,
- StoredOperator::F64Le => Instruction::F64Le,
- StoredOperator::F64Ge => Instruction::F64Ge,
-
- // Numeric operators - i32
- StoredOperator::I32Clz => Instruction::I32Clz,
- StoredOperator::I32Ctz => Instruction::I32Ctz,
- StoredOperator::I32Popcnt => Instruction::I32Popcnt,
- StoredOperator::I32Add => Instruction::I32Add,
- StoredOperator::I32Sub => Instruction::I32Sub,
- StoredOperator::I32Mul => Instruction::I32Mul,
- StoredOperator::I32DivS => Instruction::I32DivS,
- StoredOperator::I32DivU => Instruction::I32DivU,
- StoredOperator::I32RemS => Instruction::I32RemS,
- StoredOperator::I32RemU => Instruction::I32RemU,
- StoredOperator::I32And => Instruction::I32And,
- StoredOperator::I32Or => Instruction::I32Or,
- StoredOperator::I32Xor => Instruction::I32Xor,
- StoredOperator::I32Shl => Instruction::I32Shl,
- StoredOperator::I32ShrS => Instruction::I32ShrS,
- StoredOperator::I32ShrU => Instruction::I32ShrU,
- StoredOperator::I32Rotl => Instruction::I32Rotl,
- StoredOperator::I32Rotr => Instruction::I32Rotr,
-
- // Numeric operators - i64
- StoredOperator::I64Clz => Instruction::I64Clz,
- StoredOperator::I64Ctz => Instruction::I64Ctz,
- StoredOperator::I64Popcnt => Instruction::I64Popcnt,
- StoredOperator::I64Add => Instruction::I64Add,
- StoredOperator::I64Sub => Instruction::I64Sub,
- StoredOperator::I64Mul => Instruction::I64Mul,
- StoredOperator::I64DivS => Instruction::I64DivS,
- StoredOperator::I64DivU => Instruction::I64DivU,
- StoredOperator::I64RemS => Instruction::I64RemS,
- StoredOperator::I64RemU => Instruction::I64RemU,
- StoredOperator::I64And => Instruction::I64And,
- StoredOperator::I64Or => Instruction::I64Or,
- StoredOperator::I64Xor => Instruction::I64Xor,
- StoredOperator::I64Shl => Instruction::I64Shl,
- StoredOperator::I64ShrS => Instruction::I64ShrS,
- StoredOperator::I64ShrU => Instruction::I64ShrU,
- StoredOperator::I64Rotl => Instruction::I64Rotl,
- StoredOperator::I64Rotr => Instruction::I64Rotr,
-
- // Numeric operators - f32
- StoredOperator::F32Abs => Instruction::F32Abs,
- StoredOperator::F32Neg => Instruction::F32Neg,
- StoredOperator::F32Ceil => Instruction::F32Ceil,
- StoredOperator::F32Floor => Instruction::F32Floor,
- StoredOperator::F32Trunc => Instruction::F32Trunc,
- StoredOperator::F32Nearest => Instruction::F32Nearest,
- StoredOperator::F32Sqrt => Instruction::F32Sqrt,
- StoredOperator::F32Add => Instruction::F32Add,
- StoredOperator::F32Sub => Instruction::F32Sub,
- StoredOperator::F32Mul => Instruction::F32Mul,
- StoredOperator::F32Div => Instruction::F32Div,
- StoredOperator::F32Min => Instruction::F32Min,
- StoredOperator::F32Max => Instruction::F32Max,
- StoredOperator::F32Copysign => Instruction::F32Copysign,
-
- // Numeric operators - f64
- StoredOperator::F64Abs => Instruction::F64Abs,
- StoredOperator::F64Neg => Instruction::F64Neg,
- StoredOperator::F64Ceil => Instruction::F64Ceil,
- StoredOperator::F64Floor => Instruction::F64Floor,
- StoredOperator::F64Trunc => Instruction::F64Trunc,
- StoredOperator::F64Nearest => Instruction::F64Nearest,
- StoredOperator::F64Sqrt => Instruction::F64Sqrt,
- StoredOperator::F64Add => Instruction::F64Add,
- StoredOperator::F64Sub => Instruction::F64Sub,
- StoredOperator::F64Mul => Instruction::F64Mul,
- StoredOperator::F64Div => Instruction::F64Div,
- StoredOperator::F64Min => Instruction::F64Min,
- StoredOperator::F64Max => Instruction::F64Max,
- StoredOperator::F64Copysign => Instruction::F64Copysign,
-
- // Conversions
- StoredOperator::I32WrapI64 => Instruction::I32WrapI64,
- StoredOperator::I32TruncF32S => Instruction::I32TruncF32S,
- StoredOperator::I32TruncF32U => Instruction::I32TruncF32U,
- StoredOperator::I32TruncF64S => Instruction::I32TruncF64S,
- StoredOperator::I32TruncF64U => Instruction::I32TruncF64U,
- StoredOperator::I64ExtendI32S => Instruction::I64ExtendI32S,
- StoredOperator::I64ExtendI32U => Instruction::I64ExtendI32U,
- StoredOperator::I64TruncF32S => Instruction::I64TruncF32S,
- StoredOperator::I64TruncF32U => Instruction::I64TruncF32U,
- StoredOperator::I64TruncF64S => Instruction::I64TruncF64S,
- StoredOperator::I64TruncF64U => Instruction::I64TruncF64U,
- StoredOperator::F32ConvertI32S => Instruction::F32ConvertI32S,
- StoredOperator::F32ConvertI32U => Instruction::F32ConvertI32U,
- StoredOperator::F32ConvertI64S => Instruction::F32ConvertI64S,
- StoredOperator::F32ConvertI64U => Instruction::F32ConvertI64U,
- StoredOperator::F32DemoteF64 => Instruction::F32DemoteF64,
- StoredOperator::F64ConvertI32S => Instruction::F64ConvertI32S,
- StoredOperator::F64ConvertI32U => Instruction::F64ConvertI32U,
- StoredOperator::F64ConvertI64S => Instruction::F64ConvertI64S,
- StoredOperator::F64ConvertI64U => Instruction::F64ConvertI64U,
- StoredOperator::F64PromoteF32 => Instruction::F64PromoteF32,
- StoredOperator::I32ReinterpretF32 => Instruction::I32ReinterpretF32,
- StoredOperator::I64ReinterpretF64 => Instruction::I64ReinterpretF64,
- StoredOperator::F32ReinterpretI32 => Instruction::F32ReinterpretI32,
- StoredOperator::F64ReinterpretI64 => Instruction::F64ReinterpretI64,
-
- // Sign extension
- StoredOperator::I32Extend8S => Instruction::I32Extend8S,
- StoredOperator::I32Extend16S => Instruction::I32Extend16S,
- StoredOperator::I64Extend8S => Instruction::I64Extend8S,
- StoredOperator::I64Extend16S => Instruction::I64Extend16S,
- StoredOperator::I64Extend32S => Instruction::I64Extend32S,
-
- // Saturating truncation
- StoredOperator::I32TruncSatF32S => Instruction::I32TruncSatF32S,
- StoredOperator::I32TruncSatF32U => Instruction::I32TruncSatF32U,
- StoredOperator::I32TruncSatF64S => Instruction::I32TruncSatF64S,
- StoredOperator::I32TruncSatF64U => Instruction::I32TruncSatF64U,
- StoredOperator::I64TruncSatF32S => Instruction::I64TruncSatF32S,
- StoredOperator::I64TruncSatF32U => Instruction::I64TruncSatF32U,
- StoredOperator::I64TruncSatF64S => Instruction::I64TruncSatF64S,
- StoredOperator::I64TruncSatF64U => Instruction::I64TruncSatF64U,
-
- // Reference types
- StoredOperator::RefNull(kind) => match kind {
- RefTypeKind::Func => Instruction::RefNull(HeapType::Abstract {
- shared: false,
- ty: wasm_encoder::AbstractHeapType::Func,
- }),
- RefTypeKind::Extern => Instruction::RefNull(HeapType::Abstract {
- shared: false,
- ty: wasm_encoder::AbstractHeapType::Extern,
- }),
- },
- StoredOperator::RefIsNull => Instruction::RefIsNull,
- StoredOperator::RefFunc(idx) => {
- let new_idx = remap.functions.get(idx).copied().unwrap_or(*idx);
- Instruction::RefFunc(new_idx)
- }
- }
-}
-
-fn convert_val_type(ty: &wasmparser::ValType) -> ValType {
- match ty {
- wasmparser::ValType::I32 => ValType::I32,
- wasmparser::ValType::I64 => ValType::I64,
- wasmparser::ValType::F32 => ValType::F32,
- wasmparser::ValType::F64 => ValType::F64,
- wasmparser::ValType::V128 => ValType::V128,
- wasmparser::ValType::Ref(r) => {
- if r.is_func_ref() {
- ValType::Ref(RefType::FUNCREF)
- } else {
- ValType::Ref(RefType::EXTERNREF)
- }
- }
- }
-}
-
-fn convert_block_type(bt: &BlockType, remap: &IndexRemap) -> wasm_encoder::BlockType {
- match bt {
- BlockType::Empty => wasm_encoder::BlockType::Empty,
- BlockType::Type(ty) => wasm_encoder::BlockType::Result(convert_val_type(ty)),
- BlockType::FuncType(idx) => {
- let new_idx = remap.types.get(idx).copied().unwrap_or(*idx);
- wasm_encoder::BlockType::FunctionType(new_idx)
- }
- }
-}
-
-fn convert_memarg(m: &MemArg, remap: &IndexRemap) -> wasm_encoder::MemArg {
- let new_mem = remap.memories.get(&m.memory).copied().unwrap_or(m.memory);
- wasm_encoder::MemArg {
- offset: m.offset,
- align: m.align,
- memory_index: new_mem,
- }
-}
-
-fn convert_table_type(ty: &wasmparser::TableType) -> TableType {
- let ref_type = if ty.element_type.is_func_ref() {
- RefType::FUNCREF
- } else {
- RefType::EXTERNREF
- };
- TableType {
- element_type: ref_type,
- minimum: ty.initial,
- maximum: ty.maximum,
- table64: ty.table64,
- shared: ty.shared,
- }
-}
-
-fn convert_memory_type(ty: &wasmparser::MemoryType) -> MemoryType {
- MemoryType {
- minimum: ty.initial,
- maximum: ty.maximum,
- memory64: ty.memory64,
- shared: ty.shared,
- page_size_log2: ty.page_size_log2,
- }
-}
-
-fn convert_global_type(ty: &wasmparser::GlobalType) -> GlobalType {
- GlobalType {
- val_type: convert_val_type(&ty.content_type),
- mutable: ty.mutable,
- shared: ty.shared,
- }
-}
-
-fn convert_const_expr(expr: &ConstExpr) -> EncConstExpr {
- match expr {
- ConstExpr::I32Const(v) => EncConstExpr::i32_const(*v),
- ConstExpr::I64Const(v) => EncConstExpr::i64_const(*v),
- ConstExpr::F32Const(v) => EncConstExpr::f32_const(*v),
- ConstExpr::F64Const(v) => EncConstExpr::f64_const(*v),
- ConstExpr::GlobalGet(idx) => EncConstExpr::global_get(*idx),
- ConstExpr::RefNull(kind) => {
- let heap_type = match kind {
- RefTypeKind::Func => HeapType::Abstract {
- shared: false,
- ty: wasm_encoder::AbstractHeapType::Func,
- },
- RefTypeKind::Extern => HeapType::Abstract {
- shared: false,
- ty: wasm_encoder::AbstractHeapType::Extern,
- },
- };
- EncConstExpr::ref_null(heap_type)
- }
- ConstExpr::RefFunc(idx) => EncConstExpr::ref_func(*idx),
- }
-}
diff --git a/src/compose/mod.rs b/src/compose/mod.rs
index 36011d0..ed847a8 100644
--- a/src/compose/mod.rs
+++ b/src/compose/mod.rs
@@ -1,378 +1,31 @@
-//! Static Composition for WASM Packages
+//! Static composition of WASM packages into a single self-contained binary.
//!
-//! This module provides static composition of WASM packages - merging multiple
-//! WASM modules into a single binary where imports from one module are resolved
-//! to exports from another.
+//! Produces one merged `.wasm` where each package's cross-package imports are
+//! resolved to direct internal calls — no runtime cross-module dispatch, and
+//! (unlike a runtime [`crate::runtime::PicCompositionBuilder`]) no host harness:
+//! the output has zero imports and runs on any stock runtime.
//!
-//! Unlike runtime composition (see `crate::runtime::CompositionBuilder`), static
-//! composition produces a single merged WASM file that can be loaded and executed
-//! without the overhead of cross-module calls.
+//! See [`compose`] for the pipeline (binaryen `wasm-merge` + a `walrus`
+//! internalize pass). [`ParsedModule`] is a lightweight reusable WASM parser
+//! (also used by the `inspect` CLI command).
//!
//! # Example
//!
//! ```ignore
-//! use packr::compose::StaticComposer;
+//! use packr::compose::{compose, ComposeSpec, PackageSpec};
//!
-//! // Load package bytes
-//! let doubler_wasm = std::fs::read("doubler.wasm")?;
-//! let adder_wasm = std::fs::read("adder.wasm")?;
-//!
-//! // Build static composition
-//! let composed = StaticComposer::new()
-//! .add_module("doubler", doubler_wasm)?
-//! .add_module("adder", adder_wasm)?
-//! .wire("adder", "math", "double", "doubler", "transform")
-//! .export("process", "adder", "process")
-//! .export("memory", "adder", "memory")
-//! .compose()?;
-//!
-//! // `composed` is a single WASM binary with all functions merged
+//! let composed = compose(&ComposeSpec::new(vec![
+//! PackageSpec::new("pack:alloc", std::fs::read("pack_alloc.wasm")?),
+//! PackageSpec::new("math", std::fs::read("doubler.wasm")?), // adder imports "math"
+//! PackageSpec::new("adder", std::fs::read("adder.wasm")?),
+//! ]))?;
//! std::fs::write("composed.wasm", composed)?;
//! ```
-//!
-//! # How It Works
-//!
-//! 1. **Parse** each module to extract types, imports, exports, functions, etc.
-//! 2. **Topologically sort** modules so providers are processed before consumers
-//! 3. **Deduplicate types** across modules
-//! 4. **Merge functions**: resolved imports become internal `call` instructions
-//! 5. **Remap indices** in all function bodies
-//! 6. **Emit** combined module using wasm-encoder
mod error;
-mod merger;
mod parser;
+mod static_compose;
pub use error::ComposeError;
pub use parser::ParsedModule;
-
-use merger::{ExportSpec, Merger, Wiring};
-use std::collections::HashMap;
-
-/// Builder for creating statically composed WASM modules.
-///
-/// `StaticComposer` merges multiple WASM modules into a single binary,
-/// resolving inter-module imports to direct function calls.
-pub struct StaticComposer {
- modules: Vec,
- wirings: Vec,
- exports: Vec,
-}
-
-impl StaticComposer {
- /// Create a new static composer.
- pub fn new() -> Self {
- Self {
- modules: Vec::new(),
- wirings: Vec::new(),
- exports: Vec::new(),
- }
- }
-
- /// Add a WASM module to the composition.
- ///
- /// # Arguments
- ///
- /// * `name` - A unique name for this module, used to reference it in wiring
- /// * `wasm` - The raw WASM bytes of the module
- ///
- /// # Errors
- ///
- /// Returns `ComposeError::ParseError` if the WASM module is invalid.
- pub fn add_module(mut self, name: &str, wasm: Vec) -> Result {
- let parsed = ParsedModule::parse(name, &wasm)?;
- self.modules.push(parsed);
- Ok(self)
- }
-
- /// Wire an import from one module to an export from another.
- ///
- /// When the `consumer` module imports `import_module::import_fn`, it will
- /// be resolved to call the `provider_export` from the `provider` module
- /// instead of being kept as an external import.
- ///
- /// # Arguments
- ///
- /// * `consumer` - Name of the module that has the import
- /// * `import_module` - The module name in the import declaration
- /// * `import_fn` - The function name in the import declaration
- /// * `provider` - Name of the module that provides the implementation
- /// * `provider_export` - The export name in the provider module
- ///
- /// # Example
- ///
- /// If `adder.wasm` has:
- /// ```wat
- /// (import "math" "double" (func ...))
- /// ```
- ///
- /// And `doubler.wasm` exports a function called "transform", you'd wire:
- /// ```ignore
- /// composer.wire("adder", "math", "double", "doubler", "transform")
- /// ```
- pub fn wire(
- mut self,
- consumer: &str,
- import_module: &str,
- import_fn: &str,
- provider: &str,
- provider_export: &str,
- ) -> Self {
- self.wirings.push(Wiring {
- consumer: consumer.to_string(),
- import_module: import_module.to_string(),
- import_fn: import_fn.to_string(),
- provider: provider.to_string(),
- provider_export: provider_export.to_string(),
- });
- self
- }
-
- /// Automatically wire imports to exports based on name matching.
- ///
- /// For each unresolved import, if there's an export with the same name
- /// in another module, wire them together.
- ///
- /// # Errors
- ///
- /// Returns `ComposeError::TypeMismatch` if matching imports/exports have
- /// incompatible function signatures.
- pub fn auto_wire(mut self) -> Result {
- // Build a map of all exports across all modules
- let mut export_map: HashMap> = HashMap::new();
- for module in &self.modules {
- for export in &module.exports {
- export_map
- .entry(export.name.clone())
- .or_default()
- .push((&module.name, export));
- }
- }
-
- // For each module's imports, try to find a matching export
- for module in &self.modules {
- for import in &module.imports {
- // Skip if already wired
- let already_wired = self.wirings.iter().any(|w| {
- w.consumer == module.name
- && w.import_module == import.module
- && w.import_fn == import.name
- });
- if already_wired {
- continue;
- }
-
- // Look for matching export by function name
- if let Some(exports) = export_map.get(&import.name) {
- // Find an export from a different module
- for (provider_name, export) in exports {
- if *provider_name != module.name {
- // Check that it's a function export matching a function import
- if matches!(import.kind, parser::ImportKind::Function(_))
- && export.kind == parser::ExportKind::Function
- {
- self.wirings.push(Wiring {
- consumer: module.name.clone(),
- import_module: import.module.clone(),
- import_fn: import.name.clone(),
- provider: provider_name.to_string(),
- provider_export: export.name.clone(),
- });
- break;
- }
- }
- }
- }
- }
- }
-
- Ok(self)
- }
-
- /// Add an export to the composed module.
- ///
- /// This specifies which functions/memories/etc. from the source modules
- /// should be exported from the composed module.
- ///
- /// # Arguments
- ///
- /// * `name` - The name of the export in the composed module
- /// * `source_module` - The module that has the export
- /// * `source_fn` - The name of the export in the source module
- pub fn export(mut self, name: &str, source_module: &str, source_fn: &str) -> Self {
- self.exports.push(ExportSpec {
- name: name.to_string(),
- source_module: source_module.to_string(),
- source_export: source_fn.to_string(),
- });
- self
- }
-
- /// Compose the modules into a single WASM binary.
- ///
- /// # Errors
- ///
- /// Returns an error if:
- /// - No modules were added
- /// - A referenced module is not found
- /// - A wired import/export is not found
- /// - There's a circular dependency
- /// - The merged module fails to encode
- pub fn compose(self) -> Result, ComposeError> {
- if self.modules.is_empty() {
- return Err(ComposeError::NoModules);
- }
-
- let merger = Merger::new(self.modules, self.wirings, self.exports);
- merger.merge()
- }
-}
-
-impl Default for StaticComposer {
- fn default() -> Self {
- Self::new()
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_static_composer_api() {
- // Just test that the API compiles - we'll add real tests later
- let _composer = StaticComposer::new();
- }
-
- #[test]
- fn test_parse_simple_module() {
- // A minimal valid WASM module
- let wasm = wat::parse_str(
- r#"
- (module
- (func $add (param i32 i32) (result i32)
- local.get 0
- local.get 1
- i32.add
- )
- (export "add" (func $add))
- )
- "#,
- )
- .unwrap();
-
- let parsed = ParsedModule::parse("test", &wasm).unwrap();
- assert_eq!(parsed.name, "test");
- assert_eq!(parsed.functions.len(), 1);
- assert_eq!(parsed.exports.len(), 1);
- assert_eq!(parsed.exports[0].name, "add");
- }
-
- #[test]
- fn test_compose_simple_modules() {
- // Module A: exports a doubler function
- let module_a = wat::parse_str(
- r#"
- (module
- (memory (export "memory") 1)
- (func $double (param i32) (result i32)
- local.get 0
- i32.const 2
- i32.mul
- )
- (export "double" (func $double))
- )
- "#,
- )
- .unwrap();
-
- // Module B: imports double, exports add_doubled
- let module_b = wat::parse_str(
- r#"
- (module
- (import "math" "double" (func $double (param i32) (result i32)))
- (func $add_doubled (param i32 i32) (result i32)
- local.get 0
- call $double
- local.get 1
- call $double
- i32.add
- )
- (export "add_doubled" (func $add_doubled))
- )
- "#,
- )
- .unwrap();
-
- let composed = StaticComposer::new()
- .add_module("doubler", module_a)
- .unwrap()
- .add_module("adder", module_b)
- .unwrap()
- .wire("adder", "math", "double", "doubler", "double")
- .export("add_doubled", "adder", "add_doubled")
- .export("memory", "doubler", "memory")
- .compose()
- .unwrap();
-
- // Verify the composed module is valid
- assert!(!composed.is_empty());
-
- // Parse the composed module to verify structure
- let parsed = ParsedModule::parse("composed", &composed).unwrap();
-
- // Should have the exported function
- assert!(parsed.exports.iter().any(|e| e.name == "add_doubled"));
- assert!(parsed.exports.iter().any(|e| e.name == "memory"));
-
- // Should have no imports (the math::double import was resolved)
- assert_eq!(parsed.num_imported_functions, 0);
- }
-
- #[test]
- fn test_auto_wire() {
- // Module A: exports "transform"
- let module_a = wat::parse_str(
- r#"
- (module
- (func $transform (param i32) (result i32)
- local.get 0
- i32.const 10
- i32.add
- )
- (export "transform" (func $transform))
- )
- "#,
- )
- .unwrap();
-
- // Module B: imports "transform" from any module
- let module_b = wat::parse_str(
- r#"
- (module
- (import "util" "transform" (func $transform (param i32) (result i32)))
- (func $process (param i32) (result i32)
- local.get 0
- call $transform
- )
- (export "process" (func $process))
- )
- "#,
- )
- .unwrap();
-
- let composed = StaticComposer::new()
- .add_module("a", module_a)
- .unwrap()
- .add_module("b", module_b)
- .unwrap()
- .auto_wire()
- .unwrap()
- .export("process", "b", "process")
- .compose()
- .unwrap();
-
- let parsed = ParsedModule::parse("composed", &composed).unwrap();
- assert_eq!(parsed.num_imported_functions, 0);
- }
-}
+pub use static_compose::{compose, ComposeSpec, Layout, PackageSpec};
diff --git a/src/compose/static_compose.rs b/src/compose/static_compose.rs
new file mode 100644
index 0000000..97671bc
--- /dev/null
+++ b/src/compose/static_compose.rs
@@ -0,0 +1,227 @@
+//! Static composition of PIC packages into one self-contained `.wasm`.
+//!
+//! Pipeline:
+//! 1. **`wasm-merge`** (binaryen) fuses the package modules and internalizes the
+//! cross-package imports — `(import "math" "double")` becomes a direct call to
+//! the provider's `double`, `(import "pack:alloc" ...)` becomes a call to the
+//! merged-in allocator. Each package is named by the import-module-name its
+//! consumers use (`"math"`, `"pack:alloc"`, ...).
+//! 2. A **`walrus`** pass does the one thing `wasm-merge` can't: unify the several
+//! `env.memory` imports into a single internal memory, turn the allocator's
+//! `__memory_base` / `__heap_base` / `__heap_end` imports into baked-in
+//! constants, and export the memory.
+//!
+//! The result has ZERO imports and validates on any stock runtime.
+//!
+//! Requires `wasm-merge` (binaryen) on `PATH` at compose time.
+
+use std::path::PathBuf;
+use std::process::Command;
+
+use walrus::ir::VisitorMut;
+use walrus::{ConstExpr, DataKind, GlobalKind, ImportKind, MemoryId, Module};
+
+/// One package in a static composition.
+pub struct PackageSpec {
+ /// The `wasm-merge` module name — the import module-name that *consumers* use
+ /// for this package's exports. E.g. `"math"` if a consumer does
+ /// `(import "math" "double")`, or `"pack:alloc"` for the allocator.
+ ///
+ /// Providers must be listed before the packages that consume them.
+ pub name: String,
+ /// Compiled package bytes, built with the fixed-base composition recipe
+ /// (`--global-base= --stack-first -zstack-size=... --import-memory
+ /// --no-entry`), with disjoint bases per package.
+ pub wasm: Vec,
+}
+
+impl PackageSpec {
+ pub fn new(name: impl Into, wasm: Vec) -> Self {
+ Self {
+ name: name.into(),
+ wasm,
+ }
+ }
+}
+
+/// Byte-offset layout of the single shared linear memory the composed module owns.
+/// Package data segments sit at their link-time fixed bases; the allocator's
+/// mstate + heap live above them.
+#[derive(Clone, Copy, Debug)]
+pub struct Layout {
+ /// Internal memory size, in 64KiB pages.
+ pub memory_pages: u32,
+ /// Allocator `__memory_base` — where its `.data` (mstate) is placed. Must be
+ /// above every package's data region.
+ pub alloc_base: u32,
+ /// dlmalloc heap start (above `alloc_base`).
+ pub heap_base: u32,
+ /// dlmalloc heap end.
+ pub heap_end: u32,
+}
+
+impl Default for Layout {
+ fn default() -> Self {
+ // Matches the two-package (doubler @0x50000, adder @0xD0000) recipe with
+ // room to spare; callers with more/larger packages should widen this.
+ Self {
+ memory_pages: 128, // 8 MiB
+ alloc_base: 0xE_0000,
+ heap_base: 0xF_0000,
+ heap_end: 0x80_0000,
+ }
+ }
+}
+
+/// A static-composition request: the packages (in provider-before-consumer order)
+/// and the memory layout.
+pub struct ComposeSpec {
+ pub packages: Vec,
+ pub layout: Layout,
+}
+
+impl ComposeSpec {
+ pub fn new(packages: Vec) -> Self {
+ Self {
+ packages,
+ layout: Layout::default(),
+ }
+ }
+}
+
+/// Compose `spec` into a single self-contained `.wasm` (zero imports).
+pub fn compose(spec: &ComposeSpec) -> anyhow::Result> {
+ anyhow::ensure!(!spec.packages.is_empty(), "no packages to compose");
+ let merged = wasm_merge(&spec.packages)?;
+ internalize(&merged, &spec.layout)
+}
+
+/// Shell out to binaryen's `wasm-merge` to fuse modules + wire cross-imports.
+fn wasm_merge(packages: &[PackageSpec]) -> anyhow::Result> {
+ let dir = std::env::temp_dir().join(format!("pack-compose-{}", std::process::id()));
+ std::fs::create_dir_all(&dir)?;
+
+ let mut cmd = Command::new("wasm-merge");
+ let mut inputs: Vec = Vec::new();
+ for (i, p) in packages.iter().enumerate() {
+ let path = dir.join(format!("in{i}.wasm"));
+ std::fs::write(&path, &p.wasm)?;
+ cmd.arg(&path).arg(&p.name);
+ inputs.push(path);
+ }
+ let out = dir.join("merged.wasm");
+ cmd.arg("-o")
+ .arg(&out)
+ .arg("--enable-multimemory")
+ .arg("--rename-export-conflicts");
+
+ let status = cmd.status().map_err(|e| {
+ anyhow::anyhow!("failed to run `wasm-merge` (is binaryen installed and on PATH?): {e}")
+ })?;
+ anyhow::ensure!(status.success(), "`wasm-merge` exited with failure");
+
+ let bytes = std::fs::read(&out)?;
+ let _ = std::fs::remove_dir_all(&dir);
+ Ok(bytes)
+}
+
+/// Remaps every memory reference in a function body to a single canonical memory.
+struct MemRemap {
+ to: MemoryId,
+}
+impl VisitorMut for MemRemap {
+ fn visit_memory_id_mut(&mut self, id: &mut MemoryId) {
+ *id = self.to;
+ }
+}
+
+/// The `walrus` pass: unify memories, internalize the allocator's base/heap
+/// globals into constants, export the memory. Yields a zero-import module.
+fn internalize(merged: &[u8], layout: &Layout) -> anyhow::Result> {
+ let mut m = Module::from_buffer(merged)?;
+
+ // 1. Turn the allocator's imported globals into baked-in constants.
+ let mut global_fixups = Vec::new();
+ for imp in m.imports.iter() {
+ if let ImportKind::Global(g) = imp.kind {
+ let val: i32 = match imp.name.as_str() {
+ "__memory_base" => layout.alloc_base as i32,
+ "__table_base" => 0,
+ "__heap_base" => layout.heap_base as i32,
+ "__heap_end" => layout.heap_end as i32,
+ other => anyhow::bail!(
+ "unexpected global import `{}::{other}` — packages must use the \
+ fixed-base composition recipe",
+ imp.module
+ ),
+ };
+ global_fixups.push((imp.id(), g, val));
+ }
+ }
+ for (imp_id, g, val) in &global_fixups {
+ m.globals.get_mut(*g).kind =
+ GlobalKind::Local(ConstExpr::Value(walrus::ir::Value::I32(*val)));
+ m.imports.delete(*imp_id);
+ }
+
+ // 2. Unify the several `env.memory` imports into one internal memory.
+ let mut mem_imports = Vec::new();
+ for imp in m.imports.iter() {
+ if let ImportKind::Memory(mem) = imp.kind {
+ mem_imports.push((imp.id(), mem));
+ }
+ }
+ anyhow::ensure!(
+ !mem_imports.is_empty(),
+ "merged module has no memory import"
+ );
+ let canonical = mem_imports[0].1;
+
+ let mut v = MemRemap { to: canonical };
+ let fids: Vec<_> = m.funcs.iter_local().map(|(id, _)| id).collect();
+ for fid in fids {
+ let f = m.funcs.get_mut(fid).kind.unwrap_local_mut();
+ let entry = f.entry_block();
+ walrus::ir::dfs_pre_order_mut(&mut v, f, entry);
+ }
+
+ let data_ids: Vec<_> = m.data.iter().map(|d| d.id()).collect();
+ for did in data_ids {
+ let d = m.data.get_mut(did);
+ if let DataKind::Active { memory, offset } = &mut d.kind {
+ *memory = canonical;
+ // A local `global.get` is not a valid data-segment offset; the
+ // allocator segment used the (now-internalized) __memory_base global.
+ if let ConstExpr::Global(_) = offset {
+ *offset = ConstExpr::Value(walrus::ir::Value::I32(layout.alloc_base as i32));
+ }
+ }
+ }
+
+ {
+ let mem = m.memories.get_mut(canonical);
+ mem.import = None;
+ mem.initial = layout.memory_pages as u64;
+ mem.maximum = Some(layout.memory_pages as u64);
+ }
+ for (imp_id, mem_id) in mem_imports {
+ m.imports.delete(imp_id);
+ if mem_id != canonical {
+ m.memories.delete(mem_id);
+ }
+ }
+
+ // 3. Export the memory so a host can marshal args/results through it.
+ if !m.exports.iter().any(|e| e.name == "memory") {
+ m.exports.add("memory", canonical);
+ }
+
+ // 4. It must now stand alone.
+ let remaining = m.imports.iter().count();
+ anyhow::ensure!(
+ remaining == 0,
+ "expected zero imports after internalize, got {remaining}"
+ );
+
+ Ok(m.emit_wasm())
+}
diff --git a/src/lib.rs b/src/lib.rs
index 80f54c7..6019243 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -87,4 +87,4 @@ pub use transform::{InterfaceTransform, RpcTransform, TransformRegistry};
pub use types::{Arena, Case, Field, Function, Param, Type, TypePath};
pub use codegen::generate_rust;
-pub use compose::{ComposeError, ParsedModule, StaticComposer};
+pub use compose::{compose, ComposeError, ComposeSpec, Layout, PackageSpec, ParsedModule};
diff --git a/src/main.rs b/src/main.rs
index 92ea356..53ee688 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -33,6 +33,16 @@ enum Commands {
#[arg(long)]
json: bool,
},
+
+ /// Statically compose packages into one self-contained `.wasm` (zero imports)
+ Compose {
+ /// Path to the compose manifest (TOML)
+ manifest: PathBuf,
+
+ /// Output path (overrides `output` in the manifest)
+ #[arg(long, short = 'o')]
+ output: Option,
+ },
}
fn main() -> anyhow::Result<()> {
@@ -44,7 +54,115 @@ fn main() -> anyhow::Result<()> {
hashes,
json,
} => inspect_command(&wasm_file, hashes, json),
+ Commands::Compose { manifest, output } => compose_command(&manifest, output),
+ }
+}
+
+/// A `pack compose` manifest.
+///
+/// ```toml
+/// output = "composed.wasm"
+///
+/// [[package]] # providers before consumers
+/// name = "pack:alloc" # the wasm-merge module name = the import module-name
+/// wasm = "pack_alloc.wasm"
+///
+/// [[package]]
+/// name = "math" # adder does (import "math" "double")
+/// wasm = "doubler.wasm"
+///
+/// [[package]]
+/// name = "adder"
+/// wasm = "adder.wasm"
+///
+/// [layout] # optional; hex ok
+/// memory_pages = 128
+/// alloc_base = 0xE0000
+/// heap_base = 0xF0000
+/// heap_end = 0x800000
+/// ```
+#[derive(serde::Deserialize)]
+struct Manifest {
+ output: Option,
+ #[serde(default)]
+ package: Vec,
+ layout: Option,
+}
+
+#[derive(serde::Deserialize)]
+struct ManifestPackage {
+ name: String,
+ wasm: String,
+}
+
+#[derive(serde::Deserialize)]
+struct ManifestLayout {
+ memory_pages: Option,
+ alloc_base: Option,
+ heap_base: Option,
+ heap_end: Option,
+}
+
+fn compose_command(
+ manifest_path: &PathBuf,
+ output_override: Option,
+) -> anyhow::Result<()> {
+ use packr::{compose, ComposeSpec, Layout, PackageSpec};
+
+ let text = std::fs::read_to_string(manifest_path)
+ .map_err(|e| anyhow::anyhow!("failed to read manifest {}: {e}", manifest_path.display()))?;
+ let manifest: Manifest = toml::from_str(&text).map_err(|e| {
+ anyhow::anyhow!("failed to parse manifest {}: {e}", manifest_path.display())
+ })?;
+ anyhow::ensure!(
+ !manifest.package.is_empty(),
+ "manifest lists no [[package]] entries"
+ );
+
+ // Paths in the manifest are relative to the manifest's directory.
+ let base_dir = manifest_path
+ .parent()
+ .unwrap_or_else(|| std::path::Path::new("."));
+
+ let mut packages = Vec::new();
+ for p in &manifest.package {
+ let wpath = base_dir.join(&p.wasm);
+ let bytes = std::fs::read(&wpath)
+ .map_err(|e| anyhow::anyhow!("failed to read package {}: {e}", wpath.display()))?;
+ packages.push(PackageSpec::new(p.name.clone(), bytes));
}
+
+ let mut layout = Layout::default();
+ if let Some(l) = &manifest.layout {
+ if let Some(v) = l.memory_pages {
+ layout.memory_pages = v;
+ }
+ if let Some(v) = l.alloc_base {
+ layout.alloc_base = v;
+ }
+ if let Some(v) = l.heap_base {
+ layout.heap_base = v;
+ }
+ if let Some(v) = l.heap_end {
+ layout.heap_end = v;
+ }
+ }
+
+ let composed = compose(&ComposeSpec { packages, layout })?;
+
+ let out = output_override
+ .or_else(|| manifest.output.as_ref().map(PathBuf::from))
+ .unwrap_or_else(|| PathBuf::from("composed.wasm"));
+ std::fs::write(&out, &composed)
+ .map_err(|e| anyhow::anyhow!("failed to write {}: {e}", out.display()))?;
+
+ println!(
+ "composed {} package(s) -> {} ({} bytes, zero imports)",
+ manifest.package.len(),
+ out.display(),
+ composed.len()
+ );
+ Ok(())
}
fn inspect_command(wasm_file: &PathBuf, show_hashes: bool, json: bool) -> anyhow::Result<()> {
diff --git a/src/runtime/composition.rs b/src/runtime/composition.rs
deleted file mode 100644
index 4113e4b..0000000
--- a/src/runtime/composition.rs
+++ /dev/null
@@ -1,960 +0,0 @@
-//! Package Composition
-//!
-//! Enables composing multiple packages together, wiring one package's
-//! exports to another package's imports.
-//!
-//! # Example
-//!
-//! ```ignore
-//! use packr::runtime::CompositionBuilder;
-//! use packr::abi::Value;
-//!
-//! // Load package bytes
-//! let doubler_wasm = std::fs::read("doubler.wasm")?;
-//! let adder_wasm = std::fs::read("adder.wasm")?;
-//!
-//! // Build composition
-//! let mut composition = CompositionBuilder::new()
-//! .add_package("doubler", doubler_wasm)
-//! .add_package("adder", adder_wasm)
-//! .wire("adder", "math", "double", "doubler", "transform")
-//! .build()?;
-//!
-//! // Call the adder, which internally calls doubler
-//! let result = composition.call("adder", "process", &Value::S64(5))?;
-//! assert_eq!(result, Value::S64(11)); // (5 * 2) + 1
-//! ```
-
-use std::collections::HashMap;
-use std::sync::{Arc, Mutex};
-
-use crate::abi::{decode, encode, Value};
-use crate::runtime::{RuntimeError, INPUT_BUFFER_OFFSET, RESULT_LEN_OFFSET, RESULT_PTR_OFFSET};
-use wasmtime::{Caller, Engine, Linker, Module, Store};
-
-/// A host function that can be wired into compositions.
-///
-/// Host functions follow the guest-allocates ABI, taking encoded input bytes
-/// and returning encoded output bytes.
-pub type HostFn = Arc Result, String> + Send + Sync>;
-
-/// Builder for creating composed packages with cross-package imports.
-pub struct CompositionBuilder {
- engine: Engine,
- packages: Vec,
- host_functions: Vec,
-}
-
-struct PackageDefinition {
- name: String,
- wasm_bytes: Vec,
- imports: Vec,
-}
-
-struct HostFunctionDef {
- module: String,
- function: String,
- handler: HostFn,
-}
-
-#[derive(Clone)]
-struct ImportWiring {
- import_module: String,
- import_function: String,
- source_package: String,
- source_function: String,
-}
-
-impl CompositionBuilder {
- /// Create a new composition builder.
- pub fn new() -> Self {
- // Enable multi-memory for composed modules that may come from pack-compose
- let mut config = wasmtime::Config::new();
- config.wasm_multi_memory(true);
- let engine = Engine::new(&config).expect("failed to create engine");
- Self {
- engine,
- packages: Vec::new(),
- host_functions: Vec::new(),
- }
- }
-
- /// Add a package to the composition.
- pub fn add_package(mut self, name: impl Into, wasm_bytes: Vec) -> Self {
- self.packages.push(PackageDefinition {
- name: name.into(),
- wasm_bytes,
- imports: Vec::new(),
- });
- self
- }
-
- /// Add a host function that can satisfy imports.
- ///
- /// The handler receives encoded input bytes and returns encoded output bytes.
- /// Use `pack::abi::decode` and `pack::abi::encode` to work with Values.
- ///
- /// # Example
- ///
- /// ```ignore
- /// use packr::abi::{decode, encode, Value};
- ///
- /// builder.add_host_function("my:interface", "my-func", |input_bytes| {
- /// let input: Value = decode(input_bytes)?;
- /// // Process input...
- /// let output = Value::S64(42);
- /// encode(&output).map_err(|e| e.to_string())
- /// })
- /// ```
- pub fn add_host_function(
- mut self,
- module: impl Into,
- function: impl Into,
- handler: F,
- ) -> Self
- where
- F: Fn(&[u8]) -> Result, String> + Send + Sync + 'static,
- {
- self.host_functions.push(HostFunctionDef {
- module: module.into(),
- function: function.into(),
- handler: Arc::new(handler),
- });
- self
- }
-
- /// Add a typed host function that works with Values directly.
- ///
- /// This is a convenience wrapper around `add_host_function` that handles
- /// encoding/decoding automatically.
- ///
- /// # Example
- ///
- /// ```ignore
- /// builder.add_host_function_typed("my:interface", "double", |input: Value| {
- /// match input {
- /// Value::S64(n) => Ok(Value::S64(n * 2)),
- /// _ => Err("expected s64".to_string()),
- /// }
- /// })
- /// ```
- pub fn add_host_function_typed(
- self,
- module: impl Into,
- function: impl Into,
- handler: F,
- ) -> Self
- where
- F: Fn(Value) -> Result + Send + Sync + 'static,
- {
- let module = module.into();
- let function = function.into();
- self.add_host_function(module, function, move |input_bytes| {
- let input = decode(input_bytes).map_err(|e| e.to_string())?;
- let output = handler(input)?;
- encode(&output).map_err(|e| e.to_string())
- })
- }
-
- /// Wire an import from one package to an export from another.
- ///
- /// When `target_package` calls `import_module::import_function`,
- /// it will actually call `source_package`'s `source_function`.
- pub fn wire(
- mut self,
- target_package: impl Into,
- import_module: impl Into,
- import_function: impl Into,
- source_package: impl Into,
- source_function: impl Into,
- ) -> Self {
- let target = target_package.into();
- let wiring = ImportWiring {
- import_module: import_module.into(),
- import_function: import_function.into(),
- source_package: source_package.into(),
- source_function: source_function.into(),
- };
-
- for pkg in &mut self.packages {
- if pkg.name == target {
- pkg.imports.push(wiring);
- return self;
- }
- }
-
- self
- }
-
- /// Build the composition.
- pub fn build(self) -> Result {
- // Compile all modules first
- let mut compiled: HashMap = HashMap::new();
- for pkg in &self.packages {
- if !pkg.wasm_bytes.is_empty() {
- let module = Module::new(&self.engine, &pkg.wasm_bytes)
- .map_err(|e| RuntimeError::WasmError(e.to_string()))?;
- compiled.insert(pkg.name.clone(), module);
- }
- }
-
- // Shared registry for cross-package calls
- let registry: Arc> = Arc::new(Mutex::new(PackageRegistry {
- packages: HashMap::new(),
- }));
-
- // Topological sort: instantiate packages without imports first
- // Note: if there are host functions defined, ALL packages need them,
- // so treat them all as consumers in that case
- let has_host_functions = !self.host_functions.is_empty();
-
- let providers: Vec<_> = self
- .packages
- .iter()
- .filter(|p| p.imports.is_empty() && !has_host_functions)
- .collect();
-
- let consumers: Vec<_> = self
- .packages
- .iter()
- .filter(|p| !p.imports.is_empty() || has_host_functions)
- .collect();
-
- // Instantiate providers first
- for pkg in providers {
- let module = compiled.get(&pkg.name).ok_or_else(|| {
- RuntimeError::ModuleNotFound(format!("Package '{}' not found", pkg.name))
- })?;
-
- let mut linker = Linker::<()>::new(&self.engine);
- super::register_default_alloc(&mut linker)?;
- let mut store = Store::new(&self.engine, ());
-
- let instance = linker
- .instantiate(&mut store, module)
- .map_err(|e| RuntimeError::WasmError(e.to_string()))?;
-
- registry.lock().unwrap().packages.insert(
- pkg.name.clone(),
- PackageEntry {
- store: Arc::new(Mutex::new(UntypedStore::Unit(store))),
- instance,
- },
- );
- }
-
- // Now instantiate consumers with wired imports
- for pkg in consumers {
- let module = compiled.get(&pkg.name).ok_or_else(|| {
- RuntimeError::ModuleNotFound(format!("Package '{}' not found", pkg.name))
- })?;
-
- let mut linker = Linker::::new(&self.engine);
- super::register_default_alloc(&mut linker)?;
-
- // Wire host functions first
- for host_fn in &self.host_functions {
- let handler = Arc::clone(&host_fn.handler);
-
- linker
- .func_wrap(
- &host_fn.module,
- &host_fn.function,
- move |mut caller: Caller<'_, ComposedState>,
- in_ptr: i32,
- in_len: i32,
- out_ptr_ptr: i32,
- out_len_ptr: i32|
- -> i32 {
- host_function_call(
- &mut caller,
- &handler,
- in_ptr,
- in_len,
- out_ptr_ptr,
- out_len_ptr,
- )
- },
- )
- .map_err(|e| RuntimeError::WasmError(e.to_string()))?;
- }
-
- // Wire each cross-package import
- for wiring in &pkg.imports {
- let source_pkg = wiring.source_package.clone();
- let source_fn = wiring.source_function.clone();
- let reg = Arc::clone(®istry);
-
- linker
- .func_wrap(
- &wiring.import_module,
- &wiring.import_function,
- move |mut caller: Caller<'_, ComposedState>,
- in_ptr: i32,
- in_len: i32,
- out_ptr: i32,
- out_cap: i32|
- -> i32 {
- cross_package_call(
- &mut caller,
- ®,
- &source_pkg,
- &source_fn,
- in_ptr,
- in_len,
- out_ptr,
- out_cap,
- )
- },
- )
- .map_err(|e| RuntimeError::WasmError(e.to_string()))?;
- }
-
- let state = ComposedState {
- _registry: Arc::clone(®istry),
- };
- let mut store = Store::new(&self.engine, state);
-
- let instance = linker
- .instantiate(&mut store, module)
- .map_err(|e| RuntimeError::WasmError(e.to_string()))?;
-
- // Add to registry (consumers can also be called)
- registry.lock().unwrap().packages.insert(
- pkg.name.clone(),
- PackageEntry {
- store: Arc::new(Mutex::new(UntypedStore::Composed(store))),
- instance,
- },
- );
- }
-
- Ok(BuiltComposition {
- _engine: self.engine,
- registry,
- })
- }
-}
-
-/// Handle a cross-package call using the guest-allocates ABI.
-///
-/// # ABI
-///
-/// The source function has signature:
-/// ```text
-/// fn(in_ptr: i32, in_len: i32, out_ptr_ptr: i32, out_len_ptr: i32) -> i32
-/// ```
-///
-/// Returns 0 on success (output ptr/len written to slots), -1 on error.
-#[allow(clippy::too_many_arguments)]
-fn cross_package_call(
- caller: &mut Caller<'_, ComposedState>,
- registry: &Arc>,
- source_pkg: &str,
- source_fn: &str,
- in_ptr: i32,
- in_len: i32,
- out_ptr_ptr: i32,
- out_len_ptr: i32,
-) -> i32 {
- // Read input from caller's memory
- let memory = match caller.get_export("memory") {
- Some(wasmtime::Extern::Memory(m)) => m,
- _ => return -1,
- };
-
- let mut input_bytes = vec![0u8; in_len as usize];
- if memory
- .read(&caller, in_ptr as usize, &mut input_bytes)
- .is_err()
- {
- return -1;
- }
-
- // Look up the source package and get an Arc to its store
- // Important: release the registry lock before calling into WASM
- let (store_arc, instance) = {
- let reg = registry.lock().unwrap();
- let source = match reg.packages.get(source_pkg) {
- Some(p) => p,
- None => return -1,
- };
- (Arc::clone(&source.store), source.instance)
- };
- // Registry lock released here
-
- // Call the source package - try dynamic allocation, fall back to fixed buffer
- let result = {
- let mut store_guard = store_arc.lock().unwrap();
-
- // Get memory
- let src_memory = match store_guard.get_memory(&instance) {
- Some(m) => m,
- None => return -1,
- };
-
- // Try to allocate input buffer dynamically, fall back to fixed buffer
- let (in_ptr, dynamic_input) = match store_guard.get_alloc_func(&instance) {
- Some(alloc_func) => {
- match store_guard.call_alloc(&alloc_func, input_bytes.len() as i32) {
- Ok(ptr) if ptr != 0 => (ptr, true),
- _ => (INPUT_BUFFER_OFFSET as i32, false),
- }
- }
- None => (INPUT_BUFFER_OFFSET as i32, false),
- };
-
- // Write input to buffer
- if store_guard
- .write_memory(&src_memory, in_ptr as usize, &input_bytes)
- .is_err()
- {
- return -1;
- }
-
- // Call the source function
- let func = match store_guard.get_typed_func(&instance, source_fn) {
- Some(f) => f,
- None => return -1,
- };
-
- let status = match store_guard.call_func(
- &func,
- in_ptr,
- input_bytes.len() as i32,
- RESULT_PTR_OFFSET as i32,
- RESULT_LEN_OFFSET as i32,
- ) {
- Ok(s) => s,
- Err(_) => return -1,
- };
-
- // Free the input buffer if dynamically allocated
- if dynamic_input {
- if let Some(free_func) = store_guard.get_free_func(&instance) {
- let _ = store_guard.call_free(&free_func, in_ptr, input_bytes.len() as i32);
- }
- }
-
- if status != 0 {
- // Error occurred - read error message from slots and propagate
- // For now, just return -1
- return -1;
- }
-
- // Read output pointer and length from the slots
- let mut ptr_bytes = [0u8; 4];
- let mut len_bytes = [0u8; 4];
- if store_guard
- .read_memory(&src_memory, RESULT_PTR_OFFSET, &mut ptr_bytes)
- .is_err()
- {
- return -1;
- }
- if store_guard
- .read_memory(&src_memory, RESULT_LEN_OFFSET, &mut len_bytes)
- .is_err()
- {
- return -1;
- }
-
- let out_ptr = i32::from_le_bytes(ptr_bytes) as usize;
- let out_len = i32::from_le_bytes(len_bytes) as usize;
-
- // Read output from the guest-allocated buffer
- let mut output_bytes = vec![0u8; out_len];
- if store_guard
- .read_memory(&src_memory, out_ptr, &mut output_bytes)
- .is_err()
- {
- return -1;
- }
-
- // Free the guest's output buffer
- if let Some(free_func) = store_guard.get_free_func(&instance) {
- let _ = store_guard.call_free(&free_func, out_ptr as i32, out_len as i32);
- }
-
- output_bytes
- };
-
- // The caller also uses guest-allocates ABI, so we need to allocate in caller's memory
- // For cross-package calls, the caller is also a guest, so we write to caller's result slots
- // Actually, the caller provided out_ptr_ptr and out_len_ptr, so we need to allocate
- // in the caller's memory and write the ptr/len there.
-
- // For simplicity in cross-package calls, we'll allocate in caller's heap
- // This requires the caller to have __pack_alloc exported, which may not exist.
- //
- // Alternative: Use a fixed buffer region in caller for cross-package results.
- // Let's use the same RESULT region as a data buffer for now (after the ptr/len slots).
- const CROSS_CALL_BUFFER_OFFSET: usize = RESULT_LEN_OFFSET + 4;
-
- if memory
- .write(&mut *caller, CROSS_CALL_BUFFER_OFFSET, &result)
- .is_err()
- {
- return -1;
- }
-
- // Write the buffer location to caller's result slots
- let result_ptr = CROSS_CALL_BUFFER_OFFSET as i32;
- let result_len = result.len() as i32;
-
- if memory
- .write(
- &mut *caller,
- out_ptr_ptr as usize,
- &result_ptr.to_le_bytes(),
- )
- .is_err()
- {
- return -1;
- }
- if memory
- .write(
- &mut *caller,
- out_len_ptr as usize,
- &result_len.to_le_bytes(),
- )
- .is_err()
- {
- return -1;
- }
-
- 0 // Success
-}
-
-/// Handle a host function call using the guest-allocates ABI.
-///
-/// Similar to cross_package_call but invokes a host-provided function
-/// instead of another WASM package.
-fn host_function_call(
- caller: &mut Caller<'_, ComposedState>,
- handler: &HostFn,
- in_ptr: i32,
- in_len: i32,
- out_ptr_ptr: i32,
- out_len_ptr: i32,
-) -> i32 {
- // Read input from caller's memory
- let memory = match caller.get_export("memory") {
- Some(wasmtime::Extern::Memory(m)) => m,
- _ => return -1,
- };
-
- let mut input_bytes = vec![0u8; in_len as usize];
- if memory
- .read(&caller, in_ptr as usize, &mut input_bytes)
- .is_err()
- {
- return -1;
- }
-
- // Call the host function
- let result = match handler(&input_bytes) {
- Ok(bytes) => bytes,
- Err(_) => return -1,
- };
-
- // Write result to caller's memory using a fixed buffer region
- // (same approach as cross_package_call)
- const HOST_CALL_BUFFER_OFFSET: usize = RESULT_LEN_OFFSET + 4;
-
- if memory
- .write(&mut *caller, HOST_CALL_BUFFER_OFFSET, &result)
- .is_err()
- {
- return -1;
- }
-
- // Write the buffer location to caller's result slots
- let result_ptr = HOST_CALL_BUFFER_OFFSET as i32;
- let result_len = result.len() as i32;
-
- if memory
- .write(
- &mut *caller,
- out_ptr_ptr as usize,
- &result_ptr.to_le_bytes(),
- )
- .is_err()
- {
- return -1;
- }
- if memory
- .write(
- &mut *caller,
- out_len_ptr as usize,
- &result_len.to_le_bytes(),
- )
- .is_err()
- {
- return -1;
- }
-
- 0 // Success
-}
-
-impl Default for CompositionBuilder {
- fn default() -> Self {
- Self::new()
- }
-}
-
-struct ComposedState {
- _registry: Arc>,
-}
-
-struct PackageRegistry {
- packages: HashMap,
-}
-
-struct PackageEntry {
- store: Arc>,
- instance: wasmtime::Instance,
-}
-
-/// Wrapper to handle stores with different state types
-enum UntypedStore {
- Unit(Store<()>),
- Composed(Store),
-}
-
-impl UntypedStore {
- fn get_memory(&mut self, instance: &wasmtime::Instance) -> Option {
- match self {
- UntypedStore::Unit(store) => instance.get_memory(&mut *store, "memory"),
- UntypedStore::Composed(store) => instance.get_memory(&mut *store, "memory"),
- }
- }
-
- fn write_memory(
- &mut self,
- memory: &wasmtime::Memory,
- offset: usize,
- data: &[u8],
- ) -> Result<(), ()> {
- match self {
- UntypedStore::Unit(store) => memory.write(&mut *store, offset, data).map_err(|_| ()),
- UntypedStore::Composed(store) => {
- memory.write(&mut *store, offset, data).map_err(|_| ())
- }
- }
- }
-
- fn read_memory(
- &mut self,
- memory: &wasmtime::Memory,
- offset: usize,
- data: &mut [u8],
- ) -> Result<(), ()> {
- match self {
- UntypedStore::Unit(store) => memory.read(&*store, offset, data).map_err(|_| ()),
- UntypedStore::Composed(store) => memory.read(&*store, offset, data).map_err(|_| ()),
- }
- }
-
- fn get_typed_func(
- &mut self,
- instance: &wasmtime::Instance,
- name: &str,
- ) -> Option> {
- match self {
- UntypedStore::Unit(store) => instance.get_typed_func(&mut *store, name).ok(),
- UntypedStore::Composed(store) => instance.get_typed_func(&mut *store, name).ok(),
- }
- }
-
- fn get_free_func(
- &mut self,
- instance: &wasmtime::Instance,
- ) -> Option> {
- match self {
- UntypedStore::Unit(store) => instance.get_typed_func(&mut *store, "__pack_free").ok(),
- UntypedStore::Composed(store) => {
- instance.get_typed_func(&mut *store, "__pack_free").ok()
- }
- }
- }
-
- fn get_alloc_func(
- &mut self,
- instance: &wasmtime::Instance,
- ) -> Option> {
- match self {
- UntypedStore::Unit(store) => instance.get_typed_func(&mut *store, "__pack_alloc").ok(),
- UntypedStore::Composed(store) => {
- instance.get_typed_func(&mut *store, "__pack_alloc").ok()
- }
- }
- }
-
- fn call_alloc(&mut self, func: &wasmtime::TypedFunc, size: i32) -> Result {
- match self {
- UntypedStore::Unit(store) => func.call(&mut *store, size).map_err(|_| ()),
- UntypedStore::Composed(store) => func.call(&mut *store, size).map_err(|_| ()),
- }
- }
-
- fn call_func(
- &mut self,
- func: &wasmtime::TypedFunc<(i32, i32, i32, i32), i32>,
- a: i32,
- b: i32,
- c: i32,
- d: i32,
- ) -> Result {
- match self {
- UntypedStore::Unit(store) => func.call(&mut *store, (a, b, c, d)).map_err(|e| {
- eprintln!("[PACK DEBUG] call_func error: {:?}", e);
- }),
- UntypedStore::Composed(store) => func.call(&mut *store, (a, b, c, d)).map_err(|e| {
- eprintln!("[PACK DEBUG] call_func error: {:?}", e);
- }),
- }
- }
-
- fn call_free(
- &mut self,
- func: &wasmtime::TypedFunc<(i32, i32), ()>,
- ptr: i32,
- len: i32,
- ) -> Result<(), ()> {
- match self {
- UntypedStore::Unit(store) => func.call(&mut *store, (ptr, len)).map_err(|_| ()),
- UntypedStore::Composed(store) => func.call(&mut *store, (ptr, len)).map_err(|_| ()),
- }
- }
-
- fn get_types_func(
- &mut self,
- instance: &wasmtime::Instance,
- ) -> Option> {
- match self {
- UntypedStore::Unit(store) => instance.get_typed_func(&mut *store, "__pack_types").ok(),
- UntypedStore::Composed(store) => {
- instance.get_typed_func(&mut *store, "__pack_types").ok()
- }
- }
- }
-
- fn call_types_func(
- &mut self,
- func: &wasmtime::TypedFunc<(i32, i32), i32>,
- a: i32,
- b: i32,
- ) -> Result {
- match self {
- UntypedStore::Unit(store) => func.call(&mut *store, (a, b)).map_err(|_| ()),
- UntypedStore::Composed(store) => func.call(&mut *store, (a, b)).map_err(|_| ()),
- }
- }
-}
-
-/// A built composition ready for execution.
-pub struct BuiltComposition {
- _engine: Engine,
- registry: Arc>,
-}
-
-impl BuiltComposition {
- /// Call a function on a package in the composition.
- pub fn call(
- &mut self,
- package: &str,
- function: &str,
- input: &Value,
- ) -> Result {
- // Look up package and clone Arc references before releasing lock
- // This is important to avoid deadlock when WASM calls back into registry
- let (store_arc, instance) = {
- let reg = self.registry.lock().unwrap();
- let pkg = reg.packages.get(package).ok_or_else(|| {
- RuntimeError::ModuleNotFound(format!("Package '{}' not found", package))
- })?;
- (Arc::clone(&pkg.store), pkg.instance)
- };
- // Registry lock released here
-
- let mut store = store_arc.lock().unwrap();
-
- // Encode input
- let input_bytes = encode(input).map_err(|e| RuntimeError::AbiError(e.to_string()))?;
-
- // Get memory
- let memory = store
- .get_memory(&instance)
- .ok_or_else(|| RuntimeError::MemoryError("No memory export".into()))?;
-
- // Try to allocate input buffer dynamically, fall back to fixed buffer
- let (in_ptr, dynamic_input) = match store.get_alloc_func(&instance) {
- Some(alloc_func) => match store.call_alloc(&alloc_func, input_bytes.len() as i32) {
- Ok(ptr) if ptr != 0 => (ptr, true),
- _ => (INPUT_BUFFER_OFFSET as i32, false),
- },
- None => (INPUT_BUFFER_OFFSET as i32, false),
- };
-
- // Write input to buffer
- store
- .write_memory(&memory, in_ptr as usize, &input_bytes)
- .map_err(|_| RuntimeError::MemoryError("Failed to write input".into()))?;
-
- // Get and call the function
- let func = store
- .get_typed_func(&instance, function)
- .ok_or_else(|| RuntimeError::FunctionNotFound(function.to_string()))?;
-
- let status = store
- .call_func(
- &func,
- in_ptr,
- input_bytes.len() as i32,
- RESULT_PTR_OFFSET as i32,
- RESULT_LEN_OFFSET as i32,
- )
- .map_err(|e| RuntimeError::WasmError(format!("Function call failed: {:?}", e)))?;
-
- // Free the input buffer if dynamically allocated
- if dynamic_input {
- if let Some(free_func) = store.get_free_func(&instance) {
- let _ = store.call_free(&free_func, in_ptr, input_bytes.len() as i32);
- }
- }
-
- if status != 0 {
- // Error - read error message from result slots
- let mut ptr_bytes = [0u8; 4];
- let mut len_bytes = [0u8; 4];
- let _ = store.read_memory(&memory, RESULT_PTR_OFFSET, &mut ptr_bytes);
- let _ = store.read_memory(&memory, RESULT_LEN_OFFSET, &mut len_bytes);
- let err_ptr = i32::from_le_bytes(ptr_bytes) as usize;
- let err_len = i32::from_le_bytes(len_bytes) as usize;
-
- let mut err_bytes = vec![0u8; err_len];
- if store.read_memory(&memory, err_ptr, &mut err_bytes).is_ok() {
- if let Ok(err_msg) = String::from_utf8(err_bytes) {
- // Free the error buffer
- if let Some(free_func) = store.get_free_func(&instance) {
- let _ = store.call_free(&free_func, err_ptr as i32, err_len as i32);
- }
- return Err(RuntimeError::WasmError(err_msg));
- }
- }
- return Err(RuntimeError::WasmError("Function returned error".into()));
- }
-
- // Read output pointer and length from result slots
- let mut ptr_bytes = [0u8; 4];
- let mut len_bytes = [0u8; 4];
- store
- .read_memory(&memory, RESULT_PTR_OFFSET, &mut ptr_bytes)
- .map_err(|_| RuntimeError::MemoryError("Failed to read result ptr".into()))?;
- store
- .read_memory(&memory, RESULT_LEN_OFFSET, &mut len_bytes)
- .map_err(|_| RuntimeError::MemoryError("Failed to read result len".into()))?;
-
- let out_ptr = i32::from_le_bytes(ptr_bytes) as usize;
- let out_len = i32::from_le_bytes(len_bytes) as usize;
-
- // Read output from guest-allocated buffer
- let mut output_bytes = vec![0u8; out_len];
- store
- .read_memory(&memory, out_ptr, &mut output_bytes)
- .map_err(|_| RuntimeError::MemoryError("Failed to read output".into()))?;
-
- // Free the guest's buffer
- if let Some(free_func) = store.get_free_func(&instance) {
- let _ = store.call_free(&free_func, out_ptr as i32, out_len as i32);
- }
-
- decode(&output_bytes).map_err(|e| RuntimeError::AbiError(e.to_string()))
- }
-
- /// List all packages in the composition.
- pub fn packages(&self) -> Vec {
- self.registry
- .lock()
- .unwrap()
- .packages
- .keys()
- .cloned()
- .collect()
- }
-
- /// Read embedded type metadata from a package in the composition.
- ///
- /// Returns `Err(MetadataError::NotFound)` if the package doesn't exist
- /// or doesn't export `__pack_types`.
- pub fn types(
- &mut self,
- package: &str,
- ) -> Result {
- let (store_arc, instance) = {
- let reg = self.registry.lock().unwrap();
- let pkg = reg
- .packages
- .get(package)
- .ok_or(crate::metadata::MetadataError::NotFound)?;
- (Arc::clone(&pkg.store), pkg.instance)
- };
-
- let mut store = store_arc.lock().unwrap();
-
- let types_func = store
- .get_types_func(&instance)
- .ok_or(crate::metadata::MetadataError::NotFound)?;
-
- let status = store
- .call_types_func(
- &types_func,
- RESULT_PTR_OFFSET as i32,
- RESULT_LEN_OFFSET as i32,
- )
- .map_err(|_| crate::metadata::MetadataError::CallFailed("call failed".into()))?;
-
- if status != 0 {
- return Err(crate::metadata::MetadataError::CallFailed(
- "non-zero status from __pack_types".into(),
- ));
- }
-
- let memory = store
- .get_memory(&instance)
- .ok_or_else(|| crate::metadata::MetadataError::CallFailed("no memory".into()))?;
- let mut ptr_bytes = [0u8; 4];
- let mut len_bytes = [0u8; 4];
- store
- .read_memory(&memory, RESULT_PTR_OFFSET, &mut ptr_bytes)
- .map_err(|_| crate::metadata::MetadataError::CallFailed("read ptr failed".into()))?;
- store
- .read_memory(&memory, RESULT_LEN_OFFSET, &mut len_bytes)
- .map_err(|_| crate::metadata::MetadataError::CallFailed("read len failed".into()))?;
-
- let out_ptr = i32::from_le_bytes(ptr_bytes) as usize;
- let out_len = i32::from_le_bytes(len_bytes) as usize;
-
- let mut metadata_bytes = vec![0u8; out_len];
- store
- .read_memory(&memory, out_ptr, &mut metadata_bytes)
- .map_err(|_| {
- crate::metadata::MetadataError::CallFailed("read metadata failed".into())
- })?;
-
- crate::metadata::decode_metadata(&metadata_bytes)
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_composition_builder_api() {
- let _builder = CompositionBuilder::new()
- .add_package("doubler", vec![])
- .add_package("adder", vec![])
- .wire("adder", "math", "double", "doubler", "transform");
- }
-}
diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs
index 92209f5..2981dc9 100644
--- a/src/runtime/mod.rs
+++ b/src/runtime/mod.rs
@@ -2,13 +2,11 @@
//!
//! Handles package instantiation, linking, and execution.
-mod composition;
mod host;
pub mod interceptor;
mod interface_check;
mod pic_composition;
-pub use composition::{BuiltComposition, CompositionBuilder, HostFn};
pub use host::{
AsyncCtx, Ctx, DefaultHostProvider, ErrorHandler, HostFunctionError, HostFunctionErrorKind,
HostFunctionProvider, HostLinkerBuilder, InterfaceBuilder, LinkerError, INPUT_BUFFER_OFFSET,
diff --git a/tests/compose.rs b/tests/compose.rs
new file mode 100644
index 0000000..c391ee1
--- /dev/null
+++ b/tests/compose.rs
@@ -0,0 +1,102 @@
+//! Static composition (`pack compose`) end-to-end test.
+//!
+//! Composes the allocator + doubler + adder fixtures into ONE self-contained
+//! `.wasm` and runs `adder.process` — which calls `doubler.double` internally —
+//! on a bare wasmtime with NO imports provided. Proves the merge + memory
+//! unification + allocator internalization produce a working, standalone module.
+//!
+//! Requires `wasm-merge` (binaryen) on PATH; skips cleanly if absent (CI runs it
+//! under `nix develop`, which provides binaryen).
+//!
+//! Fixtures (`assets/*_fixedbase.wasm`) are the packages built with the fixed-base
+//! recipe: `RUSTFLAGS="-Clink-arg=--import-memory -Clink-arg=--initial-memory=8388608
+//! -Clink-arg=--stack-first -Clink-arg=-zstack-size=262144
+//! -Clink-arg=--global-base=<327680|851968> -Clink-arg=--no-entry"`.
+
+use std::path::Path;
+
+use packr::abi::{decode, encode, Value};
+use packr::{compose, ComposeSpec, PackageSpec};
+use wasmtime::{Engine, Instance, Linker, Module, Store};
+
+fn asset(name: &str) -> Vec {
+ let p = Path::new(env!("CARGO_MANIFEST_DIR"))
+ .join("assets")
+ .join(name);
+ std::fs::read(&p).unwrap_or_else(|e| panic!("read {}: {e}", p.display()))
+}
+
+fn wasm_merge_available() -> bool {
+ std::process::Command::new("wasm-merge")
+ .arg("--version")
+ .output()
+ .map(|o| o.status.success())
+ .unwrap_or(false)
+}
+
+#[test]
+fn compose_adder_calls_doubler_standalone() {
+ if !wasm_merge_available() {
+ eprintln!("SKIP: wasm-merge (binaryen) not on PATH");
+ return;
+ }
+
+ let composed = compose(&ComposeSpec::new(vec![
+ PackageSpec::new("pack:alloc", asset("pack_alloc_module.wasm")),
+ PackageSpec::new("math", asset("doubler_fixedbase.wasm")), // adder imports "math"
+ PackageSpec::new("adder", asset("adder_fixedbase.wasm")),
+ ]))
+ .expect("compose failed");
+
+ // Load on a BARE runtime — zero imports, no harness.
+ let engine = Engine::default();
+ let module = Module::new(&engine, &composed).expect("composed module invalid");
+ let mut store = Store::new(&engine, ());
+ let inst = Linker::new(&engine)
+ .instantiate(&mut store, &module)
+ .expect("instantiate failed (module should be self-contained)");
+ if let Ok(c) = inst.get_typed_func::<(), ()>(&mut store, "__wasm_call_ctors") {
+ c.call(&mut store, ()).unwrap();
+ }
+
+ for (input, expected) in [(5, 11), (0, 1), (100, 201), (-3, -5)] {
+ assert_eq!(
+ call_process(&mut store, &inst, input),
+ Value::S64(expected),
+ "process({input})"
+ );
+ }
+}
+
+/// Marshal `n` through the composed module's `process` via the packr ABI.
+fn call_process(store: &mut Store<()>, inst: &Instance, n: i64) -> Value {
+ let mem = inst
+ .exports(&mut *store)
+ .filter_map(|e| e.into_memory())
+ .next()
+ .expect("composed module must export a memory");
+
+ let bytes = encode(&Value::S64(n)).unwrap();
+ let pa = inst
+ .get_typed_func::(&mut *store, "__pack_alloc")
+ .unwrap();
+ let in_ptr = pa.call(&mut *store, bytes.len() as i32).unwrap();
+ mem.write(&mut *store, in_ptr as usize, &bytes).unwrap();
+ let slots = pa.call(&mut *store, 8).unwrap();
+
+ let f = inst
+ .get_typed_func::<(i32, i32, i32, i32), i32>(&mut *store, "process")
+ .unwrap();
+ let status = f
+ .call(&mut *store, (in_ptr, bytes.len() as i32, slots, slots + 4))
+ .unwrap();
+ assert_eq!(status, 0, "guest returned error status");
+
+ let mut sb = [0u8; 8];
+ mem.read(&*store, slots as usize, &mut sb).unwrap();
+ let out_ptr = i32::from_le_bytes(sb[0..4].try_into().unwrap()) as usize;
+ let out_len = i32::from_le_bytes(sb[4..8].try_into().unwrap()) as usize;
+ let mut out = vec![0u8; out_len];
+ mem.read(&*store, out_ptr, &mut out).unwrap();
+ decode(&out).unwrap()
+}
diff --git a/tests/composition.rs b/tests/composition.rs
deleted file mode 100644
index c02dbb5..0000000
--- a/tests/composition.rs
+++ /dev/null
@@ -1,175 +0,0 @@
-//! Package composition tests
-//!
-//! These tests verify that packages can be composed together,
-//! with one package's imports satisfied by another package's exports.
-
-use packr::abi::Value;
-use packr::runtime::CompositionBuilder;
-use std::path::Path;
-
-/// Load the doubler package (exports `double`)
-fn load_doubler_package() -> Vec {
- let wasm_path = Path::new(env!("CARGO_MANIFEST_DIR"))
- .join("packages/doubler/target/wasm32-unknown-unknown/release/doubler_package.wasm");
- std::fs::read(&wasm_path).unwrap_or_else(|e| {
- panic!(
- "Failed to read doubler package at {}: {}. Run: cd packages/doubler && cargo build --target wasm32-unknown-unknown --release",
- wasm_path.display(),
- e
- )
- })
-}
-
-/// Load the adder package (imports `double` from "math", exports `process`)
-fn load_adder_package() -> Vec {
- let wasm_path = Path::new(env!("CARGO_MANIFEST_DIR"))
- .join("packages/adder/target/wasm32-unknown-unknown/release/adder_package.wasm");
- std::fs::read(&wasm_path).unwrap_or_else(|e| {
- panic!(
- "Failed to read adder package at {}: {}. Run: cd packages/adder && cargo build --target wasm32-unknown-unknown --release",
- wasm_path.display(),
- e
- )
- })
-}
-
-#[test]
-fn composition_doubler_alone() {
- // Test that doubler works standalone
- let doubler_wasm = load_doubler_package();
-
- let mut composition = CompositionBuilder::new()
- .add_package("doubler", doubler_wasm)
- .build()
- .expect("failed to build composition");
-
- // Call double(5) => 10
- let result = composition
- .call("doubler", "double", &Value::S64(5))
- .expect("failed to call double");
-
- assert_eq!(result, Value::S64(10));
-}
-
-#[test]
-fn composition_adder_with_doubler() {
- // The main test: compose adder with doubler
- let doubler_wasm = load_doubler_package();
- let adder_wasm = load_adder_package();
-
- let mut composition = CompositionBuilder::new()
- .add_package("doubler", doubler_wasm)
- .add_package("adder", adder_wasm)
- // Wire adder's import of "math::double" to doubler's "double" export
- .wire("adder", "math", "double", "doubler", "double")
- .build()
- .expect("failed to build composition");
-
- // Call process(5):
- // - adder imports double(5) from "math" module
- // - doubler's double(5) returns 10
- // - adder adds 1: 10 + 1 = 11
- let result = composition
- .call("adder", "process", &Value::S64(5))
- .expect("failed to call process");
-
- assert_eq!(result, Value::S64(11));
-}
-
-#[test]
-fn composition_multiple_calls() {
- let doubler_wasm = load_doubler_package();
- let adder_wasm = load_adder_package();
-
- let mut composition = CompositionBuilder::new()
- .add_package("doubler", doubler_wasm)
- .add_package("adder", adder_wasm)
- .wire("adder", "math", "double", "doubler", "double")
- .build()
- .expect("failed to build composition");
-
- // Test with various inputs
- let test_cases = vec![
- (0, 1), // 0 * 2 + 1 = 1
- (1, 3), // 1 * 2 + 1 = 3
- (5, 11), // 5 * 2 + 1 = 11
- (10, 21), // 10 * 2 + 1 = 21
- (100, 201), // 100 * 2 + 1 = 201
- ];
-
- for (input, expected) in test_cases {
- let result = composition
- .call("adder", "process", &Value::S64(input))
- .unwrap_or_else(|_| panic!("failed to call process with {}", input));
- assert_eq!(
- result,
- Value::S64(expected),
- "process({}) should be {}",
- input,
- expected
- );
- }
-}
-
-#[test]
-fn composition_list_packages() {
- let doubler_wasm = load_doubler_package();
- let adder_wasm = load_adder_package();
-
- let composition = CompositionBuilder::new()
- .add_package("doubler", doubler_wasm)
- .add_package("adder", adder_wasm)
- .wire("adder", "math", "double", "doubler", "double")
- .build()
- .expect("failed to build composition");
-
- let packages = composition.packages();
- assert_eq!(packages.len(), 2);
- assert!(packages.contains(&"doubler".to_string()));
- assert!(packages.contains(&"adder".to_string()));
-}
-
-#[test]
-fn composition_call_both_packages() {
- let doubler_wasm = load_doubler_package();
- let adder_wasm = load_adder_package();
-
- let mut composition = CompositionBuilder::new()
- .add_package("doubler", doubler_wasm)
- .add_package("adder", adder_wasm)
- .wire("adder", "math", "double", "doubler", "double")
- .build()
- .expect("failed to build composition");
-
- // Can call either package
- let double_result = composition
- .call("doubler", "double", &Value::S64(7))
- .expect("failed to call doubler");
- assert_eq!(double_result, Value::S64(14));
-
- let process_result = composition
- .call("adder", "process", &Value::S64(7))
- .expect("failed to call adder");
- assert_eq!(process_result, Value::S64(15)); // 7 * 2 + 1
-}
-
-#[test]
-fn composition_negative_numbers() {
- let doubler_wasm = load_doubler_package();
- let adder_wasm = load_adder_package();
-
- let mut composition = CompositionBuilder::new()
- .add_package("doubler", doubler_wasm)
- .add_package("adder", adder_wasm)
- .wire("adder", "math", "double", "doubler", "double")
- .build()
- .expect("failed to build composition");
-
- // Test with negative numbers
- let result = composition
- .call("adder", "process", &Value::S64(-5))
- .expect("failed to call process with negative");
-
- // -5 * 2 + 1 = -9
- assert_eq!(result, Value::S64(-9));
-}
diff --git a/tests/type_metadata.rs b/tests/type_metadata.rs
index 389d281..83d7e66 100644
--- a/tests/type_metadata.rs
+++ b/tests/type_metadata.rs
@@ -2,7 +2,7 @@
use packr::abi::{encode, Value, ValueType};
use packr::metadata::{decode_metadata, MetadataError, TypeDesc};
-use packr::runtime::{CompositionBuilder, Runtime};
+use packr::runtime::Runtime;
fn load_wasm(name: &str) -> Vec {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
@@ -73,38 +73,6 @@ fn test_doubler_metadata() {
assert_eq!(double_fn.results[0], TypeDesc::Value);
}
-#[test]
-fn test_adder_metadata_with_imports() {
- let doubler_wasm = load_wasm("doubler");
- let adder_wasm = load_wasm("adder");
-
- let mut composition = CompositionBuilder::new()
- .add_package("doubler", doubler_wasm)
- .add_package("adder", adder_wasm)
- .wire("adder", "math", "double", "doubler", "double")
- .build()
- .expect("build composition");
-
- let metadata = composition.types("adder").expect("adder metadata");
-
- // Adder imports from "math"
- assert_eq!(metadata.imports().len(), 1);
- let import_fn = &metadata.imports()[0];
- assert_eq!(import_fn.interface, "math");
- assert_eq!(import_fn.name, "double");
- assert_eq!(import_fn.params.len(), 1);
- assert_eq!(import_fn.params[0].name, "n");
- assert_eq!(import_fn.params[0].ty, TypeDesc::S64);
- assert_eq!(import_fn.results.len(), 1);
- assert_eq!(import_fn.results[0], TypeDesc::S64);
-
- // Adder exports "process"
- assert_eq!(metadata.exports().len(), 1);
- let export_fn = &metadata.exports()[0];
- assert_eq!(export_fn.name, "process");
- assert_eq!(export_fn.params[0].ty, TypeDesc::Value);
-}
-
#[test]
fn test_no_metadata() {
// A minimal WAT module without __pack_types