From 79463500a7ade3df7dc99454f5797beaf1d7eef9 Mon Sep 17 00:00:00 2001 From: Henry Gressmann Date: Thu, 25 Jan 2024 13:49:52 +0100 Subject: chore: refactor executor and improve documentation Signed-off-by: Henry Gressmann --- crates/parser/src/error.rs | 1 + crates/tinywasm/src/error.rs | 26 +- crates/tinywasm/src/func.rs | 4 +- crates/tinywasm/src/imports.rs | 109 ++-- crates/tinywasm/src/instance.rs | 8 +- crates/tinywasm/src/lib.rs | 40 +- crates/tinywasm/src/runtime/executor/macros.rs | 245 --------- crates/tinywasm/src/runtime/executor/mod.rs | 637 ---------------------- crates/tinywasm/src/runtime/executor/traits.rs | 132 ----- crates/tinywasm/src/runtime/interpreter/macros.rs | 245 +++++++++ crates/tinywasm/src/runtime/interpreter/mod.rs | 634 +++++++++++++++++++++ crates/tinywasm/src/runtime/interpreter/traits.rs | 132 +++++ crates/tinywasm/src/runtime/mod.rs | 20 +- crates/tinywasm/src/runtime/stack/call_stack.rs | 7 +- crates/tinywasm/src/store.rs | 10 +- crates/tinywasm/tests/generated/mvp.csv | 1 + 16 files changed, 1143 insertions(+), 1108 deletions(-) delete mode 100644 crates/tinywasm/src/runtime/executor/macros.rs delete mode 100644 crates/tinywasm/src/runtime/executor/mod.rs delete mode 100644 crates/tinywasm/src/runtime/executor/traits.rs create mode 100644 crates/tinywasm/src/runtime/interpreter/macros.rs create mode 100644 crates/tinywasm/src/runtime/interpreter/mod.rs create mode 100644 crates/tinywasm/src/runtime/interpreter/traits.rs (limited to 'crates') diff --git a/crates/parser/src/error.rs b/crates/parser/src/error.rs index 714535a..35bad28 100644 --- a/crates/parser/src/error.rs +++ b/crates/parser/src/error.rs @@ -4,6 +4,7 @@ use alloc::string::{String, ToString}; use wasmparser::Encoding; #[derive(Debug)] +/// Errors that can occur when parsing a WebAssembly module pub enum ParseError { InvalidType, UnsupportedSection(String), diff --git a/crates/tinywasm/src/error.rs b/crates/tinywasm/src/error.rs index f9b41be..0344e82 100644 --- a/crates/tinywasm/src/error.rs +++ b/crates/tinywasm/src/error.rs @@ -3,24 +3,18 @@ use core::fmt::Display; use tinywasm_types::FuncType; #[cfg(feature = "parser")] -use tinywasm_parser::ParseError; +pub use tinywasm_parser::ParseError; -/// A tinywasm error +/// Errors that can occur for TinyWasm operations #[derive(Debug)] pub enum Error { - #[cfg(feature = "parser")] - /// A parsing error occurred - ParseError(ParseError), - #[cfg(feature = "std")] /// An I/O error occurred Io(crate::std::io::Error), - /// A WebAssembly feature is not supported - UnsupportedFeature(String), - - /// An unknown error occurred - Other(String), + #[cfg(feature = "parser")] + /// A parsing error occurred + ParseError(ParseError), /// A WebAssembly trap occurred Trap(Trap), @@ -28,6 +22,12 @@ pub enum Error { /// A linking error occurred Linker(LinkingError), + /// A WebAssembly feature is not supported + UnsupportedFeature(String), + + /// An unknown error occurred + Other(String), + /// A function did not return a value FuncDidNotReturn, @@ -48,7 +48,7 @@ pub enum Error { } #[derive(Debug)] -/// A linking error +/// Errors that can occur when linking a WebAssembly module pub enum LinkingError { /// An unknown import was encountered UnknownImport { @@ -210,5 +210,5 @@ impl From for Error { } } -/// A specialized [`Result`] type for tinywasm operations +/// A wrapper around [`core::result::Result`] for tinywasm operations pub type Result = crate::std::result::Result; diff --git a/crates/tinywasm/src/func.rs b/crates/tinywasm/src/func.rs index 19d1325..7a29e18 100644 --- a/crates/tinywasm/src/func.rs +++ b/crates/tinywasm/src/func.rs @@ -89,7 +89,7 @@ impl FuncHandle { #[derive(Debug)] /// A typed function handle -pub struct TypedFuncHandle { +pub struct FuncHandleTyped { /// The underlying function handle pub func: FuncHandle, pub(crate) marker: core::marker::PhantomData<(P, R)>, @@ -105,7 +105,7 @@ pub trait FromWasmValueTuple { Self: Sized; } -impl TypedFuncHandle { +impl FuncHandleTyped { /// Call a typed function pub fn call(&self, store: &mut Store, params: P) -> Result { // Convert params into Vec diff --git a/crates/tinywasm/src/imports.rs b/crates/tinywasm/src/imports.rs index 804aa12..0720d02 100644 --- a/crates/tinywasm/src/imports.rs +++ b/crates/tinywasm/src/imports.rs @@ -90,56 +90,45 @@ impl Debug for HostFunction { /// An external value pub enum Extern { /// A global value - Global(ExternGlobal), + Global { + /// The type of the global value. + ty: GlobalType, + /// The actual value of the global, encapsulated in `WasmValue`. + val: WasmValue, + }, /// A table - Table(ExternTable), + Table { + /// Defines the type of the table, including its element type and limits. + ty: TableType, + /// The initial value of the table. + init: WasmValue, + }, /// A memory - Memory(ExternMemory), + Memory { + /// Defines the type of the memory, including its limits and the type of its pages. + ty: MemoryType, + }, /// A function Function(Function), } -/// A function -#[derive(Debug, Clone)] -pub struct ExternFunc(pub(crate) HostFunction); - -/// A global value -#[derive(Debug, Clone)] -pub struct ExternGlobal { - pub(crate) ty: GlobalType, - pub(crate) val: WasmValue, -} - -/// A table -#[derive(Debug, Clone)] -pub struct ExternTable { - pub(crate) ty: TableType, - pub(crate) val: WasmValue, -} - -/// A memory -#[derive(Debug, Clone)] -pub struct ExternMemory { - pub(crate) ty: MemoryType, -} - impl Extern { /// Create a new global import pub fn global(val: WasmValue, mutable: bool) -> Self { - Self::Global(ExternGlobal { ty: GlobalType { ty: val.val_type(), mutable }, val }) + Self::Global { ty: GlobalType { ty: val.val_type(), mutable }, val } } /// Create a new table import - pub fn table(ty: TableType, val: WasmValue) -> Self { - Self::Table(ExternTable { ty, val }) + pub fn table(ty: TableType, init: WasmValue) -> Self { + Self::Table { ty, init } } /// Create a new memory import pub fn memory(ty: MemoryType) -> Self { - Self::Memory(ExternMemory { ty }) + Self::Memory { ty } } /// Create a new function import @@ -174,10 +163,10 @@ impl Extern { pub(crate) fn kind(&self) -> ExternalKind { match self { - Self::Global(_) => ExternalKind::Global, - Self::Table(_) => ExternalKind::Table, - Self::Memory(_) => ExternalKind::Memory, - Self::Function(_) => ExternalKind::Func, + Self::Global { .. } => ExternalKind::Global, + Self::Table { .. } => ExternalKind::Table, + Self::Memory { .. } => ExternalKind::Memory, + Self::Function { .. } => ExternalKind::Func, } } } @@ -197,6 +186,38 @@ impl From<&Import> for ExternName { #[derive(Debug, Default)] /// Imports for a module instance +/// +/// This is used to link a module instance to its imports +/// +/// ## Example +/// ```rust +/// # fn main() -> tinywasm::Result<()> { +/// use tinywasm::{Imports, Extern}; +/// use tinywasm::types::{ValType, TableType, MemoryType, WasmValue}; +/// let mut imports = Imports::new(); +/// +/// // function args can be either a single +/// // value that implements `TryFrom` or a tuple of them +/// let print_i32 = Extern::typed_func(|_ctx: tinywasm::FuncContext<'_>, arg: i32| { +/// log::debug!("print_i32: {}", arg); +/// Ok(()) +/// }); +/// +/// let table_type = TableType::new(ValType::RefFunc, 10, Some(20)); +/// let table_init = WasmValue::default_for(ValType::RefFunc); +/// +/// imports +/// .define("my_module", "print_i32", print_i32)? +/// .define("my_module", "table", Extern::table(table_type, table_init))? +/// .define("my_module", "memory", Extern::memory(MemoryType::new_32(1, Some(2))))? +/// .define("my_module", "global_i32", Extern::global(WasmValue::I32(666), false))? +/// .link_module("my_other_module", 0)?; +/// # Ok(()) +/// # } +/// ``` +/// +/// Note that module instance addresses for [`Imports::link_module`] can be obtained from [`crate::ModuleInstance::id`]. +/// Now, the imports object can be passed to [`crate::ModuleInstance::instantiate`]. pub struct Imports { values: BTreeMap, modules: BTreeMap, @@ -334,17 +355,17 @@ impl Imports { match val { // A link to something that needs to be added to the store ResolvedExtern::Extern(ex) => match (ex, &import.kind) { - (Extern::Global(extern_global), ImportKind::Global(ty)) => { - Self::compare_types(import, &extern_global.ty, ty)?; - imports.globals.push(store.add_global(extern_global.ty, extern_global.val.into(), idx)?); + (Extern::Global { ty, val }, ImportKind::Global(import_ty)) => { + Self::compare_types(import, &ty, import_ty)?; + imports.globals.push(store.add_global(ty, val.into(), idx)?); } - (Extern::Table(extern_table), ImportKind::Table(ty)) => { - Self::compare_table_types(import, &extern_table.ty, ty)?; - imports.tables.push(store.add_table(extern_table.ty, idx)?); + (Extern::Table { ty, .. }, ImportKind::Table(import_ty)) => { + Self::compare_table_types(import, &ty, import_ty)?; + imports.tables.push(store.add_table(ty, idx)?); } - (Extern::Memory(extern_memory), ImportKind::Memory(ty)) => { - Self::compare_memory_types(import, &extern_memory.ty, ty, None)?; - imports.memories.push(store.add_mem(extern_memory.ty, idx)?); + (Extern::Memory { ty }, ImportKind::Memory(import_ty)) => { + Self::compare_memory_types(import, &ty, import_ty, None)?; + imports.memories.push(store.add_mem(ty, idx)?); } (Extern::Function(extern_func), ImportKind::Function(ty)) => { let import_func_type = module diff --git a/crates/tinywasm/src/instance.rs b/crates/tinywasm/src/instance.rs index d1fbd6f..75d1f87 100644 --- a/crates/tinywasm/src/instance.rs +++ b/crates/tinywasm/src/instance.rs @@ -6,10 +6,10 @@ use tinywasm_types::{ use crate::{ func::{FromWasmValueTuple, IntoWasmValueTuple}, - Error, FuncHandle, Imports, Module, Result, Store, TypedFuncHandle, + Error, FuncHandle, FuncHandleTyped, Imports, Module, Result, Store, }; -/// A WebAssembly Module Instance +/// An instanciated WebAssembly module /// /// Backed by an Arc, so cloning is cheap /// @@ -178,13 +178,13 @@ impl ModuleInstance { } /// Get a typed exported function by name - pub fn typed_func(&self, store: &Store, name: &str) -> Result> + pub fn typed_func(&self, store: &Store, name: &str) -> Result> where P: IntoWasmValueTuple, R: FromWasmValueTuple, { let func = self.exported_func_by_name(store, name)?; - Ok(TypedFuncHandle { func, marker: core::marker::PhantomData }) + Ok(FuncHandleTyped { func, marker: core::marker::PhantomData }) } /// Get the start function of the module diff --git a/crates/tinywasm/src/lib.rs b/crates/tinywasm/src/lib.rs index 64d34df..1b63b99 100644 --- a/crates/tinywasm/src/lib.rs +++ b/crates/tinywasm/src/lib.rs @@ -14,12 +14,23 @@ //! to be useful for embedded systems and other environments where a full-featured //! runtime is not required. //! -//! ## Getting Started +//! ## Features +//! - `std` (default): Enables the use of `std` and `std::io` for parsing from files and streams. +//! - `logging` (default): Enables logging via the `log` crate. +//! - `parser` (default): Enables the `tinywasm_parser` crate for parsing WebAssembly modules. +//! +//! ## No-std support +//! TinyWasm supports `no_std` environments by disabling the `std` feature and registering +//! a custom allocator. This removes support for parsing from files and streams, +//! but otherwise the API is the same. +//! Additionally, to have proper error types, you currently need a `nightly` compiler to have the error trait in core. //! +//! ## Getting Started //! The easiest way to get started is to use the [`Module::parse_bytes`] function to load a //! WebAssembly module from bytes. This will parse the module and validate it, returning //! a [`Module`] that can be used to instantiate the module. //! +//! //! ```rust //! use tinywasm::{Store, Module}; //! @@ -39,25 +50,21 @@ //! //! // Get a typed handle to the exported "add" function //! // Alternatively, you can use `instance.get_func` to get an untyped handle -//! // that takes and returns WasmValue types -//! let func = instance.typed_func::<(i32, i32), (i32,)>(&mut store, "add")?; +//! // that takes and returns [`WasmValue`]s +//! let func = instance.typed_func::<(i32, i32), i32>(&mut store, "add")?; //! let res = func.call(&mut store, (1, 2))?; //! -//! assert_eq!(res, (3,)); +//! assert_eq!(res, 3); //! # Ok::<(), tinywasm::Error>(()) //! ``` //! -//! ## Features -//! - `std` (default): Enables the use of `std` and `std::io` for parsing from files and streams. -//! - `logging` (default): Enables logging via the `log` crate. -//! - `parser` (default): Enables the `tinywasm_parser` crate for parsing WebAssembly modules. +//! ## Custom Imports //! -//! ## No-std support -//! TinyWasm supports `no_std` environments by disabling the `std` feature and registering -//! a custom allocator. This removes support for parsing from files and streams, -//! but otherwise the API is the same. +//! To provide custom imports to a module, you can use the [`Imports`] struct. +//! This struct allows you to register custom functions, globals, memories, tables, +//! and other modules to be linked into the module when it is instantiated. //! -//! Additionally, if you want proper error types, you must use a `nightly` compiler to have the error trait in core. +//! See the [`Imports`] documentation for more information. mod std; extern crate alloc; @@ -87,13 +94,14 @@ mod instance; pub use instance::ModuleInstance; mod func; -pub use func::{FuncHandle, TypedFuncHandle}; +pub use func::{FuncHandle, FuncHandleTyped}; mod imports; pub use imports::*; -mod runtime; -pub use runtime::*; +/// Runtime for executing WebAssembly modules. +pub mod runtime; +pub use runtime::InterpreterRuntime; #[cfg(feature = "parser")] /// Re-export of [`tinywasm_parser`]. Requires `parser` feature. diff --git a/crates/tinywasm/src/runtime/executor/macros.rs b/crates/tinywasm/src/runtime/executor/macros.rs deleted file mode 100644 index 909acb3..0000000 --- a/crates/tinywasm/src/runtime/executor/macros.rs +++ /dev/null @@ -1,245 +0,0 @@ -//! More generic macros for various instructions -//! -//! These macros are used to generate the actual instruction implementations. -//! In some basic tests this generated better assembly than using generic functions, even when inlined. -//! (Something to revisit in the future) - -/// Load a value from memory -macro_rules! mem_load { - ($type:ty, $arg:ident, $stack:ident, $store:ident, $module:ident) => {{ - mem_load!($type, $type, $arg, $stack, $store, $module) - }}; - - ($load_type:ty, $target_type:ty, $arg:ident, $stack:ident, $store:ident, $module:ident) => {{ - // TODO: there could be a lot of performance improvements here - let mem_idx = $module.resolve_mem_addr($arg.mem_addr); - let mem = $store.get_mem(mem_idx as usize)?; - - let addr = $stack.values.pop()?.raw_value(); - - let addr = $arg.offset.checked_add(addr).ok_or_else(|| { - Error::Trap(crate::Trap::MemoryOutOfBounds { - offset: $arg.offset as usize, - len: core::mem::size_of::<$load_type>(), - max: mem.borrow().max_pages(), - }) - })?; - - let addr: usize = addr.try_into().ok().ok_or_else(|| { - Error::Trap(crate::Trap::MemoryOutOfBounds { - offset: $arg.offset as usize, - len: core::mem::size_of::<$load_type>(), - max: mem.borrow().max_pages(), - }) - })?; - - let val: [u8; core::mem::size_of::<$load_type>()] = { - let mem = mem.borrow_mut(); - let val = mem.load(addr, $arg.align as usize, core::mem::size_of::<$load_type>())?; - val.try_into().expect("slice with incorrect length") - }; - - let loaded_value = <$load_type>::from_le_bytes(val); - $stack.values.push((loaded_value as $target_type).into()); - }}; -} - -/// Store a value to memory -macro_rules! mem_store { - ($type:ty, $arg:ident, $stack:ident, $store:ident, $module:ident) => {{ - log::debug!("mem_store!({}, {:?})", stringify!($type), $arg); - - mem_store!($type, $type, $arg, $stack, $store, $module) - }}; - - ($store_type:ty, $target_type:ty, $arg:ident, $stack:ident, $store:ident, $module:ident) => {{ - // likewise, there could be a lot of performance improvements here - let mem_idx = $module.resolve_mem_addr($arg.mem_addr); - let mem = $store.get_mem(mem_idx as usize)?; - - let val = $stack.values.pop_t::<$store_type>()?; - let addr = $stack.values.pop()?.raw_value(); - - let val = val as $store_type; - let val = val.to_le_bytes(); - - mem.borrow_mut().store(($arg.offset + addr) as usize, $arg.align as usize, &val)?; - }}; -} - -/// Doing the actual conversion from float to int is a bit tricky, because -/// we need to check for overflow. This macro generates the min/max values -/// for a specific conversion, which are then used in the actual conversion. -/// Rust sadly doesn't have wrapping casts for floats yet, maybe never. -/// Alternatively, https://crates.io/crates/az could be used for this but -/// it's not worth the dependency. -macro_rules! float_min_max { - (f32, i32) => { - (-2147483904.0_f32, 2147483648.0_f32) - }; - (f64, i32) => { - (-2147483649.0_f64, 2147483648.0_f64) - }; - (f32, u32) => { - (-1.0_f32, 4294967296.0_f32) // 2^32 - }; - (f64, u32) => { - (-1.0_f64, 4294967296.0_f64) // 2^32 - }; - (f32, i64) => { - (-9223373136366403584.0_f32, 9223372036854775808.0_f32) // 2^63 + 2^40 | 2^63 - }; - (f64, i64) => { - (-9223372036854777856.0_f64, 9223372036854775808.0_f64) // 2^63 + 2^40 | 2^63 - }; - (f32, u64) => { - (-1.0_f32, 18446744073709551616.0_f32) // 2^64 - }; - (f64, u64) => { - (-1.0_f64, 18446744073709551616.0_f64) // 2^64 - }; - // other conversions are not allowed - ($from:ty, $to:ty) => { - compile_error!("invalid float conversion"); - }; -} - -/// Convert a value on the stack -macro_rules! conv { - ($from:ty, $intermediate:ty, $to:ty, $stack:ident) => {{ - let a: $from = $stack.values.pop()?.into(); - $stack.values.push((a as $intermediate as $to).into()); - }}; - ($from:ty, $to:ty, $stack:ident) => {{ - let a: $from = $stack.values.pop()?.into(); - $stack.values.push((a as $to).into()); - }}; -} - -/// Convert a value on the stack with error checking -macro_rules! checked_conv_float { - // Direct conversion with error checking (two types) - ($from:tt, $to:tt, $stack:ident) => {{ - checked_conv_float!($from, $to, $to, $stack) - }}; - // Conversion with an intermediate unsigned type and error checking (three types) - ($from:tt, $intermediate:tt, $to:tt, $stack:ident) => {{ - let (min, max) = float_min_max!($from, $intermediate); - let a: $from = $stack.values.pop()?.into(); - - if a.is_nan() { - return Err(Error::Trap(crate::Trap::InvalidConversionToInt)); - } - - if a <= min || a >= max { - return Err(Error::Trap(crate::Trap::IntegerOverflow)); - } - - $stack.values.push((a as $intermediate as $to).into()); - }}; -} - -/// Compare two values on the stack -macro_rules! comp { - ($op:tt, $ty:ty, $stack:ident) => {{ - comp!($op, $ty, $ty, $stack) - }}; - - ($op:tt, $intermediate:ty, $to:ty, $stack:ident) => {{ - let [a, b] = $stack.values.pop_n_const::<2>()?; - let a: $intermediate = a.into(); - let b: $intermediate = b.into(); - - // Cast to unsigned type before comparison - let a = a as $to; - let b = b as $to; - $stack.values.push(((a $op b) as i32).into()); - }}; -} - -/// Compare a value on the stack to zero -macro_rules! comp_zero { - ($op:tt, $ty:ty, $stack:ident) => {{ - let a: $ty = $stack.values.pop()?.into(); - $stack.values.push(((a $op 0) as i32).into()); - }}; -} - -/// Apply an arithmetic method to two values on the stack -macro_rules! arithmetic { - ($op:ident, $ty:ty, $stack:ident) => {{ - arithmetic!($op, $ty, $ty, $stack) - }}; - - // also allow operators such as +, - - ($op:tt, $ty:ty, $stack:ident) => {{ - let [a, b] = $stack.values.pop_n_const::<2>()?; - let a: $ty = a.into(); - let b: $ty = b.into(); - $stack.values.push((a $op b).into()); - }}; - - ($op:ident, $intermediate:ty, $to:ty, $stack:ident) => {{ - let [a, b] = $stack.values.pop_n_const::<2>()?; - let a: $to = a.into(); - let b: $to = b.into(); - - let a = a as $intermediate; - let b = b as $intermediate; - - let result = a.$op(b); - $stack.values.push((result as $to).into()); - }}; -} - -/// Apply an arithmetic method to a single value on the stack -macro_rules! arithmetic_single { - ($op:ident, $ty:ty, $stack:ident) => {{ - let a: $ty = $stack.values.pop()?.into(); - let result = a.$op(); - $stack.values.push((result as $ty).into()); - }}; - - ($op:ident, $from:ty, $to:ty, $stack:ident) => {{ - let a: $from = $stack.values.pop()?.into(); - let result = a.$op(); - $stack.values.push((result as $to).into()); - }}; -} - -/// Apply an arithmetic operation to two values on the stack with error checking -macro_rules! checked_int_arithmetic { - // Direct conversion with error checking (two types) - ($from:tt, $to:tt, $stack:ident) => {{ - checked_int_arithmetic!($from, $to, $to, $stack) - }}; - - ($op:ident, $from:ty, $to:ty, $stack:ident) => {{ - let [a, b] = $stack.values.pop_n_const::<2>()?; - let a: $from = a.into(); - let b: $from = b.into(); - - let a_casted: $to = a as $to; - let b_casted: $to = b as $to; - - if b_casted == 0 { - return Err(Error::Trap(crate::Trap::DivisionByZero)); - } - - let result = a_casted.$op(b_casted).ok_or_else(|| Error::Trap(crate::Trap::IntegerOverflow))?; - - // Cast back to original type if different - $stack.values.push((result as $from).into()); - }}; -} - -pub(super) use arithmetic; -pub(super) use arithmetic_single; -pub(super) use checked_conv_float; -pub(super) use checked_int_arithmetic; -pub(super) use comp; -pub(super) use comp_zero; -pub(super) use conv; -pub(super) use float_min_max; -pub(super) use mem_load; -pub(super) use mem_store; diff --git a/crates/tinywasm/src/runtime/executor/mod.rs b/crates/tinywasm/src/runtime/executor/mod.rs deleted file mode 100644 index d0f011f..0000000 --- a/crates/tinywasm/src/runtime/executor/mod.rs +++ /dev/null @@ -1,637 +0,0 @@ -use core::ops::{BitAnd, BitOr, BitXor, Neg}; - -use super::{DefaultRuntime, Stack}; -use crate::{ - log::debug, - runtime::{BlockType, LabelFrame}, - CallFrame, Error, FuncContext, LabelArgs, ModuleInstance, Result, Store, Trap, -}; -use alloc::{string::ToString, vec::Vec}; -use tinywasm_types::{ElementKind, Instruction, ValType}; - -mod macros; -mod traits; -use macros::*; -use traits::*; - -impl DefaultRuntime { - pub(crate) fn exec(&self, store: &mut Store, stack: &mut Stack) -> Result<()> { - // The current call frame, gets updated inside of exec_one - let mut cf = stack.call_stack.pop()?; - - let mut func_inst = cf.func_instance.clone(); - let mut wasm_func = func_inst.assert_wasm().expect("exec expected wasm function"); - - // The function to execute, gets updated from ExecResult::Call - let mut instrs = &wasm_func.instructions; - - let mut current_module = store.get_module_instance(func_inst.owner).unwrap().clone(); - - while let Some(instr) = instrs.get(cf.instr_ptr) { - match exec_one(&mut cf, instr, instrs, stack, store, ¤t_module)? { - // Continue execution at the new top of the call stack - ExecResult::Call => { - cf = stack.call_stack.pop()?; - func_inst = cf.func_instance.clone(); - wasm_func = func_inst.assert_wasm().expect("exec expected wasm function"); - instrs = &wasm_func.instructions; - - if cf.func_instance.owner != current_module.id() { - current_module.swap( - store - .get_module_instance(cf.func_instance.owner) - .unwrap_or_else(|| { - panic!( - "exec expected module instance {} to exist for function", - cf.func_instance.owner - ) - }) - .clone(), - ); - } - - continue; - } - - // return from the function - ExecResult::Return => return Ok(()), - - // continue to the next instruction and increment the instruction pointer - ExecResult::Ok => { - cf.instr_ptr += 1; - } - - // trap the program - ExecResult::Trap(trap) => { - cf.instr_ptr += 1; - // push the call frame back onto the stack so that it can be resumed - // if the trap can be handled - stack.call_stack.push(cf)?; - return Err(Error::Trap(trap)); - } - } - } - - debug!("end of exec"); - debug!("stack: {:?}", stack.values); - debug!("insts: {:?}", instrs); - debug!("instr_ptr: {}", cf.instr_ptr); - Err(Error::FuncDidNotReturn) - } -} - -enum ExecResult { - Ok, - Return, - Call, - Trap(crate::Trap), -} - -// Break to a block at the given index (relative to the current frame) -// If there is no block at the given index, return or call the parent function -// -// This is a bit hard to see from the spec, but it's vaild to use breaks to return -// from a function, so we need to check if the label stack is empty -macro_rules! break_to { - ($cf:ident, $stack:ident, $break_to_relative:ident) => {{ - if $cf.break_to(*$break_to_relative, &mut $stack.values).is_none() { - if $stack.call_stack.is_empty() { - return Ok(ExecResult::Return); - } else { - return Ok(ExecResult::Call); - } - } - }}; -} - -/// Run a single step of the interpreter -/// A seperate function is used so later, we can more easily implement -/// a step-by-step debugger (using generators once they're stable?) -#[inline] -fn exec_one( - cf: &mut CallFrame, - instr: &Instruction, - instrs: &[Instruction], - stack: &mut Stack, - store: &mut Store, - module: &ModuleInstance, -) -> Result { - debug!("ptr: {} instr: {:?}", cf.instr_ptr, instr); - - use tinywasm_types::Instruction::*; - match instr { - Nop => { /* do nothing */ } - Unreachable => return Ok(ExecResult::Trap(crate::Trap::Unreachable)), // we don't need to include the call frame here because it's already on the stack - Drop => stack.values.pop().map(|_| ())?, - - Select( - _valtype, // due to validation, we know that the type of the values on the stack are correct - ) => { - // due to validation, we know that the type of the values on the stack - let cond: i32 = stack.values.pop()?.into(); - let val2 = stack.values.pop()?; - - // if cond != 0, we already have the right value on the stack - if cond == 0 { - let _ = stack.values.pop()?; - stack.values.push(val2); - } - } - - Call(v) => { - // prepare the call frame - let func_idx = module.resolve_func_addr(*v); - let func_inst = store.get_func(func_idx as usize)?.clone(); - - let (locals, ty) = match &func_inst.func { - crate::Function::Wasm(ref f) => (f.locals.to_vec(), f.ty.clone()), - crate::Function::Host(host_func) => { - let func = host_func.func.clone(); - let params = stack.values.pop_params(&host_func.ty.params)?; - let res = (func)(FuncContext { store, module }, ¶ms)?; - stack.values.extend_from_typed(&res); - return Ok(ExecResult::Ok); - } - }; - - let params = stack.values.pop_n_rev(ty.params.len())?; - let call_frame = CallFrame::new_raw(func_inst, ¶ms, locals); - - // push the call frame - cf.instr_ptr += 1; // skip the call instruction - stack.call_stack.push(cf.clone())?; - stack.call_stack.push(call_frame)?; - - // call the function - return Ok(ExecResult::Call); - } - - CallIndirect(type_addr, table_addr) => { - let table = store.get_table(module.resolve_table_addr(*table_addr) as usize)?; - let table_idx = stack.values.pop_t::()?; - - // verify that the table is of the right type, this should be validated by the parser already - assert!(table.borrow().kind.element_type == ValType::RefFunc, "table is not of type funcref"); - - let func_ref = { - table - .borrow() - .get(table_idx as usize)? - .addr() - .ok_or(Trap::UninitializedElement { index: table_idx as usize })? - }; - - let func_inst = store.get_func(func_ref as usize)?.clone(); - let func_ty = func_inst.func.ty(); - - log::info!("type_addr: {}", type_addr); - log::info!("types: {:?}", module.func_tys()); - let call_ty = module.func_ty(*type_addr); - - log::info!("call_indirect: current fn owner: {:?}", module.id()); - log::info!("call_indirect: func owner: {:?}", func_inst.owner); - - if func_ty != call_ty { - log::error!("indirect call type mismatch: {:?} != {:?}", func_ty, call_ty); - return Err( - Trap::IndirectCallTypeMismatch { actual: func_ty.clone(), expected: call_ty.clone() }.into() - ); - } - - let locals = match &func_inst.func { - crate::Function::Wasm(ref f) => f.locals.to_vec(), - crate::Function::Host(host_func) => { - let func = host_func.func.clone(); - let params = stack.values.pop_params(&func_ty.params)?; - let res = (func)(FuncContext { store, module }, ¶ms)?; - stack.values.extend_from_typed(&res); - return Ok(ExecResult::Ok); - } - }; - - let params = stack.values.pop_n_rev(func_ty.params.len())?; - let call_frame = CallFrame::new_raw(func_inst, ¶ms, locals); - - // push the call frame - cf.instr_ptr += 1; // skip the call instruction - stack.call_stack.push(cf.clone())?; - stack.call_stack.push(call_frame)?; - - // call the function - return Ok(ExecResult::Call); - } - - If(args, else_offset, end_offset) => { - // truthy value is on the top of the stack, so enter the then block - if stack.values.pop_t::()? != 0 { - log::trace!("entering then"); - cf.enter_label( - LabelFrame { - instr_ptr: cf.instr_ptr, - end_instr_ptr: cf.instr_ptr + *end_offset, - stack_ptr: stack.values.len(), // - params, - args: LabelArgs::new(*args, module)?, - ty: BlockType::If, - }, - &mut stack.values, - ); - return Ok(ExecResult::Ok); - } - - // falsy value is on the top of the stack - if let Some(else_offset) = else_offset { - log::debug!("entering else at {}", cf.instr_ptr + *else_offset); - cf.enter_label( - LabelFrame { - instr_ptr: cf.instr_ptr + *else_offset, - end_instr_ptr: cf.instr_ptr + *end_offset, - stack_ptr: stack.values.len(), // - params, - args: crate::LabelArgs::new(*args, module)?, - ty: BlockType::Else, - }, - &mut stack.values, - ); - cf.instr_ptr += *else_offset; - } else { - cf.instr_ptr += *end_offset; - } - } - - Loop(args, end_offset) => { - // let params = stack.values.pop_block_params(*args, &module)?; - cf.enter_label( - LabelFrame { - instr_ptr: cf.instr_ptr, - end_instr_ptr: cf.instr_ptr + *end_offset, - stack_ptr: stack.values.len(), // - params, - args: LabelArgs::new(*args, module)?, - ty: BlockType::Loop, - }, - &mut stack.values, - ); - } - - Block(args, end_offset) => { - cf.enter_label( - LabelFrame { - instr_ptr: cf.instr_ptr, - end_instr_ptr: cf.instr_ptr + *end_offset, - stack_ptr: stack.values.len(), //- params, - args: LabelArgs::new(*args, module)?, - ty: BlockType::Block, - }, - &mut stack.values, - ); - } - - BrTable(default, len) => { - let instr = instrs[cf.instr_ptr + 1..cf.instr_ptr + 1 + *len] - .iter() - .map(|i| match i { - BrLabel(l) => Ok(*l), - _ => panic!("Expected BrLabel, this should have been validated by the parser"), - }) - .collect::>>()?; - - if instr.len() != *len { - panic!( - "Expected {} BrLabel instructions, got {}, this should have been validated by the parser", - len, - instr.len() - ); - } - - let idx = stack.values.pop_t::()? as usize; - if let Some(label) = instr.get(idx) { - break_to!(cf, stack, label); - } else { - break_to!(cf, stack, default); - } - } - - Br(v) => break_to!(cf, stack, v), - BrIf(v) => { - if stack.values.pop_t::()? != 0 { - break_to!(cf, stack, v); - } - } - - Return => match stack.call_stack.is_empty() { - true => return Ok(ExecResult::Return), - false => return Ok(ExecResult::Call), - }, - - EndFunc => { - assert!( - cf.labels.len() == 0, - "endfunc: block frames not empty, this should have been validated by the parser" - ); - - match stack.call_stack.is_empty() { - true => return Ok(ExecResult::Return), - false => return Ok(ExecResult::Call), - } - } - - // We're essentially using else as a EndBlockFrame instruction for if blocks - Else(end_offset) => { - let Some(block) = cf.labels.pop() else { - panic!("else: no label to end, this should have been validated by the parser"); - }; - - let res_count = block.args.results; - stack.values.truncate_keep(block.stack_ptr, res_count); - cf.instr_ptr += *end_offset; - } - - EndBlockFrame => { - // remove the label from the label stack - let Some(block) = cf.labels.pop() else { - panic!("end: no label to end, this should have been validated by the parser"); - }; - stack.values.truncate_keep(block.stack_ptr, block.args.results) - } - - LocalGet(local_index) => stack.values.push(cf.get_local(*local_index as usize)), - LocalSet(local_index) => cf.set_local(*local_index as usize, stack.values.pop()?), - LocalTee(local_index) => cf.set_local(*local_index as usize, *stack.values.last()?), - - GlobalGet(global_index) => { - let idx = module.resolve_global_addr(*global_index); - let global = store.get_global_val(idx as usize)?; - stack.values.push(global); - } - - GlobalSet(global_index) => { - let idx = module.resolve_global_addr(*global_index); - store.set_global_val(idx as usize, stack.values.pop()?)?; - } - - I32Const(val) => stack.values.push((*val).into()), - I64Const(val) => stack.values.push((*val).into()), - F32Const(val) => stack.values.push((*val).into()), - F64Const(val) => stack.values.push((*val).into()), - - MemorySize(addr, byte) => { - if *byte != 0 { - unimplemented!("memory.size with byte != 0"); - } - - let mem_idx = module.resolve_mem_addr(*addr); - let mem = store.get_mem(mem_idx as usize)?; - stack.values.push((mem.borrow().page_count() as i32).into()); - } - - MemoryGrow(addr, byte) => { - if *byte != 0 { - return Err(Error::UnsupportedFeature("memory.grow with byte != 0".to_string())); - } - - let mem_idx = module.resolve_mem_addr(*addr); - let mem = store.get_mem(mem_idx as usize)?; - - let (res, prev_size) = { - let mut mem = mem.borrow_mut(); - let prev_size = mem.page_count() as i32; - (mem.grow(stack.values.pop_t::()?), prev_size) - }; - - match res { - Some(_) => stack.values.push(prev_size.into()), - None => stack.values.push((-1).into()), - } - } - - I32Store(arg) => mem_store!(i32, arg, stack, store, module), - I64Store(arg) => mem_store!(i64, arg, stack, store, module), - F32Store(arg) => mem_store!(f32, arg, stack, store, module), - F64Store(arg) => mem_store!(f64, arg, stack, store, module), - I32Store8(arg) => mem_store!(i8, i32, arg, stack, store, module), - I32Store16(arg) => mem_store!(i16, i32, arg, stack, store, module), - I64Store8(arg) => mem_store!(i8, i64, arg, stack, store, module), - I64Store16(arg) => mem_store!(i16, i64, arg, stack, store, module), - I64Store32(arg) => mem_store!(i32, i64, arg, stack, store, module), - - I32Load(arg) => mem_load!(i32, arg, stack, store, module), - I64Load(arg) => mem_load!(i64, arg, stack, store, module), - F32Load(arg) => mem_load!(f32, arg, stack, store, module), - F64Load(arg) => mem_load!(f64, arg, stack, store, module), - I32Load8S(arg) => mem_load!(i8, i32, arg, stack, store, module), - I32Load8U(arg) => mem_load!(u8, i32, arg, stack, store, module), - I32Load16S(arg) => mem_load!(i16, i32, arg, stack, store, module), - I32Load16U(arg) => mem_load!(u16, i32, arg, stack, store, module), - I64Load8S(arg) => mem_load!(i8, i64, arg, stack, store, module), - I64Load8U(arg) => mem_load!(u8, i64, arg, stack, store, module), - I64Load16S(arg) => mem_load!(i16, i64, arg, stack, store, module), - I64Load16U(arg) => mem_load!(u16, i64, arg, stack, store, module), - I64Load32S(arg) => mem_load!(i32, i64, arg, stack, store, module), - I64Load32U(arg) => mem_load!(u32, i64, arg, stack, store, module), - - I64Eqz => comp_zero!(==, i64, stack), - I32Eqz => comp_zero!(==, i32, stack), - - I32Eq => comp!(==, i32, stack), - I64Eq => comp!(==, i64, stack), - F32Eq => comp!(==, f32, stack), - F64Eq => comp!(==, f64, stack), - - I32Ne => comp!(!=, i32, stack), - I64Ne => comp!(!=, i64, stack), - F32Ne => comp!(!=, f32, stack), - F64Ne => comp!(!=, f64, stack), - - I32LtS => comp!(<, i32, stack), - I64LtS => comp!(<, i64, stack), - I32LtU => comp!(<, i32, u32, stack), - I64LtU => comp!(<, i64, u64, stack), - F32Lt => comp!(<, f32, stack), - F64Lt => comp!(<, f64, stack), - - I32LeS => comp!(<=, i32, stack), - I64LeS => comp!(<=, i64, stack), - I32LeU => comp!(<=, i32, u32, stack), - I64LeU => comp!(<=, i64, u64, stack), - F32Le => comp!(<=, f32, stack), - F64Le => comp!(<=, f64, stack), - - I32GeS => comp!(>=, i32, stack), - I64GeS => comp!(>=, i64, stack), - I32GeU => comp!(>=, i32, u32, stack), - I64GeU => comp!(>=, i64, u64, stack), - F32Ge => comp!(>=, f32, stack), - F64Ge => comp!(>=, f64, stack), - - I32GtS => comp!(>, i32, stack), - I64GtS => comp!(>, i64, stack), - I32GtU => comp!(>, i32, u32, stack), - I64GtU => comp!(>, i64, u64, stack), - F32Gt => comp!(>, f32, stack), - F64Gt => comp!(>, f64, stack), - - I64Add => arithmetic!(wrapping_add, i64, stack), - I32Add => arithmetic!(wrapping_add, i32, stack), - F32Add => arithmetic!(+, f32, stack), - F64Add => arithmetic!(+, f64, stack), - - I32Sub => arithmetic!(wrapping_sub, i32, stack), - I64Sub => arithmetic!(wrapping_sub, i64, stack), - F32Sub => arithmetic!(-, f32, stack), - F64Sub => arithmetic!(-, f64, stack), - - F32Div => arithmetic!(/, f32, stack), - F64Div => arithmetic!(/, f64, stack), - - I32Mul => arithmetic!(wrapping_mul, i32, stack), - I64Mul => arithmetic!(wrapping_mul, i64, stack), - F32Mul => arithmetic!(*, f32, stack), - F64Mul => arithmetic!(*, f64, stack), - - // these can trap - I32DivS => checked_int_arithmetic!(checked_div, i32, stack), - I64DivS => checked_int_arithmetic!(checked_div, i64, stack), - I32DivU => checked_int_arithmetic!(checked_div, i32, u32, stack), - I64DivU => checked_int_arithmetic!(checked_div, i64, u64, stack), - - I32RemS => checked_int_arithmetic!(checked_wrapping_rem, i32, stack), - I64RemS => checked_int_arithmetic!(checked_wrapping_rem, i64, stack), - I32RemU => checked_int_arithmetic!(checked_wrapping_rem, i32, u32, stack), - I64RemU => checked_int_arithmetic!(checked_wrapping_rem, i64, u64, stack), - - I32And => arithmetic!(bitand, i32, stack), - I64And => arithmetic!(bitand, i64, stack), - I32Or => arithmetic!(bitor, i32, stack), - I64Or => arithmetic!(bitor, i64, stack), - I32Xor => arithmetic!(bitxor, i32, stack), - I64Xor => arithmetic!(bitxor, i64, stack), - I32Shl => arithmetic!(wasm_shl, i32, stack), - I64Shl => arithmetic!(wasm_shl, i64, stack), - I32ShrS => arithmetic!(wasm_shr, i32, stack), - I64ShrS => arithmetic!(wasm_shr, i64, stack), - I32ShrU => arithmetic!(wasm_shr, u32, i32, stack), - I64ShrU => arithmetic!(wasm_shr, u64, i64, stack), - I32Rotl => arithmetic!(wasm_rotl, i32, stack), - I64Rotl => arithmetic!(wasm_rotl, i64, stack), - I32Rotr => arithmetic!(wasm_rotr, i32, stack), - I64Rotr => arithmetic!(wasm_rotr, i64, stack), - - I32Clz => arithmetic_single!(leading_zeros, i32, stack), - I64Clz => arithmetic_single!(leading_zeros, i64, stack), - I32Ctz => arithmetic_single!(trailing_zeros, i32, stack), - I64Ctz => arithmetic_single!(trailing_zeros, i64, stack), - I32Popcnt => arithmetic_single!(count_ones, i32, stack), - I64Popcnt => arithmetic_single!(count_ones, i64, stack), - - F32ConvertI32S => conv!(i32, f32, stack), - F32ConvertI64S => conv!(i64, f32, stack), - F64ConvertI32S => conv!(i32, f64, stack), - F64ConvertI64S => conv!(i64, f64, stack), - F32ConvertI32U => conv!(i32, u32, f32, stack), - F32ConvertI64U => conv!(i64, u64, f32, stack), - F64ConvertI32U => conv!(i32, u32, f64, stack), - F64ConvertI64U => conv!(i64, u64, f64, stack), - I32Extend8S => conv!(i32, i8, i32, stack), - I32Extend16S => conv!(i32, i16, i32, stack), - I64Extend8S => conv!(i64, i8, i64, stack), - I64Extend16S => conv!(i64, i16, i64, stack), - I64Extend32S => conv!(i64, i32, i64, stack), - I64ExtendI32U => conv!(i32, u32, i64, stack), - I64ExtendI32S => conv!(i32, i64, stack), - I32WrapI64 => conv!(i64, i32, stack), - - F32DemoteF64 => conv!(f64, f32, stack), - F64PromoteF32 => conv!(f32, f64, stack), - - F32Abs => arithmetic_single!(abs, f32, stack), - F64Abs => arithmetic_single!(abs, f64, stack), - F32Neg => arithmetic_single!(neg, f32, stack), - F64Neg => arithmetic_single!(neg, f64, stack), - F32Ceil => arithmetic_single!(ceil, f32, stack), - F64Ceil => arithmetic_single!(ceil, f64, stack), - F32Floor => arithmetic_single!(floor, f32, stack), - F64Floor => arithmetic_single!(floor, f64, stack), - F32Trunc => arithmetic_single!(trunc, f32, stack), - F64Trunc => arithmetic_single!(trunc, f64, stack), - F32Nearest => arithmetic_single!(wasm_nearest, f32, stack), - F64Nearest => arithmetic_single!(wasm_nearest, f64, stack), - F32Sqrt => arithmetic_single!(sqrt, f32, stack), - F64Sqrt => arithmetic_single!(sqrt, f64, stack), - F32Min => arithmetic!(wasm_min, f32, stack), - F64Min => arithmetic!(wasm_min, f64, stack), - F32Max => arithmetic!(wasm_max, f32, stack), - F64Max => arithmetic!(wasm_max, f64, stack), - F32Copysign => arithmetic!(copysign, f32, stack), - F64Copysign => arithmetic!(copysign, f64, stack), - - // no-op instructions since types are erased at runtime - I32ReinterpretF32 => {} - I64ReinterpretF64 => {} - F32ReinterpretI32 => {} - F64ReinterpretI64 => {} - - // unsigned versions of these are a bit broken atm - I32TruncF32S => checked_conv_float!(f32, i32, stack), - I32TruncF64S => checked_conv_float!(f64, i32, stack), - I32TruncF32U => checked_conv_float!(f32, u32, i32, stack), - I32TruncF64U => checked_conv_float!(f64, u32, i32, stack), - I64TruncF32S => checked_conv_float!(f32, i64, stack), - I64TruncF64S => checked_conv_float!(f64, i64, stack), - I64TruncF32U => checked_conv_float!(f32, u64, i64, stack), - I64TruncF64U => checked_conv_float!(f64, u64, i64, stack), - - TableGet(table_index) => { - let table_idx = module.resolve_table_addr(*table_index); - let table = store.get_table(table_idx as usize)?; - let idx = stack.values.pop_t::()? as usize; - let v = table.borrow().get_wasm_val(idx)?; - stack.values.push(v.into()); - } - - TableSet(table_index) => { - let table_idx = module.resolve_table_addr(*table_index); - let table = store.get_table(table_idx as usize)?; - let val = stack.values.pop_t::()?; - let idx = stack.values.pop_t::()? as usize; - table.borrow_mut().set(idx, val)?; - } - - TableSize(table_index) => { - let table_idx = module.resolve_table_addr(*table_index); - let table = store.get_table(table_idx as usize)?; - stack.values.push(table.borrow().size().into()); - } - - TableInit(table_index, elem_index) => { - let table_idx = module.resolve_table_addr(*table_index); - let table = store.get_table(table_idx as usize)?; - - let elem_idx = module.resolve_elem_addr(*elem_index); - let elem = store.get_elem(elem_idx as usize)?; - - if elem.kind != ElementKind::Passive { - return Err(Trap::TableOutOfBounds { offset: 0, len: 0, max: 0 }.into()); - } - - let Some(items) = elem.items.as_ref() else { - return Err(Trap::TableOutOfBounds { offset: 0, len: 0, max: 0 }.into()); - }; - - table.borrow_mut().init(module.func_addrs(), 0, items)?; - } - - I32TruncSatF32S => arithmetic_single!(trunc, f32, i32, stack), - I32TruncSatF32U => arithmetic_single!(trunc, f32, u32, stack), - I32TruncSatF64S => arithmetic_single!(trunc, f64, i32, stack), - I32TruncSatF64U => arithmetic_single!(trunc, f64, u32, stack), - I64TruncSatF32S => arithmetic_single!(trunc, f32, i64, stack), - I64TruncSatF32U => arithmetic_single!(trunc, f32, u64, stack), - I64TruncSatF64S => arithmetic_single!(trunc, f64, i64, stack), - I64TruncSatF64U => arithmetic_single!(trunc, f64, u64, stack), - - i => { - log::error!("unimplemented instruction: {:?}", i); - return Err(Error::UnsupportedFeature(alloc::format!("unimplemented instruction: {:?}", i))); - } - }; - - Ok(ExecResult::Ok) -} diff --git a/crates/tinywasm/src/runtime/executor/traits.rs b/crates/tinywasm/src/runtime/executor/traits.rs deleted file mode 100644 index 06aab2a..0000000 --- a/crates/tinywasm/src/runtime/executor/traits.rs +++ /dev/null @@ -1,132 +0,0 @@ -pub(crate) trait CheckedWrappingRem -where - Self: Sized, -{ - fn checked_wrapping_rem(self, rhs: Self) -> Option; -} - -pub(crate) trait WasmFloatOps { - fn wasm_min(self, other: Self) -> Self; - fn wasm_max(self, other: Self) -> Self; - fn wasm_nearest(self) -> Self; -} - -macro_rules! impl_wasm_float_ops { - ($($t:ty)*) => ($( - impl WasmFloatOps for $t { - // https://webassembly.github.io/spec/core/exec/numerics.html#op-fnearest - fn wasm_nearest(self) -> Self { - match self { - x if x.is_nan() => x, // preserve NaN - x if x.is_infinite() || x == 0.0 => x, // preserve infinities and zeros - x if (0.0..=0.5).contains(&x) => 0.0, - x if (-0.5..0.0).contains(&x) => -0.0, - // x => x.round(), - x => { - // Handle normal and halfway cases - let rounded = x.round(); - let diff = (x - rounded).abs(); - - if diff == 0.5 { - // Halfway case: round to even - if rounded % 2.0 == 0.0 { - rounded // Already even - } else { - rounded - x.signum() // Make even - } - } else { - // Normal case - rounded - } - } - } - } - - // https://webassembly.github.io/spec/core/exec/numerics.html#op-fmin - // Based on f32::minimum (which is not yet stable) - #[inline] - fn wasm_min(self, other: Self) -> Self { - if self < other { - self - } else if other < self { - other - } else if self == other { - if self.is_sign_negative() && other.is_sign_positive() { self } else { other } - } else { - // At least one input is NaN. Use `+` to perform NaN propagation and quieting. - self + other - } - } - - // https://webassembly.github.io/spec/core/exec/numerics.html#op-fmax - // Based on f32::maximum (which is not yet stable) - #[inline] - fn wasm_max(self, other: Self) -> Self { - if self > other { - self - } else if other > self { - other - } else if self == other { - if self.is_sign_negative() && other.is_sign_positive() { other } else { self } - } else { - // At least one input is NaN. Use `+` to perform NaN propagation and quieting. - self + other - } - } - } - )*) -} - -impl_wasm_float_ops! { f32 f64 } - -pub(crate) trait WasmIntOps { - fn wasm_shl(self, rhs: Self) -> Self; - fn wasm_shr(self, rhs: Self) -> Self; - fn wasm_rotl(self, rhs: Self) -> Self; - fn wasm_rotr(self, rhs: Self) -> Self; -} - -macro_rules! impl_wrapping_self_sh { - ($($t:ty)*) => ($( - impl WasmIntOps for $t { - #[inline] - fn wasm_shl(self, rhs: Self) -> Self { - self.wrapping_shl(rhs as u32) - } - - #[inline] - fn wasm_shr(self, rhs: Self) -> Self { - self.wrapping_shr(rhs as u32) - } - - #[inline] - fn wasm_rotl(self, rhs: Self) -> Self { - self.rotate_left(rhs as u32) - } - - #[inline] - fn wasm_rotr(self, rhs: Self) -> Self { - self.rotate_right(rhs as u32) - } - } - )*) -} - -impl_wrapping_self_sh! { i32 i64 u32 u64 } - -macro_rules! impl_checked_wrapping_rem { - ($($t:ty)*) => ($( - impl CheckedWrappingRem for $t { - #[inline] - fn checked_wrapping_rem(self, rhs: Self) -> Option { - if rhs == 0 { - None - } else { - Some(self.wrapping_rem(rhs)) - } - } - } - )*) -} - -impl_checked_wrapping_rem! { i32 i64 u32 u64 } diff --git a/crates/tinywasm/src/runtime/interpreter/macros.rs b/crates/tinywasm/src/runtime/interpreter/macros.rs new file mode 100644 index 0000000..909acb3 --- /dev/null +++ b/crates/tinywasm/src/runtime/interpreter/macros.rs @@ -0,0 +1,245 @@ +//! More generic macros for various instructions +//! +//! These macros are used to generate the actual instruction implementations. +//! In some basic tests this generated better assembly than using generic functions, even when inlined. +//! (Something to revisit in the future) + +/// Load a value from memory +macro_rules! mem_load { + ($type:ty, $arg:ident, $stack:ident, $store:ident, $module:ident) => {{ + mem_load!($type, $type, $arg, $stack, $store, $module) + }}; + + ($load_type:ty, $target_type:ty, $arg:ident, $stack:ident, $store:ident, $module:ident) => {{ + // TODO: there could be a lot of performance improvements here + let mem_idx = $module.resolve_mem_addr($arg.mem_addr); + let mem = $store.get_mem(mem_idx as usize)?; + + let addr = $stack.values.pop()?.raw_value(); + + let addr = $arg.offset.checked_add(addr).ok_or_else(|| { + Error::Trap(crate::Trap::MemoryOutOfBounds { + offset: $arg.offset as usize, + len: core::mem::size_of::<$load_type>(), + max: mem.borrow().max_pages(), + }) + })?; + + let addr: usize = addr.try_into().ok().ok_or_else(|| { + Error::Trap(crate::Trap::MemoryOutOfBounds { + offset: $arg.offset as usize, + len: core::mem::size_of::<$load_type>(), + max: mem.borrow().max_pages(), + }) + })?; + + let val: [u8; core::mem::size_of::<$load_type>()] = { + let mem = mem.borrow_mut(); + let val = mem.load(addr, $arg.align as usize, core::mem::size_of::<$load_type>())?; + val.try_into().expect("slice with incorrect length") + }; + + let loaded_value = <$load_type>::from_le_bytes(val); + $stack.values.push((loaded_value as $target_type).into()); + }}; +} + +/// Store a value to memory +macro_rules! mem_store { + ($type:ty, $arg:ident, $stack:ident, $store:ident, $module:ident) => {{ + log::debug!("mem_store!({}, {:?})", stringify!($type), $arg); + + mem_store!($type, $type, $arg, $stack, $store, $module) + }}; + + ($store_type:ty, $target_type:ty, $arg:ident, $stack:ident, $store:ident, $module:ident) => {{ + // likewise, there could be a lot of performance improvements here + let mem_idx = $module.resolve_mem_addr($arg.mem_addr); + let mem = $store.get_mem(mem_idx as usize)?; + + let val = $stack.values.pop_t::<$store_type>()?; + let addr = $stack.values.pop()?.raw_value(); + + let val = val as $store_type; + let val = val.to_le_bytes(); + + mem.borrow_mut().store(($arg.offset + addr) as usize, $arg.align as usize, &val)?; + }}; +} + +/// Doing the actual conversion from float to int is a bit tricky, because +/// we need to check for overflow. This macro generates the min/max values +/// for a specific conversion, which are then used in the actual conversion. +/// Rust sadly doesn't have wrapping casts for floats yet, maybe never. +/// Alternatively, https://crates.io/crates/az could be used for this but +/// it's not worth the dependency. +macro_rules! float_min_max { + (f32, i32) => { + (-2147483904.0_f32, 2147483648.0_f32) + }; + (f64, i32) => { + (-2147483649.0_f64, 2147483648.0_f64) + }; + (f32, u32) => { + (-1.0_f32, 4294967296.0_f32) // 2^32 + }; + (f64, u32) => { + (-1.0_f64, 4294967296.0_f64) // 2^32 + }; + (f32, i64) => { + (-9223373136366403584.0_f32, 9223372036854775808.0_f32) // 2^63 + 2^40 | 2^63 + }; + (f64, i64) => { + (-9223372036854777856.0_f64, 9223372036854775808.0_f64) // 2^63 + 2^40 | 2^63 + }; + (f32, u64) => { + (-1.0_f32, 18446744073709551616.0_f32) // 2^64 + }; + (f64, u64) => { + (-1.0_f64, 18446744073709551616.0_f64) // 2^64 + }; + // other conversions are not allowed + ($from:ty, $to:ty) => { + compile_error!("invalid float conversion"); + }; +} + +/// Convert a value on the stack +macro_rules! conv { + ($from:ty, $intermediate:ty, $to:ty, $stack:ident) => {{ + let a: $from = $stack.values.pop()?.into(); + $stack.values.push((a as $intermediate as $to).into()); + }}; + ($from:ty, $to:ty, $stack:ident) => {{ + let a: $from = $stack.values.pop()?.into(); + $stack.values.push((a as $to).into()); + }}; +} + +/// Convert a value on the stack with error checking +macro_rules! checked_conv_float { + // Direct conversion with error checking (two types) + ($from:tt, $to:tt, $stack:ident) => {{ + checked_conv_float!($from, $to, $to, $stack) + }}; + // Conversion with an intermediate unsigned type and error checking (three types) + ($from:tt, $intermediate:tt, $to:tt, $stack:ident) => {{ + let (min, max) = float_min_max!($from, $intermediate); + let a: $from = $stack.values.pop()?.into(); + + if a.is_nan() { + return Err(Error::Trap(crate::Trap::InvalidConversionToInt)); + } + + if a <= min || a >= max { + return Err(Error::Trap(crate::Trap::IntegerOverflow)); + } + + $stack.values.push((a as $intermediate as $to).into()); + }}; +} + +/// Compare two values on the stack +macro_rules! comp { + ($op:tt, $ty:ty, $stack:ident) => {{ + comp!($op, $ty, $ty, $stack) + }}; + + ($op:tt, $intermediate:ty, $to:ty, $stack:ident) => {{ + let [a, b] = $stack.values.pop_n_const::<2>()?; + let a: $intermediate = a.into(); + let b: $intermediate = b.into(); + + // Cast to unsigned type before comparison + let a = a as $to; + let b = b as $to; + $stack.values.push(((a $op b) as i32).into()); + }}; +} + +/// Compare a value on the stack to zero +macro_rules! comp_zero { + ($op:tt, $ty:ty, $stack:ident) => {{ + let a: $ty = $stack.values.pop()?.into(); + $stack.values.push(((a $op 0) as i32).into()); + }}; +} + +/// Apply an arithmetic method to two values on the stack +macro_rules! arithmetic { + ($op:ident, $ty:ty, $stack:ident) => {{ + arithmetic!($op, $ty, $ty, $stack) + }}; + + // also allow operators such as +, - + ($op:tt, $ty:ty, $stack:ident) => {{ + let [a, b] = $stack.values.pop_n_const::<2>()?; + let a: $ty = a.into(); + let b: $ty = b.into(); + $stack.values.push((a $op b).into()); + }}; + + ($op:ident, $intermediate:ty, $to:ty, $stack:ident) => {{ + let [a, b] = $stack.values.pop_n_const::<2>()?; + let a: $to = a.into(); + let b: $to = b.into(); + + let a = a as $intermediate; + let b = b as $intermediate; + + let result = a.$op(b); + $stack.values.push((result as $to).into()); + }}; +} + +/// Apply an arithmetic method to a single value on the stack +macro_rules! arithmetic_single { + ($op:ident, $ty:ty, $stack:ident) => {{ + let a: $ty = $stack.values.pop()?.into(); + let result = a.$op(); + $stack.values.push((result as $ty).into()); + }}; + + ($op:ident, $from:ty, $to:ty, $stack:ident) => {{ + let a: $from = $stack.values.pop()?.into(); + let result = a.$op(); + $stack.values.push((result as $to).into()); + }}; +} + +/// Apply an arithmetic operation to two values on the stack with error checking +macro_rules! checked_int_arithmetic { + // Direct conversion with error checking (two types) + ($from:tt, $to:tt, $stack:ident) => {{ + checked_int_arithmetic!($from, $to, $to, $stack) + }}; + + ($op:ident, $from:ty, $to:ty, $stack:ident) => {{ + let [a, b] = $stack.values.pop_n_const::<2>()?; + let a: $from = a.into(); + let b: $from = b.into(); + + let a_casted: $to = a as $to; + let b_casted: $to = b as $to; + + if b_casted == 0 { + return Err(Error::Trap(crate::Trap::DivisionByZero)); + } + + let result = a_casted.$op(b_casted).ok_or_else(|| Error::Trap(crate::Trap::IntegerOverflow))?; + + // Cast back to original type if different + $stack.values.push((result as $from).into()); + }}; +} + +pub(super) use arithmetic; +pub(super) use arithmetic_single; +pub(super) use checked_conv_float; +pub(super) use checked_int_arithmetic; +pub(super) use comp; +pub(super) use comp_zero; +pub(super) use conv; +pub(super) use float_min_max; +pub(super) use mem_load; +pub(super) use mem_store; diff --git a/crates/tinywasm/src/runtime/interpreter/mod.rs b/crates/tinywasm/src/runtime/interpreter/mod.rs new file mode 100644 index 0000000..48ce21d --- /dev/null +++ b/crates/tinywasm/src/runtime/interpreter/mod.rs @@ -0,0 +1,634 @@ +use core::ops::{BitAnd, BitOr, BitXor, Neg}; + +use super::{InterpreterRuntime, Stack}; +use crate::{ + log::debug, + runtime::{BlockType, CallFrame, LabelArgs, LabelFrame}, + Error, FuncContext, ModuleInstance, Result, Store, Trap, +}; +use alloc::{string::ToString, vec::Vec}; +use tinywasm_types::{ElementKind, Instruction, ValType}; + +mod macros; +mod traits; +use macros::*; +use traits::*; + +impl InterpreterRuntime { + pub(crate) fn exec(&self, store: &mut Store, stack: &mut Stack) -> Result<()> { + // The current call frame, gets updated inside of exec_one + let mut cf = stack.call_stack.pop()?; + + let mut func_inst = cf.func_instance.clone(); + let mut wasm_func = func_inst.assert_wasm().expect("exec expected wasm function"); + + // The function to execute, gets updated from ExecResult::Call + let mut instrs = &wasm_func.instructions; + + let mut current_module = store.get_module_instance(func_inst.owner).unwrap().clone(); + + while let Some(instr) = instrs.get(cf.instr_ptr) { + match exec_one(&mut cf, instr, instrs, stack, store, ¤t_module)? { + // Continue execution at the new top of the call stack + ExecResult::Call => { + cf = stack.call_stack.pop()?; + func_inst = cf.func_instance.clone(); + wasm_func = func_inst.assert_wasm().expect("exec expected wasm function"); + instrs = &wasm_func.instructions; + + if cf.func_instance.owner != current_module.id() { + current_module.swap( + store + .get_module_instance(cf.func_instance.owner) + .unwrap_or_else(|| { + panic!( + "exec expected module instance {} to exist for function", + cf.func_instance.owner + ) + }) + .clone(), + ); + } + + continue; + } + + // return from the function + ExecResult::Return => return Ok(()), + + // continue to the next instruction and increment the instruction pointer + ExecResult::Ok => { + cf.instr_ptr += 1; + } + + // trap the program + ExecResult::Trap(trap) => { + cf.instr_ptr += 1; + // push the call frame back onto the stack so that it can be resumed + // if the trap can be handled + stack.call_stack.push(cf)?; + return Err(Error::Trap(trap)); + } + } + } + + debug!("end of exec"); + debug!("stack: {:?}", stack.values); + debug!("insts: {:?}", instrs); + debug!("instr_ptr: {}", cf.instr_ptr); + Err(Error::FuncDidNotReturn) + } +} + +enum ExecResult { + Ok, + Return, + Call, + Trap(crate::Trap), +} + +// Break to a block at the given index (relative to the current frame) +// If there is no block at the given index, return or call the parent function +// +// This is a bit hard to see from the spec, but it's vaild to use breaks to return +// from a function, so we need to check if the label stack is empty +macro_rules! break_to { + ($cf:ident, $stack:ident, $break_to_relative:ident) => {{ + if $cf.break_to(*$break_to_relative, &mut $stack.values).is_none() { + if $stack.call_stack.is_empty() { + return Ok(ExecResult::Return); + } else { + return Ok(ExecResult::Call); + } + } + }}; +} + +/// Run a single step of the interpreter +/// A seperate function is used so later, we can more easily implement +/// a step-by-step debugger (using generators once they're stable?) +#[inline] +fn exec_one( + cf: &mut CallFrame, + instr: &Instruction, + instrs: &[Instruction], + stack: &mut Stack, + store: &mut Store, + module: &ModuleInstance, +) -> Result { + debug!("ptr: {} instr: {:?}", cf.instr_ptr, instr); + + use tinywasm_types::Instruction::*; + match instr { + Nop => { /* do nothing */ } + Unreachable => return Ok(ExecResult::Trap(crate::Trap::Unreachable)), // we don't need to include the call frame here because it's already on the stack + Drop => stack.values.pop().map(|_| ())?, + + Select( + _valtype, // due to validation, we know that the type of the values on the stack are correct + ) => { + // due to validation, we know that the type of the values on the stack + let cond: i32 = stack.values.pop()?.into(); + let val2 = stack.values.pop()?; + + // if cond != 0, we already have the right value on the stack + if cond == 0 { + let _ = stack.values.pop()?; + stack.values.push(val2); + } + } + + Call(v) => { + // prepare the call frame + let func_idx = module.resolve_func_addr(*v); + let func_inst = store.get_func(func_idx as usize)?.clone(); + + let (locals, ty) = match &func_inst.func { + crate::Function::Wasm(ref f) => (f.locals.to_vec(), f.ty.clone()), + crate::Function::Host(host_func) => { + let func = host_func.func.clone(); + let params = stack.values.pop_params(&host_func.ty.params)?; + let res = (func)(FuncContext { store, module }, ¶ms)?; + stack.values.extend_from_typed(&res); + return Ok(ExecResult::Ok); + } + }; + + let params = stack.values.pop_n_rev(ty.params.len())?; + let call_frame = CallFrame::new_raw(func_inst, ¶ms, locals); + + // push the call frame + cf.instr_ptr += 1; // skip the call instruction + stack.call_stack.push(cf.clone())?; + stack.call_stack.push(call_frame)?; + + // call the function + return Ok(ExecResult::Call); + } + + CallIndirect(type_addr, table_addr) => { + let table = store.get_table(module.resolve_table_addr(*table_addr) as usize)?; + let table_idx = stack.values.pop_t::()?; + + // verify that the table is of the right type, this should be validated by the parser already + assert!(table.borrow().kind.element_type == ValType::RefFunc, "table is not of type funcref"); + + let func_ref = { + table + .borrow() + .get(table_idx as usize)? + .addr() + .ok_or(Trap::UninitializedElement { index: table_idx as usize })? + }; + + let func_inst = store.get_func(func_ref as usize)?.clone(); + let func_ty = func_inst.func.ty(); + + log::info!("type_addr: {}", type_addr); + log::info!("types: {:?}", module.func_tys()); + let call_ty = module.func_ty(*type_addr); + + log::info!("call_indirect: current fn owner: {:?}", module.id()); + log::info!("call_indirect: func owner: {:?}", func_inst.owner); + + if func_ty != call_ty { + log::error!("indirect call type mismatch: {:?} != {:?}", func_ty, call_ty); + return Err( + Trap::IndirectCallTypeMismatch { actual: func_ty.clone(), expected: call_ty.clone() }.into() + ); + } + + let locals = match &func_inst.func { + crate::Function::Wasm(ref f) => f.locals.to_vec(), + crate::Function::Host(host_func) => { + let func = host_func.func.clone(); + let params = stack.values.pop_params(&func_ty.params)?; + let res = (func)(FuncContext { store, module }, ¶ms)?; + stack.values.extend_from_typed(&res); + return Ok(ExecResult::Ok); + } + }; + + let params = stack.values.pop_n_rev(func_ty.params.len())?; + let call_frame = CallFrame::new_raw(func_inst, ¶ms, locals); + + // push the call frame + cf.instr_ptr += 1; // skip the call instruction + stack.call_stack.push(cf.clone())?; + stack.call_stack.push(call_frame)?; + + // call the function + return Ok(ExecResult::Call); + } + + If(args, else_offset, end_offset) => { + // truthy value is on the top of the stack, so enter the then block + if stack.values.pop_t::()? != 0 { + log::trace!("entering then"); + cf.enter_label( + LabelFrame { + instr_ptr: cf.instr_ptr, + end_instr_ptr: cf.instr_ptr + *end_offset, + stack_ptr: stack.values.len(), // - params, + args: LabelArgs::new(*args, module)?, + ty: BlockType::If, + }, + &mut stack.values, + ); + return Ok(ExecResult::Ok); + } + + // falsy value is on the top of the stack + if let Some(else_offset) = else_offset { + log::debug!("entering else at {}", cf.instr_ptr + *else_offset); + cf.enter_label( + LabelFrame { + instr_ptr: cf.instr_ptr + *else_offset, + end_instr_ptr: cf.instr_ptr + *end_offset, + stack_ptr: stack.values.len(), // - params, + args: LabelArgs::new(*args, module)?, + ty: BlockType::Else, + }, + &mut stack.values, + ); + cf.instr_ptr += *else_offset; + } else { + cf.instr_ptr += *end_offset; + } + } + + Loop(args, end_offset) => { + // let params = stack.values.pop_block_params(*args, &module)?; + cf.enter_label( + LabelFrame { + instr_ptr: cf.instr_ptr, + end_instr_ptr: cf.instr_ptr + *end_offset, + stack_ptr: stack.values.len(), // - params, + args: LabelArgs::new(*args, module)?, + ty: BlockType::Loop, + }, + &mut stack.values, + ); + } + + Block(args, end_offset) => { + cf.enter_label( + LabelFrame { + instr_ptr: cf.instr_ptr, + end_instr_ptr: cf.instr_ptr + *end_offset, + stack_ptr: stack.values.len(), //- params, + args: LabelArgs::new(*args, module)?, + ty: BlockType::Block, + }, + &mut stack.values, + ); + } + + BrTable(default, len) => { + let instr = instrs[cf.instr_ptr + 1..cf.instr_ptr + 1 + *len] + .iter() + .map(|i| match i { + BrLabel(l) => Ok(*l), + _ => panic!("Expected BrLabel, this should have been validated by the parser"), + }) + .collect::>>()?; + + if instr.len() != *len { + panic!( + "Expected {} BrLabel instructions, got {}, this should have been validated by the parser", + len, + instr.len() + ); + } + + let idx = stack.values.pop_t::()? as usize; + let to = instr.get(idx).unwrap_or(default); + break_to!(cf, stack, to); + } + + Br(v) => break_to!(cf, stack, v), + BrIf(v) => { + if stack.values.pop_t::()? != 0 { + break_to!(cf, stack, v); + } + } + + Return => match stack.call_stack.is_empty() { + true => return Ok(ExecResult::Return), + false => return Ok(ExecResult::Call), + }, + + EndFunc => { + assert!( + cf.labels.len() == 0, + "endfunc: block frames not empty, this should have been validated by the parser" + ); + + match stack.call_stack.is_empty() { + true => return Ok(ExecResult::Return), + false => return Ok(ExecResult::Call), + } + } + + // We're essentially using else as a EndBlockFrame instruction for if blocks + Else(end_offset) => { + let Some(block) = cf.labels.pop() else { + panic!("else: no label to end, this should have been validated by the parser"); + }; + + let res_count = block.args.results; + stack.values.truncate_keep(block.stack_ptr, res_count); + cf.instr_ptr += *end_offset; + } + + EndBlockFrame => { + // remove the label from the label stack + let Some(block) = cf.labels.pop() else { + panic!("end: no label to end, this should have been validated by the parser"); + }; + stack.values.truncate_keep(block.stack_ptr, block.args.results) + } + + LocalGet(local_index) => stack.values.push(cf.get_local(*local_index as usize)), + LocalSet(local_index) => cf.set_local(*local_index as usize, stack.values.pop()?), + LocalTee(local_index) => cf.set_local(*local_index as usize, *stack.values.last()?), + + GlobalGet(global_index) => { + let idx = module.resolve_global_addr(*global_index); + let global = store.get_global_val(idx as usize)?; + stack.values.push(global); + } + + GlobalSet(global_index) => { + let idx = module.resolve_global_addr(*global_index); + store.set_global_val(idx as usize, stack.values.pop()?)?; + } + + I32Const(val) => stack.values.push((*val).into()), + I64Const(val) => stack.values.push((*val).into()), + F32Const(val) => stack.values.push((*val).into()), + F64Const(val) => stack.values.push((*val).into()), + + MemorySize(addr, byte) => { + if *byte != 0 { + unimplemented!("memory.size with byte != 0"); + } + + let mem_idx = module.resolve_mem_addr(*addr); + let mem = store.get_mem(mem_idx as usize)?; + stack.values.push((mem.borrow().page_count() as i32).into()); + } + + MemoryGrow(addr, byte) => { + if *byte != 0 { + return Err(Error::UnsupportedFeature("memory.grow with byte != 0".to_string())); + } + + let mem_idx = module.resolve_mem_addr(*addr); + let mem = store.get_mem(mem_idx as usize)?; + + let (res, prev_size) = { + let mut mem = mem.borrow_mut(); + let prev_size = mem.page_count() as i32; + (mem.grow(stack.values.pop_t::()?), prev_size) + }; + + match res { + Some(_) => stack.values.push(prev_size.into()), + None => stack.values.push((-1).into()), + } + } + + I32Store(arg) => mem_store!(i32, arg, stack, store, module), + I64Store(arg) => mem_store!(i64, arg, stack, store, module), + F32Store(arg) => mem_store!(f32, arg, stack, store, module), + F64Store(arg) => mem_store!(f64, arg, stack, store, module), + I32Store8(arg) => mem_store!(i8, i32, arg, stack, store, module), + I32Store16(arg) => mem_store!(i16, i32, arg, stack, store, module), + I64Store8(arg) => mem_store!(i8, i64, arg, stack, store, module), + I64Store16(arg) => mem_store!(i16, i64, arg, stack, store, module), + I64Store32(arg) => mem_store!(i32, i64, arg, stack, store, module), + + I32Load(arg) => mem_load!(i32, arg, stack, store, module), + I64Load(arg) => mem_load!(i64, arg, stack, store, module), + F32Load(arg) => mem_load!(f32, arg, stack, store, module), + F64Load(arg) => mem_load!(f64, arg, stack, store, module), + I32Load8S(arg) => mem_load!(i8, i32, arg, stack, store, module), + I32Load8U(arg) => mem_load!(u8, i32, arg, stack, store, module), + I32Load16S(arg) => mem_load!(i16, i32, arg, stack, store, module), + I32Load16U(arg) => mem_load!(u16, i32, arg, stack, store, module), + I64Load8S(arg) => mem_load!(i8, i64, arg, stack, store, module), + I64Load8U(arg) => mem_load!(u8, i64, arg, stack, store, module), + I64Load16S(arg) => mem_load!(i16, i64, arg, stack, store, module), + I64Load16U(arg) => mem_load!(u16, i64, arg, stack, store, module), + I64Load32S(arg) => mem_load!(i32, i64, arg, stack, store, module), + I64Load32U(arg) => mem_load!(u32, i64, arg, stack, store, module), + + I64Eqz => comp_zero!(==, i64, stack), + I32Eqz => comp_zero!(==, i32, stack), + + I32Eq => comp!(==, i32, stack), + I64Eq => comp!(==, i64, stack), + F32Eq => comp!(==, f32, stack), + F64Eq => comp!(==, f64, stack), + + I32Ne => comp!(!=, i32, stack), + I64Ne => comp!(!=, i64, stack), + F32Ne => comp!(!=, f32, stack), + F64Ne => comp!(!=, f64, stack), + + I32LtS => comp!(<, i32, stack), + I64LtS => comp!(<, i64, stack), + I32LtU => comp!(<, i32, u32, stack), + I64LtU => comp!(<, i64, u64, stack), + F32Lt => comp!(<, f32, stack), + F64Lt => comp!(<, f64, stack), + + I32LeS => comp!(<=, i32, stack), + I64LeS => comp!(<=, i64, stack), + I32LeU => comp!(<=, i32, u32, stack), + I64LeU => comp!(<=, i64, u64, stack), + F32Le => comp!(<=, f32, stack), + F64Le => comp!(<=, f64, stack), + + I32GeS => comp!(>=, i32, stack), + I64GeS => comp!(>=, i64, stack), + I32GeU => comp!(>=, i32, u32, stack), + I64GeU => comp!(>=, i64, u64, stack), + F32Ge => comp!(>=, f32, stack), + F64Ge => comp!(>=, f64, stack), + + I32GtS => comp!(>, i32, stack), + I64GtS => comp!(>, i64, stack), + I32GtU => comp!(>, i32, u32, stack), + I64GtU => comp!(>, i64, u64, stack), + F32Gt => comp!(>, f32, stack), + F64Gt => comp!(>, f64, stack), + + I64Add => arithmetic!(wrapping_add, i64, stack), + I32Add => arithmetic!(wrapping_add, i32, stack), + F32Add => arithmetic!(+, f32, stack), + F64Add => arithmetic!(+, f64, stack), + + I32Sub => arithmetic!(wrapping_sub, i32, stack), + I64Sub => arithmetic!(wrapping_sub, i64, stack), + F32Sub => arithmetic!(-, f32, stack), + F64Sub => arithmetic!(-, f64, stack), + + F32Div => arithmetic!(/, f32, stack), + F64Div => arithmetic!(/, f64, stack), + + I32Mul => arithmetic!(wrapping_mul, i32, stack), + I64Mul => arithmetic!(wrapping_mul, i64, stack), + F32Mul => arithmetic!(*, f32, stack), + F64Mul => arithmetic!(*, f64, stack), + + // these can trap + I32DivS => checked_int_arithmetic!(checked_div, i32, stack), + I64DivS => checked_int_arithmetic!(checked_div, i64, stack), + I32DivU => checked_int_arithmetic!(checked_div, i32, u32, stack), + I64DivU => checked_int_arithmetic!(checked_div, i64, u64, stack), + + I32RemS => checked_int_arithmetic!(checked_wrapping_rem, i32, stack), + I64RemS => checked_int_arithmetic!(checked_wrapping_rem, i64, stack), + I32RemU => checked_int_arithmetic!(checked_wrapping_rem, i32, u32, stack), + I64RemU => checked_int_arithmetic!(checked_wrapping_rem, i64, u64, stack), + + I32And => arithmetic!(bitand, i32, stack), + I64And => arithmetic!(bitand, i64, stack), + I32Or => arithmetic!(bitor, i32, stack), + I64Or => arithmetic!(bitor, i64, stack), + I32Xor => arithmetic!(bitxor, i32, stack), + I64Xor => arithmetic!(bitxor, i64, stack), + I32Shl => arithmetic!(wasm_shl, i32, stack), + I64Shl => arithmetic!(wasm_shl, i64, stack), + I32ShrS => arithmetic!(wasm_shr, i32, stack), + I64ShrS => arithmetic!(wasm_shr, i64, stack), + I32ShrU => arithmetic!(wasm_shr, u32, i32, stack), + I64ShrU => arithmetic!(wasm_shr, u64, i64, stack), + I32Rotl => arithmetic!(wasm_rotl, i32, stack), + I64Rotl => arithmetic!(wasm_rotl, i64, stack), + I32Rotr => arithmetic!(wasm_rotr, i32, stack), + I64Rotr => arithmetic!(wasm_rotr, i64, stack), + + I32Clz => arithmetic_single!(leading_zeros, i32, stack), + I64Clz => arithmetic_single!(leading_zeros, i64, stack), + I32Ctz => arithmetic_single!(trailing_zeros, i32, stack), + I64Ctz => arithmetic_single!(trailing_zeros, i64, stack), + I32Popcnt => arithmetic_single!(count_ones, i32, stack), + I64Popcnt => arithmetic_single!(count_ones, i64, stack), + + F32ConvertI32S => conv!(i32, f32, stack), + F32ConvertI64S => conv!(i64, f32, stack), + F64ConvertI32S => conv!(i32, f64, stack), + F64ConvertI64S => conv!(i64, f64, stack), + F32ConvertI32U => conv!(i32, u32, f32, stack), + F32ConvertI64U => conv!(i64, u64, f32, stack), + F64ConvertI32U => conv!(i32, u32, f64, stack), + F64ConvertI64U => conv!(i64, u64, f64, stack), + I32Extend8S => conv!(i32, i8, i32, stack), + I32Extend16S => conv!(i32, i16, i32, stack), + I64Extend8S => conv!(i64, i8, i64, stack), + I64Extend16S => conv!(i64, i16, i64, stack), + I64Extend32S => conv!(i64, i32, i64, stack), + I64ExtendI32U => conv!(i32, u32, i64, stack), + I64ExtendI32S => conv!(i32, i64, stack), + I32WrapI64 => conv!(i64, i32, stack), + + F32DemoteF64 => conv!(f64, f32, stack), + F64PromoteF32 => conv!(f32, f64, stack), + + F32Abs => arithmetic_single!(abs, f32, stack), + F64Abs => arithmetic_single!(abs, f64, stack), + F32Neg => arithmetic_single!(neg, f32, stack), + F64Neg => arithmetic_single!(neg, f64, stack), + F32Ceil => arithmetic_single!(ceil, f32, stack), + F64Ceil => arithmetic_single!(ceil, f64, stack), + F32Floor => arithmetic_single!(floor, f32, stack), + F64Floor => arithmetic_single!(floor, f64, stack), + F32Trunc => arithmetic_single!(trunc, f32, stack), + F64Trunc => arithmetic_single!(trunc, f64, stack), + F32Nearest => arithmetic_single!(wasm_nearest, f32, stack), + F64Nearest => arithmetic_single!(wasm_nearest, f64, stack), + F32Sqrt => arithmetic_single!(sqrt, f32, stack), + F64Sqrt => arithmetic_single!(sqrt, f64, stack), + F32Min => arithmetic!(wasm_min, f32, stack), + F64Min => arithmetic!(wasm_min, f64, stack), + F32Max => arithmetic!(wasm_max, f32, stack), + F64Max => arithmetic!(wasm_max, f64, stack), + F32Copysign => arithmetic!(copysign, f32, stack), + F64Copysign => arithmetic!(copysign, f64, stack), + + // no-op instructions since types are erased at runtime + I32ReinterpretF32 => {} + I64ReinterpretF64 => {} + F32ReinterpretI32 => {} + F64ReinterpretI64 => {} + + // unsigned versions of these are a bit broken atm + I32TruncF32S => checked_conv_float!(f32, i32, stack), + I32TruncF64S => checked_conv_float!(f64, i32, stack), + I32TruncF32U => checked_conv_float!(f32, u32, i32, stack), + I32TruncF64U => checked_conv_float!(f64, u32, i32, stack), + I64TruncF32S => checked_conv_float!(f32, i64, stack), + I64TruncF64S => checked_conv_float!(f64, i64, stack), + I64TruncF32U => checked_conv_float!(f32, u64, i64, stack), + I64TruncF64U => checked_conv_float!(f64, u64, i64, stack), + + TableGet(table_index) => { + let table_idx = module.resolve_table_addr(*table_index); + let table = store.get_table(table_idx as usize)?; + let idx = stack.values.pop_t::()? as usize; + let v = table.borrow().get_wasm_val(idx)?; + stack.values.push(v.into()); + } + + TableSet(table_index) => { + let table_idx = module.resolve_table_addr(*table_index); + let table = store.get_table(table_idx as usize)?; + let val = stack.values.pop_t::()?; + let idx = stack.values.pop_t::()? as usize; + table.borrow_mut().set(idx, val)?; + } + + TableSize(table_index) => { + let table_idx = module.resolve_table_addr(*table_index); + let table = store.get_table(table_idx as usize)?; + stack.values.push(table.borrow().size().into()); + } + + TableInit(table_index, elem_index) => { + let table_idx = module.resolve_table_addr(*table_index); + let table = store.get_table(table_idx as usize)?; + + let elem_idx = module.resolve_elem_addr(*elem_index); + let elem = store.get_elem(elem_idx as usize)?; + + if elem.kind != ElementKind::Passive { + return Err(Trap::TableOutOfBounds { offset: 0, len: 0, max: 0 }.into()); + } + + let Some(items) = elem.items.as_ref() else { + return Err(Trap::TableOutOfBounds { offset: 0, len: 0, max: 0 }.into()); + }; + + table.borrow_mut().init(module.func_addrs(), 0, items)?; + } + + I32TruncSatF32S => arithmetic_single!(trunc, f32, i32, stack), + I32TruncSatF32U => arithmetic_single!(trunc, f32, u32, stack), + I32TruncSatF64S => arithmetic_single!(trunc, f64, i32, stack), + I32TruncSatF64U => arithmetic_single!(trunc, f64, u32, stack), + I64TruncSatF32S => arithmetic_single!(trunc, f32, i64, stack), + I64TruncSatF32U => arithmetic_single!(trunc, f32, u64, stack), + I64TruncSatF64S => arithmetic_single!(trunc, f64, i64, stack), + I64TruncSatF64U => arithmetic_single!(trunc, f64, u64, stack), + + i => { + log::error!("unimplemented instruction: {:?}", i); + return Err(Error::UnsupportedFeature(alloc::format!("unimplemented instruction: {:?}", i))); + } + }; + + Ok(ExecResult::Ok) +} diff --git a/crates/tinywasm/src/runtime/interpreter/traits.rs b/crates/tinywasm/src/runtime/interpreter/traits.rs new file mode 100644 index 0000000..06aab2a --- /dev/null +++ b/crates/tinywasm/src/runtime/interpreter/traits.rs @@ -0,0 +1,132 @@ +pub(crate) trait CheckedWrappingRem +where + Self: Sized, +{ + fn checked_wrapping_rem(self, rhs: Self) -> Option; +} + +pub(crate) trait WasmFloatOps { + fn wasm_min(self, other: Self) -> Self; + fn wasm_max(self, other: Self) -> Self; + fn wasm_nearest(self) -> Self; +} + +macro_rules! impl_wasm_float_ops { + ($($t:ty)*) => ($( + impl WasmFloatOps for $t { + // https://webassembly.github.io/spec/core/exec/numerics.html#op-fnearest + fn wasm_nearest(self) -> Self { + match self { + x if x.is_nan() => x, // preserve NaN + x if x.is_infinite() || x == 0.0 => x, // preserve infinities and zeros + x if (0.0..=0.5).contains(&x) => 0.0, + x if (-0.5..0.0).contains(&x) => -0.0, + // x => x.round(), + x => { + // Handle normal and halfway cases + let rounded = x.round(); + let diff = (x - rounded).abs(); + + if diff == 0.5 { + // Halfway case: round to even + if rounded % 2.0 == 0.0 { + rounded // Already even + } else { + rounded - x.signum() // Make even + } + } else { + // Normal case + rounded + } + } + } + } + + // https://webassembly.github.io/spec/core/exec/numerics.html#op-fmin + // Based on f32::minimum (which is not yet stable) + #[inline] + fn wasm_min(self, other: Self) -> Self { + if self < other { + self + } else if other < self { + other + } else if self == other { + if self.is_sign_negative() && other.is_sign_positive() { self } else { other } + } else { + // At least one input is NaN. Use `+` to perform NaN propagation and quieting. + self + other + } + } + + // https://webassembly.github.io/spec/core/exec/numerics.html#op-fmax + // Based on f32::maximum (which is not yet stable) + #[inline] + fn wasm_max(self, other: Self) -> Self { + if self > other { + self + } else if other > self { + other + } else if self == other { + if self.is_sign_negative() && other.is_sign_positive() { other } else { self } + } else { + // At least one input is NaN. Use `+` to perform NaN propagation and quieting. + self + other + } + } + } + )*) +} + +impl_wasm_float_ops! { f32 f64 } + +pub(crate) trait WasmIntOps { + fn wasm_shl(self, rhs: Self) -> Self; + fn wasm_shr(self, rhs: Self) -> Self; + fn wasm_rotl(self, rhs: Self) -> Self; + fn wasm_rotr(self, rhs: Self) -> Self; +} + +macro_rules! impl_wrapping_self_sh { + ($($t:ty)*) => ($( + impl WasmIntOps for $t { + #[inline] + fn wasm_shl(self, rhs: Self) -> Self { + self.wrapping_shl(rhs as u32) + } + + #[inline] + fn wasm_shr(self, rhs: Self) -> Self { + self.wrapping_shr(rhs as u32) + } + + #[inline] + fn wasm_rotl(self, rhs: Self) -> Self { + self.rotate_left(rhs as u32) + } + + #[inline] + fn wasm_rotr(self, rhs: Self) -> Self { + self.rotate_right(rhs as u32) + } + } + )*) +} + +impl_wrapping_self_sh! { i32 i64 u32 u64 } + +macro_rules! impl_checked_wrapping_rem { + ($($t:ty)*) => ($( + impl CheckedWrappingRem for $t { + #[inline] + fn checked_wrapping_rem(self, rhs: Self) -> Option { + if rhs == 0 { + None + } else { + Some(self.wrapping_rem(rhs)) + } + } + } + )*) +} + +impl_checked_wrapping_rem! { i32 i64 u32 u64 } diff --git a/crates/tinywasm/src/runtime/mod.rs b/crates/tinywasm/src/runtime/mod.rs index 610810f..3b9a57c 100644 --- a/crates/tinywasm/src/runtime/mod.rs +++ b/crates/tinywasm/src/runtime/mod.rs @@ -1,19 +1,23 @@ -mod executor; +mod interpreter; mod stack; mod value; pub use stack::*; pub(crate) use value::RawWasmValue; +use crate::Result; + #[allow(rustdoc::private_intra_doc_links)] -/// A WebAssembly Runtime. -/// -/// Generic over `CheckTypes` to enable type checking at runtime. -/// This is useful for debugging, but should be disabled if you know -/// that the module is valid. +/// A WebAssembly runtime. /// /// See +pub trait Runtime { + /// Execute all call-frames on the stack until the stack is empty. + fn exec(&self, store: &mut crate::Store, stack: &mut crate::runtime::Stack) -> Result<()>; +} + +/// The main TinyWasm runtime. /// -/// Execution is implemented in the [`crate::runtime::executor`] module +/// This is the default runtime used by TinyWasm. #[derive(Debug, Default)] -pub struct DefaultRuntime {} +pub struct InterpreterRuntime {} diff --git a/crates/tinywasm/src/runtime/stack/call_stack.rs b/crates/tinywasm/src/runtime/stack/call_stack.rs index 1bcd474..098b918 100644 --- a/crates/tinywasm/src/runtime/stack/call_stack.rs +++ b/crates/tinywasm/src/runtime/stack/call_stack.rs @@ -1,8 +1,11 @@ -use crate::{runtime::RawWasmValue, BlockType, Error, FunctionInstance, LabelFrame, Result, Trap}; +use crate::{ + runtime::{BlockType, RawWasmValue}, + Error, FunctionInstance, Result, Trap, +}; use alloc::{boxed::Box, rc::Rc, vec::Vec}; use tinywasm_types::{ValType, WasmValue}; -use super::blocks::Labels; +use super::{blocks::Labels, LabelFrame}; // minimum call stack size const CALL_STACK_SIZE: usize = 128; diff --git a/crates/tinywasm/src/store.rs b/crates/tinywasm/src/store.rs index aabbf71..4d3e326 100644 --- a/crates/tinywasm/src/store.rs +++ b/crates/tinywasm/src/store.rs @@ -7,8 +7,8 @@ use alloc::{boxed::Box, format, rc::Rc, string::ToString, vec, vec::Vec}; use tinywasm_types::*; use crate::{ - runtime::{self, DefaultRuntime}, - Error, Function, ModuleInstance, RawWasmValue, Result, Trap, + runtime::{self, InterpreterRuntime, RawWasmValue}, + Error, Function, ModuleInstance, Result, Trap, }; // global store id counter @@ -51,9 +51,9 @@ impl Store { } /// Create a new store with the given runtime - pub(crate) fn runtime(&self) -> runtime::DefaultRuntime { + pub(crate) fn runtime(&self) -> runtime::InterpreterRuntime { match self.runtime { - Runtime::Default => DefaultRuntime::default(), + Runtime::Default => InterpreterRuntime::default(), } } } @@ -441,7 +441,7 @@ impl Store { /// A WebAssembly Function Instance /// /// See -pub struct FunctionInstance { +pub(crate) struct FunctionInstance { pub(crate) func: Function, pub(crate) _type_idx: TypeAddr, pub(crate) owner: ModuleInstanceAddr, // index into store.module_instances, none for host functions diff --git a/crates/tinywasm/tests/generated/mvp.csv b/crates/tinywasm/tests/generated/mvp.csv index 23c080d..03241b3 100644 --- a/crates/tinywasm/tests/generated/mvp.csv +++ b/crates/tinywasm/tests/generated/mvp.csv @@ -3,3 +3,4 @@ 0.1.0,17630,2598,[{"name":"address.wast","passed":5,"failed":255},{"name":"align.wast","passed":108,"failed":48},{"name":"binary-leb128.wast","passed":91,"failed":0},{"name":"binary.wast","passed":110,"failed":2},{"name":"block.wast","passed":193,"failed":30},{"name":"br.wast","passed":84,"failed":13},{"name":"br_if.wast","passed":90,"failed":28},{"name":"br_table.wast","passed":25,"failed":149},{"name":"call.wast","passed":29,"failed":62},{"name":"call_indirect.wast","passed":36,"failed":134},{"name":"comments.wast","passed":7,"failed":1},{"name":"const.wast","passed":778,"failed":0},{"name":"conversions.wast","passed":371,"failed":248},{"name":"custom.wast","passed":11,"failed":0},{"name":"data.wast","passed":47,"failed":14},{"name":"elem.wast","passed":50,"failed":49},{"name":"endianness.wast","passed":1,"failed":68},{"name":"exports.wast","passed":92,"failed":4},{"name":"f32.wast","passed":2514,"failed":0},{"name":"f32_bitwise.wast","passed":364,"failed":0},{"name":"f32_cmp.wast","passed":2407,"failed":0},{"name":"f64.wast","passed":2514,"failed":0},{"name":"f64_bitwise.wast","passed":364,"failed":0},{"name":"f64_cmp.wast","passed":2407,"failed":0},{"name":"fac.wast","passed":2,"failed":6},{"name":"float_exprs.wast","passed":761,"failed":139},{"name":"float_literals.wast","passed":163,"failed":0},{"name":"float_memory.wast","passed":6,"failed":84},{"name":"float_misc.wast","passed":437,"failed":4},{"name":"forward.wast","passed":1,"failed":4},{"name":"func.wast","passed":124,"failed":48},{"name":"func_ptrs.wast","passed":10,"failed":26},{"name":"global.wast","passed":51,"failed":59},{"name":"i32.wast","passed":460,"failed":0},{"name":"i64.wast","passed":416,"failed":0},{"name":"if.wast","passed":120,"failed":121},{"name":"imports.wast","passed":74,"failed":109},{"name":"inline-module.wast","passed":1,"failed":0},{"name":"int_exprs.wast","passed":108,"failed":0},{"name":"int_literals.wast","passed":51,"failed":0},{"name":"labels.wast","passed":14,"failed":15},{"name":"left-to-right.wast","passed":1,"failed":95},{"name":"linking.wast","passed":21,"failed":111},{"name":"load.wast","passed":60,"failed":37},{"name":"local_get.wast","passed":32,"failed":4},{"name":"local_set.wast","passed":50,"failed":3},{"name":"local_tee.wast","passed":68,"failed":29},{"name":"loop.wast","passed":93,"failed":27},{"name":"memory.wast","passed":34,"failed":45},{"name":"memory_grow.wast","passed":12,"failed":84},{"name":"memory_redundancy.wast","passed":1,"failed":7},{"name":"memory_size.wast","passed":6,"failed":36},{"name":"memory_trap.wast","passed":2,"failed":180},{"name":"names.wast","passed":485,"failed":1},{"name":"nop.wast","passed":46,"failed":42},{"name":"return.wast","passed":73,"failed":11},{"name":"select.wast","passed":86,"failed":62},{"name":"skip-stack-guard-page.wast","passed":1,"failed":10},{"name":"stack.wast","passed":2,"failed":5},{"name":"start.wast","passed":9,"failed":11},{"name":"store.wast","passed":59,"failed":9},{"name":"switch.wast","passed":2,"failed":26},{"name":"token.wast","passed":58,"failed":0},{"name":"traps.wast","passed":22,"failed":14},{"name":"type.wast","passed":3,"failed":0},{"name":"unreachable.wast","passed":50,"failed":14},{"name":"unreached-invalid.wast","passed":118,"failed":0},{"name":"unwind.wast","passed":35,"failed":15},{"name":"utf8-custom-section-id.wast","passed":176,"failed":0},{"name":"utf8-import-field.wast","passed":176,"failed":0},{"name":"utf8-import-module.wast","passed":176,"failed":0},{"name":"utf8-invalid-encoding.wast","passed":176,"failed":0}] 0.2.0,19344,884,[{"name":"address.wast","passed":181,"failed":79},{"name":"align.wast","passed":156,"failed":0},{"name":"binary-leb128.wast","passed":91,"failed":0},{"name":"binary.wast","passed":112,"failed":0},{"name":"block.wast","passed":220,"failed":3},{"name":"br.wast","passed":97,"failed":0},{"name":"br_if.wast","passed":118,"failed":0},{"name":"br_table.wast","passed":171,"failed":3},{"name":"call.wast","passed":73,"failed":18},{"name":"call_indirect.wast","passed":50,"failed":120},{"name":"comments.wast","passed":7,"failed":1},{"name":"const.wast","passed":778,"failed":0},{"name":"conversions.wast","passed":439,"failed":180},{"name":"custom.wast","passed":11,"failed":0},{"name":"data.wast","passed":47,"failed":14},{"name":"elem.wast","passed":56,"failed":43},{"name":"endianness.wast","passed":29,"failed":40},{"name":"exports.wast","passed":92,"failed":4},{"name":"f32.wast","passed":2514,"failed":0},{"name":"f32_bitwise.wast","passed":364,"failed":0},{"name":"f32_cmp.wast","passed":2407,"failed":0},{"name":"f64.wast","passed":2514,"failed":0},{"name":"f64_bitwise.wast","passed":364,"failed":0},{"name":"f64_cmp.wast","passed":2407,"failed":0},{"name":"fac.wast","passed":6,"failed":2},{"name":"float_exprs.wast","passed":890,"failed":10},{"name":"float_literals.wast","passed":163,"failed":0},{"name":"float_memory.wast","passed":78,"failed":12},{"name":"float_misc.wast","passed":437,"failed":4},{"name":"forward.wast","passed":5,"failed":0},{"name":"func.wast","passed":168,"failed":4},{"name":"func_ptrs.wast","passed":10,"failed":26},{"name":"global.wast","passed":103,"failed":7},{"name":"i32.wast","passed":460,"failed":0},{"name":"i64.wast","passed":416,"failed":0},{"name":"if.wast","passed":231,"failed":10},{"name":"imports.wast","passed":80,"failed":103},{"name":"inline-module.wast","passed":1,"failed":0},{"name":"int_exprs.wast","passed":108,"failed":0},{"name":"int_literals.wast","passed":51,"failed":0},{"name":"labels.wast","passed":26,"failed":3},{"name":"left-to-right.wast","passed":92,"failed":4},{"name":"linking.wast","passed":29,"failed":103},{"name":"load.wast","passed":93,"failed":4},{"name":"local_get.wast","passed":36,"failed":0},{"name":"local_set.wast","passed":53,"failed":0},{"name":"local_tee.wast","passed":93,"failed":4},{"name":"loop.wast","passed":116,"failed":4},{"name":"memory.wast","passed":78,"failed":1},{"name":"memory_grow.wast","passed":91,"failed":5},{"name":"memory_redundancy.wast","passed":8,"failed":0},{"name":"memory_size.wast","passed":35,"failed":7},{"name":"memory_trap.wast","passed":180,"failed":2},{"name":"names.wast","passed":485,"failed":1},{"name":"nop.wast","passed":78,"failed":10},{"name":"return.wast","passed":84,"failed":0},{"name":"select.wast","passed":114,"failed":34},{"name":"skip-stack-guard-page.wast","passed":1,"failed":10},{"name":"stack.wast","passed":7,"failed":0},{"name":"start.wast","passed":11,"failed":9},{"name":"store.wast","passed":68,"failed":0},{"name":"switch.wast","passed":28,"failed":0},{"name":"token.wast","passed":58,"failed":0},{"name":"traps.wast","passed":36,"failed":0},{"name":"type.wast","passed":3,"failed":0},{"name":"unreachable.wast","passed":64,"failed":0},{"name":"unreached-invalid.wast","passed":118,"failed":0},{"name":"unwind.wast","passed":50,"failed":0},{"name":"utf8-custom-section-id.wast","passed":176,"failed":0},{"name":"utf8-import-field.wast","passed":176,"failed":0},{"name":"utf8-import-module.wast","passed":176,"failed":0},{"name":"utf8-invalid-encoding.wast","passed":176,"failed":0}] 0.3.0,20254,0,[{"name":"address.wast","passed":260,"failed":0},{"name":"align.wast","passed":156,"failed":0},{"name":"binary-leb128.wast","passed":91,"failed":0},{"name":"binary.wast","passed":112,"failed":0},{"name":"block.wast","passed":223,"failed":0},{"name":"br.wast","passed":97,"failed":0},{"name":"br_if.wast","passed":118,"failed":0},{"name":"br_table.wast","passed":174,"failed":0},{"name":"call.wast","passed":91,"failed":0},{"name":"call_indirect.wast","passed":170,"failed":0},{"name":"comments.wast","passed":8,"failed":0},{"name":"const.wast","passed":778,"failed":0},{"name":"conversions.wast","passed":619,"failed":0},{"name":"custom.wast","passed":11,"failed":0},{"name":"data.wast","passed":61,"failed":0},{"name":"elem.wast","passed":99,"failed":0},{"name":"endianness.wast","passed":69,"failed":0},{"name":"exports.wast","passed":96,"failed":0},{"name":"f32.wast","passed":2514,"failed":0},{"name":"f32_bitwise.wast","passed":364,"failed":0},{"name":"f32_cmp.wast","passed":2407,"failed":0},{"name":"f64.wast","passed":2514,"failed":0},{"name":"f64_bitwise.wast","passed":364,"failed":0},{"name":"f64_cmp.wast","passed":2407,"failed":0},{"name":"fac.wast","passed":8,"failed":0},{"name":"float_exprs.wast","passed":900,"failed":0},{"name":"float_literals.wast","passed":163,"failed":0},{"name":"float_memory.wast","passed":90,"failed":0},{"name":"float_misc.wast","passed":441,"failed":0},{"name":"forward.wast","passed":5,"failed":0},{"name":"func.wast","passed":172,"failed":0},{"name":"func_ptrs.wast","passed":36,"failed":0},{"name":"global.wast","passed":110,"failed":0},{"name":"i32.wast","passed":460,"failed":0},{"name":"i64.wast","passed":416,"failed":0},{"name":"if.wast","passed":241,"failed":0},{"name":"imports.wast","passed":183,"failed":0},{"name":"inline-module.wast","passed":1,"failed":0},{"name":"int_exprs.wast","passed":108,"failed":0},{"name":"int_literals.wast","passed":51,"failed":0},{"name":"labels.wast","passed":29,"failed":0},{"name":"left-to-right.wast","passed":96,"failed":0},{"name":"linking.wast","passed":132,"failed":0},{"name":"load.wast","passed":97,"failed":0},{"name":"local_get.wast","passed":36,"failed":0},{"name":"local_set.wast","passed":53,"failed":0},{"name":"local_tee.wast","passed":97,"failed":0},{"name":"loop.wast","passed":120,"failed":0},{"name":"memory.wast","passed":79,"failed":0},{"name":"memory_grow.wast","passed":96,"failed":0},{"name":"memory_redundancy.wast","passed":8,"failed":0},{"name":"memory_size.wast","passed":42,"failed":0},{"name":"memory_trap.wast","passed":182,"failed":0},{"name":"names.wast","passed":486,"failed":0},{"name":"nop.wast","passed":88,"failed":0},{"name":"return.wast","passed":84,"failed":0},{"name":"select.wast","passed":148,"failed":0},{"name":"skip-stack-guard-page.wast","passed":11,"failed":0},{"name":"stack.wast","passed":7,"failed":0},{"name":"start.wast","passed":20,"failed":0},{"name":"store.wast","passed":68,"failed":0},{"name":"switch.wast","passed":28,"failed":0},{"name":"table.wast","passed":19,"failed":0},{"name":"token.wast","passed":58,"failed":0},{"name":"traps.wast","passed":36,"failed":0},{"name":"type.wast","passed":3,"failed":0},{"name":"unreachable.wast","passed":64,"failed":0},{"name":"unreached-invalid.wast","passed":118,"failed":0},{"name":"unreached-valid.wast","passed":7,"failed":0},{"name":"unwind.wast","passed":50,"failed":0},{"name":"utf8-custom-section-id.wast","passed":176,"failed":0},{"name":"utf8-import-field.wast","passed":176,"failed":0},{"name":"utf8-import-module.wast","passed":176,"failed":0},{"name":"utf8-invalid-encoding.wast","passed":176,"failed":0}] +0.3.0-alpha.0,20254,0,[{"name":"address.wast","passed":260,"failed":0},{"name":"align.wast","passed":156,"failed":0},{"name":"binary-leb128.wast","passed":91,"failed":0},{"name":"binary.wast","passed":112,"failed":0},{"name":"block.wast","passed":223,"failed":0},{"name":"br.wast","passed":97,"failed":0},{"name":"br_if.wast","passed":118,"failed":0},{"name":"br_table.wast","passed":174,"failed":0},{"name":"call.wast","passed":91,"failed":0},{"name":"call_indirect.wast","passed":170,"failed":0},{"name":"comments.wast","passed":8,"failed":0},{"name":"const.wast","passed":778,"failed":0},{"name":"conversions.wast","passed":619,"failed":0},{"name":"custom.wast","passed":11,"failed":0},{"name":"data.wast","passed":61,"failed":0},{"name":"elem.wast","passed":99,"failed":0},{"name":"endianness.wast","passed":69,"failed":0},{"name":"exports.wast","passed":96,"failed":0},{"name":"f32.wast","passed":2514,"failed":0},{"name":"f32_bitwise.wast","passed":364,"failed":0},{"name":"f32_cmp.wast","passed":2407,"failed":0},{"name":"f64.wast","passed":2514,"failed":0},{"name":"f64_bitwise.wast","passed":364,"failed":0},{"name":"f64_cmp.wast","passed":2407,"failed":0},{"name":"fac.wast","passed":8,"failed":0},{"name":"float_exprs.wast","passed":900,"failed":0},{"name":"float_literals.wast","passed":163,"failed":0},{"name":"float_memory.wast","passed":90,"failed":0},{"name":"float_misc.wast","passed":441,"failed":0},{"name":"forward.wast","passed":5,"failed":0},{"name":"func.wast","passed":172,"failed":0},{"name":"func_ptrs.wast","passed":36,"failed":0},{"name":"global.wast","passed":110,"failed":0},{"name":"i32.wast","passed":460,"failed":0},{"name":"i64.wast","passed":416,"failed":0},{"name":"if.wast","passed":241,"failed":0},{"name":"imports.wast","passed":183,"failed":0},{"name":"inline-module.wast","passed":1,"failed":0},{"name":"int_exprs.wast","passed":108,"failed":0},{"name":"int_literals.wast","passed":51,"failed":0},{"name":"labels.wast","passed":29,"failed":0},{"name":"left-to-right.wast","passed":96,"failed":0},{"name":"linking.wast","passed":132,"failed":0},{"name":"load.wast","passed":97,"failed":0},{"name":"local_get.wast","passed":36,"failed":0},{"name":"local_set.wast","passed":53,"failed":0},{"name":"local_tee.wast","passed":97,"failed":0},{"name":"loop.wast","passed":120,"failed":0},{"name":"memory.wast","passed":79,"failed":0},{"name":"memory_grow.wast","passed":96,"failed":0},{"name":"memory_redundancy.wast","passed":8,"failed":0},{"name":"memory_size.wast","passed":42,"failed":0},{"name":"memory_trap.wast","passed":182,"failed":0},{"name":"names.wast","passed":486,"failed":0},{"name":"nop.wast","passed":88,"failed":0},{"name":"return.wast","passed":84,"failed":0},{"name":"select.wast","passed":148,"failed":0},{"name":"skip-stack-guard-page.wast","passed":11,"failed":0},{"name":"stack.wast","passed":7,"failed":0},{"name":"start.wast","passed":20,"failed":0},{"name":"store.wast","passed":68,"failed":0},{"name":"switch.wast","passed":28,"failed":0},{"name":"table.wast","passed":19,"failed":0},{"name":"token.wast","passed":58,"failed":0},{"name":"traps.wast","passed":36,"failed":0},{"name":"type.wast","passed":3,"failed":0},{"name":"unreachable.wast","passed":64,"failed":0},{"name":"unreached-invalid.wast","passed":118,"failed":0},{"name":"unreached-valid.wast","passed":7,"failed":0},{"name":"unwind.wast","passed":50,"failed":0},{"name":"utf8-custom-section-id.wast","passed":176,"failed":0},{"name":"utf8-import-field.wast","passed":176,"failed":0},{"name":"utf8-import-module.wast","passed":176,"failed":0},{"name":"utf8-invalid-encoding.wast","passed":176,"failed":0}] -- cgit v1.3.1