From 1a2451b1ca255db22d41c0d11e727cbc5ccbe7ed Mon Sep 17 00:00:00 2001 From: colin Date: Sun, 12 Jul 2026 22:54:52 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20pack=20compose=20=E2=80=94=20static=20c?= =?UTF-8?q?omposition=20of=20packages=20into=20one=20self-contained=20.was?= =?UTF-8?q?m?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `packr compose ` merges compiled packages (allocator + providers + consumers) into a single ZERO-IMPORT .wasm that runs on any stock runtime, with cross-package imports internalized to direct calls (validates under wasm-tools). Pipeline: binaryen `wasm-merge` fuses modules + wires cross-imports; a `walrus` pass unifies the several env.memory imports into one internal memory, bakes the allocator's __memory_base/__heap_base/__heap_end into constants, and exports the memory. Requires wasm-merge (binaryen), added to the dev shell. Rips two superseded composers this replaces: - compose::StaticComposer (bump-allocator merger, wrong model since 0.8.0) - runtime::CompositionBuilder (pre-PIC runtime composer, red since PIC migration) both superseded by PicCompositionBuilder (runtime) + pack compose (static). Packaging win, not perf: the ABI marshalling stays fossilized in the packages. Co-Authored-By: Claude Opus 4.8 (1M context) --- Cargo.lock | 126 ++- Cargo.toml | 2 + assets/adder_fixedbase.wasm | Bin 0 -> 115962 bytes assets/doubler_fixedbase.wasm | Bin 0 -> 73087 bytes docs/pic-composition.md | 47 +- flake.nix | 1 + src/compose/merger.rs | 1387 --------------------------------- src/compose/mod.rs | 379 +-------- src/compose/static_compose.rs | 227 ++++++ src/lib.rs | 2 +- src/main.rs | 118 +++ src/runtime/composition.rs | 960 ----------------------- src/runtime/mod.rs | 2 - tests/compose.rs | 102 +++ tests/composition.rs | 175 ----- tests/type_metadata.rs | 34 +- 16 files changed, 615 insertions(+), 2947 deletions(-) create mode 100755 assets/adder_fixedbase.wasm create mode 100755 assets/doubler_fixedbase.wasm delete mode 100644 src/compose/merger.rs create mode 100644 src/compose/static_compose.rs delete mode 100644 src/runtime/composition.rs create mode 100644 tests/compose.rs delete mode 100644 tests/composition.rs 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 0000000000000000000000000000000000000000..d641e7b96da218906e053b26840dbcb55fe9f416 GIT binary patch literal 115962 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

n0^h-)Eq^4u!VVjUn{OJPC{ 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 literal 0 HcmV?d00001 diff --git a/assets/doubler_fixedbase.wasm b/assets/doubler_fixedbase.wasm new file mode 100755 index 0000000000000000000000000000000000000000..8ae27a2b418b61eff6717f036bf2d26dd2ebd1c6 GIT binary patch literal 73087 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 literal 0 HcmV?d00001 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