From 31d3eab095901be726144ebd5070a7ffa9eeb6c4 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Tue, 8 Aug 2023 20:48:41 +0200 Subject: [PATCH] Support polyfilling multi-memory (#125) * Support polyfilling multi-memory Currently JS does not generally support the WebAssembly multi-memory proposal. Wasmtime will, however, generate adapter modules which use multi-memory to communicate between components. This means that composed components are typically not compatible with the transpile process as they produce a core module that doesn't actually run in any JS runtime. This commit fixes this issue by adding polyfill support for multi-memory. Whenever an adapter is produced that uses multi-memory jco will now rewrite the module such that any references to memory that isn't at index 0 to be indirected through functions. These imported functions then operate on the specified memory on behalf of the wasm itself. This is a horribly slow process because all memory reads/writes become function calls, but given that the baseline is otherwise "does not work" it's hopefully a bit better than before. The end goal here is to work up towards strings between components, but for now this just gets everything else working with multi-memory such as transferring lists. * Fill out comments --------- Co-authored-by: Guy Bedford --- Cargo.lock | 2 + .../js-component-bindgen-component/Cargo.toml | 1 + .../js-component-bindgen-component/src/lib.rs | 1 + crates/js-component-bindgen/Cargo.toml | 1 + crates/js-component-bindgen/src/core.rs | 804 ++++++++++++++++++ crates/js-component-bindgen/src/lib.rs | 12 +- .../src/transpile_bindgen.rs | 119 ++- test/codegen.js | 2 +- .../components/list-adapter-fusion.wat | 156 ++++ test/runtime.js | 4 +- test/runtime/list-adapter-fusion.ts | 108 +++ 11 files changed, 1192 insertions(+), 18 deletions(-) create mode 100644 crates/js-component-bindgen/src/core.rs create mode 100644 test/fixtures/components/list-adapter-fusion.wat create mode 100644 test/runtime/list-adapter-fusion.ts diff --git a/Cargo.lock b/Cargo.lock index 2f5df24b0..75655d827 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -188,6 +188,7 @@ dependencies = [ "base64", "heck", "indexmap 2.0.0", + "wasm-encoder", "wasmtime-environ", "wit-component", "wit-parser", @@ -200,6 +201,7 @@ dependencies = [ "anyhow", "js-component-bindgen", "wasmtime-environ", + "wat", "wit-bindgen", ] diff --git a/crates/js-component-bindgen-component/Cargo.toml b/crates/js-component-bindgen-component/Cargo.toml index d91c5cbb3..0ba391261 100644 --- a/crates/js-component-bindgen-component/Cargo.toml +++ b/crates/js-component-bindgen-component/Cargo.toml @@ -14,3 +14,4 @@ anyhow = { workspace = true } js-component-bindgen = { path = "../js-component-bindgen" } wasmtime-environ = { workspace = true } wit-bindgen = { workspace = true } +wat = { workspace = true } diff --git a/crates/js-component-bindgen-component/src/lib.rs b/crates/js-component-bindgen-component/src/lib.rs index bde3b9736..8c2d65a12 100644 --- a/crates/js-component-bindgen-component/src/lib.rs +++ b/crates/js-component-bindgen-component/src/lib.rs @@ -44,6 +44,7 @@ struct JsComponentBindgenComponent; impl JsComponentBindgen for JsComponentBindgenComponent { fn generate(component: Vec, options: GenerateOptions) -> Result { + let component = wat::parse_bytes(&component).map_err(|e| format!("{e}"))?; let opts = js_component_bindgen::TranspileOpts { name: options.name, no_typescript: options.no_typescript.unwrap_or(false), diff --git a/crates/js-component-bindgen/Cargo.toml b/crates/js-component-bindgen/Cargo.toml index 210a10faa..07e94d329 100644 --- a/crates/js-component-bindgen/Cargo.toml +++ b/crates/js-component-bindgen/Cargo.toml @@ -25,3 +25,4 @@ wit-component = { workspace = true } wit-parser = { workspace = true } indexmap = { workspace = true } base64 = { workspace = true } +wasm-encoder = { workspace = true } diff --git a/crates/js-component-bindgen/src/core.rs b/crates/js-component-bindgen/src/core.rs new file mode 100644 index 000000000..7a90be0f3 --- /dev/null +++ b/crates/js-component-bindgen/src/core.rs @@ -0,0 +1,804 @@ +//! Support for transpiling core modules using multi-memory to those that don't +//! use multi-memory. +//! +//! Wasmtime's implementation of adapter modules between components requires the +//! usage of multi-memory for copying data back and forth between two +//! components. The multi-memory proposal is, at this time, not stable in any JS +//! engine. This module is an attempt to polyfill this until at such a time that +//! multi-memory can be used natively. +//! +//! The purpose of this module is to identify core wasms which require +//! multi-memory coming out of Wasmtime. These wasms are rewritten to not +//! actually use more than one memory. The implementation here is to replace all +//! memory instructions operating on memory index 1 or greater with function +//! calls where JS is the one that does the load/store/etc. This is not expected +//! to be fast at runtime but is intended to be just enough to get this working +//! in JS environments at this time. The true speed is expected to come with the +//! multi-memory proposal. +//! +//! This module exports a [`Translation`] which wraps a [`ModuleTranslation`] +//! either as a pass-through "normal" or an "augmented" version where +//! "augmented" means that the original wasm is not used but instead a +//! recompiled copy without multiple memories is used. When calculating the +//! imports for the "augmented" module the arguments for JS functions that +//! read/write memory are automatically injected and handled. +//! +//! Callers of this module need to have an implementation in JS for all of the +//! entries listed in [`AugmentedOp`], likely through the `DataView` class in +//! JS. +//! +//! Note that at this time this module is not intended to be a complete and +//! general purpose method of compiling multiple memories to single-memory +//! modules. This does not handle all instructions that use memory for example, +//! but only those that Wasmtime's adapter modules emits. It's possible to add +//! support for more instructions but such support isn't required at this time. +//! Examples of unsupported instructions are `i64.load8_u` and `memory.copy`. +//! Additionally core wasm sections such as data sections and tables are not +//! supported because, again, Wasmtime doesn't use it at this time. + +use anyhow::{bail, Result}; +use indexmap::IndexMap; +use std::collections::{HashMap, HashSet}; +use wasm_encoder::*; +use wasmparser::*; +use wasmtime_environ::component::CoreDef; +use wasmtime_environ::wasmparser; +use wasmtime_environ::{EntityIndex, MemoryIndex, ModuleTranslation, PrimaryMap}; + +pub enum Translation<'a> { + Normal(ModuleTranslation<'a>), + Augmented { + original: ModuleTranslation<'a>, + wasm: Vec, + imports_removed: HashSet<(String, String)>, + imports_added: Vec<(String, String, MemoryIndex, AugmentedOp)>, + }, +} + +pub enum AugmentedImport<'a> { + CoreDef(&'a CoreDef), + Memory { mem: &'a CoreDef, op: AugmentedOp }, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum AugmentedOp { + I32Load, + I32Load8U, + I32Load8S, + I32Load16U, + I32Load16S, + I64Load, + F32Load, + F64Load, + I32Store, + I32Store8, + I32Store16, + I64Store, + F32Store, + F64Store, + + MemorySize, +} + +impl<'a> Translation<'a> { + pub fn new(translation: ModuleTranslation<'a>) -> Result> { + let mut features = WasmFeatures::default(); + features.multi_memory = false; + match Validator::new_with_features(features).validate_all(&translation.wasm) { + // This module validates without multi-memory, no need to augment + // it + Ok(_) => return Ok(Translation::Normal(translation)), + Err(e) => { + features.multi_memory = true; + match Validator::new_with_features(features).validate_all(&translation.wasm) { + // This module validates with multi-memory, so fall through + // to augmentation. + Ok(_) => {} + + // This appears to not validate at all. + Err(_) => return Err(e.into()), + } + } + } + + let mut augmenter = Augmenter { + translation: &translation, + imports_removed: Default::default(), + imports_added: Default::default(), + imported_funcs: Default::default(), + imported_memories: Default::default(), + imports: Default::default(), + exports: Default::default(), + local_func_tys: Default::default(), + local_funcs: Default::default(), + types: Default::default(), + augments: Default::default(), + }; + let wasm = augmenter.run()?; + Ok(Translation::Augmented { + wasm, + imports_removed: augmenter.imports_removed, + imports_added: augmenter.imports_added, + original: translation, + }) + } + + /// Returns the encoded wasm that represents this module, automatically + /// returning the augmented version if multi-memory augmentation was + /// required. + pub fn wasm(&self) -> &[u8] { + match self { + Translation::Normal(translation) => translation.wasm, + Translation::Augmented { wasm, .. } => wasm, + } + } + + /// Returns an iterator over the imports for this module using the `args` as + /// supplied to the original module. + /// + /// The returned imports are either those within `args` or augmented + /// versions based on `args` that perform an `AugmentedOp`. + pub fn imports<'b>( + &'b self, + args: &'b [CoreDef], + ) -> Vec<(&'b str, &'b str, AugmentedImport<'b>)> { + match self { + Translation::Normal(translation) => { + assert_eq!(translation.module.imports().len(), args.len()); + translation + .module + .imports() + .zip(args) + .map(|((module, name, _), arg)| (module, name, AugmentedImport::CoreDef(arg))) + .collect() + } + Translation::Augmented { + original, + imports_removed, + imports_added, + .. + } => { + let mut ret = Vec::new(); + let mut memories: PrimaryMap = PrimaryMap::new(); + for ((module, name, _), arg) in original.module.imports().zip(args) { + if imports_removed.contains(&(module.to_string(), name.to_string())) { + memories.push(arg); + } else { + ret.push((module, name, AugmentedImport::CoreDef(arg))); + } + } + for (module, name, index, op) in imports_added { + ret.push(( + module, + name, + AugmentedImport::Memory { + mem: memories[*index], + op: *op, + }, + )); + } + ret + } + } + } + + /// Returns the exports of this module, which are not modified by + /// augmentation. + pub fn exports(&self) -> &IndexMap { + match self { + Translation::Normal(translation) => &translation.module.exports, + Translation::Augmented { original, .. } => &original.module.exports, + } + } +} + +pub struct Augmenter<'a> { + translation: &'a ModuleTranslation<'a>, + imports_removed: HashSet<(String, String)>, + imports_added: Vec<(String, String, MemoryIndex, AugmentedOp)>, + augments: HashMap<(MemoryIndex, AugmentedOp), u32>, + + types: Vec, + imports: Vec>, + imported_funcs: u32, + imported_memories: u32, + exports: Vec>, + local_funcs: Vec>, + local_func_tys: Vec, +} + +impl Augmenter<'_> { + fn run(&mut self) -> Result> { + // The first step is to parse the input original wasm and learn about + // its structure. This validates that all the sections are supported and + // records various bits of information about the module within `self`. + for payload in Parser::new(0).parse_all(self.translation.wasm) { + match payload? { + Payload::Version { .. } => {} + Payload::End(_) => {} + + Payload::TypeSection(s) => { + for ty in s { + self.types.push(ty?); + } + } + Payload::ImportSection(s) => { + for i in s { + let i = i?; + match i.ty { + TypeRef::Func(_) => self.imported_funcs += 1, + TypeRef::Memory(_) => { + if self.imported_memories > 0 { + let ok = self + .imports_removed + .insert((i.module.to_string(), i.name.to_string())); + assert!(ok); + continue; + } + self.imported_memories += 1; + } + _ => {} + } + self.imports.push(i); + } + } + + Payload::ExportSection(s) => { + for e in s { + let e = e?; + self.exports.push(e); + } + } + + Payload::FunctionSection(s) => { + for ty in s { + let ty = ty?; + self.local_func_tys.push(ty); + } + } + + Payload::CodeSectionStart { .. } => {} + Payload::CodeSectionEntry(body) => { + self.local_funcs.push(body); + } + + // Ignore all custom sections for now + Payload::CustomSection(_) => {} + + // NB: these sections are theoretically possible to handle but + // are not required at this time. + Payload::DataCountSection { .. } + | Payload::GlobalSection(_) + | Payload::TableSection(_) + | Payload::MemorySection(_) + | Payload::ElementSection(_) + | Payload::DataSection(_) + | Payload::StartSection { .. } + | Payload::TagSection(_) + | Payload::UnknownSection { .. } => { + bail!("unsupported section found in module using multiple memories") + } + + // component-model related things that shouldn't show up + Payload::ModuleSection { .. } + | Payload::ComponentSection { .. } + | Payload::InstanceSection(_) + | Payload::ComponentInstanceSection(_) + | Payload::ComponentAliasSection(_) + | Payload::ComponentCanonicalSection(_) + | Payload::ComponentStartSection { .. } + | Payload::ComponentImportSection(_) + | Payload::CoreTypeSection(_) + | Payload::ComponentExportSection(_) + | Payload::ComponentTypeSection(_) => { + bail!("component section found in module using multiple memories") + } + } + } + + // After the module has been parsed next the set of adapter functions is + // determined. This is done by parsing all instructions in the module + // and looking for anything that operates on memory index 1 or greater. + // + // This will fill out `self.augments` which is a list of functionality + // that must be provided by JS to mutate non-index-0 memories. + for body in self.local_funcs.clone() { + let mut reader = body.get_operators_reader()?; + while !reader.eof() { + reader.visit_operator(&mut CollectMemOps(self))?; + } + } + + // And now at the end we've got all the information for encoding so + // begin that process. + self.encode() + } + + fn augment_op(&mut self, mem: u32, op: AugmentedOp) { + // Memory 0 stays in the module and isn't removed, so no need to + // register an augmentation. + if mem == 0 { + return; + } + let index = MemoryIndex::from_u32(mem - 1); + self.augments.entry((index, op)).or_insert_with(|| { + let idx = self.imported_funcs + self.imports_added.len() as u32; + self.imports_added.push(( + "augments".to_string(), + format!("mem{mem} {op:?}"), + index, + op, + )); + idx + }); + } + + fn encode(&self) -> Result> { + let mut module = Module::new(); + + // Types are all passed through as-is to retain the same type section as + // before. + let mut types = TypeSection::new(); + for ty in self.types.iter() { + assert!(!ty.is_final); + assert!(ty.supertype_idx.is_none()); + match &ty.structural_type { + wasmparser::StructuralType::Func(f) => { + types.function( + f.params().iter().map(|v| valtype(*v)), + f.results().iter().map(|v| valtype(*v)), + ); + } + wasmparser::StructuralType::Array(_) | wasmparser::StructuralType::Struct(_) => { + unimplemented!() + } + } + } + + // Pass through all of `self.imports` into the import section. This will + // already have imports of multiple memories removed so this will import + // at most one memory. + let mut imports = ImportSection::new(); + for import in self.imports.iter() { + let ty = match import.ty { + TypeRef::Func(f) => EntityType::Function(f), + TypeRef::Global(g) => EntityType::Global(wasm_encoder::GlobalType { + mutable: g.mutable, + val_type: valtype(g.content_type), + }), + TypeRef::Memory(m) => EntityType::Memory(wasm_encoder::MemoryType { + maximum: m.maximum, + minimum: m.initial, + memory64: m.memory64, + shared: m.shared, + }), + TypeRef::Table(_) => unimplemented!(), + TypeRef::Tag(_) => unimplemented!(), + }; + imports.import(import.module, import.name, ty); + } + + // After the normal imports are all registered next the + // memory-modification-functions are all imported. This is a new + // addition to this module which shifts all functions in the index + // space, hence the rewriting of all function bodies below. + // + // Each augmentation function declares its type signature in the type + // section at the end of the type section to avoid tampering with the + // type section's original index spaces. It would be more efficient to + // not redeclare function signatures and reuse existing function + // signatures, but that's left as an optimization for a later date. + for (module, name, _, op) in self.imports_added.iter() { + let cnt = types.len(); + op.encode_type(&mut types); + imports.import(module, name, EntityType::Function(cnt)); + } + + // The function section remains the same as we're not tampering with the + // count or types of all local functions. + let mut funcs = FunctionSection::new(); + for ty in self.local_func_tys.iter() { + funcs.function(*ty); + } + + // Exports all remain the same with the one caveat that the function + // index space has changed so those indices are remapped. + let mut exports = ExportSection::new(); + for e in self.exports.iter() { + let (kind, index) = match e.kind { + ExternalKind::Func => (ExportKind::Func, self.remap_func(e.index)), + ExternalKind::Table => (ExportKind::Table, e.index), + ExternalKind::Global => (ExportKind::Global, e.index), + ExternalKind::Memory => { + assert!(e.index < 1); + (ExportKind::Memory, e.index) + } + ExternalKind::Tag => (ExportKind::Tag, e.index), + }; + exports.export(e.name, kind, index); + } + + // Finally the code section is remapped. This is done by translating + // operator-by-operator from `wasmparser` to `wasm-encoder`. This + // is where instructions like `i32.load 1` will become `call + // $i32_load_memory_1`. + let mut code = CodeSection::new(); + for body in self.local_funcs.iter() { + let mut locals = Vec::new(); + + for local in body.get_locals_reader()? { + let (cnt, ty) = local?; + locals.push((cnt, valtype(ty))); + } + + let mut f = Function::new(locals); + + let mut ops = body.get_operators_reader()?; + while !ops.eof() { + ops.visit_operator(&mut Translator { + func: &mut f, + augmenter: self, + })?; + } + + code.function(&f); + } + + module.section(&types); + module.section(&imports); + module.section(&funcs); + module.section(&exports); + module.section(&code); + + Ok(module.finish()) + } + + fn remap_func(&self, index: u32) -> u32 { + if index < self.imported_funcs { + index + } else { + index + self.imports_added.len() as u32 + } + } + + fn remap_memory(&self, index: u32) -> u32 { + assert!(index < 1); + index + } +} + +fn valtype(ty: wasmparser::ValType) -> wasm_encoder::ValType { + match ty { + wasmparser::ValType::I32 => wasm_encoder::ValType::I32, + wasmparser::ValType::I64 => wasm_encoder::ValType::I64, + wasmparser::ValType::F32 => wasm_encoder::ValType::F32, + wasmparser::ValType::F64 => wasm_encoder::ValType::F64, + wasmparser::ValType::V128 => wasm_encoder::ValType::V128, + wasmparser::ValType::Ref(_) => unimplemented!(), + } +} + +struct CollectMemOps<'a, 'b>(&'a mut Augmenter<'b>); + +macro_rules! define_visit { + ($(@$p:ident $op:ident $({ $($arg:ident: $argty:ty),* })? => $visit:ident)*) => { + $( + #[allow(unreachable_code)] + fn $visit(&mut self $( $( ,$arg: $argty)* )?) { + define_visit!(augment self $op $($($arg)*)?); + } + )* + }; + + // List of instructions that are augmented which register the memory index + // and the relevant augmentation operation. + (augment $self:ident I32Load $memarg:ident) => { + $self.0.augment_op($memarg.memory, AugmentedOp::I32Load); + }; + (augment $self:ident I64Load $memarg:ident) => { + $self.0.augment_op($memarg.memory, AugmentedOp::I64Load); + }; + (augment $self:ident F32Load $memarg:ident) => { + $self.0.augment_op($memarg.memory, AugmentedOp::F32Load); + }; + (augment $self:ident F64Load $memarg:ident) => { + $self.0.augment_op($memarg.memory, AugmentedOp::F64Load); + }; + (augment $self:ident I32Load8U $memarg:ident) => { + $self.0.augment_op($memarg.memory, AugmentedOp::I32Load8U); + }; + (augment $self:ident I32Load8S $memarg:ident) => { + $self.0.augment_op($memarg.memory, AugmentedOp::I32Load8S); + }; + (augment $self:ident I32Load16U $memarg:ident) => { + $self.0.augment_op($memarg.memory, AugmentedOp::I32Load16U); + }; + (augment $self:ident I32Load16S $memarg:ident) => { + $self.0.augment_op($memarg.memory, AugmentedOp::I32Load16S); + }; + (augment $self:ident I32Store $memarg:ident) => { + $self.0.augment_op($memarg.memory, AugmentedOp::I32Store); + }; + (augment $self:ident I64Store $memarg:ident) => { + $self.0.augment_op($memarg.memory, AugmentedOp::I64Store); + }; + (augment $self:ident F32Store $memarg:ident) => { + $self.0.augment_op($memarg.memory, AugmentedOp::F32Store); + }; + (augment $self:ident F64Store $memarg:ident) => { + $self.0.augment_op($memarg.memory, AugmentedOp::F64Store); + }; + (augment $self:ident I32Store8 $memarg:ident) => { + $self.0.augment_op($memarg.memory, AugmentedOp::I32Store8); + }; + (augment $self:ident I32Store16 $memarg:ident) => { + $self.0.augment_op($memarg.memory, AugmentedOp::I32Store16); + }; + (augment $self:ident MemorySize $mem:ident $byte:ident) => { + $self.0.augment_op($mem, AugmentedOp::MemorySize); + }; + + // Catch-all which asserts that none of the `$arg` looks like a memory + // index ty catch any missing instructions from the list above. + (augment $self:ident $op:ident $($arg:ident)*) => { + $( + define_visit!(assert_not_mem $op $arg); + )* + }; + + (assert_not_mem $op:ident mem) => {panic!(concat!("missed case ", stringify!($op)));}; + (assert_not_mem $op:ident src_mem) => {panic!(concat!("missed case ", stringify!($op)));}; + (assert_not_mem $op:ident dst_mem) => {panic!(concat!("missed case ", stringify!($op)));}; + (assert_not_mem $op:ident memarg) => {panic!(concat!("missed case ", stringify!($op)));}; + (assert_not_mem $op:ident $other:ident) => {}; +} + +impl<'a> VisitOperator<'a> for CollectMemOps<'_, 'a> { + type Output = (); + + wasmparser::for_each_operator!(define_visit); +} + +impl AugmentedOp { + fn encode_type(&self, section: &mut TypeSection) { + use wasm_encoder::ValType::*; + match self { + // Loads take two arguments: the first is the address being loaded + // from and the second is the static offset that was listed on the + // relevant load instruction. + AugmentedOp::I32Load + | AugmentedOp::I32Load8U + | AugmentedOp::I32Load8S + | AugmentedOp::I32Load16U + | AugmentedOp::I32Load16S => { + section.function([I32, I32], [I32]); + } + AugmentedOp::I64Load => { + section.function([I32, I32], [I64]); + } + AugmentedOp::F32Load => { + section.function([I32, I32], [F32]); + } + AugmentedOp::F64Load => { + section.function([I32, I32], [F64]); + } + + // Stores, like loads, take an additional argument than usual which + // is the static offset on the store instruction. + AugmentedOp::I32Store | AugmentedOp::I32Store8 | AugmentedOp::I32Store16 => { + section.function([I32, I32, I32], []); + } + AugmentedOp::I64Store => { + section.function([I32, I64, I32], []); + } + AugmentedOp::F32Store => { + section.function([I32, F32, I32], []); + } + AugmentedOp::F64Store => { + section.function([I32, F64, I32], []); + } + + AugmentedOp::MemorySize => { + section.function([], [I32]); + } + } + } +} + +struct Translator<'a, 'b> { + func: &'a mut wasm_encoder::Function, + augmenter: &'a Augmenter<'b>, +} + +// Helper macro to create a wasmparser visitor which will translate each +// individual instruction from wasmparser to wasm-encoder. +macro_rules! define_translate { + // This is the base case where all methods are defined and the body of each + // method delegates to a recursive invocation of this macro to hit one of + // the cases below. + ($(@$p:ident $op:ident $({ $($arg:ident: $argty:ty),* })? => $visit:ident)*) => { + $( + #[allow(clippy::drop_copy)] + fn $visit(&mut self $(, $($arg: $argty),*)?) { + #[allow(unused_imports)] + use wasm_encoder::Instruction::*; + + define_translate!(translate self $op $($($arg)*)?) + } + )* + }; + + // Memory-related operations are translated to augmentations which are a + // `Call` of an imported function (or are passed through natively as the + // same instruction if they use memory 0). + (translate $self:ident I32Load $memarg:ident) => {{ + $self.augment(AugmentedOp::I32Load, I32Load, $memarg) + }}; + (translate $self:ident I32Load8U $memarg:ident) => {{ + $self.augment(AugmentedOp::I32Load8U, I32Load8U, $memarg) + }}; + (translate $self:ident I32Load8S $memarg:ident) => {{ + $self.augment(AugmentedOp::I32Load8S, I32Load8S, $memarg) + }}; + (translate $self:ident I32Load16U $memarg:ident) => {{ + $self.augment(AugmentedOp::I32Load16U, I32Load16U, $memarg) + }}; + (translate $self:ident I32Load16S $memarg:ident) => {{ + $self.augment(AugmentedOp::I32Load16S, I32Load16S, $memarg) + }}; + (translate $self:ident I64Load $memarg:ident) => {{ + $self.augment(AugmentedOp::I64Load, I64Load, $memarg) + }}; + (translate $self:ident F32Load $memarg:ident) => {{ + $self.augment(AugmentedOp::F32Load, F32Load, $memarg) + }}; + (translate $self:ident F64Load $memarg:ident) => {{ + $self.augment(AugmentedOp::F64Load, F64Load, $memarg) + }}; + (translate $self:ident I32Store $memarg:ident) => {{ + $self.augment(AugmentedOp::I32Store, I32Store, $memarg) + }}; + (translate $self:ident I32Store8 $memarg:ident) => {{ + $self.augment(AugmentedOp::I32Store8, I32Store8, $memarg) + }}; + (translate $self:ident I32Store16 $memarg:ident) => {{ + $self.augment(AugmentedOp::I32Store16, I32Store16, $memarg) + }}; + (translate $self:ident I64Store $memarg:ident) => {{ + $self.augment(AugmentedOp::I64Store, I64Store, $memarg) + }}; + (translate $self:ident F32Store $memarg:ident) => {{ + $self.augment(AugmentedOp::F32Store, F32Store, $memarg) + }}; + (translate $self:ident F64Store $memarg:ident) => {{ + $self.augment(AugmentedOp::F64Store, F64Store, $memarg) + }}; + (translate $self:ident MemorySize $mem:ident $byte:ident) => {{ + if $mem < 1 { + $self.func.instruction(&MemorySize($mem)); + } else { + let mem = MemoryIndex::from_u32($mem - 1); + let func = $self.augmenter.augments[&(mem, AugmentedOp::MemorySize)]; + $self.func.instruction(&Call(func)); + } + }}; + + // All other instructions not listed above are caught here and fall through + // to below cases to translate arguments/types from wasmparser to + // wasm-encoder and then create the new instruction. + (translate $self:ident $op:ident $($arg:ident)*) => {{ + $( + let $arg = define_translate!(map $self $arg $arg); + )* + let insn = define_translate!(mk $op $($arg)*); + $self.func.instruction(&insn); + }}; + + // No-payload instructions are named the same in wasmparser as they are in + // wasm-encoder + (mk $op:ident) => ($op); + + // Instructions which need "special care" to map from wasmparser to + // wasm-encoder + (mk BrTable $arg:ident) => ({ + BrTable($arg.0, $arg.1) + }); + (mk CallIndirect $ty:ident $table:ident $table_byte:ident) => ({ + let _ = $table_byte; + CallIndirect { ty: $ty, table: $table } + }); + (mk ReturnCallIndirect $ty:ident $table:ident) => ( + ReturnCallIndirect { ty: $ty, table: $table } + ); + (mk I32Const $v:ident) => (I32Const($v)); + (mk I64Const $v:ident) => (I64Const($v)); + (mk F32Const $v:ident) => (F32Const(f32::from_bits($v.bits()))); + (mk F64Const $v:ident) => (F64Const(f64::from_bits($v.bits()))); + (mk V128Const $v:ident) => (V128Const($v.i128())); + (mk MemoryGrow $($x:tt)*) => ({ + if true { unimplemented!() } Nop + }); + + // Catch-all for the translation of one payload argument which is typically + // represented as a tuple-enum in wasm-encoder. + (mk $op:ident $arg:ident) => ($op($arg)); + + // Catch-all of everything else where the wasmparser fields are simply + // translated to wasm-encoder fields. + (mk $op:ident $($arg:ident)*) => ($op { $($arg),* }); + + // Individual cases of mapping one argument type to another, similar to the + // `define_visit` macro above. + (map $self:ident $arg:ident memarg) => {$self.memarg($arg)}; + (map $self:ident $arg:ident blockty) => {$self.blockty($arg)}; + (map $self:ident $arg:ident hty) => {$self.heapty($arg)}; + (map $self:ident $arg:ident tag_index) => {$arg}; + (map $self:ident $arg:ident relative_depth) => {$arg}; + (map $self:ident $arg:ident function_index) => {$self.augmenter.remap_func($arg)}; + (map $self:ident $arg:ident global_index) => {$arg}; + (map $self:ident $arg:ident mem) => {$self.augmenter.remap_memory($arg)}; + (map $self:ident $arg:ident src_mem) => {$self.augmenter.remap_memory($arg)}; + (map $self:ident $arg:ident dst_mem) => {$self.augmenter.remap_memory($arg)}; + (map $self:ident $arg:ident table) => {$arg}; + (map $self:ident $arg:ident table_index) => {$arg}; + (map $self:ident $arg:ident src_table) => {$arg}; + (map $self:ident $arg:ident dst_table) => {$arg}; + (map $self:ident $arg:ident type_index) => {$arg}; + (map $self:ident $arg:ident ty) => {valtype($arg)}; + (map $self:ident $arg:ident local_index) => {$arg}; + (map $self:ident $arg:ident lane) => {$arg}; + (map $self:ident $arg:ident lanes) => {$arg}; + (map $self:ident $arg:ident elem_index) => {$arg}; + (map $self:ident $arg:ident data_index) => {$arg}; + (map $self:ident $arg:ident table_byte) => {$arg}; + (map $self:ident $arg:ident mem_byte) => {$arg}; + (map $self:ident $arg:ident value) => {$arg}; + (map $self:ident $arg:ident targets) => (( + $arg.targets().map(|i| i.unwrap()).collect::>().into(), + $arg.default(), + )); +} + +impl<'a> VisitOperator<'a> for Translator<'_, 'a> { + type Output = (); + + wasmparser::for_each_operator!(define_translate); +} + +impl Translator<'_, '_> { + fn blockty(&self, ty: wasmparser::BlockType) -> wasm_encoder::BlockType { + match ty { + wasmparser::BlockType::Empty => wasm_encoder::BlockType::Empty, + wasmparser::BlockType::Type(t) => wasm_encoder::BlockType::Result(valtype(t)), + wasmparser::BlockType::FuncType(i) => wasm_encoder::BlockType::FunctionType(i), + } + } + fn heapty(&self, _ty: wasmparser::HeapType) -> wasm_encoder::HeapType { + unimplemented!() + } + + fn memarg(&self, ty: wasmparser::MemArg) -> wasm_encoder::MemArg { + wasm_encoder::MemArg { + align: ty.align.into(), + offset: ty.offset, + memory_index: self.augmenter.remap_memory(ty.memory), + } + } + + fn augment( + &mut self, + op: AugmentedOp, + insn: fn(wasm_encoder::MemArg) -> wasm_encoder::Instruction<'static>, + memarg: wasmparser::MemArg, + ) { + use wasm_encoder::Instruction::*; + if memarg.memory < 1 { + self.func.instruction(&insn(self.memarg(memarg))); + return; + } + let idx = MemoryIndex::from_u32(memarg.memory - 1); + let func = self.augmenter.augments[&(idx, op)]; + self.func.instruction(&I32Const(memarg.offset as i32)); + self.func.instruction(&Call(func)); + } +} diff --git a/crates/js-component-bindgen/src/lib.rs b/crates/js-component-bindgen/src/lib.rs index 036eb8824..0eb1c1e6d 100644 --- a/crates/js-component-bindgen/src/lib.rs +++ b/crates/js-component-bindgen/src/lib.rs @@ -1,3 +1,4 @@ +mod core; mod files; mod transpile_bindgen; mod ts_bindgen; @@ -14,9 +15,9 @@ use transpile_bindgen::transpile_bindgen; use anyhow::{bail, Context}; use wasmtime_environ::component::Export; -use wasmtime_environ::component::{ComponentTypesBuilder, Translator}; +use wasmtime_environ::component::{ComponentTypesBuilder, StaticModuleIndex, Translator}; use wasmtime_environ::wasmparser::{Validator, WasmFeatures}; -use wasmtime_environ::{ScopeVec, Tunables}; +use wasmtime_environ::{PrimaryMap, ScopeVec, Tunables}; use wit_component::DecodedWasm; use ts_bindgen::ts_bindgen; @@ -123,10 +124,15 @@ pub fn transpile(component: &[u8], opts: TranspileOpts) -> Result> = modules + .into_iter() + .map(|(_i, module)| core::Translation::new(module)) + .collect::>()?; + // Insert all core wasm modules into the generated `Files` which will // end up getting used in the `generate_instantiate` method. for (i, module) in modules.iter() { - files.push(&core_file_name(&name, i.as_u32()), module.wasm); + files.push(&core_file_name(&name, i.as_u32()), module.wasm()); } if !opts.no_typescript { diff --git a/crates/js-component-bindgen/src/transpile_bindgen.rs b/crates/js-component-bindgen/src/transpile_bindgen.rs index f3533025a..ce8861fcb 100644 --- a/crates/js-component-bindgen/src/transpile_bindgen.rs +++ b/crates/js-component-bindgen/src/transpile_bindgen.rs @@ -1,3 +1,4 @@ +use crate::core; use crate::esm_bindgen::EsmBindgen; use crate::files::Files; use crate::function_bindgen::{ErrHandling, FunctionBindgen}; @@ -19,7 +20,7 @@ use wasmtime_environ::{ RuntimeInstanceIndex, StaticModuleIndex, Trampoline, TrampolineIndex, }, }; -use wasmtime_environ::{EntityIndex, ModuleTranslation, PrimaryMap}; +use wasmtime_environ::{EntityIndex, PrimaryMap}; use wit_component::StringEncoding; use wit_parser::abi::{AbiVariant, LiftLower}; use wit_parser::*; @@ -73,7 +74,7 @@ struct JsBindgen<'a> { pub fn transpile_bindgen( name: &str, component: &ComponentTranslation, - modules: &PrimaryMap>, + modules: &PrimaryMap>, resolve: &Resolve, id: WorldId, opts: TranspileOpts, @@ -294,7 +295,7 @@ impl<'a> JsBindgen<'a> { struct Instantiator<'a, 'b> { src: Source, gen: &'a mut JsBindgen<'b>, - modules: &'a PrimaryMap>, + modules: &'a PrimaryMap>, instances: PrimaryMap, resolve: &'a Resolve, world: WorldId, @@ -416,19 +417,19 @@ impl<'a> Instantiator<'a, '_> { } fn instantiate_static_module(&mut self, idx: StaticModuleIndex, args: &[CoreDef]) { - let module = &self.modules[idx].module; - // Build a JS "import object" which represents `args`. The `args` is a // flat representation which needs to be zip'd with the list of names to // correspond to the JS wasm embedding API. This is one of the major // differences between Wasmtime's and JS's embedding API. let mut import_obj = BTreeMap::new(); - assert_eq!(module.imports().len(), args.len()); - for ((module, name, _), arg) in module.imports().zip(args) { - let def = self.core_def(arg); + for (module, name, arg) in self.modules[idx].imports(args) { + let def = self.augmented_import_def(arg); let dst = import_obj.entry(module).or_insert(BTreeMap::new()); let prev = dst.insert(name, def); - assert!(prev.is_none()); + assert!( + prev.is_none(), + "unsupported duplicate import of `{module}::{name}`" + ); } let mut imports = String::new(); if !import_obj.is_empty() { @@ -617,12 +618,106 @@ impl<'a> Instantiator<'a, '_> { self.src.js("}"); } + fn augmented_import_def(&self, def: core::AugmentedImport<'_>) -> String { + match def { + core::AugmentedImport::CoreDef(def) => self.core_def(def), + core::AugmentedImport::Memory { mem, op } => { + let mem = self.core_def(mem); + match op { + core::AugmentedOp::I32Load => { + format!( + "(ptr, off) => new DataView({mem}.buffer).getInt32(ptr + off, true)" + ) + } + core::AugmentedOp::I32Load8U => { + format!( + "(ptr, off) => new DataView({mem}.buffer).getUint8(ptr + off, true)" + ) + } + core::AugmentedOp::I32Load8S => { + format!("(ptr, off) => new DataView({mem}.buffer).getInt8(ptr + off, true)") + } + core::AugmentedOp::I32Load16U => { + format!( + "(ptr, off) => new DataView({mem}.buffer).getUint16(ptr + off, true)" + ) + } + core::AugmentedOp::I32Load16S => { + format!( + "(ptr, off) => new DataView({mem}.buffer).getInt16(ptr + off, true)" + ) + } + core::AugmentedOp::I64Load => { + format!( + "(ptr, off) => new DataView({mem}.buffer).getBigInt64(ptr + off, true)" + ) + } + core::AugmentedOp::F32Load => { + format!( + "(ptr, off) => new DataView({mem}.buffer).getFloat32(ptr + off, true)" + ) + } + core::AugmentedOp::F64Load => { + format!( + "(ptr, off) => new DataView({mem}.buffer).getFloat64(ptr + off, true)" + ) + } + core::AugmentedOp::I32Store8 => { + format!( + "(ptr, val, offset) => {{ + new DataView({mem}.buffer).setInt8(ptr + offset, val, true); + }}" + ) + } + core::AugmentedOp::I32Store16 => { + format!( + "(ptr, val, offset) => {{ + new DataView({mem}.buffer).setInt16(ptr + offset, val, true); + }}" + ) + } + core::AugmentedOp::I32Store => { + format!( + "(ptr, val, offset) => {{ + new DataView({mem}.buffer).setInt32(ptr + offset, val, true); + }}" + ) + } + core::AugmentedOp::I64Store => { + format!( + "(ptr, val, offset) => {{ + new DataView({mem}.buffer).setBigInt64(ptr + offset, val, true); + }}" + ) + } + core::AugmentedOp::F32Store => { + format!( + "(ptr, val, offset) => {{ + new DataView({mem}.buffer).setFloat32(ptr + offset, val, true); + }}" + ) + } + core::AugmentedOp::F64Store => { + format!( + "(ptr, val, offset) => {{ + new DataView({mem}.buffer).setFloat64(ptr + offset, val, true); + }}" + ) + } + core::AugmentedOp::MemorySize => { + format!("ptr => {mem}.buffer.byteLength / 65536") + } + } + } + } + } + fn core_def(&self, def: &CoreDef) -> String { match def { CoreDef::Export(e) => self.core_export(e), CoreDef::Trampoline(i) => format!("trampoline{}", i.as_u32()), CoreDef::InstanceFlags(i) => { - format!("instance_flags{}", i.as_u32()) + format!("instanceFlags{}", i.as_u32()) } } } @@ -633,10 +728,10 @@ impl<'a> Instantiator<'a, '_> { { let name = match &export.item { ExportItem::Index(idx) => { - let module = &self.modules[self.instances[export.instance]].module; + let module = &self.modules[self.instances[export.instance]]; let idx = (*idx).into(); module - .exports + .exports() .iter() .filter_map(|(name, i)| if *i == idx { Some(name) } else { None }) .next() diff --git a/test/codegen.js b/test/codegen.js index a5106e245..319324e0f 100644 --- a/test/codegen.js +++ b/test/codegen.js @@ -25,7 +25,7 @@ export async function codegenTest (fixtures) { suite(`Transpiler codegen`, () => { for (const fixture of fixtures) { - const name = fixture.replace('.component.wasm', ''); + const name = fixture.replace('.component.wasm', '').replace('.wat', ''); test(`${fixture} transpile`, async () => { const flags = await readFlags(`test/runtime/${name}.ts`); var { stderr } = await exec(jcoPath, 'transpile', `test/fixtures/components/${fixture}`, '--name', name, ...flags, '-o', `test/output/${name}`); diff --git a/test/fixtures/components/list-adapter-fusion.wat b/test/fixtures/components/list-adapter-fusion.wat new file mode 100644 index 000000000..557911748 --- /dev/null +++ b/test/fixtures/components/list-adapter-fusion.wat @@ -0,0 +1,156 @@ +(component + (type $test (instance + (export "list-u8" (func (param "x" (list u8)) (result (list u8)))) + (export "list-s8" (func (param "x" (list s8)) (result (list s8)))) + (export "list-u16" (func (param "x" (list u16)) (result (list u16)))) + (export "list-s16" (func (param "x" (list s16)) (result (list s16)))) + (export "list-u32" (func (param "x" (list u32)) (result (list u32)))) + (export "list-s32" (func (param "x" (list s32)) (result (list s32)))) + (export "list-u64" (func (param "x" (list u64)) (result (list u64)))) + (export "list-s64" (func (param "x" (list s64)) (result (list s64)))) + (export "list-float32" (func (param "x" (list float32)) (result (list float32)))) + (export "list-float64" (func (param "x" (list float64)) (result (list float64)))) + )) + (import "test" (instance $test (type $test))) + + (component $C + (import "test" (instance $test (type $test))) + + (core module $libc + (memory (export "memory") 1) + (global $next (mut i32) i32.const 128) + (func $realloc (export "realloc") (param i32 i32 i32 i32) (result i32) + (local $ret i32) + (local $next i32) + (local $page i32) + + ;; assert no realloc is actually happening and this is only an + ;; allocation function + (if (local.get 0) (unreachable)) + (if (local.get 1) (unreachable)) + + (local.set $ret (global.get $next)) + + (local.set $next (i32.add (global.get $next) (local.get 3))) + (local.set $next (i32.add (local.get $next) (i32.const 15))) + (local.set $next (i32.and (local.get $next) (i32.const 0xfffffff0))) + (global.set $next (local.get $next)) + + (local.set $page (i32.div_u (local.get $next) (i32.const 65536))) + + (loop $grow + (i32.ge_u (local.get $page) (memory.size)) + if + i32.const 1 + memory.grow + drop + br $grow + end + ) + + (local.get $next) + ) + ) + (core instance $libc (instantiate $libc)) + (alias core export $libc "memory" (core memory $mem)) + (alias core export $libc "realloc" (core func $realloc)) + + (core func $list-u8 + (canon lower (func $test "list-u8") (memory $mem) (realloc (func $realloc)))) + (core func $list-s8 + (canon lower (func $test "list-s8") (memory $mem) (realloc (func $realloc)))) + (core func $list-u16 + (canon lower (func $test "list-u16") (memory $mem) (realloc (func $realloc)))) + (core func $list-s16 + (canon lower (func $test "list-s16") (memory $mem) (realloc (func $realloc)))) + (core func $list-u32 + (canon lower (func $test "list-u32") (memory $mem) (realloc (func $realloc)))) + (core func $list-s32 + (canon lower (func $test "list-s32") (memory $mem) (realloc (func $realloc)))) + (core func $list-u64 + (canon lower (func $test "list-u64") (memory $mem) (realloc (func $realloc)))) + (core func $list-s64 + (canon lower (func $test "list-s64") (memory $mem) (realloc (func $realloc)))) + (core func $list-float32 + (canon lower (func $test "list-float32") (memory $mem) (realloc (func $realloc)))) + (core func $list-float64 + (canon lower (func $test "list-float64") (memory $mem) (realloc (func $realloc)))) + + (core module $m + (import "" "list-u8" (func $list-u8 (param i32 i32 i32))) + (import "" "list-s8" (func $list-s8 (param i32 i32 i32))) + (import "" "list-u16" (func $list-u16 (param i32 i32 i32))) + (import "" "list-s16" (func $list-s16 (param i32 i32 i32))) + (import "" "list-u32" (func $list-u32 (param i32 i32 i32))) + (import "" "list-s32" (func $list-s32 (param i32 i32 i32))) + (import "" "list-u64" (func $list-u64 (param i32 i32 i32))) + (import "" "list-s64" (func $list-s64 (param i32 i32 i32))) + (import "" "list-float32" (func $list-float32 (param i32 i32 i32))) + (import "" "list-float64" (func $list-float64 (param i32 i32 i32))) + (import "libc" "memory" (memory 1)) + + (func (export "list-u8") (param i32 i32) (result i32) + (call $list-u8 (local.get 0) (local.get 1) (i32.const 8)) (i32.const 8)) + (func (export "list-s8") (param i32 i32) (result i32) + (call $list-s8 (local.get 0) (local.get 1) (i32.const 8)) (i32.const 8)) + (func (export "list-u16") (param i32 i32) (result i32) + (call $list-u16 (local.get 0) (local.get 1) (i32.const 8)) (i32.const 8)) + (func (export "list-s16") (param i32 i32) (result i32) + (call $list-s16 (local.get 0) (local.get 1) (i32.const 8)) (i32.const 8)) + (func (export "list-u32") (param i32 i32) (result i32) + (call $list-u32 (local.get 0) (local.get 1) (i32.const 8)) (i32.const 8)) + (func (export "list-s32") (param i32 i32) (result i32) + (call $list-s32 (local.get 0) (local.get 1) (i32.const 8)) (i32.const 8)) + (func (export "list-u64") (param i32 i32) (result i32) + (call $list-u64 (local.get 0) (local.get 1) (i32.const 8)) (i32.const 8)) + (func (export "list-s64") (param i32 i32) (result i32) + (call $list-s64 (local.get 0) (local.get 1) (i32.const 8)) (i32.const 8)) + (func (export "list-float32") (param i32 i32) (result i32) + (call $list-float32 (local.get 0) (local.get 1) (i32.const 8)) (i32.const 8)) + (func (export "list-float64") (param i32 i32) (result i32) + (call $list-float64 (local.get 0) (local.get 1) (i32.const 8)) (i32.const 8)) + ) + + (core instance $i (instantiate $m + (with "libc" (instance $libc)) + (with "" (instance + (export "list-u8" (func $list-u8)) + (export "list-s8" (func $list-s8)) + (export "list-u16" (func $list-u16)) + (export "list-s16" (func $list-s16)) + (export "list-u32" (func $list-u32)) + (export "list-s32" (func $list-s32)) + (export "list-u64" (func $list-u64)) + (export "list-s64" (func $list-s64)) + (export "list-float32" (func $list-float32)) + (export "list-float64" (func $list-float64)) + )) + )) + + (func (export "list-u8") (param "x" (list u8)) (result (list u8)) + (canon lift (core func $i "list-u8") (memory $mem) (realloc (func $realloc)))) + (func (export "list-s8") (param "x" (list s8)) (result (list s8)) + (canon lift (core func $i "list-s8") (memory $mem) (realloc (func $realloc)))) + (func (export "list-u16") (param "x" (list u16)) (result (list u16)) + (canon lift (core func $i "list-u16") (memory $mem) (realloc (func $realloc)))) + (func (export "list-s16") (param "x" (list s16)) (result (list s16)) + (canon lift (core func $i "list-s16") (memory $mem) (realloc (func $realloc)))) + (func (export "list-u32") (param "x" (list u32)) (result (list u32)) + (canon lift (core func $i "list-u32") (memory $mem) (realloc (func $realloc)))) + (func (export "list-s32") (param "x" (list s32)) (result (list s32)) + (canon lift (core func $i "list-s32") (memory $mem) (realloc (func $realloc)))) + (func (export "list-u64") (param "x" (list u64)) (result (list u64)) + (canon lift (core func $i "list-u64") (memory $mem) (realloc (func $realloc)))) + (func (export "list-s64") (param "x" (list s64)) (result (list s64)) + (canon lift (core func $i "list-s64") (memory $mem) (realloc (func $realloc)))) + (func (export "list-float32") (param "x" (list float32)) (result (list float32)) + (canon lift (core func $i "list-float32") (memory $mem) (realloc (func $realloc)))) + (func (export "list-float64") (param "x" (list float64)) (result (list float64)) + (canon lift (core func $i "list-float64") (memory $mem) (realloc (func $realloc)))) + ) + + (instance $i1 (instantiate $C (with "test" (instance $test)))) + (instance $i2 (instantiate $C (with "test" (instance $i1)))) + + (export "result" (instance $i2)) +) diff --git a/test/runtime.js b/test/runtime.js index c54518301..78420ffbe 100644 --- a/test/runtime.js +++ b/test/runtime.js @@ -4,10 +4,10 @@ import { exec } from './helpers.js'; export async function runtimeTest (fixtures) { suite('Runtime', () => { - + for (const fixture of fixtures) { if (fixture.startsWith('dummy_')) continue; - const runtimeJs = fixture.replace('.component.wasm', '.js'); + const runtimeJs = fixture.replace('.component.wasm', '.js').replace('.wat', '.js'); if (!existsSync(`test/output/${runtimeJs}`)) continue; test(runtimeJs, async () => { diff --git a/test/runtime/list-adapter-fusion.ts b/test/runtime/list-adapter-fusion.ts new file mode 100644 index 000000000..52f2c70bc --- /dev/null +++ b/test/runtime/list-adapter-fusion.ts @@ -0,0 +1,108 @@ +// Flags: --tla-compat --map test=../list-adapter-fusion.js + +import * as assert from 'assert'; + +let expected: any = null; + +export function listU8(f: Uint8Array) { + assert.deepStrictEqual(expected, f); + return f; +} + +export function listS8(f: Int8Array) { + assert.deepStrictEqual(expected, f); + return f; +} + +export function listU16(f: Uint16Array) { + assert.deepStrictEqual(expected, f); + return f; +} + +export function listS16(f: Int16Array) { + assert.deepStrictEqual(expected, f); + return f; +} + +export function listU32(f: Uint32Array) { + assert.deepStrictEqual(expected, f); + return f; +} + +export function listS32(f: Int32Array) { + assert.deepStrictEqual(expected, f); + return f; +} + +export function listU64(f: BigUint64Array) { + assert.deepStrictEqual(expected, f); + return f; +} + +export function listS64(f: BigInt64Array) { + assert.deepStrictEqual(expected, f); + return f; +} + +export function listFloat32(f: Float32Array) { + assert.deepStrictEqual(expected, f); + return f; +} + +export function listFloat64(f: Float64Array) { + assert.deepStrictEqual(expected, f); + return f; +} + +async function run() { + const wasm = await import('../output/list-adapter-fusion/list-adapter-fusion.js'); + await wasm.$init; + + function test(f: (arg0: T) => void, arg: T) { + expected = arg; + const ret = f(arg); + expected = null; + assert.deepStrictEqual(arg, ret); + } + + test(wasm.result.listU8, new Uint8Array([])); + test(wasm.result.listU8, new Uint8Array([1])); + test(wasm.result.listU8, new Uint8Array([1, 2, 3, -1, -2, 0])); + + test(wasm.result.listS8, new Int8Array([])); + test(wasm.result.listS8, new Int8Array([1])); + test(wasm.result.listS8, new Int8Array([1, 2, 3, -1, -2, 0])); + + test(wasm.result.listU16, new Uint16Array([])); + test(wasm.result.listU16, new Uint16Array([1])); + test(wasm.result.listU16, new Uint16Array([1, 2, 3, -1, -2, 0])); + + test(wasm.result.listS16, new Int16Array([])); + test(wasm.result.listS16, new Int16Array([1])); + test(wasm.result.listS16, new Int16Array([1, 2, 3, -1, -2, 0])); + + test(wasm.result.listU32, new Uint32Array([])); + test(wasm.result.listU32, new Uint32Array([1])); + test(wasm.result.listU32, new Uint32Array([1, 2, 3, -1, -2, 0])); + + test(wasm.result.listS32, new Int32Array([])); + test(wasm.result.listS32, new Int32Array([1])); + test(wasm.result.listS32, new Int32Array([1, 2, 3, -1, -2, 0])); + + test(wasm.result.listU64, new BigUint64Array([])); + test(wasm.result.listU64, new BigUint64Array([1n])); + test(wasm.result.listU64, new BigUint64Array([1n, 2n, 3n, -1n, -2n, 0n])); + + test(wasm.result.listS64, new BigInt64Array([])); + test(wasm.result.listS64, new BigInt64Array([1n])); + test(wasm.result.listS64, new BigInt64Array([1n, 2n, 3n, -1n, -2n, 0n])); + + test(wasm.result.listFloat32, new Float32Array([])); + test(wasm.result.listFloat32, new Float32Array([1, 1.2, 0.3])); + + test(wasm.result.listFloat64, new Float64Array([])); + test(wasm.result.listFloat64, new Float64Array([1, 1.2, 0.3])); +} + +// Async cycle handling +setTimeout(run);