diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 422dbb59..9405d380 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,62 +1,74 @@ # TinyWasm Architecture -TinyWasm follows the general runtime model described in the [WebAssembly specification](https://webassembly.github.io/spec/core/exec/runtime.html), but lowers validated WebAssembly into a compact internal instruction format before execution. +TinyWasm follows the general runtime model described in the [WebAssembly specification](https://webassembly.github.io/spec/core/exec/runtime.html). It is a stack-based interpreter with a compact internal bytecode, width-specific value stacks, and configurable linear-memory backends. -## Runtime Layout +## Execution Pipeline -- Values are stored in untyped stacks: - - `stack_32` for `i32`, `f32`, `funcref`, and `externref` - - `stack_64` for `i64` and `f64` - - `stack_128` for `v128` -- Locals are stored directly in the value stacks. Each `CallFrame` stores a `locals_base`, and local instructions index from that base. -- Structured control flow (`block`, `loop`, `if`, `br*`) is lowered during parsing to jump-oriented internal instructions such as `Jump`, `JumpIfZero`, `BranchTable*`, `DropKeep*`, and `Return`. -- Execution is a single iterative interpreter loop over the lowered instruction stream. +TinyWasm does not execute WebAssembly instructions directly. Parsing lowers them into an internal bytecode designed to make execution simpler and cheaper: -## Internal Bytecode +- structured control flow (`block`, `loop`, `if`, and `br*`) becomes jump-oriented instructions such as `Jump`, `JumpIfZero`, `BranchTable*`, `DropKeep*`, and `Return` +- operand widths are encoded in instruction variants, and branch stack reshaping is explicit +- instructions retain compact module-local indexes, which each instance maps to Store-wide runtime addresses +- when enabled, the optimizer applies local rewrites, including superinstruction fusion, specialized calls and returns, and redundant-instruction removal +- modules can be serialized as `.twasm` archives containing this lowered representation +- execution uses a single iterative dispatch loop over the resulting instruction stream -TinyWasm does not interpret WebAssembly instructions directly. During parsing and validation, WebAssembly is translated into TinyWasm's internal bytecode format. +## Value Stacks -This internal representation is designed to make execution simpler and cheaper: +WebAssembly combines an operand stack with function-scoped locals. TinyWasm stores both in the same width-specific physical stacks: -- structured control flow is resolved ahead of time -- stack effects are made explicit -- common instruction sequences can be fused into superinstructions -- modules can optionally be serialized as `.twasm` for reuse +- `stack_32` for `i32`, `f32`, and reference values, including GC and exception references +- `stack_64` for `i64` and `f64` +- `stack_128` for `v128` -## Optimizer +The interpreter does not maintain a runtime type stack or tag individual stack slots. Lowered instructions encode the physical lane they operate on, while WebAssembly validation guarantees type correctness. Splitting values by width lets each value use its natural storage size, reducing stack memory and the data moved by common operations. -During parsing, a peephole optimizer (`optimize.rs`) fuses common instruction sequences into superinstructions. These reduce interpreter dispatch overhead by combining multiple logical operations into one internal instruction. +Locals are stored directly in these stacks. Each `CallFrame` records a base for every lane, and lowered local instructions index from those bases. The value stacks and call stack can use either a fixed capacity or dynamic initial and maximum sizes. Dynamic stacks keep the initial allocation small, grow when needed, and retain a hard limit. -Examples include: +## Interpreter Optimization -- **Fused binops**: `BinOpLocalLocal*`, `BinOpLocalConst*`, `BinOpStackGlobal*` - Combine local/global access, a binary operation, and sometimes a store/tee. -- **Fused jumps**: `JumpCmpLocalConst*`, `JumpCmpLocalLocal*`, `JumpCmpStackConst*` - Combine comparison and conditional branch logic. +Instruction dispatch is one of the interpreter's main costs. TinyWasm reduces it through superinstructions and by shaping the large Rust dispatch match based on benchmarks and assembly inspection. Most simple arithmetic remains directly in the interpreter loop. Small, frequently used stack, value, and global operations use `#[inline]` or `#[inline(always)]` where measurements show a benefit, while unlikely error paths use `core::hint::cold_path()`. + +Superinstructions also reduce value-stack traffic. They can read locals, globals, and constants directly, perform an operation, and write `set` or `tee` destinations without materializing intermediate operand-stack values. Examples include: + +- fused binary operations such as `BinOpLocalLocal*`, `BinOpLocalConst*`, and `BinOpStackGlobal*` +- fused conditional branches such as `JumpCmpLocalConst*`, `JumpCmpLocalLocal*`, and `JumpCmpStackConst*` + +The default runtime remains safe Rust throughout rather than relying on unchecked operations. + +## SIMD + +SIMD instructions have a portable safe-Rust implementation built from fixed-size arrays and lane operations, relying on the compiler to auto-vectorize where possible. Generated code is inspected with `cargo asm`, and benchmarks determine where architecture-specific alternatives are worthwhile. WebAssembly targets use native SIMD intrinsics where available, while the optional `simd-x86` feature provides selected x86 implementations for operations where the generic code produces worse results. ## Memory Backends Linear memory is implemented through the `LinearMemory` trait. The backend is selected with `engine::Config::with_memory_backend()`. +`LinearMemory` exposes separate fixed-width read and write methods for 8-, 16-, 32-, 64-, and 128-bit accesses. A const-generic method would not be callable through a `dyn LinearMemory` trait object, so each width is an explicit vtable entry that backends can optimize independently. + +This flexibility has a measurable cost: guest loads and stores cross the `dyn LinearMemory` boundary, adding an indirect call and generally preventing the backend operation from being inlined into the interpreter. The fixed-width methods keep the work behind that boundary as small and specialized as possible. + Available backends: -- `VecMemory` - contiguous `Vec` backing; the default backend. -- `PagedMemory` - chunk-based allocation, useful when growing memory without reallocating one large buffer. -- `LazyLinearMemory` - wraps another backend and allocates memory on first access. +- `VecMemory` - contiguous `Vec` backing and the default backend. +- `PagedMemory` - sparse chunk-based allocation, with untouched chunks left unallocated and growth avoiding relocation of one contiguous buffer. +- `LazyLinearMemory` - serves zero-filled reads without allocation and creates the configured backend on the first mutation or growth. - Custom backends through `MemoryBackend::custom()`. +`VecMemory` growth may reallocate, though operating-system allocators can often grow page-backed allocations without copying the full buffer. Applications on conventional operating systems should generally keep it unless sparse allocation or non-relocating growth is specifically needed. Bounded dynamic stacks and sparse paged memory trade some runtime overhead for a smaller initial footprint on embedded and other resource-constrained systems. + ## Future Experiments -TinyWasm's interpreter is intentionally simple today: validated WebAssembly is lowered to internal instructions, optimized with peephole fusion, and executed by an iterative dispatch loop. +Future work may explore additional dispatch and code-generation strategies, including Rust's experimental `loop_match` state-machine work, a tail-call-based interpreter once Rust's explicit tail-call support matures, more aggressive superinstruction fusion, top-of-stack register allocation, or optional JIT compilation. -Future work may explore additional dispatch and code-generation strategies, including Rust's experimental `loop_match` state-machine work, explicit tail calls, more aggressive superinstruction fusion, top-of-stack register allocation, or even optional JIT compilation. +For conventional operating systems, a future `mmap`-based memory backend could reserve virtual address space and use guard pages to move more bounds enforcement to the operating system, reducing explicit checks in linear-memory hot paths. This is the same broad approach described in [Wasmtime's linear-memory architecture](https://docs.wasmtime.dev/contributing-architecture.html#linear-memory), where virtual-memory reservations and guard regions eliminate or deduplicate explicit bounds checks. -## Code Map +## Important Modules -- [visit.rs](./crates/parser/src/visit.rs) - WebAssembly binary visitor +- [visit.rs](./crates/parser/src/visit.rs) - function-body operator lowering - [optimize.rs](./crates/parser/src/optimize.rs) - peephole optimizer and superinstruction fusion -- [parallel.rs](./crates/parser/src/parallel.rs) - multithreaded function parsing +- [parallel.rs](./crates/parser/src/parallel.rs) - parallel function parsing - [instructions.rs](./crates/types/src/instructions.rs) - internal instruction set -- [value_stack.rs](./crates/tinywasm/src/interpreter/stack/value_stack.rs) - typed value stacks +- [value_stack.rs](./crates/tinywasm/src/interpreter/stack/value_stack.rs) - width-specific stacks - [call_stack.rs](./crates/tinywasm/src/interpreter/stack/call_stack.rs) - call frame stack - [memory/mod.rs](./crates/tinywasm/src/store/memory/mod.rs) - memory backend trait and implementations diff --git a/CHANGELOG.md b/CHANGELOG.md index 79fb6b03..3427a802 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,19 +10,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added support for the WebAssembly function-references proposal +- Added basic support for the WebAssembly garbage-collection proposal +- Added support for the WebAssembly exception-handling proposal, including tags, `try_table`, `throw`, and `throw_ref` +- Added support for the WebAssembly compact-imports proposal - Added `WasmValue::ty` and `WasmValue::matches_type` +- Added `ValueLane` for mapping WebAssembly value types to their physical 32-bit, 64-bit, or 128-bit storage lane. +- Added a `validate` feature to `tinywasm` and `tinywasm-parser` (enabled by default) to optionally skip wasmparser validation for faster parsing of trusted modules. ### Changed -- Function types are now stored separately and resolved through `Function::ty(&Store)`. +- `HostFunction` is now a reusable definition, and module instantiation borrows `Imports` so host imports can be shared across stores. +- Host function callbacks now require `Send + Sync` so the same definition can be used safely with multiple stores. +- Typed function tuples now support up to 20 parameters or results. `WasmTupleChain` is deprecated. Use untyped functions for larger signatures. +- Module types now use one dense recursive type space, while function types are resolved through `Function::ty(&Store)`. +- Globals are stored in separate 32-bit, 64-bit, and 128-bit value lanes, avoiding tagged value conversion during guest execution. +- `LinearMemory` mutation methods and custom memory-backend factories now return `Trap` errors so lazy backend failures can be propagated. ### Fixed +- Directly defined imports now reject handles from a different `Store`. - Tail calls to host functions now return directly to the caller frame. +- Fixed Memory64 bulk-memory operations and optimized stores using the wrong value-stack lane. +- Fixed `memory.init` bounds checks, operand lowering, and local-memory allocation analysis. +- Fixed Memory64 default limits and host-size handling, including 32-bit targets. ### Breaking Changes -- Renamed `ModuleInstanceAddr` to `ModuleInstanceId` for consistency. +- `HostFunction::from` and `HostFunction::from_untyped` no longer take a `Store` and now return reusable `HostFunction` definitions +- `ModuleInstance::instantiate` and `instantiate_no_start` now borrow `Imports`. +- `Store::id` now returns `u32` instead of `usize`. +- `Parser::new` now takes `ParserOptions`, use `Parser::default()` for default settings. `Parser::with_options` was removed. +- Renamed `ModuleInstanceAddr` to `ModuleInstanceId`. - Removed `HostFunction::ty` and `WasmFunction::ty`. Use `Function::ty(&Store)` for runtime function types. - Changed `TableType::element_type` and `Element::ty` from `WasmType` to `RefType`, and replaced module `table_types` with `TableDefinition { ty, init }`. diff --git a/Cargo.lock b/Cargo.lock index 680b8fbd..b06f70fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] @@ -76,6 +76,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + [[package]] name = "assert_cmd" version = "2.2.2" @@ -105,9 +111,9 @@ checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "bstr" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" dependencies = [ "memchr", "regex-automata", @@ -128,9 +134,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e" dependencies = [ "find-msvc-tools", "shlex", @@ -171,9 +177,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.5" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" dependencies = [ "clap_builder", "clap_derive", @@ -181,9 +187,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.5" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" dependencies = [ "anstream", "anstyle", @@ -193,9 +199,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.6.8" +version = "4.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f84a88507dbd05c695f2cb5e8558e747179134005e9893882dec964190ed89" +checksum = "3be2ad0423bdbbb0e25bc89add796f3559706d4a95e1bc98e4d9662a957b6a19" dependencies = [ "clap", ] @@ -351,16 +357,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "eyre" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" -dependencies = [ - "indenter", - "once_cell", -] - [[package]] name = "fastrand" version = "2.5.0" @@ -369,9 +365,9 @@ checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" [[package]] name = "float-cmp" @@ -447,12 +443,6 @@ dependencies = [ "quote", ] -[[package]] -name = "indenter" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" - [[package]] name = "indexmap" version = "2.14.0" @@ -690,9 +680,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.16" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -840,18 +830,18 @@ checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "thiserror" -version = "2.0.19" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.19" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", @@ -872,13 +862,8 @@ dependencies = [ name = "tinywasm" version = "0.11.0-pre.0" dependencies = [ - "criterion", - "eyre", "libm", "log", - "owo-colors", - "serde", - "serde_json", "tinywasm-cli", "tinywasm-parser", "tinywasm-types", @@ -891,16 +876,19 @@ name = "tinywasm-cli" version = "0.11.0-pre.0" dependencies = [ "anstream", + "anyhow", "assert_cmd", "clap", "clap_complete", - "eyre", "log", "owo-colors", "predicates", "pretty_env_logger", + "serde", + "serde_json", "tempfile", "tinywasm", + "wasm-testsuite", "wast", "wat", ] @@ -918,9 +906,11 @@ dependencies = [ name = "tinywasm-root" version = "0.11.0-pre.0" dependencies = [ - "eyre", + "anyhow", + "criterion", "pretty_env_logger", "tinywasm", + "tinywasm-parser", "wat", ] @@ -972,9 +962,9 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.255.0" +version = "0.256.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b524283fb5df62eec102ed0574838961bdd7ba5ac9c50d38e2756c51c971a42" +checksum = "ec1492381bfd5ea51c2a99a919b676662559925cb8d7490547ec2e14c1ad3eb1" dependencies = [ "leb128fmt", "wasmparser", @@ -982,9 +972,9 @@ dependencies = [ [[package]] name = "wasm-testsuite" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fd62746cfcc437a1bdadbe75bf29df08d5a3ae67336e4ac6cea37e0194fce47" +checksum = "9d451d78ea8b19bbed461065802e8797edf21cfb6aa2e9cbeea0aede114f8fb4" dependencies = [ "include_dir", "wast", @@ -992,9 +982,9 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.255.0" +version = "0.256.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8e329ef4b5d46e73b91d3ac6924417cad55a8cbbf869c199283383427c3320b" +checksum = "60bd825ffedc6cba8a642924ba7ae424afbc47811cffbcb7b92031ec24e59b4c" dependencies = [ "bitflags", "indexmap", @@ -1003,9 +993,9 @@ dependencies = [ [[package]] name = "wast" -version = "255.0.0" +version = "256.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ffec530f199bd3d553ac442c13dd108353cad533cad8514bc41e1f1f0fe686" +checksum = "a3ad42723fc9222da007f05812a3249c9a4ee7af79d497fc2a2079579ad7efe6" dependencies = [ "bumpalo", "leb128fmt", @@ -1016,9 +1006,9 @@ dependencies = [ [[package]] name = "wat" -version = "1.255.0" +version = "1.256.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dda82c82e1486c7eed42a0465e544d80fff37abc3b39482e0d34dbaabe2fe5b1" +checksum = "37cc86c54d8011b3202e265bfebada440733ea3a788ffaf46d395f7d2fdeacfb" dependencies = [ "wast", ] @@ -1071,18 +1061,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.55" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.55" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index e57b6ea5..7ea51814 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,35 +21,52 @@ categories = ["compilers", "embedded", "no-std", "virtualization", "wasm"] [workspace.dependencies] tinywasm = { path = "crates/tinywasm", version = "0.11.0-pre.0", default-features = false } -tinywasm-cli = { path = "crates/cli", version = "0.11.0-pre.0", default-features = false } tinywasm-parser = { path = "crates/parser", version = "0.11.0-pre.0", default-features = false } tinywasm-types = { path = "crates/types", version = "0.11.0-pre.0", default-features = false } -eyre = "0.6" -indexmap = "2.14" +anyhow = "1.0" log = "0.4" owo-colors = { version = "4.3" } pretty_env_logger = "0.5" serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0" } wasm-testsuite = { version = "0.7" } -wasmparser = { version = "0.255", default-features = false } -wast = "255" -wat = "1.255" +wasmparser = { version = "0.256", default-features = false } +wast = "256" +wat = "1.256" -criterion = { version = "0.8", default-features = false, features = [ - "cargo_bench_support", - "rayon" -] } +criterion = { version = "0.8", default-features = false, features = ["cargo_bench_support", "rayon"] } [[example]] name = "wasm-rust" test = false +[[bench]] +harness = false +name = "argon2id" + +[[bench]] +harness = false +name = "fibonacci" + +[[bench]] +harness = false +name = "tinywasm" + +[[bench]] +harness = false +name = "tinywasm_modes" + +[[bench]] +harness = false +name = "memory_backends" + [dev-dependencies] -eyre.workspace = true +anyhow.workspace = true +criterion.workspace = true pretty_env_logger.workspace = true tinywasm = { path = "crates/tinywasm" } +tinywasm-parser.workspace = true wat.workspace = true [profile.bench] diff --git a/README.md b/README.md index 77ade909..3d3bc4c0 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,13 @@ +> [!WARNING] +> This is the `next` branch and contains unreleased changes. See [`v0.10.0`](https://github.com/explodingcamera/tinywasm/tree/v0.10.0) for the latest released version. + # `tinywasm` [![docs.rs](https://img.shields.io/docsrs/tinywasm?logo=rust&style=flat-square)](https://docs.rs/tinywasm) [![Crates.io](https://img.shields.io/crates/v/tinywasm.svg?logo=rust&style=flat-square)](https://crates.io/crates/tinywasm) [![Crates.io](https://img.shields.io/crates/l/tinywasm.svg?style=flat-square)](./LICENSE-APACHE) ## Why `tinywasm`? -- **Tiny**: Small by design, without significantly compromising performance or functionality. +- **Tiny**: Small by design, while still passing the full WebAssembly 3.0 core testsuite. - **Portable**: Runs anywhere Rust can target, supports `no_std`, has minimal dependencies, and can itself compile to WebAssembly. - **Safe**: Written in safe Rust, with optional `unsafe` limited to the `simd-x86` feature. Its sandbox is designed to prevent untrusted Wasm from accessing host memory or escaping the runtime. @@ -39,59 +42,62 @@ assert_eq!(result, 3); See the [examples](./examples) directory and [documentation](https://docs.rs/tinywasm) for more information. +## Precompiled Modules + +TinyWasm modules can be compiled to the internal `twasm` bytecode format, which stores the optimized instruction representation for faster loading and reuse. + ## Cargo Features - **`std`:** Enables `std` and parsing from files and streams. Enabled by default. - **`log`:** Enables integration with the `log` crate. Enabled by default. - **`parser`:** Enables `tinywasm-parser` and top-level parse helpers. Enabled by default. +- **`validate`:** Enables WebAssembly validation while parsing. Enabled by default and configurable through `ParserOptions`. - **`archive`:** Enables serialization and deserialization of the internal `twasm` format. Enabled by default. - **`canonicalize-nans`:** Canonicalizes NaN values. Enabled by default. - **`debug`:** Derives `Debug` for runtime types. Enabled by default. -- **`parallel-parser`:** Parallelizes function parsing and validation when `std` is enabled. Enabled by default. +- **`parallel-parser`:** Parallelizes function parsing when `std` is enabled. Enabled by default. - **`guest-debug`:** Exposes module-internal by-index inspection APIs (`*_by_index`). - **`simd-x86`:** Enables x86-specific SIMD intrinsics and uses `unsafe` internally. With default features disabled, `tinywasm` depends only on `core`, `alloc`, and `libm`[^libm], making it usable in `no_std + alloc` environments. -Use `Engine` and `engine::Config` when you need non-default runtime settings such as fuel accounting, stack sizing, memory backend selection, or trap-on-OOM behavior. +Use `Engine` and `engine::Config` when you need non-default runtime settings such as fuel accounting, stack sizing, memory backend selection, the GC collection threshold, or trap-on-OOM behavior. [^libm]: [rust-lang/rust#137578](https://github.com/rust-lang/rust/issues/137578) — tracking issue for floating-point math support in `no_std`. -## Current Status - -`tinywasm` passes the WebAssembly MVP and WebAssembly 2.0 core testsuites and supports the [Lime1](https://github.com/WebAssembly/tool-conventions/blob/main/Lime.md#lime1) interoperability target. WebAssembly 3.0 support is still in progress, and some newer proposal suites are tracked in-repo as experimental coverage rather than release guarantees; see [Supported Proposals](#supported-proposals) for details. - -TinyWasm also has its own internal bytecode format, `twasm`. WebAssembly modules can be compiled to `twasm`, which stores TinyWasm's optimized instruction representation for faster loading and reuse. - ## Safety -TinyWasm only uses safe Rust by default. The optional `simd-x86` feature enables x86-specific SIMD intrinsics and uses `unsafe` internally. WebAssembly input is validated by default and runs inside a sandbox: untrusted Wasm should not be able to access host memory, escape the sandbox, or cause undefined behavior in the runtime. Validation should only be disabled for trusted input. +TinyWasm only uses safe Rust by default. The optional `simd-x86` feature enables x86-specific SIMD intrinsics and uses `unsafe` internally. WebAssembly input is validated by default through the `validate` feature. Disabling validation should not let Wasm access host memory or escape the sandbox, but malformed input may panic or otherwise crash the process, so only disable it for trusted input. The internal `twasm` bytecode format is not currently validated as an untrusted input format. Malformed `twasm` may panic, but should not compromise memory safety or allow sandbox escape. Only run trusted `twasm` bytecode, or generate it through TinyWasm from Wasm input. ## Supported Proposals -| Proposal | Status | `tinywasm` Version | -| --------------------------------------------------------------------------------------------------------------------------------------- | ------ | ------------------ | -| [**Multi-value**](https://github.com/WebAssembly/spec/blob/master/proposals/multi-value/Overview.md) | 🟢 | 0.2.0 | -| [**Mutable Globals**](https://github.com/WebAssembly/mutable-global/blob/master/proposals/mutable-global/Overview.md) | 🟢 | 0.2.0 | -| [**Non-trapping float-to-int Conversion**](https://github.com/WebAssembly/nontrapping-float-to-int-conversions) | 🟢 | 0.2.0 | -| [**Sign-extension operators**](https://github.com/WebAssembly/sign-extension-ops) | 🟢 | 0.2.0 | -| [**Bulk Memory Operations**](https://github.com/WebAssembly/spec/blob/master/proposals/bulk-memory-operations/Overview.md) | 🟢 | 0.4.0 | -| [**Reference Types**](https://github.com/WebAssembly/reference-types/blob/master/proposals/reference-types/Overview.md) | 🟢 | 0.7.0 | -| [**Multi-memory**](https://github.com/WebAssembly/multi-memory/blob/master/proposals/multi-memory/Overview.md) | 🟢 | 0.8.0 | -| [**Custom Page Sizes**](https://github.com/WebAssembly/custom-page-sizes/blob/main/proposals/custom-page-sizes/Overview.md) | 🟢 | 0.9.0 | -| [**Extended Const**](https://github.com/WebAssembly/extended-const/blob/main/proposals/extended-const/Overview.md) | 🟢 | 0.9.0 | -| [**Fixed-Width SIMD**](https://github.com/WebAssembly/simd/blob/main/proposals/simd/Overview.md) | 🟢 | 0.9.0 | -| [**Memory64**](https://github.com/WebAssembly/memory64/blob/master/proposals/memory64/Overview.md) | 🟢 | 0.9.0 | -| [**Tail Call**](https://github.com/WebAssembly/tail-call/blob/main/proposals/tail-call/Overview.md) | 🟢 | 0.9.0 | -| [**Relaxed SIMD**](https://github.com/WebAssembly/relaxed-simd/blob/main/proposals/relaxed-simd/Overview.md) | 🟢 | 0.9.0 | -| [**Wide Arithmetic**](https://github.com/WebAssembly/wide-arithmetic/blob/main/proposals/wide-arithmetic/Overview.md) | 🟢 | 0.9.0 | -| [**Typed Function References**](https://github.com/WebAssembly/function-references/blob/main/proposals/function-references/Overview.md) | 🚧 | `next` | -| [**Exception Handling**](https://github.com/WebAssembly/exception-handling/blob/main/proposals/exception-handling/Exceptions.md) | 🌑 | - | -| [**Garbage Collection**](https://github.com/WebAssembly/gc/blob/main/proposals/gc/Overview.md) | 🌑 | - | -| [**Stack Switching**](https://github.com/WebAssembly/stack-switching/blob/main/proposals/stack-switching/Explainer.md) | 🌑 | - | -| [**Threads**](https://github.com/WebAssembly/threads/blob/main-legacy/proposals/threads/Overview.md) | 🌑 | - | +TinyWasm targets non-JavaScript core proposals through [phase 3](https://github.com/WebAssembly/proposals). JavaScript integrations and optional embedding or tooling APIs are not included here. + +| Proposal | Status | `tinywasm` Version | +| ---------------------------------------------------------------------------------------------------------------- | ------ | ------------------ | +| [**Import/Export of Mutable Globals**](https://github.com/WebAssembly/mutable-global) | 🟢 | 0.2.0 | +| [**Multi-value**](https://github.com/WebAssembly/multi-value) | 🟢 | 0.2.0 | +| [**Non-trapping Float-to-int Conversions**](https://github.com/WebAssembly/nontrapping-float-to-int-conversions) | 🟢 | 0.2.0 | +| [**Sign-extension Operators**](https://github.com/WebAssembly/sign-extension-ops) | 🟢 | 0.2.0 | +| [**Bulk Memory Operations**](https://github.com/WebAssembly/bulk-memory-operations) | 🟢 | 0.4.0 | +| [**Reference Types**](https://github.com/WebAssembly/reference-types) | 🟢 | 0.7.0 | +| [**Fixed-width SIMD**](https://github.com/WebAssembly/simd) | 🟢 | 0.9.0 | +| [**Tail Calls**](https://github.com/WebAssembly/tail-call) | 🟢 | 0.9.0 | +| [**Extended Constant Expressions**](https://github.com/WebAssembly/extended-const) | 🟢 | 0.9.0 | +| [**Multiple Memories**](https://github.com/WebAssembly/multi-memory) | 🟢 | 0.8.0 | +| [**Relaxed SIMD**](https://github.com/WebAssembly/relaxed-simd) | 🟢 | 0.9.0 | +| [**Custom Annotation Syntax**](https://github.com/WebAssembly/annotations) | 🟢 | 0.8.0 | +| [**Memory64**](https://github.com/WebAssembly/memory64) | 🟢 | 0.9.0 | +| [**Wide Arithmetic**](https://github.com/WebAssembly/wide-arithmetic) | 🟢 | 0.9.0 | +| [**Custom Page Sizes**](https://github.com/WebAssembly/custom-page-sizes) | 🟢 | 0.9.0 | +| [**Typed Function References**](https://github.com/WebAssembly/function-references) | 🟢 | `next` | +| [**Garbage Collection**](https://github.com/WebAssembly/gc) | 🟢 | `next` | +| [**Exception Handling**](https://github.com/WebAssembly/exception-handling) | 🟢 | `next` | +| [**Stack Switching**](https://github.com/WebAssembly/stack-switching) | 🌑 | - | +| [**Compact Import Section**](https://github.com/WebAssembly/compact-import-section) | 🌑 | - | +| [**Threads**](https://github.com/WebAssembly/threads) | 🌑 | - | **Legend**\ 🌑 -- not available\ diff --git a/crates/tinywasm/benches/argon2id.rs b/benches/argon2id.rs similarity index 88% rename from crates/tinywasm/benches/argon2id.rs rename to benches/argon2id.rs index dbd86e50..3121062a 100644 --- a/crates/tinywasm/benches/argon2id.rs +++ b/benches/argon2id.rs @@ -1,12 +1,11 @@ use criterion::{Criterion, criterion_group, criterion_main}; -use eyre::Result; -use tinywasm::{ModuleInstance, Store, types}; +use tinywasm::{ModuleInstance, Result, Store, types}; use types::Module; -const WASM: &[u8] = include_bytes!("../../../examples/rust/out/argon2id.wasm"); +const WASM: &[u8] = include_bytes!("../examples/rust/out/argon2id.wasm"); fn argon2id_parse() -> Result { - let parser = tinywasm_parser::Parser::new(); + let parser = tinywasm_parser::Parser::default(); let data = parser.parse_module_bytes(WASM)?; Ok(data) } diff --git a/crates/tinywasm/benches/fibonacci.rs b/benches/fibonacci.rs similarity index 89% rename from crates/tinywasm/benches/fibonacci.rs rename to benches/fibonacci.rs index e0737da4..51fd2d39 100644 --- a/crates/tinywasm/benches/fibonacci.rs +++ b/benches/fibonacci.rs @@ -1,12 +1,11 @@ use criterion::{Criterion, criterion_group, criterion_main}; -use eyre::Result; -use tinywasm::{ModuleInstance, Store, types}; +use tinywasm::{ModuleInstance, Result, Store, types}; use types::Module; -const WASM: &[u8] = include_bytes!("../../../examples/rust/out/fibonacci.wasm"); +const WASM: &[u8] = include_bytes!("../examples/rust/out/fibonacci.wasm"); fn fibonacci_parse() -> Result { - let parser = tinywasm_parser::Parser::new(); + let parser = tinywasm_parser::Parser::default(); let data = parser.parse_module_bytes(WASM)?; Ok(data) } diff --git a/crates/tinywasm/benches/memory_backends.rs b/benches/memory_backends.rs similarity index 99% rename from crates/tinywasm/benches/memory_backends.rs rename to benches/memory_backends.rs index 3a59f8c8..d7b8703f 100644 --- a/crates/tinywasm/benches/memory_backends.rs +++ b/benches/memory_backends.rs @@ -78,7 +78,6 @@ fn criterion_benchmark(c: &mut Criterion) { bench_grow(&mut group, "paged", || { PagedMemory::try_new(PAGE_SIZE, CHUNK_SIZE).expect("bench memory should be constructible") }); - bench_write_all( &mut group, "vec", diff --git a/crates/tinywasm/benches/tinywasm.rs b/benches/tinywasm.rs similarity index 75% rename from crates/tinywasm/benches/tinywasm.rs rename to benches/tinywasm.rs index 9a4de802..aff8c70c 100644 --- a/crates/tinywasm/benches/tinywasm.rs +++ b/benches/tinywasm.rs @@ -1,13 +1,12 @@ use criterion::{Criterion, criterion_group, criterion_main}; -use eyre::Result; use tinywasm::engine::Config; -use tinywasm::{Engine, FuncContext, HostFunction, Imports, MemoryBackend, ModuleInstance, Store, types}; +use tinywasm::{Engine, FuncContext, HostFunction, Imports, ModuleInstance, Result, Store, types}; use types::Module; -const WASM: &[u8] = include_bytes!("../../../examples/rust/out/tinywasm.wasm"); +const WASM: &[u8] = include_bytes!("../examples/rust/out/tinywasm.wasm"); fn tinywasm_parse() -> Result { - let parser = tinywasm_parser::Parser::new(); + let parser = tinywasm_parser::Parser::default(); let data = parser.parse_module_bytes(WASM)?; Ok(data) } @@ -23,11 +22,11 @@ fn tinywasm_from_twasm(twasm: &[u8]) -> Result { } fn tinywasm_run(module: &Module) -> Result<()> { - let engine = Engine::new(Config::default().with_memory_backend(MemoryBackend::paged(64 * 1024))); + let engine = Engine::new(Config::default()); let mut store = Store::new(engine); let mut imports = Imports::default(); - imports.define("env", "printi32", HostFunction::from(&mut store, |_: FuncContext<'_>, _: i32| Ok(()))); - let instance = ModuleInstance::instantiate(&mut store, module, Some(imports)).expect("instantiate"); + imports.define("env", "printi32", HostFunction::from(|_: FuncContext<'_>, _: i32| Ok(()))); + let instance = ModuleInstance::instantiate(&mut store, module, Some(&imports)).expect("instantiate"); let hello = instance.func::<(), ()>(&store, "hello").expect("func_typed"); hello.call(&mut store, ()).expect("call"); Ok(()) diff --git a/crates/tinywasm/benches/tinywasm_modes.rs b/benches/tinywasm_modes.rs similarity index 89% rename from crates/tinywasm/benches/tinywasm_modes.rs rename to benches/tinywasm_modes.rs index ed89b7b6..7d1d9938 100644 --- a/crates/tinywasm/benches/tinywasm_modes.rs +++ b/benches/tinywasm_modes.rs @@ -1,16 +1,17 @@ use criterion::{BatchSize, Criterion, criterion_group, criterion_main}; -use eyre::Result; use tinywasm::engine::{Config, FuelPolicy}; use tinywasm::types::Module; -use tinywasm::{Engine, ExecProgress, FuncContext, FunctionTyped, HostFunction, Imports, ModuleInstance, Store}; +use tinywasm::{ + Engine, ExecProgress, FuncContext, FunctionTyped, HostFunction, Imports, ModuleInstance, Result, Store, +}; -const WASM: &[u8] = include_bytes!("../../../examples/rust/out/tinywasm.wasm"); +const WASM: &[u8] = include_bytes!("../examples/rust/out/tinywasm.wasm"); const FUEL_PER_ROUND: u32 = 512; const TIME_BUDGET_PER_ROUND: core::time::Duration = core::time::Duration::from_micros(50); const BENCH_MEASUREMENT_TIME: core::time::Duration = core::time::Duration::from_secs(10); fn tinywasm_parse() -> Result { - let parser = tinywasm_parser::Parser::new(); + let parser = tinywasm_parser::Parser::default(); Ok(parser.parse_module_bytes(WASM)?) } @@ -21,9 +22,9 @@ fn setup_typed_func(module: &Module, engine: Option) -> Result<(Store, F }; let mut imports = Imports::default(); - imports.define("env", "printi32", HostFunction::from(&mut store, |_: FuncContext<'_>, _: i32| Ok(()))); + imports.define("env", "printi32", HostFunction::from(|_: FuncContext<'_>, _: i32| Ok(()))); - let instance = ModuleInstance::instantiate(&mut store, module, Some(imports))?; + let instance = ModuleInstance::instantiate(&mut store, module, Some(&imports))?; let func = instance.func::<(), ()>(&store, "hello")?; Ok((store, func)) } diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 7dbd4d24..1aae437d 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -23,12 +23,14 @@ path = "src/bin.rs" [dependencies] anstream = { version = "1.0" } +anyhow.workspace = true clap = { version = "4.6", features = ["derive"] } clap_complete = "4.6" -eyre.workspace = true log.workspace = true owo-colors = { workspace = true } pretty_env_logger.workspace = true +serde = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } tinywasm = { workspace = true, features = [ "archive", "canonicalize-nans", @@ -37,7 +39,9 @@ tinywasm = { workspace = true, features = [ "parallel-parser", "parser", "std", + "validate", ] } +wasm-testsuite = { workspace = true, optional = true } wast = { workspace = true, optional = true } wat = { workspace = true, optional = true } @@ -48,5 +52,6 @@ tempfile = "3.27" [features] default = ["wast", "wat"] +tests = ["dep:serde", "dep:serde_json", "dep:wasm-testsuite", "wast"] wast = ["dep:wast"] wat = ["dep:wat"] diff --git a/crates/cli/src/bin.rs b/crates/cli/src/bin.rs index 592fda7f..d630fcf3 100644 --- a/crates/cli/src/bin.rs +++ b/crates/cli/src/bin.rs @@ -1,5 +1,5 @@ +use anyhow::Result; use clap::Parser; -use eyre::Result; use tinywasm_cli::{Cli, run_cli}; fn main() -> Result<()> { diff --git a/crates/cli/src/cmd/compile.rs b/crates/cli/src/cmd/compile.rs index 93e38d5a..c7007eee 100644 --- a/crates/cli/src/cmd/compile.rs +++ b/crates/cli/src/cmd/compile.rs @@ -1,4 +1,4 @@ -use eyre::Result; +use anyhow::Result; use crate::cli::CompileArgs; use crate::load::{default_twasm_output_path, load_compilable_module, write_output_bytes}; diff --git a/crates/cli/src/cmd/completion.rs b/crates/cli/src/cmd/completion.rs index 5815f579..b3aec7c2 100644 --- a/crates/cli/src/cmd/completion.rs +++ b/crates/cli/src/cmd/completion.rs @@ -1,7 +1,7 @@ use std::io; +use anyhow::Result; use clap::CommandFactory; -use eyre::Result; use crate::cli::{Cli, CompletionArgs}; diff --git a/crates/cli/src/cmd/dump.rs b/crates/cli/src/cmd/dump.rs index 7a88cc20..da98a0bc 100644 --- a/crates/cli/src/cmd/dump.rs +++ b/crates/cli/src/cmd/dump.rs @@ -1,5 +1,5 @@ use anstream::println; -use eyre::Result; +use anyhow::Result; use owo_colors::OwoColorize; use tinywasm::types::{ExternalKind, ImportKind}; diff --git a/crates/cli/src/cmd/inspect.rs b/crates/cli/src/cmd/inspect.rs index ea2cec7e..cf7cb5f1 100644 --- a/crates/cli/src/cmd/inspect.rs +++ b/crates/cli/src/cmd/inspect.rs @@ -1,4 +1,4 @@ -use eyre::Result; +use anyhow::Result; use crate::cli::ModuleInputArgs; use crate::load::load_module; diff --git a/crates/cli/src/cmd/run.rs b/crates/cli/src/cmd/run.rs index 06f29f11..3d256410 100644 --- a/crates/cli/src/cmd/run.rs +++ b/crates/cli/src/cmd/run.rs @@ -1,4 +1,4 @@ -use eyre::Result; +use anyhow::Result; use tinywasm::types::ExportType; use tinywasm::{ModuleInstance, Store}; @@ -8,7 +8,7 @@ use crate::output::print_results; use crate::value_parse::parse_invocation_args; pub fn run(args: RunArgs) -> Result<()> { - let module_path = args.module.as_deref().ok_or_else(|| eyre::eyre!("missing module path"))?; + let module_path = args.module.as_deref().ok_or_else(|| anyhow::anyhow!("missing module path"))?; let loaded = load_module(module_path)?; let mut store = Store::new(args.engine.build_engine()?); let instance = ModuleInstance::instantiate_no_start(&mut store, &loaded.module, None)?; @@ -26,7 +26,7 @@ pub fn run(args: RunArgs) -> Result<()> { (true, ExportType::Func(ty)) => Some(ty), _ => None, }) - .ok_or_else(|| eyre::eyre!("export is not a function: {export}"))?; + .ok_or_else(|| anyhow::anyhow!("export is not a function: {export}"))?; let func = instance.func_untyped(&store, export)?; let params = parse_invocation_args(func_ty, &args.args)?; let results = func.call(&mut store, ¶ms)?; @@ -40,7 +40,7 @@ pub fn run(args: RunArgs) -> Result<()> { } let start = instance.func_untyped(&store, "_start").map_err(|_| { - eyre::eyre!( + anyhow::anyhow!( "module has no start function or `_start` export. Use `tinywasm inspect {module_path}` or `tinywasm run --invoke {module_path}`" ) })?; diff --git a/crates/cli/src/cmd/wast.rs b/crates/cli/src/cmd/wast.rs index ef9c2638..c669a0ab 100644 --- a/crates/cli/src/cmd/wast.rs +++ b/crates/cli/src/cmd/wast.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use eyre::Result; +use anyhow::Result; use crate::cli::WastArgs; use crate::wast_runner::WastRunner; diff --git a/crates/cli/src/engine_flags.rs b/crates/cli/src/engine_flags.rs index c8df1fe5..3d90847e 100644 --- a/crates/cli/src/engine_flags.rs +++ b/crates/cli/src/engine_flags.rs @@ -1,5 +1,5 @@ +use anyhow::{Result, bail}; use clap::{Args, ValueEnum}; -use eyre::{Result, bail}; use tinywasm::{Engine, StackConfig, engine::FuelPolicy}; #[derive(Args, Clone, Default)] diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index bef6b72a..877bfe18 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -3,12 +3,14 @@ pub mod cmd; pub mod engine_flags; pub mod load; pub mod output; +#[cfg(feature = "tests")] +pub mod testsuite; pub mod value_parse; #[cfg(feature = "wast")] pub mod wast_runner; +use anyhow::Result; use clap::CommandFactory; -use eyre::Result; pub use cli::{Cli, Commands}; diff --git a/crates/cli/src/load.rs b/crates/cli/src/load.rs index 9f7c6167..12c0e1f8 100644 --- a/crates/cli/src/load.rs +++ b/crates/cli/src/load.rs @@ -2,7 +2,7 @@ use std::ffi::OsStr; use std::io::{Read, Write}; use std::path::Path; -use eyre::{Context, Result, bail}; +use anyhow::{Context, Result, bail}; use tinywasm::Module; #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/crates/cli/src/output.rs b/crates/cli/src/output.rs index 40a271ea..75be9f84 100644 --- a/crates/cli/src/output.rs +++ b/crates/cli/src/output.rs @@ -48,7 +48,7 @@ pub fn format_memory_type(ty: &MemoryType) -> String { MemoryArch::I32 => "i32", MemoryArch::I64 => "i64", }; - let max = if ty.page_count_max() == ty.page_count_initial() && ty.max_size() == ty.initial_size() { + let max = if ty.page_count_max() == ty.page_count_initial() { ty.page_count_initial().to_string() } else { ty.page_count_max().to_string() @@ -77,6 +77,7 @@ pub fn format_export_type(ty: ExportType<'_>) -> String { ExportType::Memory(ty) => format_memory_type(ty), ExportType::Table(ty) => format_table_type(ty), ExportType::Global(ty) => format_global_type(ty), + ExportType::Tag(ty) => format!("tag {}", format_func_type(ty)), } } @@ -86,5 +87,6 @@ pub fn format_import_type(ty: ImportType<'_>) -> String { ImportType::Memory(ty) => format_memory_type(ty), ImportType::Table(ty) => format_table_type(ty), ImportType::Global(ty) => format_global_type(ty), + ImportType::Tag(ty) => format!("tag {}", format_func_type(ty)), } } diff --git a/crates/tinywasm/tests/testsuite/mod.rs b/crates/cli/src/testsuite.rs similarity index 81% rename from crates/tinywasm/tests/testsuite/mod.rs rename to crates/cli/src/testsuite.rs index ea44c297..032aa88f 100644 --- a/crates/tinywasm/tests/testsuite/mod.rs +++ b/crates/cli/src/testsuite.rs @@ -1,11 +1,22 @@ -#![allow(dead_code)] - -use eyre::{Result, eyre}; +use crate::wast_runner::{GroupResult, TestFile as RunnerTestFile, WastRunner}; use owo_colors::OwoColorize; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; use std::io::{BufRead, BufReader, Seek, SeekFrom}; -use tinywasm_cli::wast_runner::{GroupResult, TestFile as RunnerTestFile, WastRunner}; + +/// Result type used by the WAST test utilities. +pub type TestResult = core::result::Result>; + +#[derive(Debug)] +struct TestFailure(String); + +impl Display for TestFailure { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0) + } +} + +impl core::error::Error for TestFailure {} #[derive(Serialize, Deserialize)] pub struct TestGroupResult { @@ -27,7 +38,7 @@ impl TestSuite { Self { runner: WastRunner::new() } } - pub fn run_paths(&mut self, tests: &[std::path::PathBuf]) -> Result<()> { + pub fn run_paths(&mut self, tests: &[std::path::PathBuf]) -> TestResult<()> { let mut files = Vec::new(); for path in tests { if path.is_dir() { @@ -51,38 +62,43 @@ impl TestSuite { let name = path.to_string_lossy().into_owned(); Ok((name, contents)) }) - .collect::>>()?; + .collect::>>()?; self.runner.run_files(runner_files.iter().map(|(name, contents)| RunnerTestFile { name: name.clone(), parent: name.clone(), contents, - })) + }))?; + Ok(()) } - pub fn run_files<'a>(&mut self, tests: impl IntoIterator>) -> Result<()> { + pub fn run_files<'a>( + &mut self, + tests: impl IntoIterator>, + ) -> TestResult<()> { self.runner.run_files(tests.into_iter().map(|file| RunnerTestFile { name: file.name().to_string(), parent: file.parent().to_string(), contents: file.raw(), - })) + }))?; + Ok(()) } pub fn print_errors(&self) { self.runner.print_errors(); } - pub fn report_status(&self) -> Result<()> { + pub fn report_status(&self) -> TestResult<()> { if self.runner.failed() { println!(); - Err(eyre!(format!("{}:\n{self}", "failed one or more tests".red().bold()))) + Err(TestFailure(format!("{}:\n{self}", "failed one or more tests".red().bold())).into()) } else { println!("{self}"); Ok(()) } } - pub fn save_csv(&self, path: &str, version: &str) -> Result<()> { + pub fn save_csv(&self, path: &str, version: &str) -> TestResult<()> { use std::fs::OpenOptions; use std::io::Write; diff --git a/crates/cli/src/value_parse.rs b/crates/cli/src/value_parse.rs index eabbdc4f..3191eaf1 100644 --- a/crates/cli/src/value_parse.rs +++ b/crates/cli/src/value_parse.rs @@ -1,4 +1,4 @@ -use eyre::{Result, bail}; +use anyhow::{Result, bail}; use tinywasm::types::{FuncType, WasmType, WasmValue}; use crate::output::format_wasm_type; @@ -33,8 +33,8 @@ fn parse_arg(index: usize, ty: WasmType, value: &str) -> Result { Ok(parsed) } -fn format_error(index: usize, ty: WasmType, value: &str, error: impl core::fmt::Display) -> eyre::Report { - eyre::eyre!("failed to parse argument {} as {} from `{value}`: {error}", index + 1, format_wasm_type(ty)) +fn format_error(index: usize, ty: WasmType, value: &str, error: impl core::fmt::Display) -> anyhow::Error { + anyhow::anyhow!("failed to parse argument {} as {} from `{value}`: {error}", index + 1, format_wasm_type(ty)) } #[cfg(test)] diff --git a/crates/cli/src/wast_runner.rs b/crates/cli/src/wast_runner.rs index c7736637..1b8ae9c7 100644 --- a/crates/cli/src/wast_runner.rs +++ b/crates/cli/src/wast_runner.rs @@ -5,9 +5,12 @@ use std::panic::{self, AssertUnwindSafe}; use std::path::PathBuf; use std::time::Duration; -use eyre::{Context, Result, bail, eyre}; +use anyhow::{Context, Result, anyhow, bail}; use log::{debug, error}; -use tinywasm::types::{ExternRef, FuncRef, MemoryType, RefType, RefValue, TableType, WasmType, WasmValue}; +use tinywasm::types::{ + AbstractHeapType as TinyAbstractHeapType, AnyRef, ExternRef, FuncRef, MemoryType, RefType, RefValue, TableType, + WasmType, WasmValue, +}; use tinywasm::{ExecProgress, Global, HostFunction, Imports, Memory, Module, ModuleInstance, Store, Table}; use wast::{QuoteWat, core::AbstractHeapType}; @@ -16,53 +19,63 @@ const TEST_MAX_SUSPENSIONS: u32 = 1000; #[derive(Default)] struct ModuleRegistry { - modules: HashMap, - named_modules: HashMap, - last_module: Option, + definitions: HashMap, + instances: HashMap, + registered: HashMap, + last_definition: Option, + last_instance: Option, } impl ModuleRegistry { - fn modules(&self) -> &HashMap { - &self.modules + fn registered(&self) -> &HashMap { + &self.registered } - fn update_last_module(&mut self, module: ModuleInstance, name: Option) { - self.last_module = Some(module.clone()); + fn define(&mut self, module: Module, name: Option) { + self.last_definition = Some(module.clone()); if let Some(name) = name { - self.named_modules.insert(name, module); + self.definitions.insert(name, module); } } - fn register(&mut self, name: String, module: ModuleInstance) { + fn definition(&self, id: Option>) -> Option { + match id { + Some(id) => self.definitions.get(id.name()).cloned(), + None => self.last_definition.clone(), + } + } + + fn update_last_instance(&mut self, instance: ModuleInstance, name: Option) { + self.last_instance = Some(instance.clone()); + if let Some(name) = name { + self.instances.insert(name, instance); + } + } + + fn register(&mut self, name: String, id: Option>) -> bool { + let Some(instance) = self.get(id) else { return false }; debug!("registering module: {name}"); - self.modules.insert(name.clone(), module.clone()); - self.last_module = Some(module.clone()); - self.named_modules.insert(name, module); + self.registered.insert(name, instance); + true } fn get_idx(&self, module_id: Option>) -> Option { match module_id { - Some(module) => self - .modules - .get(module.name()) - .or_else(|| self.named_modules.get(module.name())) - .map(ModuleInstance::id), - None => self.last_module.as_ref().map(ModuleInstance::id), + Some(module) => { + self.registered.get(module.name()).or_else(|| self.instances.get(module.name())).map(ModuleInstance::id) + } + None => self.last_instance.as_ref().map(ModuleInstance::id), } } fn get(&self, module_id: Option>) -> Option { match module_id { Some(module_id) => { - self.modules.get(module_id.name()).or_else(|| self.named_modules.get(module_id.name())).cloned() + self.registered.get(module_id.name()).or_else(|| self.instances.get(module_id.name())).cloned() } - None => self.last_module.clone(), + None => self.last_instance.clone(), } } - - fn last(&self) -> Option { - self.last_module.clone() - } } #[derive(Default)] @@ -98,7 +111,7 @@ impl WastRunner { self.print_errors(); if self.failed() { anstream::println!("{self}"); - Err(eyre!("failed one or more tests")) + Err(anyhow!("failed one or more tests")) } else { anstream::println!("{self}"); Ok(()) @@ -174,20 +187,20 @@ impl WastRunner { .define("spectest", "global_i64", global_i64) .define("spectest", "global_f32", global_f32) .define("spectest", "global_f64", global_f64) - .define("spectest", "print", HostFunction::from(store, |_ctx: tinywasm::FuncContext, (): ()| Ok(()))) - .define("spectest", "print_i32", HostFunction::from(store, |_ctx: tinywasm::FuncContext, _arg: i32| Ok(()))) - .define("spectest", "print_i64", HostFunction::from(store, |_ctx: tinywasm::FuncContext, _arg: i64| Ok(()))) - .define("spectest", "print_f32", HostFunction::from(store, |_ctx: tinywasm::FuncContext, _arg: f32| Ok(()))) - .define("spectest", "print_f64", HostFunction::from(store, |_ctx: tinywasm::FuncContext, _arg: f64| Ok(()))) + .define("spectest", "print", HostFunction::from(|_ctx: tinywasm::FuncContext, (): ()| Ok(()))) + .define("spectest", "print_i32", HostFunction::from(|_ctx: tinywasm::FuncContext, _arg: i32| Ok(()))) + .define("spectest", "print_i64", HostFunction::from(|_ctx: tinywasm::FuncContext, _arg: i64| Ok(()))) + .define("spectest", "print_f32", HostFunction::from(|_ctx: tinywasm::FuncContext, _arg: f32| Ok(()))) + .define("spectest", "print_f64", HostFunction::from(|_ctx: tinywasm::FuncContext, _arg: f64| Ok(()))) .define( "spectest", "print_i32_f32", - HostFunction::from(store, |_ctx: tinywasm::FuncContext, _args: (i32, f32)| Ok(())), + HostFunction::from(|_ctx: tinywasm::FuncContext, _args: (i32, f32)| Ok(())), ) .define( "spectest", "print_f64_f64", - HostFunction::from(store, |_ctx: tinywasm::FuncContext, _args: (f64, f64)| Ok(())), + HostFunction::from(|_ctx: tinywasm::FuncContext, _args: (f64, f64)| Ok(())), ); for (name, module) in modules { @@ -197,6 +210,11 @@ impl WastRunner { Ok(imports) } + fn instantiate_module(store: &mut Store, registry: &ModuleRegistry, module: &Module) -> Result { + let imports = Self::imports(store, registry.registered())?; + Ok(ModuleInstance::instantiate(store, module, Some(&imports))?) + } + pub fn run_file(&mut self, file: TestFile<'_>) -> Result<()> { let test_group = self.test_group(file.name(), file.parent()); let wast_raw = file.raw(); @@ -210,51 +228,72 @@ impl WastRunner { for (i, directive) in directives.into_iter().enumerate() { let span = directive.span(); use wast::WastDirective::{ - AssertExhaustion, AssertInvalid, AssertMalformed, AssertReturn, AssertTrap, AssertUnlinkable, Invoke, - Module as Wat, ModuleDefinition, Register, + AssertException, AssertExhaustion, AssertInvalid, AssertMalformed, AssertReturn, AssertTrap, + AssertUnlinkable, Invoke, Module as Wat, ModuleDefinition, ModuleInstance as Instance, Register, }; match directive { - Register { span, name, .. } => { - let Some(last) = module_registry.last() else { + Register { span, name, module } => { + if !module_registry.register(name.to_string(), module) { test_group.add_result( &format!("Register({i})"), span.linecol_in(wast_raw), - Err(eyre!("no module to register")), + Err(anyhow!("module instance to register was not found")), ); continue; - }; - module_registry.register(name.to_string(), last); + } test_group.add_result(&format!("Register({i})"), span.linecol_in(wast_raw), Ok(())); } Wat(module) => { let result = catch_unwind_silent(|| { - let (name, bytes) = encode_quote_wat(module); - let module = parse_module_bytes(&bytes).expect("failed to parse module bytes"); - let imports = Self::imports(&mut store, module_registry.modules()).unwrap(); - let module_instance = ModuleInstance::instantiate(&mut store, &module, Some(imports)) + let (name, module) = parse_quote_module(module).expect("failed to parse module bytes"); + let instance = Self::instantiate_module(&mut store, &module_registry, &module) .expect("failed to instantiate module"); - (name, module_instance) + (name, instance) }) - .map_err(|e| eyre!("failed to parse wat module: {}", try_downcast_panic(e))); + .map_err(|e| anyhow!("failed to parse wat module: {}", try_downcast_panic(e))); match &result { Err(err) => debug!("failed to parse module: {err:?}"), - Ok((name, module)) => module_registry.update_last_module(module.clone(), name.clone()), + Ok((name, instance)) => module_registry.update_last_instance(instance.clone(), name.clone()), }; test_group.add_result(&format!("Wat({i})"), span.linecol_in(wast_raw), result.map(|_| ())); } ModuleDefinition(module) => { + let result = + catch_unwind_silent(|| parse_quote_module(module).expect("failed to parse module definition")) + .map_err(|err| anyhow!("failed to parse module definition: {}", try_downcast_panic(err))); + + if let Ok((name, module)) = &result { + module_registry.define(module.clone(), name.clone()); + } + + test_group.add_result( + &format!("ModuleDefinition({i})"), + span.linecol_in(wast_raw), + result.map(|_| ()), + ); + } + Instance { span, instance, module } => { + let name = instance.map(|id| id.name().to_string()); let result = catch_unwind_silent(|| { - let (_, bytes) = encode_quote_wat(module); - parse_module_bytes(&bytes) + let module = module_registry + .definition(module) + .ok_or_else(|| anyhow!("module definition was not found"))?; + Self::instantiate_module(&mut store, &module_registry, &module) }) - .map_err(|err| eyre!("failed to parse module definition: {}", try_downcast_panic(err))) - .and_then(|result| result) - .map(|_| ()); + .map_err(|err| anyhow!("failed to instantiate module definition: {}", try_downcast_panic(err))) + .and_then(|result| result); - test_group.add_result(&format!("ModuleDefinition({i})"), span.linecol_in(wast_raw), result); + if let Ok(instance) = &result { + module_registry.update_last_instance(instance.clone(), name); + } + test_group.add_result( + &format!("ModuleInstance({i})"), + span.linecol_in(wast_raw), + result.map(|_| ()), + ); } AssertMalformed { span, mut module, message } => { let Ok(encoded) = module.encode() else { @@ -262,7 +301,7 @@ impl WastRunner { continue; }; let res = catch_unwind_silent(|| parse_module_bytes(&encoded)) - .map_err(|e| eyre!("failed to parse module (expected): {}", try_downcast_panic(e))) + .map_err(|e| anyhow!("failed to parse module (expected): {}", try_downcast_panic(e))) .and_then(|res| res); test_group.add_result( &format!("AssertMalformed({i})"), @@ -275,7 +314,7 @@ impl WastRunner { { continue; } - Err(eyre!("expected module to be malformed: {message}")) + Err(anyhow!("expected module to be malformed: {message}")) } Err(_) => Ok(()), }, @@ -287,13 +326,13 @@ impl WastRunner { continue; } let res = catch_unwind_silent(move || parse_module_bytes(&module.encode().unwrap())) - .map_err(|e| eyre!("failed to parse module (invalid): {}", try_downcast_panic(e))) + .map_err(|e| anyhow!("failed to parse module (invalid): {}", try_downcast_panic(e))) .and_then(|res| res); test_group.add_result( &format!("AssertInvalid({i})"), span.linecol_in(wast_raw), match res { - Ok(_) => Err(eyre!("expected module to be invalid")), + Ok(_) => Err(anyhow!("expected module to be invalid")), Err(_) => Ok(()), }, ); @@ -307,7 +346,7 @@ impl WastRunner { test_group.add_result( &format!("AssertExhaustion({i})"), span.linecol_in(wast_raw), - Err(eyre!("expected trap")), + Err(anyhow!("expected trap")), ); continue; }; @@ -315,7 +354,7 @@ impl WastRunner { test_group.add_result( &format!("AssertExhaustion({i})"), span.linecol_in(wast_raw), - Err(eyre!("expected trap: {}, got: {}", message, trap.message())), + Err(anyhow!("expected trap: {}, got: {}", message, trap.message())), ); continue; } @@ -327,8 +366,8 @@ impl WastRunner { wast::WastExecute::Wat(mut wat) => { let module = parse_module_bytes(&wat.encode().expect("failed to encode module")) .expect("failed to parse module"); - let imports = Self::imports(&mut store, module_registry.modules()).unwrap(); - ModuleInstance::instantiate(&mut store, &module, Some(imports))?; + let imports = Self::imports(&mut store, module_registry.registered()).unwrap(); + ModuleInstance::instantiate(&mut store, &module, Some(&imports))?; return Ok(()); } wast::WastExecute::Get { .. } => panic!("get not supported"), @@ -343,14 +382,14 @@ impl WastRunner { Err(err) => test_group.add_result( &format!("AssertTrap({i})"), span.linecol_in(wast_raw), - Err(eyre!("test panicked: {}", try_downcast_panic(err))), + Err(anyhow!("test panicked: {}", try_downcast_panic(err))), ), Ok(Err(tinywasm::Error::Trap(trap))) => { if !message.starts_with(trap.message()) && !trap.message().starts_with(message) { test_group.add_result( &format!("AssertTrap({i})"), span.linecol_in(wast_raw), - Err(eyre!("expected trap: {}, got: {}", message, trap.message())), + Err(anyhow!("expected trap: {}, got: {}", message, trap.message())), ); continue; } @@ -359,27 +398,53 @@ impl WastRunner { Ok(Err(err)) => test_group.add_result( &format!("AssertTrap({i})"), span.linecol_in(wast_raw), - Err(eyre!("expected trap, {}, got: {:?}", message, err)), + Err(anyhow!("expected trap, {}, got: {:?}", message, err)), ), Ok(Ok(())) => test_group.add_result( &format!("AssertTrap({i})"), span.linecol_in(wast_raw), - Err(eyre!("expected trap {}, got Ok", message)), + Err(anyhow!("expected trap {}, got Ok", message)), ), } } + AssertException { exec, span } => { + let res: Result, _> = catch_unwind_silent(|| { + let invoke = match exec { + wast::WastExecute::Wat(mut wat) => { + let module = parse_module_bytes(&wat.encode().expect("failed to encode module")) + .expect("failed to parse module"); + let imports = Self::imports(&mut store, module_registry.registered()).unwrap(); + ModuleInstance::instantiate(&mut store, &module, Some(&imports))?; + return Ok(()); + } + wast::WastExecute::Get { .. } => panic!("get not supported"), + wast::WastExecute::Invoke(invoke) => invoke, + }; + let module = module_registry.get_idx(invoke.module); + let args = + convert_wastargs(invoke.args).map_err(|err| tinywasm::Error::Other(err.to_string()))?; + exec_fn_instance(module, &mut store, invoke.name, &args).map(|_| ()) + }); + let result = match res { + Err(err) => Err(anyhow!("test panicked: {}", try_downcast_panic(err))), + Ok(Err(tinywasm::Error::Exception(_))) => Ok(()), + Ok(Err(err)) => Err(anyhow!("expected exception, got: {err:?}")), + Ok(Ok(())) => Err(anyhow!("expected exception, got Ok")), + }; + test_group.add_result(&format!("AssertException({i})"), span.linecol_in(wast_raw), result); + } AssertUnlinkable { mut module, span, message } => { let res = catch_unwind_silent(|| { let module = parse_module_bytes(&module.encode().expect("failed to encode module")) .expect("failed to parse module"); - let imports = Self::imports(&mut store, module_registry.modules()).unwrap(); - ModuleInstance::instantiate(&mut store, &module, Some(imports)) + let imports = Self::imports(&mut store, module_registry.registered()).unwrap(); + ModuleInstance::instantiate(&mut store, &module, Some(&imports)) }); match res { Err(err) => test_group.add_result( &format!("AssertUnlinkable({i})"), span.linecol_in(wast_raw), - Err(eyre!("test panicked: {}", try_downcast_panic(err))), + Err(anyhow!("test panicked: {}", try_downcast_panic(err))), ), Ok(Err(tinywasm::Error::Linker(err))) => { if err.message() != message @@ -389,7 +454,7 @@ impl WastRunner { test_group.add_result( &format!("AssertUnlinkable({i})"), span.linecol_in(wast_raw), - Err(eyre!("expected linker error: {}, got: {}", message, err.message())), + Err(anyhow!("expected linker error: {}, got: {}", message, err.message())), ); continue; } @@ -398,12 +463,12 @@ impl WastRunner { Ok(Err(err)) => test_group.add_result( &format!("AssertUnlinkable({i})"), span.linecol_in(wast_raw), - Err(eyre!("expected linker error, {}, got: {:?}", message, err)), + Err(anyhow!("expected linker error, {}, got: {:?}", message, err)), ), Ok(Ok(_)) => test_group.add_result( &format!("AssertUnlinkable({i})"), span.linecol_in(wast_raw), - Err(eyre!("expected linker error {}, got Ok", message)), + Err(anyhow!("expected linker error {}, got Ok", message)), ), } } @@ -418,7 +483,7 @@ impl WastRunner { })?; Ok(()) }); - let res = res.map_err(|e| eyre!("test panicked: {}", try_downcast_panic(e))).and_then(|r| r); + let res = res.map_err(|e| anyhow!("test panicked: {}", try_downcast_panic(e))).and_then(|r| r); test_group.add_result(&format!("Invoke({name}-{i})"), span.linecol_in(wast_raw), res); } AssertReturn { span, exec, results } => { @@ -427,7 +492,7 @@ impl WastRunner { test_group.add_result( &format!("AssertReturn(unsupported-{i})"), span.linecol_in(wast_raw), - Err(eyre!("failed to convert expected results: {err:?}")), + Err(anyhow!("failed to convert expected results: {err:?}")), ); continue; } @@ -435,13 +500,13 @@ impl WastRunner { }; let invoke = match match exec { - wast::WastExecute::Wat(_) => Err(eyre!("wat not supported")), + wast::WastExecute::Wat(_) => Err(anyhow!("wat not supported")), wast::WastExecute::Get { module: module_id, global, .. } => { let Some(module) = module_registry.get(module_id) else { test_group.add_result( &format!("AssertReturn(unsupported-{i})"), span.linecol_in(wast_raw), - Err(eyre!("no module to get global from")), + Err(anyhow!("no module to get global from")), ); continue; }; @@ -451,7 +516,7 @@ impl WastRunner { test_group.add_result( &format!("AssertReturn(unsupported-{i})"), span.linecol_in(wast_raw), - Err(eyre!("failed to get global: {err:?}")), + Err(anyhow!("failed to get global: {err:?}")), ); continue; } @@ -459,12 +524,12 @@ impl WastRunner { let expected = expected_alternatives .iter() .filter_map(|alts| alts.first()) - .find(|exp| exp.matches(&module_global)); + .find(|exp| exp.matches(&module_global, &store)); if expected.is_none() { test_group.add_result( &format!("AssertReturn(unsupported-{i})"), span.linecol_in(wast_raw), - Err(eyre!( + Err(anyhow!( "global value did not match any expected alternative: {:?}", module_global )), @@ -485,7 +550,7 @@ impl WastRunner { test_group.add_result( &format!("AssertReturn(unsupported-{i})"), span.linecol_in(wast_raw), - Err(eyre!("unsupported directive: {err:?}")), + Err(anyhow!("unsupported directive: {err:?}")), ); continue; } @@ -500,7 +565,7 @@ impl WastRunner { e })?; if !expected_alternatives.iter().any(|expected| expected.len() == outcomes.len()) { - return Err(eyre!( + return Err(anyhow!( "expected {} results, got {}", expected_alternatives.first().map_or(0, |v| v.len()), outcomes.len() @@ -508,21 +573,24 @@ impl WastRunner { } if expected_alternatives.iter().any(|expected| { expected.len() == outcomes.len() - && outcomes.iter().zip(expected.iter()).all(|(outcome, exp)| exp.matches(outcome)) + && outcomes + .iter() + .zip(expected.iter()) + .all(|(outcome, exp)| exp.matches(outcome, &store)) }) { Ok(()) } else { - Err(eyre!("results did not match any expected alternative")) + Err(anyhow!("results did not match any expected alternative")) } }); - let res = res.map_err(|e| eyre!("test panicked: {}", try_downcast_panic(e))).and_then(|r| r); + let res = res.map_err(|e| anyhow!("test panicked: {}", try_downcast_panic(e))).and_then(|r| r); test_group.add_result(&format!("AssertReturn({invoke_name}-{i})"), span.linecol_in(wast_raw), res); } _ => test_group.add_result( &format!("Unknown({i})"), span.linecol_in(wast_raw), - Err(eyre!("unsupported directive")), + Err(anyhow!("unsupported directive")), ), } } @@ -723,6 +791,11 @@ fn parse_module_bytes(bytes: &[u8]) -> Result { Ok(tinywasm::parse_bytes(bytes)?) } +fn parse_quote_module(module: QuoteWat) -> Result<(Option, Module)> { + let (name, bytes) = encode_quote_wat(module); + Ok((name, parse_module_bytes(&bytes)?)) +} + fn convert_wastargs(args: Vec) -> Result> { args.into_iter().map(wastarg2tinywasmvalue).collect() } @@ -755,16 +828,19 @@ fn wastarg2tinywasmvalue(arg: wast::WastArg) -> Result { I32(i) => WasmValue::I32(i), I64(i) => WasmValue::I64(i), V128(i) => WasmValue::V128(i.to_le_bytes()), - RefExtern(v) => ExternRef::new(v).into(), + RefExtern(v) => ExternRef::try_new(v).ok_or_else(|| anyhow!("external reference address is too large"))?.into(), RefNull(t) => match t { wast::core::HeapType::Abstract { shared: false, ty: AbstractHeapType::Func } => RefValue::Null.into(), - wast::core::HeapType::Abstract { shared: false, ty: AbstractHeapType::Extern } => RefValue::Null.into(), + wast::core::HeapType::Abstract { shared: false, ty: AbstractHeapType::Extern | AbstractHeapType::Any } => { + RefValue::Null.into() + } _ => { bail!("unsupported arg type: refnull: {:?}", t); } }, - RefHost(_) => { - bail!("unsupported arg type: RefHost"); + RefHost(value) => { + RefValue::Any(AnyRef::from_host(value).ok_or_else(|| anyhow!("host reference address is too large"))?) + .into() } }) } @@ -791,15 +867,29 @@ enum ExpectedValue { RefNull, RefFunc, RefExtern, + RefAny, + RefEq, + RefI31, + RefStruct, + RefArray, } impl ExpectedValue { - fn matches(&self, value: &WasmValue) -> bool { + fn matches(&self, value: &WasmValue, store: &Store) -> bool { match self { Self::Exact(expected) => value.eq_loose(expected), Self::RefNull => matches!(value, WasmValue::Ref(RefValue::Null)), Self::RefFunc => matches!(value, WasmValue::Ref(RefValue::Func(_))), Self::RefExtern => matches!(value, WasmValue::Ref(RefValue::Extern(_))), + Self::RefAny => matches!(value, WasmValue::Ref(RefValue::Any(_))), + Self::RefEq => { + store.value_matches_type(*value, WasmType::Ref(RefType::new_abstract(false, TinyAbstractHeapType::Eq))) + } + Self::RefI31 => matches!(value, WasmValue::Ref(RefValue::Any(value)) if value.as_i31().is_some()), + Self::RefStruct => store + .value_matches_type(*value, WasmType::Ref(RefType::new_abstract(false, TinyAbstractHeapType::Struct))), + Self::RefArray => store + .value_matches_type(*value, WasmType::Ref(RefType::new_abstract(false, TinyAbstractHeapType::Array))), } } } @@ -817,7 +907,10 @@ fn wastret2tinywasmvalues(ret: wast::WastRet) -> Result> { } fn wastretcore2tinywasmvalue(ret: wast::core::WastRetCore) -> Result { - use wast::core::WastRetCore::{F32, F64, I32, I64, RefExtern, RefFunc, RefNull, V128}; + use wast::core::WastRetCore::{ + F32, F64, I32, I64, RefAny, RefArray, RefEq, RefExtern, RefFunc, RefHost, RefI31, RefI31Shared, RefNull, + RefStruct, V128, + }; Ok(match ret { F32(f) => ExpectedValue::Exact(nanpattern2tinywasmvalue(f)?), F64(f) => ExpectedValue::Exact(nanpattern2tinywasmvalue(f)?), @@ -825,13 +918,24 @@ fn wastretcore2tinywasmvalue(ret: wast::core::WastRetCore) -> Result ExpectedValue::Exact(WasmValue::I64(i)), V128(i) => ExpectedValue::Exact(WasmValue::V128(wast_v128_to_bytes(i))), RefNull(_) => ExpectedValue::RefNull, - RefExtern(Some(v)) => ExpectedValue::Exact(ExternRef::new(v).into()), + RefExtern(Some(v)) => ExpectedValue::Exact( + ExternRef::try_new(v).ok_or_else(|| anyhow!("external reference address is too large"))?.into(), + ), RefExtern(None) => ExpectedValue::RefExtern, RefFunc(Some(wast::token::Index::Num(n, _))) => ExpectedValue::Exact(FuncRef::new(n).into()), RefFunc(None) => ExpectedValue::RefFunc, RefFunc(v) => { bail!("unsupported arg type: reffunc: {:?}", v); } + RefAny => ExpectedValue::RefAny, + RefEq => ExpectedValue::RefEq, + RefI31 | RefI31Shared => ExpectedValue::RefI31, + RefStruct => ExpectedValue::RefStruct, + RefArray => ExpectedValue::RefArray, + RefHost(value) => ExpectedValue::Exact( + RefValue::Any(AnyRef::from_host(value).ok_or_else(|| anyhow!("host reference address is too large"))?) + .into(), + ), a => { bail!("unsupported arg type {:?}", a); } @@ -912,4 +1016,26 @@ mod tests { let mut runner = WastRunner::new(); runner.run_paths(&[path]).unwrap(); } + + #[test] + fn runs_module_definition_and_instance_directives() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("instance.wast"); + std::fs::write( + &path, + r#" + (module definition $M (global (export "g") i32 (i32.const 42))) + (module instance $I $M) + (register "I" $I) + (module + (import "I" "g" (global $g i32)) + (func (export "get") (result i32) global.get $g)) + (assert_return (invoke "get") (i32.const 42)) + "#, + ) + .unwrap(); + + let mut runner = WastRunner::new(); + runner.run_paths(&[path]).unwrap(); + } } diff --git a/crates/parser/Cargo.toml b/crates/parser/Cargo.toml index 3e714031..561ac450 100644 --- a/crates/parser/Cargo.toml +++ b/crates/parser/Cargo.toml @@ -13,10 +13,11 @@ categories.workspace = true [dependencies] log = { workspace = true, optional = true } tinywasm-types = { workspace = true } -wasmparser = { workspace = true, features = ["features", "simd", "validate"] } +wasmparser = { workspace = true, features = ["simd"] } [features] -default = ["log", "parallel", "std"] +default = ["log", "parallel", "std", "validate"] log = ["dep:log"] parallel = ["std"] std = ["tinywasm-types/std", "wasmparser/std"] +validate = ["wasmparser/features", "wasmparser/validate"] diff --git a/crates/parser/README.md b/crates/parser/README.md index 91145f1f..cf1a6c49 100644 --- a/crates/parser/README.md +++ b/crates/parser/README.md @@ -6,7 +6,8 @@ This crate provides the parser and lowering pipeline that converts WebAssembly b - `std`: Enables the use of `std` and `std::io` for parsing from files and streams. - `log`: Enables logging of the parsing process using the `log` crate. -- `parallel`: Enables multithreaded parsing and validation when `std` is available. +- `parallel`: Enables multithreaded function parsing. Requires `std`. +- `validate`: Enables `wasmparser` validation. Enabled by default and configurable through `ParserOptions`. ## Usage @@ -15,10 +16,10 @@ use tinywasm_parser::{Parser, ParserOptions}; let bytes = include_bytes!("./file.wasm"); -let parser = Parser::new(); +let parser = Parser::default(); let module = parser.parse_module_bytes(bytes)?; -let parser = Parser::with_options(ParserOptions::default().with_rewrite_optimization(false)); +let parser = Parser::new(ParserOptions::default().with_rewrite_optimization(false)); let module = parser.parse_module_bytes(bytes)?; let module = parser.parse_module_file("path/to/file.wasm")?; @@ -26,4 +27,4 @@ let mut stream = std::fs::File::open("path/to/file.wasm")?; let module = parser.parse_module_stream(&mut stream)?; ``` -If you just want the default configuration, the top-level `parse_bytes`, `parse_file`, and `parse_stream` helpers are thin wrappers around `Parser::new()`. +If you just want the default configuration, the top-level `parse_bytes`, `parse_file`, and `parse_stream` helpers are thin wrappers around `Parser::default()`. diff --git a/crates/parser/src/conversion.rs b/crates/parser/src/conversion.rs index db8d2aa7..480a8d06 100644 --- a/crates/parser/src/conversion.rs +++ b/crates/parser/src/conversion.rs @@ -1,16 +1,27 @@ -use crate::{Result, module::FunctionCode, visit::process_operators_and_validate}; -use alloc::{boxed::Box, format, string::ToString, vec::Vec}; +use crate::validation::{FuncValidator, FuncValidatorAllocations, ValidatorResources}; +#[cfg(feature = "validate")] +use crate::visit::process_operators_and_validate; +use crate::{Result, module::FunctionCode, visit::process_operators}; +use alloc::{boxed::Box, format, vec::Vec}; use tinywasm_types::*; -use wasmparser::{ - CompositeInnerType, FuncValidator, FuncValidatorAllocations, OperatorsReader, OperatorsReaderAllocations, - ValidatorResources, -}; +use wasmparser::{CompositeInnerType, OperatorsReader, OperatorsReaderAllocations, UnpackedIndex}; -pub(crate) fn convert_module_element(element: wasmparser::Element<'_>) -> Result { +pub(crate) fn value_lane(ty: wasmparser::ValType) -> ValueLane { + match ty { + wasmparser::ValType::I32 | wasmparser::ValType::F32 | wasmparser::ValType::Ref(_) => ValueLane::S32, + wasmparser::ValType::I64 | wasmparser::ValType::F64 => ValueLane::S64, + wasmparser::ValType::V128 => ValueLane::S128, + } +} + +pub(crate) fn convert_module_element( + element: wasmparser::Element<'_>, + global_types: &[WasmType], +) -> Result { let kind = match element.kind { wasmparser::ElementKind::Active { table_index, offset_expr } => tinywasm_types::ElementKind::Active { table: table_index.unwrap_or(0), - offset: process_const_operators(offset_expr.get_operators_reader())?, + offset: process_const_operators(offset_expr.get_operators_reader(), global_types)?, }, wasmparser::ElementKind::Passive => tinywasm_types::ElementKind::Passive, wasmparser::ElementKind::Declared => tinywasm_types::ElementKind::Declared, @@ -30,7 +41,7 @@ pub(crate) fn convert_module_element(element: wasmparser::Element<'_>) -> Result wasmparser::ElementItems::Expressions(ty, exprs) => { let items = exprs .into_iter() - .map(|expr| Ok(ElementItem::Expr(process_const_operators(expr?.get_operators_reader())?))) + .map(|expr| Ok(ElementItem::Expr(process_const_operators(expr?.get_operators_reader(), global_types)?))) .collect::>>()? .into_boxed_slice(); @@ -39,13 +50,16 @@ pub(crate) fn convert_module_element(element: wasmparser::Element<'_>) -> Result } } -pub(crate) fn convert_module_data(data: wasmparser::Data<'_>) -> Result { +pub(crate) fn convert_module_data( + data: wasmparser::Data<'_>, + global_types: &[WasmType], +) -> Result { Ok(tinywasm_types::Data { data: data.data.to_vec().into_boxed_slice(), range: data.range, kind: match data.kind { wasmparser::DataKind::Active { memory_index, offset_expr } => { - let offset = process_const_operators(offset_expr.get_operators_reader())?; + let offset = process_const_operators(offset_expr.get_operators_reader(), global_types)?; tinywasm_types::DataKind::Active { mem: memory_index, offset } } wasmparser::DataKind::Passive => tinywasm_types::DataKind::Passive, @@ -68,9 +82,7 @@ pub(crate) fn convert_module_import(import: wasmparser::Import<'_>) -> Result { ImportKind::Global(GlobalType::new(convert_valtype(&ty.content_type)?, ty.mutable)) } - wasmparser::TypeRef::Tag(ty) => { - return Err(crate::ParseError::UnsupportedOperator(format!("Unsupported import kind: {ty:?}"))); - } + wasmparser::TypeRef::Tag(ty) => ImportKind::Tag(convert_tag_type(ty)), _ => { return Err(crate::ParseError::UnsupportedOperator(format!("Unsupported import kind: {:?}", import.ty))); } @@ -90,16 +102,17 @@ pub(crate) fn convert_module_memory(memory: wasmparser::MemoryType) -> MemoryTyp pub(crate) fn convert_module_globals( globals: wasmparser::SectionLimited<'_, wasmparser::Global<'_>>, + global_types: &mut Vec, ) -> Result> { - globals - .into_iter() - .map(|global| { - let global = global?; - let ty = convert_valtype(&global.ty.content_type)?; - let ops = global.init_expr.get_operators_reader(); - Ok(Global { init: process_const_operators(ops)?, ty: GlobalType::new(ty, global.ty.mutable) }) - }) - .collect::>>() + let mut out = Vec::with_capacity(globals.count() as usize); + for global in globals { + let global = global?; + let ty = convert_valtype(&global.ty.content_type)?; + let init = process_const_operators(global.init_expr.get_operators_reader(), global_types)?; + global_types.push(ty); + out.push(Global { init, ty: GlobalType::new(ty, global.ty.mutable) }); + } + Ok(out.into_boxed_slice()) } pub(crate) fn convert_module_export(export: wasmparser::Export<'_>) -> Result { @@ -108,7 +121,8 @@ pub(crate) fn convert_module_export(export: wasmparser::Export<'_>) -> Result ExternalKind::Table, wasmparser::ExternalKind::Memory => ExternalKind::Memory, wasmparser::ExternalKind::Global => ExternalKind::Global, - wasmparser::ExternalKind::Tag | wasmparser::ExternalKind::FuncExact => { + wasmparser::ExternalKind::Tag => ExternalKind::Tag, + wasmparser::ExternalKind::FuncExact => { return Err(crate::ParseError::UnsupportedOperator(format!("Unsupported export kind: {:?}", export.kind))); } }; @@ -116,28 +130,48 @@ pub(crate) fn convert_module_export(export: wasmparser::Export<'_>) -> Result TagType { + TagType::new(ty.func_type_idx) +} + +fn extend_local_types(local_types: &mut Vec, count: u32, ty: wasmparser::ValType) -> Result<()> { + let size = value_lane(ty); + let count = + usize::try_from(count).map_err(|_| crate::ParseError::Other("local declaration count is too large".into()))?; + local_types.reserve(count); + local_types.extend(core::iter::repeat_n(size, count)); + Ok(()) +} + pub(crate) fn convert_module_code( func: wasmparser::FunctionBody<'_>, - mut validator: Option>, + validator: Option>, reader_allocs: OperatorsReaderAllocations, metadata: &crate::visit::ModuleMetadata, ty_idx: u32, ) -> Result<(FunctionCode, Option, OperatorsReaderAllocations)> { let locals_reader = func.get_locals_reader()?; - let pos = locals_reader.original_position(); + #[cfg(feature = "validate")] + let locals_position = locals_reader.original_position(); let signature = metadata.signature(ty_idx)?.clone(); let mut local_types = signature.params.clone(); - for (i, local) in locals_reader.into_iter().enumerate() { + #[cfg(feature = "validate")] + let mut validator = validator; + + #[cfg(feature = "validate")] + for (local_index, local) in locals_reader.into_iter().enumerate() { let local = local?; if let Some(validator) = validator.as_mut() { - validator.define_locals(pos + i, local.0, local.1)?; + validator.define_locals(locals_position + local_index, local.0, local.1)?; } - let size = crate::visit::OperandSize::from(local.1); - let count = usize::try_from(local.0) - .map_err(|_| crate::ParseError::Other("local declaration count is too large".into()))?; - local_types.reserve(count); - local_types.extend(core::iter::repeat_n(size, count)); + extend_local_types(&mut local_types, local.0, local.1)?; + } + + #[cfg(not(feature = "validate"))] + for local in locals_reader { + let local = local?; + extend_local_types(&mut local_types, local.0, local.1)?; } // maps a local's address to the index in the type's locals array @@ -146,16 +180,41 @@ pub(crate) fn convert_module_code( for ty in &local_types { let (count, error) = match ty { - crate::visit::OperandSize::S32 => (&mut local_counts.c32, "too many 32-bit locals"), - crate::visit::OperandSize::S64 => (&mut local_counts.c64, "too many 64-bit locals"), - crate::visit::OperandSize::S128 => (&mut local_counts.c128, "too many 128-bit locals"), + ValueLane::S32 => (&mut local_counts.c32, "too many 32-bit locals"), + ValueLane::S64 => (&mut local_counts.c64, "too many 64-bit locals"), + ValueLane::S128 => (&mut local_counts.c128, "too many 128-bit locals"), }; local_addr_map.push(*count); *count = count.checked_add(1).ok_or_else(|| crate::ParseError::Other(error.into()))?; } - let (body, data, validator_allocs, reader_allocs) = - process_operators_and_validate(validator, func, local_types, local_addr_map, metadata, ty_idx, reader_allocs)?; + #[cfg(feature = "validate")] + let (body, data, validator_allocs, reader_allocs) = match validator { + Some(validator) => { + let (body, data, validator_allocs, reader_allocs) = process_operators_and_validate( + validator, + func, + local_types, + local_addr_map, + metadata, + ty_idx, + reader_allocs, + )?; + (body, data, Some(validator_allocs), reader_allocs) + } + None => { + let (body, data, reader_allocs) = + process_operators(func, local_types, local_addr_map, metadata, ty_idx, reader_allocs)?; + (body, data, None, reader_allocs) + } + }; + #[cfg(not(feature = "validate"))] + let (body, data, validator_allocs, reader_allocs) = { + let _ = validator; + let (body, data, reader_allocs) = + process_operators(func, local_types, local_addr_map, metadata, ty_idx, reader_allocs)?; + (body, data, None, reader_allocs) + }; Ok(( FunctionCode { instructions: body, data, locals: local_counts, uses_local_memory: false }, validator_allocs, @@ -163,25 +222,77 @@ pub(crate) fn convert_module_code( )) } -pub(crate) fn convert_module_type(ty: wasmparser::RecGroup) -> Result { - let mut types = ty.types(); - // TODO(wasm3): Preserve recursive groups and non-function composite types instead of flattening singleton funcs. - if types.len() != 1 { - return Err(crate::ParseError::UnsupportedOperator( - "Expected exactly one type in the type section".to_string(), - )); +pub(crate) fn convert_rec_group(ty: wasmparser::RecGroup, group_start: u32, types: &mut Vec) -> Result { + let group_len = u32::try_from(ty.types().len()) + .map_err(|_| crate::ParseError::Other("recursive type group is too large".into()))?; + types.reserve(group_len as usize); + for ty in ty.into_types() { + let composite = &ty.composite_type; + if composite.shared { + return Err(crate::ParseError::UnsupportedOperator("shared composite types are unsupported".into())); + } + if composite.descriptor_idx.is_some() || composite.describes_idx.is_some() { + return Err(crate::ParseError::UnsupportedOperator("descriptor types are unsupported".into())); + } + + let supertype = + ty.supertype_idx.map(|idx| convert_type_index(idx.unpack(), group_start, group_len)).transpose()?; + let composite = match &composite.inner { + CompositeInnerType::Func(ty) => { + let params = ty + .params() + .iter() + .map(|ty| convert_valtype_in_group(ty, group_start, group_len)) + .collect::>>()?; + let results = ty + .results() + .iter() + .map(|ty| convert_valtype_in_group(ty, group_start, group_len)) + .collect::>>()?; + CompositeType::Func(FuncType::new(¶ms, &results)) + } + CompositeInnerType::Struct(ty) => CompositeType::Struct(StructType { + fields: ty + .fields + .iter() + .map(|field| convert_field_type(field, group_start, group_len)) + .collect::>()?, + }), + CompositeInnerType::Array(ty) => { + CompositeType::Array(ArrayType { field: convert_field_type(&ty.0, group_start, group_len)? }) + } + CompositeInnerType::Cont(_) => { + return Err(crate::ParseError::UnsupportedOperator("continuation types are unsupported".into())); + } + }; + types.push(SubType { is_final: ty.is_final, supertype, composite }); } + Ok(group_len) +} - let ty = types.next().unwrap(); - let CompositeInnerType::Func(ty) = &ty.composite_type.inner else { - return Err(crate::ParseError::UnsupportedOperator(format!( - "Unsupported non-function type in type section: {}", - ty.composite_type - ))); +fn convert_type_index(index: UnpackedIndex, group_start: u32, group_len: u32) -> Result { + match index { + UnpackedIndex::Module(index) => Ok(index), + UnpackedIndex::RecGroup(index) if index < group_len => { + group_start.checked_add(index).ok_or_else(|| crate::ParseError::Other("type index is too large".into())) + } + UnpackedIndex::RecGroup(index) => { + Err(crate::ParseError::Other(format!("recursive group type index out of bounds: {index}"))) + } + #[cfg(feature = "validate")] + UnpackedIndex::Id(_) => { + Err(crate::ParseError::UnsupportedOperator(format!("unsupported canonical type index: {index}"))) + } + } +} + +fn convert_field_type(field: &wasmparser::FieldType, group_start: u32, group_len: u32) -> Result { + let storage = match &field.element_type { + wasmparser::StorageType::I8 => StorageType::I8, + wasmparser::StorageType::I16 => StorageType::I16, + wasmparser::StorageType::Val(ty) => StorageType::Value(convert_valtype_in_group(ty, group_start, group_len)?), }; - let params = ty.params().iter().map(convert_valtype).collect::>>()?; - let results = ty.results().iter().map(convert_valtype).collect::>>()?; - Ok(FuncType::new(¶ms, &results)) + Ok(FieldType { storage, mutable: field.mutable }) } pub(crate) fn convert_ref_type(ty: wasmparser::RefType) -> Result { @@ -189,17 +300,30 @@ pub(crate) fn convert_ref_type(ty: wasmparser::RefType) -> Result { } pub(crate) fn convert_valtype(valtype: &wasmparser::ValType) -> Result { + convert_valtype_with_group(valtype, None) +} + +fn convert_valtype_in_group(valtype: &wasmparser::ValType, group_start: u32, group_len: u32) -> Result { + convert_valtype_with_group(valtype, Some((group_start, group_len))) +} + +fn convert_valtype_with_group(valtype: &wasmparser::ValType, group: Option<(u32, u32)>) -> Result { match valtype { wasmparser::ValType::I32 => Ok(WasmType::I32), wasmparser::ValType::I64 => Ok(WasmType::I64), wasmparser::ValType::F32 => Ok(WasmType::F32), wasmparser::ValType::F64 => Ok(WasmType::F64), wasmparser::ValType::V128 => Ok(WasmType::V128), - wasmparser::ValType::Ref(r) => Ok(WasmType::Ref(convert_ref_type(*r)?)), + wasmparser::ValType::Ref(r) => { + Ok(WasmType::Ref(convert_heap_type_with_group(r.heap_type(), r.is_nullable(), group)?)) + } } } -pub(crate) fn process_const_operators(ops: OperatorsReader<'_>) -> Result> { +pub(crate) fn process_const_operators( + ops: OperatorsReader<'_>, + global_types: &[WasmType], +) -> Result> { let mut out = Vec::new(); let mut operator_count = 0; let mut end_reached = false; @@ -220,12 +344,32 @@ pub(crate) fn process_const_operators(ops: OperatorsReader<'_>) -> Result { ConstInstruction::Ref(RefValue::Func(FuncRef::new(function_index))) } + wasmparser::Operator::RefI31 => ConstInstruction::RefI31, + wasmparser::Operator::AnyConvertExtern => ConstInstruction::AnyConvertExtern, + wasmparser::Operator::ExternConvertAny => ConstInstruction::ExternConvertAny, + wasmparser::Operator::StructNew { struct_type_index } => ConstInstruction::StructNew(struct_type_index), + wasmparser::Operator::StructNewDefault { struct_type_index } => { + ConstInstruction::StructNewDefault(struct_type_index) + } + wasmparser::Operator::ArrayNew { array_type_index } => ConstInstruction::ArrayNew(array_type_index), + wasmparser::Operator::ArrayNewDefault { array_type_index } => { + ConstInstruction::ArrayNewDefault(array_type_index) + } + wasmparser::Operator::ArrayNewFixed { array_type_index, array_size } => { + ConstInstruction::ArrayNewFixed(array_type_index, array_size) + } wasmparser::Operator::I32Const { value } => ConstInstruction::I32Const(value), wasmparser::Operator::I64Const { value } => ConstInstruction::I64Const(value), wasmparser::Operator::F32Const { value } => ConstInstruction::F32Const(f32::from_bits(value.bits())), wasmparser::Operator::F64Const { value } => ConstInstruction::F64Const(f64::from_bits(value.bits())), wasmparser::Operator::V128Const { value } => ConstInstruction::V128Const(*value.bytes()), - wasmparser::Operator::GlobalGet { global_index } => ConstInstruction::GlobalGet(global_index), + wasmparser::Operator::GlobalGet { global_index } => match global_types.get(global_index as usize) { + Some(WasmType::I32 | WasmType::F32) => ConstInstruction::GlobalGet32(global_index), + Some(WasmType::I64 | WasmType::F64) => ConstInstruction::GlobalGet64(global_index), + Some(WasmType::V128) => ConstInstruction::GlobalGet128(global_index), + Some(WasmType::Ref(_)) => ConstInstruction::GlobalGetRef(global_index), + None => return Err(crate::ParseError::Other(format!("global index out of bounds: {global_index}"))), + }, wasmparser::Operator::I32Add => ConstInstruction::I32Add, wasmparser::Operator::I32Sub => ConstInstruction::I32Sub, wasmparser::Operator::I32Mul => ConstInstruction::I32Mul, @@ -249,6 +393,14 @@ pub(crate) fn process_const_operators(ops: OperatorsReader<'_>) -> Result Result { + convert_heap_type_with_group(heap, nullable, None) +} + +fn convert_heap_type_with_group( + heap: wasmparser::HeapType, + nullable: bool, + group: Option<(u32, u32)>, +) -> Result { match heap { wasmparser::HeapType::Abstract { shared: false, ty } => Ok(RefType::new_abstract( nullable, @@ -271,11 +423,24 @@ pub(crate) fn convert_heap_type(heap: wasmparser::HeapType, nullable: bool) -> R }, )), wasmparser::HeapType::Concrete(index) => { - let index = index.as_module_index().ok_or_else(|| { - crate::ParseError::UnsupportedOperator(format!("Unsupported non-module heap type index: {index:?}")) - })?; - RefType::new_concrete(nullable, index) - .ok_or_else(|| crate::ParseError::Other(format!("heap type index is too large: {index}"))) + let index = match index { + UnpackedIndex::Module(index) => index, + index @ UnpackedIndex::RecGroup(_) => { + let (group_start, group_len) = group.ok_or_else(|| { + crate::ParseError::UnsupportedOperator(format!( + "recursive-group heap type outside a type group: {index}" + )) + })?; + convert_type_index(index, group_start, group_len)? + } + #[cfg(feature = "validate")] + index @ UnpackedIndex::Id(_) => { + return Err(crate::ParseError::UnsupportedOperator(format!( + "unsupported canonical heap type index: {index}" + ))); + } + }; + Ok(RefType::new_concrete(nullable, index)) } wasmparser::HeapType::Abstract { shared: true, .. } | wasmparser::HeapType::Exact(_) => { Err(crate::ParseError::UnsupportedOperator(format!("Unsupported heap type: {heap:?}"))) diff --git a/crates/parser/src/lib.rs b/crates/parser/src/lib.rs index aa5e0b0f..8db7920e 100644 --- a/crates/parser/src/lib.rs +++ b/crates/parser/src/lib.rs @@ -34,6 +34,7 @@ mod error; mod macros; mod module; mod optimize; +mod validation; mod visit; #[cfg(parallel_parser)] @@ -41,7 +42,10 @@ mod parallel; pub use error::*; use module::ModuleReader; -use wasmparser::{Validator, WasmFeatures}; +use validation::Validator; + +#[cfg(feature = "validate")] +use wasmparser::WasmFeatures; pub use tinywasm_types::Module; @@ -49,7 +53,10 @@ pub use tinywasm_types::Module; #[non_exhaustive] #[derive(Debug, Clone)] pub struct ParserOptions { - /// Whether to validate modules while parsing. Enabled by default. + /// Whether to validate modules while parsing. Enabled by default when the + /// `validate` feature is enabled. + /// + /// Requires the `validate` feature to have any effect. /// /// Disable this only for trusted input. Parsing without validation may produce /// a module that violates runtime assumptions. @@ -73,7 +80,7 @@ pub struct ParserOptions { impl Default for ParserOptions { fn default() -> Self { Self { - validation: true, + validation: cfg!(feature = "validate"), optimize_local_memory_allocation: true, optimize_rewrite: true, #[cfg(parallel_parser)] @@ -83,11 +90,19 @@ impl Default for ParserOptions { } impl ParserOptions { + /// Create parser options with default settings. + pub fn new() -> Self { + Self::default() + } + /// Enable or disable WebAssembly validation. /// + /// Requires the `validate` feature to have any effect. + /// /// Disable this only for trusted input. Parsing without validation may produce /// a module that violates runtime assumptions. pub const fn with_validation(mut self, enabled: bool) -> Self { + assert!(!enabled || cfg!(feature = "validate"), "validation requires the `validate` feature"); self.validation = enabled; self } @@ -142,13 +157,8 @@ pub struct Parser { } impl Parser { - /// Create a new parser instance - pub fn new() -> Self { - Self::default() - } - - /// Create a new parser with explicit options. - pub fn with_options(options: ParserOptions) -> Self { + /// Create a parser with the given options. + pub const fn new(options: ParserOptions) -> Self { Self { options } } @@ -157,27 +167,21 @@ impl Parser { &self.options } - fn create_validator(_options: ParserOptions) -> Validator { - let features = WasmFeatures::CALL_INDIRECT_OVERLONG - | WasmFeatures::BULK_MEMORY_OPT - | WasmFeatures::RELAXED_SIMD - | WasmFeatures::GC_TYPES - | WasmFeatures::REFERENCE_TYPES - | WasmFeatures::MUTABLE_GLOBAL - | WasmFeatures::MULTI_VALUE - | WasmFeatures::FLOATS - | WasmFeatures::BULK_MEMORY - | WasmFeatures::SATURATING_FLOAT_TO_INT - | WasmFeatures::SIGN_EXTENSION - | WasmFeatures::EXTENDED_CONST - | WasmFeatures::FUNCTION_REFERENCES - | WasmFeatures::TAIL_CALL - | WasmFeatures::MULTI_MEMORY - | WasmFeatures::SIMD - | WasmFeatures::MEMORY64 - | WasmFeatures::CUSTOM_PAGE_SIZES - | WasmFeatures::WIDE_ARITHMETIC; - Validator::new_with_features(features) + fn validator(&self) -> Option { + #[cfg(feature = "validate")] + { + let features = WasmFeatures::WASM3 + .difference(WasmFeatures::THREADS) + .union(WasmFeatures::CUSTOM_PAGE_SIZES) + .union(WasmFeatures::WIDE_ARITHMETIC) + .union(WasmFeatures::COMPACT_IMPORTS); + self.options.validation().then(|| Validator::new_with_features(features)) + } + #[cfg(not(feature = "validate"))] + { + assert!(!self.options.validation(), "validation requires the `validate` feature"); + None + } } #[cfg(feature = "std")] @@ -194,7 +198,7 @@ impl Parser { /// Parse a [`Module`] from bytes pub fn parse_module_bytes(&self, wasm: impl AsRef<[u8]>) -> Result { let wasm = wasm.as_ref(); - let mut validator = self.options.validation().then(|| Self::create_validator(self.options.clone())); + let mut validator = self.validator(); let mut reader = ModuleReader::default(); for payload in wasmparser::Parser::new(0).parse_all(wasm) { @@ -228,7 +232,7 @@ impl Parser { #[cfg(feature = "std")] /// Parse a [`Module`] from a stream. Requires `std` feature. pub fn parse_module_stream(&self, mut stream: impl std::io::Read) -> Result { - let mut validator = self.options.validation().then(|| Self::create_validator(self.options.clone())); + let mut validator = self.validator(); let mut reader = ModuleReader::default(); let mut buffer = alloc::vec::Vec::new(); let mut parser = wasmparser::Parser::new(0); @@ -333,17 +337,17 @@ impl TryFrom> for Module { /// Parse a module from bytes pub fn parse_bytes(wasm: &[u8]) -> Result { - Parser::new().parse_module_bytes(wasm) + Parser::default().parse_module_bytes(wasm) } #[cfg(feature = "std")] /// Parse a module from a file. Requires the `std` feature. pub fn parse_file(path: impl AsRef + Clone) -> Result { - Parser::new().parse_module_file(path) + Parser::default().parse_module_file(path) } #[cfg(feature = "std")] /// Parse a module from a stream. Requires the `std` feature. pub fn parse_stream(stream: impl crate::std::io::Read) -> Result { - Parser::new().parse_module_stream(stream) + Parser::default().parse_module_stream(stream) } diff --git a/crates/parser/src/macros.rs b/crates/parser/src/macros.rs index 723f691c..9fa7a01d 100644 --- a/crates/parser/src/macros.rs +++ b/crates/parser/src/macros.rs @@ -1,26 +1,24 @@ pub(crate) mod visit { + #[cfg(feature = "validate")] macro_rules! validate_then_visit { ($( @$proposal:ident $op:ident $({ $($arg:ident: $argty:ty),* })? => $visit:ident ($($ann:tt)*))*) => {$( fn $visit(&mut self $($(,$arg: $argty)*)?) -> Self::Output { - if let Some(validator) = self.validator.as_mut() { - if let Err(e) = validator.visitor(self.position).$visit($($($arg.clone()),*)?) { - core::hint::cold_path(); - return Err(crate::ParseError::ParseError { message: e.to_string(), offset: self.position }); - } + if let Err(e) = self.validator.visitor(self.position).$visit($($($arg.clone()),*)?) { + core::hint::cold_path(); + return Err(crate::ParseError::ParseError { message: e.to_string(), offset: self.position }); } self.builder.$visit($($($arg),*)?) } )*}; } + #[cfg(feature = "validate")] macro_rules! validate_then_visit_simd { ($( @$proposal:ident $op:ident $({ $($arg:ident: $argty:ty),* })? => $visit:ident ($($ann:tt)*))*) => {$( fn $visit(&mut self $($(,$arg: $argty)*)?) -> Self::Output { - if let Some(validator) = self.validator.as_mut() { - if let Err(e) = validator.simd_visitor(self.position).$visit($($($arg.clone()),*)?) { - core::hint::cold_path(); - return Err(crate::ParseError::ParseError { message: e.to_string(), offset: self.position }); - } + if let Err(e) = self.validator.simd_visitor(self.position).$visit($($($arg.clone()),*)?) { + core::hint::cold_path(); + return Err(crate::ParseError::ParseError { message: e.to_string(), offset: self.position }); } self.builder.$visit($($($arg),*)?) } @@ -39,6 +37,27 @@ pub(crate) mod visit { $(lowering_ops!(@effect $inputs => $outputs $visit);)* lowering_ops!($($rest)*); }; + (unsupported $args:tt { $($visit:ident),* $(,)? } $($rest:tt)*) => { + $(lowering_ops!(@unsupported $args $visit);)* + lowering_ops!($($rest)*); + }; + (heap $nullable:literal $inputs:tt => $outputs:tt { + $($visit:ident => $instr:ident),* $(,)? + } $($rest:tt)*) => { + $( + fn $visit(&mut self, heap_type: wasmparser::HeapType) -> Self::Output { + let ty = convert_heap_type(heap_type, $nullable)?; + lowering_ops!(@emit self fixed $inputs => $outputs Instruction::$instr(ty)) + } + )* + lowering_ops!($($rest)*); + }; + + (@unsupported [$($argty:ty),*] $visit:ident) => { + fn $visit(&mut self $(, _: $argty)*) -> Self::Output { + Err(crate::ParseError::UnsupportedOperator(stringify!($visit).to_string())) + } + }; (@fixed [$($input:ident),*] => [$($output:ident),*] $visit:ident $(($($arg:ident: $ty:ty),+))? => $instr:ident @@ -66,6 +85,15 @@ pub(crate) mod visit { (@table $inputs:tt => $outputs:tt $($operator:tt)*) => { lowering_ops!(@resolved table_size $inputs => $outputs $($operator)*); }; + (@array_field [$($input:ident),*] => [$($output:ident),*] + $visit:ident($type_index:ident: $type_ty:ty $(, $arg:ident: $arg_ty:ty)*) => $instr:ident + ) => { + fn $visit(&mut self, $type_index: $type_ty $(, $arg: $arg_ty)*) -> Self::Output { + let size = self.metadata.array_field($type_index)?; + lowering_ops!(@emit self address(size) [$($input),*] => [$($output),*] + Instruction::$instr($type_index $(, $arg)*).into()) + } + }; (@resolved $resolver:ident [$($input:ident),*] => [$($output:ident),*] $visit:ident($index:ident: $ty:ty) => $instr:ident ) => { @@ -112,7 +140,8 @@ pub(crate) mod visit { }; (@size Addr, $address:ident) => { $address }; - (@size $size:ident $(, $address:ident)?) => { OperandSize::$size }; + (@size Field, $address:ident) => { $address }; + (@size $size:ident $(, $address:ident)?) => { ValueLane::$size }; } macro_rules! impl_visit_operator { @@ -130,6 +159,8 @@ pub(crate) mod visit { (@@relaxed_simd $($rest:tt)* ) => {}; (@@tail_call $($rest:tt)* ) => {}; (@@function_references $($rest:tt)* ) => {}; + (@@gc $($rest:tt)* ) => {}; + (@@exceptions $($rest:tt)* ) => {}; (@@$proposal:ident $op:ident $({ $($arg:ident: $argty:ty),* })? => $visit:ident ($($ann:tt)*)) => { fn $visit(&mut self $($(,_: $argty)*)?) -> Self::Output { @@ -138,7 +169,9 @@ pub(crate) mod visit { }; } - pub(crate) use {impl_visit_operator, lowering_ops, validate_then_visit, validate_then_visit_simd}; + pub(crate) use {impl_visit_operator, lowering_ops}; + #[cfg(feature = "validate")] + pub(crate) use {validate_then_visit, validate_then_visit_simd}; } pub(crate) mod optimize { diff --git a/crates/parser/src/module.rs b/crates/parser/src/module.rs index c2199f34..3d38ad6e 100644 --- a/crates/parser/src/module.rs +++ b/crates/parser/src/module.rs @@ -1,10 +1,13 @@ use crate::log::debug; +#[cfg(parallel_parser)] +use crate::validation::{FuncToValidate, ValidatorResources}; +use crate::validation::{FuncValidatorAllocations, Validator}; use crate::{ParseError, ParserOptions, Result, conversion::*, optimize}; use alloc::{boxed::Box, format, string::ToString, sync::Arc, vec::Vec}; use core::marker::PhantomData; use core::ops::Range; use tinywasm_types::*; -use wasmparser::{FuncValidatorAllocations, OperatorsReaderAllocations, Payload, Validator}; +use wasmparser::{OperatorsReaderAllocations, Payload}; pub(crate) struct FunctionCode { pub instructions: Vec, @@ -41,11 +44,12 @@ pub(crate) struct ModuleReader<'a> { translation_metadata: Option>, has_code_section: bool, + has_type_section: bool, marker: PhantomData<&'a [u8]>, pub(crate) version: Option, pub(crate) start_func: Option, - pub(crate) func_types: Box<[FuncType]>, + pub(crate) types: TypeSection, pub(crate) code_type_addrs: Box<[u32]>, code_results: Box<[ValueCounts]>, pub(crate) exports: Arc<[Export]>, @@ -53,12 +57,14 @@ pub(crate) struct ModuleReader<'a> { pub(crate) globals: Box<[Global]>, pub(crate) tables: Box<[TableDefinition]>, pub(crate) memory_types: Box<[MemoryType]>, + pub(crate) tags: Box<[TagType]>, pub(crate) imports: Box<[Import]>, pub(crate) data: Box<[Data]>, pub(crate) elements: Box<[Element]>, pub(crate) end_reached: bool, imported_func_count: usize, imported_memory_count: u32, + global_types: Vec, #[cfg(parallel_parser)] pending_functions: Option>>, @@ -68,22 +74,23 @@ impl<'a> ModuleReader<'a> { fn translation_metadata(&mut self) -> &crate::visit::ModuleMetadata { if self.translation_metadata.is_none() { self.translation_metadata = Some(Arc::new(crate::visit::ModuleMetadata::new( - &self.func_types, + &self.types, &self.code_type_addrs, &self.imports, &self.globals, &self.memory_types, &self.tables, + &self.tags, ))); } self.translation_metadata.as_deref().unwrap() } - pub(crate) fn process_payload( - &mut self, - payload: Payload<'_>, - mut validator: Option<&mut Validator>, - ) -> Result<()> { + pub(crate) fn process_payload(&mut self, payload: Payload<'_>, validator: Option<&mut Validator>) -> Result<()> { + #[cfg(feature = "validate")] + let mut validator = validator; + #[cfg(not(feature = "validate"))] + let _ = validator; fn check_section(section: &str, duplicate: bool) -> Result<()> { debug!("found {section} section"); if duplicate { @@ -94,9 +101,12 @@ impl<'a> ModuleReader<'a> { match payload { Payload::Version { num, encoding, range } => { + #[cfg(feature = "validate")] if let Some(validator) = validator.as_mut() { validator.version(num, encoding, &range)?; } + #[cfg(not(feature = "validate"))] + let _ = range; self.version = Some(num); if let wasmparser::Encoding::Component = encoding { return Err(ParseError::InvalidEncoding(encoding)); @@ -104,27 +114,46 @@ impl<'a> ModuleReader<'a> { } Payload::StartSection { func, range } => { check_section("start", self.start_func.is_some())?; + #[cfg(feature = "validate")] if let Some(validator) = validator.as_mut() { validator.start_section(func, &range)?; } + #[cfg(not(feature = "validate"))] + let _ = range; self.start_func = Some(func); } Payload::TypeSection(reader) => { - check_section("type", !self.func_types.is_empty())?; + check_section("type", self.has_type_section)?; + self.has_type_section = true; + #[cfg(feature = "validate")] if let Some(validator) = validator.as_mut() { validator.type_section(&reader)?; } - self.func_types = reader.into_iter().map(|t| convert_module_type(t?)).collect::>()?; + let mut types = Vec::with_capacity(reader.count() as usize); + let mut rec_group_lengths = Vec::with_capacity(reader.count() as usize); + for group in reader { + let group = group?; + let group_start = u32::try_from(types.len()) + .map_err(|_| ParseError::Other("type section is too large".into()))?; + let group_len = convert_rec_group(group, group_start, &mut types)?; + rec_group_lengths.push(group_len); + } + self.types = TypeSection { + types: types.into_boxed_slice(), + rec_group_lengths: rec_group_lengths.into_boxed_slice(), + }; } Payload::GlobalSection(reader) => { check_section("global", !self.globals.is_empty())?; + #[cfg(feature = "validate")] if let Some(validator) = validator.as_mut() { validator.global_section(&reader)?; } - self.globals = convert_module_globals(reader)?; + self.globals = convert_module_globals(reader, &mut self.global_types)?; } Payload::TableSection(reader) => { check_section("table", !self.tables.is_empty())?; + #[cfg(feature = "validate")] if let Some(validator) = validator.as_mut() { validator.table_section(&reader)?; } @@ -140,7 +169,7 @@ impl<'a> ModuleReader<'a> { let init = match table.init { wasmparser::TableInit::RefNull => None, wasmparser::TableInit::Expr(expr) => { - Some(process_const_operators(expr.get_operators_reader())?) + Some(process_const_operators(expr.get_operators_reader(), &self.global_types)?) } }; tables.push(TableDefinition { ty, init }); @@ -149,38 +178,69 @@ impl<'a> ModuleReader<'a> { } Payload::MemorySection(reader) => { check_section("memory", !self.memory_types.is_empty())?; + #[cfg(feature = "validate")] if let Some(validator) = validator.as_mut() { validator.memory_section(&reader)?; } self.memory_types = reader.into_iter().map(|memory| Ok(convert_module_memory(memory?))).collect::>()?; } + Payload::TagSection(reader) => { + check_section("tag", !self.tags.is_empty())?; + #[cfg(feature = "validate")] + if let Some(validator) = validator.as_mut() { + validator.tag_section(&reader)?; + } + let mut tags = Vec::with_capacity(reader.count() as usize); + for tag in reader { + let tag = convert_tag_type(tag?); + let ty = self.types.get(tag.type_idx).and_then(SubType::as_func).ok_or_else(|| { + ParseError::Other(format!("tag type index does not reference a function: {}", tag.type_idx)) + })?; + if !ty.results().is_empty() { + return Err(ParseError::Other(format!("tag type must not have results: {}", tag.type_idx))); + } + tags.push(tag); + } + self.tags = tags.into_boxed_slice(); + } Payload::ElementSection(reader) => { debug!("Found element section"); + #[cfg(feature = "validate")] if let Some(validator) = validator.as_mut() { validator.element_section(&reader)?; } - self.elements = - reader.into_iter().map(|element| convert_module_element(element?)).collect::>()?; + self.elements = reader + .into_iter() + .map(|element| convert_module_element(element?, &self.global_types)) + .collect::>()?; } Payload::DataSection(reader) => { check_section("data", !self.data.is_empty())?; + #[cfg(feature = "validate")] if let Some(validator) = validator.as_mut() { validator.data_section(&reader)?; } - self.data = reader.into_iter().map(|data| convert_module_data(data?)).collect::>()?; + self.data = reader + .into_iter() + .map(|data| convert_module_data(data?, &self.global_types)) + .collect::>()?; } Payload::DataCountSection { count, range } => { debug!("Found data count section"); if !self.data.is_empty() { return Err(ParseError::UnsupportedSection("Data count section after data section".into())); } + #[cfg(feature = "validate")] if let Some(validator) = validator.as_mut() { validator.data_count_section(count, &range)?; } + #[cfg(not(feature = "validate"))] + let _ = (count, range); } Payload::FunctionSection(reader) => { check_section("function", !self.code_type_addrs.is_empty())?; + #[cfg(feature = "validate")] if let Some(validator) = validator.as_mut() { validator.function_section(&reader)?; } @@ -188,10 +248,9 @@ impl<'a> ModuleReader<'a> { let mut results = Vec::with_capacity(reader.count() as usize); for ty_idx in reader { let ty_idx = ty_idx?; - let ty = self - .func_types - .get(ty_idx as usize) - .ok_or_else(|| ParseError::Other(format!("function type index out of bounds: {ty_idx}")))?; + let ty = self.types.get(ty_idx).and_then(SubType::as_func).ok_or_else(|| { + ParseError::Other(format!("function type index does not reference a function: {ty_idx}")) + })?; type_addrs.push(ty_idx); results.push(ValueCounts::from_iter(ty.results())); } @@ -200,6 +259,7 @@ impl<'a> ModuleReader<'a> { } Payload::ImportSection(reader) => { check_section("import", !self.imports.is_empty())?; + #[cfg(feature = "validate")] if let Some(validator) = validator.as_mut() { validator.import_section(&reader)?; } @@ -207,8 +267,30 @@ impl<'a> ModuleReader<'a> { for import in reader.into_imports() { let import = convert_module_import(import?)?; match import.kind { - ImportKind::Function(_) => self.imported_func_count += 1, + ImportKind::Function(type_idx) => { + if self.types.get(type_idx).and_then(SubType::as_func).is_none() { + return Err(ParseError::Other(format!( + "function import type index does not reference a function: {type_idx}" + ))); + } + self.imported_func_count += 1; + } ImportKind::Memory(_) => self.imported_memory_count += 1, + ImportKind::Global(ty) => self.global_types.push(ty.ty), + ImportKind::Tag(tag) => { + let ty = self.types.get(tag.type_idx).and_then(SubType::as_func).ok_or_else(|| { + ParseError::Other(format!( + "tag import type index does not reference a function: {}", + tag.type_idx + )) + })?; + if !ty.results().is_empty() { + return Err(ParseError::Other(format!( + "tag import type must not have results: {}", + tag.type_idx + ))); + } + } _ => {} } imports.push(import); @@ -217,6 +299,7 @@ impl<'a> ModuleReader<'a> { } Payload::ExportSection(reader) => { check_section("export", !self.exports.is_empty())?; + #[cfg(feature = "validate")] if let Some(validator) = validator.as_mut() { validator.export_section(&reader)?; } @@ -228,9 +311,12 @@ impl<'a> ModuleReader<'a> { return Err(ParseError::DuplicateSection("End section".into())); } + #[cfg(feature = "validate")] if let Some(validator) = validator.as_mut() { validator.end(offset)?; } + #[cfg(not(feature = "validate"))] + let _ = offset; self.end_reached = true; } Payload::CustomSection(_reader) => { @@ -262,9 +348,12 @@ impl<'a> ModuleReader<'a> { self.has_code_section = true; self.code.reserve(count as usize); + #[cfg(feature = "validate")] if let Some(validator) = validator { validator.code_section_start(&range)?; } + #[cfg(not(feature = "validate"))] + let _ = (range, validator); #[cfg(parallel_parser)] { @@ -294,10 +383,16 @@ impl<'a> ModuleReader<'a> { let func_validator_allocs = self.func_validator_allocations.take(); let operators_reader_allocs = self.operators_reader_allocations.take().unwrap_or_default(); + #[cfg(feature = "validate")] let func_validator = validator .map(|validator| validator.code_section_entry(&function)) .transpose()? .map(|func| func.into_validator(func_validator_allocs.unwrap_or_default())); + #[cfg(not(feature = "validate"))] + let func_validator = { + let _ = (validator, func_validator_allocs); + None + }; let ordinal = self.code.len(); let ty_idx = *self @@ -332,7 +427,10 @@ impl<'a> ModuleReader<'a> { #[cfg(parallel_parser)] if self.pending_functions.is_some() { + #[cfg(feature = "validate")] let func_to_validate = validator.map(|validator| validator.code_section_entry(&function)).transpose()?; + #[cfg(not(feature = "validate"))] + let func_to_validate = None; return self.queue_function(crate::parallel::FunctionBodyInput::Borrowed(function), func_to_validate); } @@ -345,15 +443,23 @@ impl<'a> ModuleReader<'a> { count: u32, body_offset: usize, section_bytes: Arc<[u8]>, - mut validator: Option<&mut Validator>, + validator: Option<&mut Validator>, ) -> Result<()> { + #[cfg(feature = "validate")] + let mut validator = validator; + #[cfg(not(feature = "validate"))] + let _ = validator; let mut reader = wasmparser::BinaryReader::new(§ion_bytes, body_offset); for _ in 0..count { let body_reader = reader.read_reader()?; let body_range = body_reader.range(); - let function = wasmparser::FunctionBody::new(body_reader); - let func_to_validate = - validator.as_mut().map(|validator| validator.code_section_entry(&function)).transpose()?; + #[cfg(feature = "validate")] + let func_to_validate = { + let function = wasmparser::FunctionBody::new(body_reader); + validator.as_mut().map(|validator| validator.code_section_entry(&function)).transpose()? + }; + #[cfg(not(feature = "validate"))] + let func_to_validate = None; self.queue_function( crate::parallel::FunctionBodyInput::Owned(crate::parallel::OwnedFunctionBody { section_bytes: section_bytes.clone(), @@ -378,7 +484,7 @@ impl<'a> ModuleReader<'a> { fn queue_function( &mut self, body: crate::parallel::FunctionBodyInput<'a>, - func_to_validate: Option>, + func_to_validate: Option>, ) -> Result<()> { let ordinal = self.code.len() + self.pending_functions.as_ref().map_or(0, Vec::len); let results = *self @@ -460,7 +566,8 @@ impl<'a> ModuleReader<'a> { .zip(self.code_type_addrs) .zip(self.code_results) .map(|((code, ty_idx), results)| { - let ty = self.func_types.get(ty_idx as usize).expect("function type was checked while parsing").clone(); + let ty = + self.types.get(ty_idx).and_then(SubType::as_func).expect("function type was checked while parsing"); let params = ValueCounts::from_iter(ty.params()); if code.uses_local_memory { local_memory_allocation = LocalMemoryAllocation::Eager; @@ -478,7 +585,7 @@ impl<'a> ModuleReader<'a> { Ok(ModuleInner { funcs, - func_types: self.func_types, + types: self.types, func_type_idxs, globals: self.globals, tables: self.tables, @@ -488,6 +595,7 @@ impl<'a> ModuleReader<'a> { exports: self.exports, elements: self.elements, memory_types: self.memory_types, + tags: self.tags, local_memory_allocation, } .into()) diff --git a/crates/parser/src/optimize.rs b/crates/parser/src/optimize.rs index dcb75683..969d686f 100644 --- a/crates/parser/src/optimize.rs +++ b/crates/parser/src/optimize.rs @@ -92,18 +92,27 @@ fn rewrite( let Some(op) = int_bin_op(instr) else { unreachable!() }; rewrite!(instrs, i, [LocalGet32(a), LocalGet32(b)] => BinOpLocalLocal32(op, a, b)); rewrite!(instrs, i, [LocalGet32(local), Const32(c)] => BinOpLocalConst32(op, local, c)); + rewrite!(instrs, i, [GlobalGet32(global), Const32(c)] => BinOpGlobalConst32(op, global, c)); rewrite!(instrs, i, [Const32(c), LocalGet32(local)] => BinOpLocalConst32(op, local, c)); - rewrite!(instrs, i, [GlobalGet(global)] => BinOpStackGlobal32(op, global)); + rewrite!(instrs, i, [Const32(c), GlobalGet32(global)] => BinOpGlobalConst32(op, global, c)); + rewrite!(instrs, i, [GlobalGet32(global)] => BinOpStackGlobal32(op, global)); + rewrite!(instrs, i, [LocalGet32(local)] => BinOpStackLocal32(op, local)); if matches!(op, BinOp::IAdd) { rewrite!(instrs, i, [Const32(c)] => AddConst32(c)); rewrite!(instrs, i, [I32Add] => I32Add3); + rewrite!(instrs, i, + [BinOpStackLocal32(BinOp::IAdd, local)] => + [LocalGet32(local), I32Add3] + ); } } instr @ (I32Sub | I32Shl | I32ShrS | I32ShrU | I32Rotl | I32Rotr) => { let Some(op) = int_bin_op(instr) else { unreachable!() }; rewrite!(instrs, i, [LocalGet32(a), LocalGet32(b)] => BinOpLocalLocal32(op, a, b)); rewrite!(instrs, i, [LocalGet32(local), Const32(c)] => BinOpLocalConst32(op, local, c)); - rewrite!(instrs, i, [GlobalGet(global)] => BinOpStackGlobal32(op, global)); + rewrite!(instrs, i, [GlobalGet32(global), Const32(c)] => BinOpGlobalConst32(op, global, c)); + rewrite!(instrs, i, [GlobalGet32(global)] => BinOpStackGlobal32(op, global)); + rewrite!(instrs, i, [LocalGet32(local)] => BinOpStackLocal32(op, local)); if matches!(op, BinOp::IShrS) { rewrite!(instrs, i, [BinOpLocalConst32(BinOp::IShl, local, 8), Const32(8)] => [LocalGet32(local), I32Extend8S]); rewrite!(instrs, i, [BinOpLocalConst32(BinOp::IShl, local, 16), Const32(16)] => [LocalGet32(local), I32Extend16S]); @@ -113,8 +122,10 @@ fn rewrite( let Some(op) = int_bin_op(instr) else { unreachable!() }; rewrite!(instrs, i, [LocalGet64(a), LocalGet64(b)] => BinOpLocalLocal64(op, a, b)); rewrite!(instrs, i, [LocalGet64(local), Const64(c)] => BinOpLocalConst64(op, local, c)); + rewrite!(instrs, i, [GlobalGet64(global), Const64(c)] => BinOpGlobalConst64(op, global, c)); rewrite!(instrs, i, [Const64(c), LocalGet64(local)] => BinOpLocalConst64(op, local, c)); - rewrite!(instrs, i, [GlobalGet(global)] => BinOpStackGlobal64(op, global)); + rewrite!(instrs, i, [Const64(c), GlobalGet64(global)] => BinOpGlobalConst64(op, global, c)); + rewrite!(instrs, i, [GlobalGet64(global)] => BinOpStackGlobal64(op, global)); if matches!(op, BinOp::IAdd) { rewrite!(instrs, i, [Const64(c)] => AddConst64(c)); rewrite!(instrs, i, [I64Add] => I64Add3); @@ -124,44 +135,64 @@ fn rewrite( let Some(op) = int_bin_op(instr) else { unreachable!() }; rewrite!(instrs, i, [LocalGet64(a), LocalGet64(b)] => BinOpLocalLocal64(op, a, b)); rewrite!(instrs, i, [LocalGet64(local), Const64(c)] => BinOpLocalConst64(op, local, c)); - rewrite!(instrs, i, [GlobalGet(global)] => BinOpStackGlobal64(op, global)); + rewrite!(instrs, i, [GlobalGet64(global), Const64(c)] => BinOpGlobalConst64(op, global, c)); + rewrite!(instrs, i, [GlobalGet64(global)] => BinOpStackGlobal64(op, global)); if matches!(op, BinOp::IShrS) { rewrite!(instrs, i, [BinOpLocalConst64(BinOp::IShl, local, 8), Const64(8)] => [LocalGet64(local), I64Extend8S]); rewrite!(instrs, i, [BinOpLocalConst64(BinOp::IShl, local, 16), Const64(16)] => [LocalGet64(local), I64Extend16S]); rewrite!(instrs, i, [BinOpLocalConst64(BinOp::IShl, local, 32), Const64(32)] => [LocalGet64(local), I64Extend32S]); } } + instr @ (I32Eq | I32Ne | I32LtS | I32LtU | I32GtS | I32GtU | I32LeS | I32LeU | I32GeS | I32GeU) => { + let Some(op) = cmp_op(instr) else { unreachable!() }; + rewrite!(instrs, i, [LocalGet32(left), LocalGet32(right)] => CmpLocalLocal32(op, left, right)); + } + instr @ (I64Eq | I64Ne | I64LtS | I64LtU | I64GtS | I64GtU | I64LeS | I64LeU | I64GeS | I64GeU) => { + let Some(op) = cmp_op(instr) else { unreachable!() }; + rewrite!(instrs, i, [LocalGet64(left), LocalGet64(right)] => CmpLocalLocal64(op, left, right)); + } instr @ (F32Add | F32Mul | F32Min | F32Max) => { let Some(op) = float_bin_op(instr) else { unreachable!() }; rewrite!(instrs, i, [LocalGet32(a), LocalGet32(b)] => BinOpLocalLocal32(op, a, b)); rewrite!(instrs, i, [LocalGet32(local), Const32(c)] => BinOpLocalConst32(op, local, c)); + rewrite!(instrs, i, [GlobalGet32(global), Const32(c)] => BinOpGlobalConst32(op, global, c)); rewrite!(instrs, i, [Const32(c), LocalGet32(local)] => BinOpLocalConst32(op, local, c)); + rewrite!(instrs, i, [Const32(c), GlobalGet32(global)] => BinOpGlobalConst32(op, global, c)); + rewrite!(instrs, i, [LocalGet32(local)] => BinOpStackLocal32(op, local)); } instr @ (F32Sub | F32Div | F32Copysign) => { let Some(op) = float_bin_op(instr) else { unreachable!() }; rewrite!(instrs, i, [LocalGet32(a), LocalGet32(b)] => BinOpLocalLocal32(op, a, b)); rewrite!(instrs, i, [LocalGet32(local), Const32(c)] => BinOpLocalConst32(op, local, c)); + rewrite!(instrs, i, [GlobalGet32(global), Const32(c)] => BinOpGlobalConst32(op, global, c)); + rewrite!(instrs, i, [LocalGet32(local)] => BinOpStackLocal32(op, local)); } instr @ (F64Add | F64Mul | F64Min | F64Max) => { let Some(op) = float_bin_op(instr) else { unreachable!() }; rewrite!(instrs, i, [LocalGet64(a), LocalGet64(b)] => BinOpLocalLocal64(op, a, b)); rewrite!(instrs, i, [LocalGet64(local), Const64(c)] => BinOpLocalConst64(op, local, c)); + rewrite!(instrs, i, [GlobalGet64(global), Const64(c)] => BinOpGlobalConst64(op, global, c)); rewrite!(instrs, i, [Const64(c), LocalGet64(local)] => BinOpLocalConst64(op, local, c)); + rewrite!(instrs, i, [Const64(c), GlobalGet64(global)] => BinOpGlobalConst64(op, global, c)); } instr @ (F64Sub | F64Div | F64Copysign) => { let Some(op) = float_bin_op(instr) else { unreachable!() }; rewrite!(instrs, i, [LocalGet64(a), LocalGet64(b)] => BinOpLocalLocal64(op, a, b)); rewrite!(instrs, i, [LocalGet64(local), Const64(c)] => BinOpLocalConst64(op, local, c)); + rewrite!(instrs, i, [GlobalGet64(global), Const64(c)] => BinOpGlobalConst64(op, global, c)); } instr @ (V128And | V128Or | V128Xor | I64x2Add | I64x2Mul) => { let Some(op) = bin_op_128(instr) else { unreachable!() }; rewrite!(instrs, i, [LocalGet128(a), LocalGet128(b)] => BinOpLocalLocal128(op, a, b)); rewrite!(instrs, i, [LocalGet128(local), Const128(c)] => BinOpLocalConst128(op, local, c)); + rewrite!(instrs, i, [GlobalGet128(global), Const128(c)] => BinOpGlobalConst128(op, global, c)); rewrite!(instrs, i, [Const128(c), LocalGet128(local)] => BinOpLocalConst128(op, local, c)); + rewrite!(instrs, i, [Const128(c), GlobalGet128(global)] => BinOpGlobalConst128(op, global, c)); } V128AndNot => { rewrite!(instrs, i, [LocalGet128(a), LocalGet128(b)] => BinOpLocalLocal128(BinOp128::AndNot, a, b)); rewrite!(instrs, i, [LocalGet128(local), Const128(c)] => BinOpLocalConst128(BinOp128::AndNot, local, c)); + rewrite!(instrs, i, [GlobalGet128(global), Const128(c)] => BinOpGlobalConst128(BinOp128::AndNot, global, c)); } I32Store(memarg) | F32Store(memarg) => { rewrite!(instrs, i, @@ -175,6 +206,10 @@ fn rewrite( IncMemoryLocal32(memarg, load_addr) ); rewrite!(instrs, i, [F32Mul, F32Add] => FMaStoreF32(memarg)); + rewrite!(instrs, i, + [BinOpStackLocal32(BinOp::FMul, local), F32Add] => + [LocalGet32(local), FMaStoreF32(memarg)] + ); rewrite!(instrs, i, [LocalGet32(addr_local), LocalGet32(value_local)] if (let (Ok(addr_local), Ok(value_local)) = (u8::try_from(addr_local), u8::try_from(value_local))) => @@ -269,6 +304,9 @@ fn rewrite( MemoryFill(mem) => { rewrite!(instrs, i, [Const32(val), Const32(size)] => MemoryFillImm(mem, val as u8, size)) } + GlobalGet32(dst) => rewrite!(instrs, i, [GlobalSet32(src)] if (src == dst) => GlobalTee32(src)), + GlobalGet64(dst) => rewrite!(instrs, i, [GlobalSet64(src)] if (src == dst) => GlobalTee64(src)), + GlobalGet128(dst) => rewrite!(instrs, i, [GlobalSet128(src)] if (src == dst) => GlobalTee128(src)), LocalGet32(dst) => rewrite!(instrs, i, [LocalSet32(src)] if (src == dst) => LocalTee32(src)), LocalGet64(dst) => rewrite!(instrs, i, [LocalSet64(src)] if (src == dst) => LocalTee64(src)), LocalGet128(dst) => rewrite!(instrs, i, [LocalSet128(src)] if (src == dst) => LocalTee128(src)), @@ -287,6 +325,15 @@ fn rewrite( ); rewrite!(instrs, i, [I32Mul, LocalGet32(acc), I32Add] if (acc == dst) => MulAccLocal32(dst)); rewrite!(instrs, i, [F32Mul, LocalGet32(acc), F32Add] if (acc == dst) => FMulAccLocal32(dst)); + rewrite!(instrs, i, + [I32Mul, BinOpStackLocal32(BinOp::IAdd, acc)] if (acc == dst) => + MulAccLocal32(dst) + ); + rewrite!(instrs, i, + [F32Mul, BinOpStackLocal32(BinOp::FAdd, acc)] if (acc == dst) => + FMulAccLocal32(dst) + ); + rewrite!(instrs, i, [BinOpStackLocal32(op, local)] => BinOpStackLocalSet32(op, local, dst)); rewrite_local_set_direct!( instrs, i, @@ -377,6 +424,7 @@ fn rewrite( ); } LocalTee32(dst) => { + rewrite!(instrs, i, [BinOpStackLocal32(op, local)] => BinOpStackLocalTee32(op, local, dst)); fold_local_binop!( instrs, i, dst, source = resolve_local_source_32, @@ -475,16 +523,22 @@ fn rewrite( LoadLocalTee128(memarg, addr, dst) ); } - Drop32 => rewrite_drop_tee_direct!( - instrs, - i, - tee = LocalTee32, - set = LocalSet32, - binop_local_local_tee = BinOpLocalLocalTee32, - binop_local_local_set = BinOpLocalLocalSet32, - binop_local_const_tee = BinOpLocalConstTee32, - binop_local_const_set = BinOpLocalConstSet32 - ), + Drop32 => { + rewrite!(instrs, i, + [BinOpStackLocalTee32(op, local, dst)] => + BinOpStackLocalSet32(op, local, dst) + ); + rewrite_drop_tee_direct!( + instrs, + i, + tee = LocalTee32, + set = LocalSet32, + binop_local_local_tee = BinOpLocalLocalTee32, + binop_local_local_set = BinOpLocalLocalSet32, + binop_local_const_tee = BinOpLocalConstTee32, + binop_local_const_set = BinOpLocalConstSet32 + ); + } Drop64 => rewrite_drop_tee_direct!( instrs, i, @@ -534,7 +588,6 @@ fn rewrite( replace!(instrs, i, 1 => JumpIfNonZero32(target)); continue; }); - rewrite!(instrs, i, [LocalGet32(local)] => JumpIfLocalZero32 { target_ip: target, local }); rewrite!(instrs, i, [LocalGet64(local), I64Eqz] => { replace!(instrs, i, 2 => JumpIfLocalNonZero64 { target_ip: target, local }); continue; @@ -543,6 +596,56 @@ fn rewrite( replace!(instrs, i, 1 => JumpIfNonZero64(target)); continue; }); + rewrite!(instrs, i, + [BinOpLocalConstTee32(op, local, imm, dst)] if (local == dst) => + match inc_delta(op, imm) { + Some(delta) => IncLocalJump32 { target_ip: target, local, delta, on_zero: true }, + None => BinOpLocalConstJump32 { target_ip: target, local, imm, op, on_zero: true }, + } + ); + rewrite!(instrs, i, + [AddConst32(imm), LocalTee32(local), LocalGet32(cond)] if (local == cond) => + IncStackTeeLocalJump32 { target_ip: target, local, delta: imm, on_zero: true } + ); + rewrite!(instrs, i, + [AndConstTee32(imm, local), LocalGet32(cond)] if (local == cond) => + BinOpStackConstTeeLocalJump32 { target_ip: target, local, imm, op: BinOp::IAnd, on_zero: true } + ); + rewrite!(instrs, i, + [SubConstTee32(imm, local), LocalGet32(cond)] if (local == cond) => + IncStackTeeLocalJump32 { target_ip: target, local, delta: imm.wrapping_neg(), on_zero: true } + ); + rewrite!(instrs, i, + [BinOpGlobalConst32(op, global, imm), GlobalTee32(dst)] if (global == dst) => + match inc_delta(op, imm) { + Some(delta) => IncGlobalJump32 { target_ip: target, global, delta, on_zero: true }, + None => BinOpGlobalConstJump32 { target_ip: target, global, imm, op, on_zero: true }, + } + ); + rewrite!(instrs, i, + [CmpLocalLocal32(op, left, right)] => + JumpCmpLocalLocal32 { target_ip: target, left, right, op: inverse_cmp_op(op) } + ); + rewrite!(instrs, i, + [CmpLocalLocal64(op, left, right)] => + JumpCmpLocalLocal64 { target_ip: target, left, right, op: inverse_cmp_op(op) } + ); + rewrite!(instrs, i, + [BinOpLocalConstTee32(binop, local, imm, dst), LocalGet32(right), cmp] if + (local == dst && let Some(cmp) = cmp_op(cmp)) => + match inc_delta(binop, imm) { + Some(delta) => IncLocalJumpCmpLocal32 { target_ip: target, local, delta, right, op: inverse_cmp_op(cmp) }, + None => BinOpLocalConstJumpCmpLocal32 { target_ip: target, local, imm, binop, right, cmp: inverse_cmp_op(cmp) }, + } + ); + rewrite!(instrs, i, + [LocalGet32(local), cmp] if (let Some(op) = cmp_op(cmp)) => + JumpCmpStackLocal32 { target_ip: target, local, op: inverse_cmp_op(op) } + ); + rewrite!(instrs, i, + [LocalGet64(local), cmp] if (let Some(op) = cmp_op(cmp)) => + JumpCmpStackLocal64 { target_ip: target, local, op: inverse_cmp_op(op) } + ); rewrite!(instrs, i, [LocalGet32(local), Const32(imm), cmp] if (let Some(op) = cmp_op(cmp)) => match (imm, inverse_cmp_op(op)) { @@ -578,6 +681,8 @@ fn rewrite( (0, CmpOp::Ne) => JumpIfNonZero64(target), (imm, op) => JumpCmpStackConst64 { target_ip: target, imm, op }, }); + rewrite!(instrs, i, [LocalGet32(local)] => JumpIfLocalZero32 { target_ip: target, local }); + rewrite!(instrs, i, [LocalGet64(local)] => JumpIfLocalZero64 { target_ip: target, local }); canonicalize_jump_like_with_target(&mut instrs, i, target, old_idx as u32 + 1); } JumpIfNonZero32(ip) => { @@ -590,7 +695,6 @@ fn rewrite( replace!(instrs, i, 1 => JumpIfZero32(target)); continue; }); - rewrite!(instrs, i, [LocalGet32(local)] => JumpIfLocalNonZero32 { target_ip: target, local }); rewrite!(instrs, i, [LocalGet64(local), I64Eqz] => { replace!(instrs, i, 2 => JumpIfLocalZero64 { target_ip: target, local }); continue; @@ -599,6 +703,56 @@ fn rewrite( replace!(instrs, i, 1 => JumpIfZero64(target)); continue; }); + rewrite!(instrs, i, + [BinOpLocalConstTee32(op, local, imm, dst)] if (local == dst) => + match inc_delta(op, imm) { + Some(delta) => IncLocalJump32 { target_ip: target, local, delta, on_zero: false }, + None => BinOpLocalConstJump32 { target_ip: target, local, imm, op, on_zero: false }, + } + ); + rewrite!(instrs, i, + [AddConst32(imm), LocalTee32(local), LocalGet32(cond)] if (local == cond) => + IncStackTeeLocalJump32 { target_ip: target, local, delta: imm, on_zero: false } + ); + rewrite!(instrs, i, + [AndConstTee32(imm, local), LocalGet32(cond)] if (local == cond) => + BinOpStackConstTeeLocalJump32 { target_ip: target, local, imm, op: BinOp::IAnd, on_zero: false } + ); + rewrite!(instrs, i, + [SubConstTee32(imm, local), LocalGet32(cond)] if (local == cond) => + IncStackTeeLocalJump32 { target_ip: target, local, delta: imm.wrapping_neg(), on_zero: false } + ); + rewrite!(instrs, i, + [BinOpGlobalConst32(op, global, imm), GlobalTee32(dst)] if (global == dst) => + match inc_delta(op, imm) { + Some(delta) => IncGlobalJump32 { target_ip: target, global, delta, on_zero: false }, + None => BinOpGlobalConstJump32 { target_ip: target, global, imm, op, on_zero: false }, + } + ); + rewrite!(instrs, i, + [CmpLocalLocal32(op, left, right)] => + JumpCmpLocalLocal32 { target_ip: target, left, right, op } + ); + rewrite!(instrs, i, + [CmpLocalLocal64(op, left, right)] => + JumpCmpLocalLocal64 { target_ip: target, left, right, op } + ); + rewrite!(instrs, i, + [BinOpLocalConstTee32(binop, local, imm, dst), LocalGet32(right), cmp] if + (local == dst && let Some(cmp) = cmp_op(cmp)) => + match inc_delta(binop, imm) { + Some(delta) => IncLocalJumpCmpLocal32 { target_ip: target, local, delta, right, op: cmp }, + None => BinOpLocalConstJumpCmpLocal32 { target_ip: target, local, imm, binop, right, cmp }, + } + ); + rewrite!(instrs, i, + [LocalGet32(local), cmp] if (let Some(op) = cmp_op(cmp)) => + JumpCmpStackLocal32 { target_ip: target, local, op } + ); + rewrite!(instrs, i, + [LocalGet64(local), cmp] if (let Some(op) = cmp_op(cmp)) => + JumpCmpStackLocal64 { target_ip: target, local, op } + ); rewrite!(instrs, i, [LocalGet32(local), Const32(imm), cmp] if (let Some(op) = cmp_op(cmp)) => match (imm, op) { @@ -634,6 +788,8 @@ fn rewrite( (0, CmpOp::Ne) => JumpIfNonZero64(target), (imm, op) => JumpCmpStackConst64 { target_ip: target, imm, op }, }); + rewrite!(instrs, i, [LocalGet32(local)] => JumpIfLocalNonZero32 { target_ip: target, local }); + rewrite!(instrs, i, [LocalGet64(local)] => JumpIfLocalNonZero64 { target_ip: target, local }); canonicalize_jump_like_with_target(&mut instrs, i, target, old_idx as u32 + 1); } JumpIfZero64(ip) => { @@ -716,6 +872,14 @@ fn cmp_op(instr: Instruction) -> Option { }) } +fn inc_delta(op: BinOp, imm: i32) -> Option { + match op { + BinOp::IAdd => Some(imm), + BinOp::ISub => Some(imm.wrapping_neg()), + _ => None, + } +} + fn int_bin_op(instr: Instruction) -> Option { Some(match instr { Instruction::I32Add | Instruction::I64Add => BinOp::IAdd, @@ -858,8 +1022,19 @@ fn instruction_target_mut(instr: &mut Instruction) -> Option<&mut u32> { | Instruction::JumpIfNonZero64(ip) | Instruction::JumpIfRefNull(ip) | Instruction::JumpIfRefNonNull(ip) + | Instruction::BrOnCast(ip, _, _) | Instruction::JumpCmpStackConst32 { target_ip: ip, .. } | Instruction::JumpCmpStackConst64 { target_ip: ip, .. } + | Instruction::JumpCmpStackLocal32 { target_ip: ip, .. } + | Instruction::JumpCmpStackLocal64 { target_ip: ip, .. } + | Instruction::BinOpLocalConstJump32 { target_ip: ip, .. } + | Instruction::BinOpLocalConstJumpCmpLocal32 { target_ip: ip, .. } + | Instruction::BinOpStackConstTeeLocalJump32 { target_ip: ip, .. } + | Instruction::BinOpGlobalConstJump32 { target_ip: ip, .. } + | Instruction::IncLocalJump32 { target_ip: ip, .. } + | Instruction::IncStackTeeLocalJump32 { target_ip: ip, .. } + | Instruction::IncGlobalJump32 { target_ip: ip, .. } + | Instruction::IncLocalJumpCmpLocal32 { target_ip: ip, .. } | Instruction::JumpIfLocalZero32 { target_ip: ip, .. } | Instruction::JumpIfLocalNonZero32 { target_ip: ip, .. } | Instruction::JumpIfLocalZero64 { target_ip: ip, .. } @@ -909,11 +1084,28 @@ fn is_unconditional_terminator(instr: Instruction) -> bool { | Instruction::ReturnCallSelf | Instruction::ReturnCallIndirect(..) | Instruction::ReturnCallRef(_) + | Instruction::Throw(_) + | Instruction::ThrowRef ) } fn target_boundaries(instructions: &[Instruction], function_data: &WasmFunctionData) -> Result> { let mut boundaries = alloc::vec![false; instructions.len() + 1]; + for handler in &function_data.exception_handlers { + for target in [handler.start_ip, handler.end_ip] { + let boundary = boundaries.get_mut(target as usize).ok_or_else(|| { + ParseError::Other(alloc::format!("exception handler boundary out of bounds: {target}")) + })?; + *boundary = true; + } + for catch in &handler.catches { + let target = catch.landing_pad(); + let boundary = boundaries + .get_mut(target as usize) + .ok_or_else(|| ParseError::Other(alloc::format!("exception landing pad out of bounds: {target}")))?; + *boundary = true; + } + } for instr in instructions { if let Some(target) = instruction_target(instr) { let boundary = boundaries @@ -947,6 +1139,31 @@ fn finalize( imported_memory_count: u32, ) -> Result { let len = instructions.len() as u32; + for handler in &mut function_data.exception_handlers { + if let Some(old_to_new) = old_to_new { + handler.start_ip = *old_to_new.get(handler.start_ip as usize).ok_or_else(|| { + ParseError::Other(alloc::format!("exception handler boundary out of bounds: {}", handler.start_ip)) + })?; + handler.end_ip = *old_to_new.get(handler.end_ip as usize).ok_or_else(|| { + ParseError::Other(alloc::format!("exception handler boundary out of bounds: {}", handler.end_ip)) + })?; + for catch in &mut handler.catches { + let landing_pad = match catch { + tinywasm_types::ExceptionCatch::Tag { landing_pad, .. } + | tinywasm_types::ExceptionCatch::All { landing_pad, .. } => landing_pad, + }; + *landing_pad = *old_to_new.get(*landing_pad as usize).ok_or_else(|| { + ParseError::Other(alloc::format!("exception landing pad out of bounds: {landing_pad}")) + })?; + } + } + if handler.start_ip > handler.end_ip || handler.end_ip > len { + return Err(ParseError::Other("exception handler range out of bounds".into())); + } + if handler.catches.iter().any(|catch| catch.landing_pad() >= len) { + return Err(ParseError::Other("exception landing pad out of bounds".into())); + } + } for target in &mut function_data.branch_table_targets { if let Some(old_to_new) = old_to_new { *target = *old_to_new diff --git a/crates/parser/src/parallel.rs b/crates/parser/src/parallel.rs index 88d10353..e0e38584 100644 --- a/crates/parser/src/parallel.rs +++ b/crates/parser/src/parallel.rs @@ -1,10 +1,11 @@ use crate::module::{FunctionCode, optimize_function_code}; +use crate::validation::{FuncToValidate, FuncValidatorAllocations, ValidatorResources}; use crate::{ParseError, ParserOptions, Result, conversion}; use alloc::sync::Arc; use alloc::vec::Vec; use core::ops::Range; use tinywasm_types::ValueCounts; -use wasmparser::{FuncValidatorAllocations, OperatorsReaderAllocations, ValidatorResources}; +use wasmparser::OperatorsReaderAllocations; pub(crate) enum FunctionBodyInput<'a> { Borrowed(wasmparser::FunctionBody<'a>), @@ -22,7 +23,7 @@ pub(crate) struct OwnedFunctionBody { pub(crate) struct PendingFunction<'a> { pub ordinal: usize, pub results: ValueCounts, - pub func_to_validate: Option>, + pub func_to_validate: Option>, pub ty_idx: u32, pub body: FunctionBodyInput<'a>, } @@ -64,7 +65,13 @@ fn process_function_job( validator_allocs: Option, reader_allocs: OperatorsReaderAllocations, ) -> Result<(FunctionCode, Option, OperatorsReaderAllocations)> { + #[cfg(feature = "validate")] let validator = job.func_to_validate.map(|func| func.into_validator(validator_allocs.unwrap_or_default())); + #[cfg(not(feature = "validate"))] + let validator = { + let _ = (job.func_to_validate, validator_allocs); + None + }; let (code, validator_allocs, reader_allocs) = match job.body { FunctionBodyInput::Borrowed(func) => { conversion::convert_module_code(func, validator, reader_allocs, metadata, job.ty_idx)? diff --git a/crates/parser/src/validation.rs b/crates/parser/src/validation.rs new file mode 100644 index 00000000..b2bb0842 --- /dev/null +++ b/crates/parser/src/validation.rs @@ -0,0 +1,15 @@ +#[cfg(all(feature = "validate", parallel_parser))] +pub(crate) use wasmparser::FuncToValidate; +#[cfg(feature = "validate")] +pub(crate) use wasmparser::{FuncValidator, FuncValidatorAllocations, Validator, ValidatorResources}; + +#[cfg(all(not(feature = "validate"), parallel_parser))] +pub(crate) type FuncToValidate = core::marker::PhantomData; +#[cfg(not(feature = "validate"))] +pub(crate) type FuncValidator = core::marker::PhantomData; +#[cfg(not(feature = "validate"))] +pub(crate) type FuncValidatorAllocations = (); +#[cfg(not(feature = "validate"))] +pub(crate) type Validator = (); +#[cfg(not(feature = "validate"))] +pub(crate) type ValidatorResources = (); diff --git a/crates/parser/src/visit.rs b/crates/parser/src/visit.rs index d0e970bc..cecbb8c1 100644 --- a/crates/parser/src/visit.rs +++ b/crates/parser/src/visit.rs @@ -1,14 +1,17 @@ -use crate::{Result, conversion::convert_heap_type, macros::visit::*}; -use alloc::string::ToString; -use alloc::vec::Vec; -use tinywasm_types::{ - FuncType, Global, Import, ImportKind, Instruction, MemoryArch, MemoryArg, MemoryType, TableDefinition, ValueCounts, - WasmFunctionData, WasmType, +use crate::{ + Result, + conversion::{convert_heap_type, value_lane}, + macros::visit::*, }; -use wasmparser::{ - FuncValidator, FuncValidatorAllocations, FunctionBody, OperatorsReader, OperatorsReaderAllocations, - ValidatorResources, VisitOperator, VisitSimdOperator, +use alloc::{boxed::Box, string::ToString, vec::Vec}; +use tinywasm_types::{ + Global, Import, ImportKind, Instruction, MemoryArg, MemoryType, StorageType, TableDefinition, TagType, TypeSection, + ValueCounts, ValueLane, WasmFunctionData, }; +use wasmparser::{FunctionBody, OperatorsReader, OperatorsReaderAllocations, VisitSimdOperator}; + +#[cfg(feature = "validate")] +use wasmparser::{FuncValidator, FuncValidatorAllocations, ValidatorResources, VisitOperator}; #[derive(Debug, Clone, Copy)] enum BlockKind { @@ -16,52 +19,7 @@ enum BlockKind { Block, Loop, If, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum OperandSize { - S32, - S64, - S128, -} - -impl OperandSize { - fn choose(self, s32: T, s64: T, s128: T) -> T { - match self { - Self::S32 => s32, - Self::S64 => s64, - Self::S128 => s128, - } - } -} - -impl From for OperandSize { - fn from(ty: wasmparser::ValType) -> Self { - match ty { - wasmparser::ValType::I32 | wasmparser::ValType::F32 | wasmparser::ValType::Ref(_) => Self::S32, - wasmparser::ValType::I64 | wasmparser::ValType::F64 => Self::S64, - wasmparser::ValType::V128 => Self::S128, - } - } -} - -impl From<&WasmType> for OperandSize { - fn from(ty: &WasmType) -> Self { - match ty { - WasmType::I32 | WasmType::F32 | WasmType::Ref(_) => Self::S32, - WasmType::I64 | WasmType::F64 => Self::S64, - WasmType::V128 => Self::S128, - } - } -} - -impl From for OperandSize { - fn from(arch: MemoryArch) -> Self { - match arch { - MemoryArch::I32 => Self::S32, - MemoryArch::I64 => Self::S64, - } - } + TryTable(usize), } struct ControlFrame { @@ -71,8 +29,8 @@ struct ControlFrame { branch_jumps: Vec, height: usize, base: ValueCounts, - params: Vec, - results: Vec, + params: Vec, + results: Vec, unreachable: bool, entry_unreachable: bool, end_reachable: bool, @@ -80,32 +38,41 @@ struct ControlFrame { #[derive(Clone)] pub(crate) struct Signature { - pub params: Vec, - results: Vec, + pub params: Vec, + results: Vec, } pub(crate) struct ModuleMetadata { - signatures: Vec, + signatures: Vec>, functions: Vec, - globals: Vec, - memories: Vec, - tables: Vec, + globals: Vec, + memories: Vec, + tables: Vec, + tags: Vec, + aggregate_fields: Vec, +} + +enum AggregateFields { + Other, + Struct(Box<[ValueLane]>), + Array(ValueLane), } #[derive(Default)] struct FunctionDataBuilder { v128_constants: Vec<[u8; 16]>, branch_table_targets: Vec, + exception_handlers: Vec, } pub(crate) struct FunctionBuilder<'a> { instructions: Vec, data: FunctionDataBuilder, control_stack: Vec, - operand_stack: Vec, + operand_stack: Vec, lane_counts: ValueCounts, metadata: &'a ModuleMetadata, - local_types: Vec, + local_types: Vec, local_addr_map: Vec, } @@ -113,7 +80,7 @@ impl<'a> FunctionBuilder<'a> { pub(crate) fn new( metadata: &'a ModuleMetadata, signature: Signature, - local_types: Vec, + local_types: Vec, local_addr_map: Vec, body_size: usize, ) -> Self { @@ -140,56 +107,103 @@ impl<'a> FunctionBuilder<'a> { lane_counts: ValueCounts::default(), } } + + fn visit_struct_get_impl( + &mut self, + type_index: u32, + field_index: u32, + instruction: fn(u32, u32) -> Instruction, + ) -> Result<()> { + let size = self.metadata.struct_field(type_index, field_index)?; + self.emit(&[ValueLane::S32], &[size], instruction(type_index, field_index)) + } } +#[cfg(feature = "validate")] struct ValidateThenVisit<'a, 'm> { - validator: Option<&'a mut FuncValidator>, + validator: &'a mut FuncValidator, builder: &'a mut FunctionBuilder<'m>, position: usize, } impl ModuleMetadata { + fn address_lane(arch: tinywasm_types::MemoryArch) -> ValueLane { + match arch { + tinywasm_types::MemoryArch::I32 => ValueLane::S32, + tinywasm_types::MemoryArch::I64 => ValueLane::S64, + } + } + pub(crate) fn new( - types: &[FuncType], + types: &TypeSection, code_type_addrs: &[u32], imports: &[Import], globals: &[Global], memories: &[MemoryType], tables: &[TableDefinition], + tags: &[TagType], ) -> Self { let mut functions = Vec::with_capacity(imports.len() + code_type_addrs.len()); let mut global_sizes = Vec::with_capacity(imports.len() + globals.len()); let mut memory_sizes = Vec::with_capacity(imports.len() + memories.len()); let mut table_sizes = Vec::with_capacity(imports.len() + tables.len()); + let mut tag_types = Vec::with_capacity(imports.len() + tags.len()); for import in imports { match &import.kind { ImportKind::Function(ty) => functions.push(*ty), - ImportKind::Global(ty) => global_sizes.push(OperandSize::from(&ty.ty)), - ImportKind::Memory(ty) => memory_sizes.push(OperandSize::from(ty.arch())), - ImportKind::Table(ty) => table_sizes.push(OperandSize::from(ty.arch())), + ImportKind::Global(ty) => global_sizes.push(ValueLane::from(&ty.ty)), + ImportKind::Memory(ty) => memory_sizes.push(Self::address_lane(ty.arch())), + ImportKind::Table(ty) => table_sizes.push(Self::address_lane(ty.arch())), + ImportKind::Tag(ty) => tag_types.push(ty.type_idx), } } functions.extend_from_slice(code_type_addrs); - global_sizes.extend(globals.iter().map(|global| OperandSize::from(&global.ty.ty))); - memory_sizes.extend(memories.iter().map(|ty| OperandSize::from(ty.arch()))); - table_sizes.extend(tables.iter().map(|table| OperandSize::from(table.ty.arch()))); + global_sizes.extend(globals.iter().map(|global| ValueLane::from(&global.ty.ty))); + memory_sizes.extend(memories.iter().map(|ty| Self::address_lane(ty.arch()))); + table_sizes.extend(tables.iter().map(|table| Self::address_lane(table.ty.arch()))); + tag_types.extend(tags.iter().map(|tag| tag.type_idx)); let signatures = types + .types + .iter() + .map(|ty| { + ty.as_func().map(|ty| Signature { + params: ty.params().iter().map(ValueLane::from).collect(), + results: ty.results().iter().map(ValueLane::from).collect(), + }) + }) + .collect(); + let aggregate_fields = types + .types .iter() - .map(|ty| Signature { - params: ty.params().iter().map(OperandSize::from).collect(), - results: ty.results().iter().map(OperandSize::from).collect(), + .map(|ty| { + if let Some(ty) = ty.as_struct() { + AggregateFields::Struct(ty.fields.iter().map(|field| Self::storage_size(field.storage)).collect()) + } else if let Some(ty) = ty.as_array() { + AggregateFields::Array(Self::storage_size(ty.field.storage)) + } else { + AggregateFields::Other + } }) .collect(); - Self { signatures, functions, globals: global_sizes, memories: memory_sizes, tables: table_sizes } + Self { + signatures, + functions, + globals: global_sizes, + memories: memory_sizes, + tables: table_sizes, + tags: tag_types, + aggregate_fields, + } } pub(crate) fn signature(&self, idx: u32) -> Result<&Signature> { self.signatures .get(idx as usize) - .ok_or_else(|| crate::ParseError::Other(alloc::format!("type index out of bounds: {idx}"))) + .and_then(Option::as_ref) + .ok_or_else(|| crate::ParseError::Other(alloc::format!("type index is not a function type: {idx}"))) } fn function_signature(&self, idx: u32) -> Result<&Signature> { @@ -200,26 +214,69 @@ impl ModuleMetadata { self.signature(ty) } - fn global_size(&self, idx: u32) -> Result { + fn tag_signature(&self, idx: u32) -> Result<&Signature> { + let ty = *self + .tags + .get(idx as usize) + .ok_or_else(|| crate::ParseError::Other(alloc::format!("tag index out of bounds: {idx}")))?; + self.signature(ty) + } + + fn global_size(&self, idx: u32) -> Result { Self::indexed_size(&self.globals, "global", idx) } - fn memory_size(&self, idx: u32) -> Result { + fn memory_size(&self, idx: u32) -> Result { Self::indexed_size(&self.memories, "memory", idx) } - fn table_size(&self, idx: u32) -> Result { + fn table_size(&self, idx: u32) -> Result { Self::indexed_size(&self.tables, "table", idx) } - fn indexed_size(sizes: &[OperandSize], entity: &str, idx: u32) -> Result { + fn indexed_size(sizes: &[ValueLane], entity: &str, idx: u32) -> Result { sizes .get(idx as usize) .copied() .ok_or_else(|| crate::ParseError::Other(alloc::format!("{entity} index out of bounds: {idx}"))) } + + fn storage_size(storage: StorageType) -> ValueLane { + match storage { + StorageType::I8 | StorageType::I16 => ValueLane::S32, + StorageType::Value(ref ty) => ValueLane::from(ty), + } + } + + fn struct_fields(&self, idx: u32) -> Result<&[ValueLane]> { + self.aggregate_fields + .get(idx as usize) + .and_then(|fields| match fields { + AggregateFields::Struct(fields) => Some(fields.as_ref()), + AggregateFields::Other | AggregateFields::Array(_) => None, + }) + .ok_or_else(|| crate::ParseError::Other(alloc::format!("type index is not a struct type: {idx}"))) + } + + fn struct_field(&self, type_index: u32, field_index: u32) -> Result { + self.struct_fields(type_index)? + .get(field_index as usize) + .copied() + .ok_or_else(|| crate::ParseError::Other("struct field index out of bounds".into())) + } + + fn array_field(&self, idx: u32) -> Result { + self.aggregate_fields + .get(idx as usize) + .and_then(|fields| match fields { + AggregateFields::Array(field) => Some(*field), + AggregateFields::Other | AggregateFields::Struct(_) => None, + }) + .ok_or_else(|| crate::ParseError::Other(alloc::format!("type index is not an array type: {idx}"))) + } } +#[cfg(feature = "validate")] impl<'a> VisitOperator<'a> for ValidateThenVisit<'_, '_> { type Output = Result<()>; @@ -230,19 +287,19 @@ impl<'a> VisitOperator<'a> for ValidateThenVisit<'_, '_> { } } +#[cfg(feature = "validate")] impl VisitSimdOperator<'_> for ValidateThenVisit<'_, '_> { wasmparser::for_each_visit_simd_operator!(validate_then_visit_simd); } -pub(crate) fn process_operators_and_validate( - mut validator: Option>, +pub(crate) fn process_operators( body: FunctionBody<'_>, - local_types: Vec, + local_types: Vec, local_addr_map: Vec, metadata: &ModuleMetadata, ty_idx: u32, allocs: OperatorsReaderAllocations, -) -> Result<(Vec, WasmFunctionData, Option, OperatorsReaderAllocations)> { +) -> Result<(Vec, WasmFunctionData, OperatorsReaderAllocations)> { let body_size = body.as_bytes().len(); let reader = body.get_binary_reader_for_operators()?; let mut reader = OperatorsReader::new_with_allocs(reader, allocs); @@ -252,7 +309,7 @@ pub(crate) fn process_operators_and_validate( while !reader.eof() { let position = reader.original_position(); let res = reader - .visit_operator(&mut ValidateThenVisit { validator: validator.as_mut(), builder: &mut builder, position }) + .visit_operator(&mut builder) .map_err(|e| crate::ParseError::ParseError { message: e.to_string(), offset: position }); if let Err(e) = res.flatten() { @@ -262,13 +319,49 @@ pub(crate) fn process_operators_and_validate( } reader.finish()?; + let data = WasmFunctionData { + v128_constants: builder.data.v128_constants.into_boxed_slice(), + branch_table_targets: builder.data.branch_table_targets.into_boxed_slice(), + exception_handlers: builder.data.exception_handlers.into_boxed_slice(), + }; + Ok((builder.instructions, data, reader.into_allocations())) +} + +#[cfg(feature = "validate")] +pub(crate) fn process_operators_and_validate( + mut validator: FuncValidator, + body: FunctionBody<'_>, + local_types: Vec, + local_addr_map: Vec, + metadata: &ModuleMetadata, + ty_idx: u32, + allocs: OperatorsReaderAllocations, +) -> Result<(Vec, WasmFunctionData, FuncValidatorAllocations, OperatorsReaderAllocations)> { + let body_size = body.as_bytes().len(); + let reader = body.get_binary_reader_for_operators()?; + let mut reader = OperatorsReader::new_with_allocs(reader, allocs); + let signature = metadata.signature(ty_idx)?.clone(); + let mut builder = FunctionBuilder::new(metadata, signature, local_types, local_addr_map, body_size); - let validator_allocations = validator.map(FuncValidator::into_allocations); + while !reader.eof() { + let position = reader.original_position(); + let res = reader + .visit_operator(&mut ValidateThenVisit { validator: &mut validator, builder: &mut builder, position }) + .map_err(|e| crate::ParseError::ParseError { message: e.to_string(), offset: position }); + + if let Err(e) = res.flatten() { + core::hint::cold_path(); + return Err(e); + } + } + + reader.finish()?; let data = WasmFunctionData { v128_constants: builder.data.v128_constants.into_boxed_slice(), branch_table_targets: builder.data.branch_table_targets.into_boxed_slice(), + exception_handlers: builder.data.exception_handlers.into_boxed_slice(), }; - Ok((builder.instructions, data, validator_allocations, reader.into_allocations())) + Ok((builder.instructions, data, validator.into_allocations(), reader.into_allocations())) } impl<'a> wasmparser::VisitOperator<'a> for FunctionBuilder<'_> { @@ -303,8 +396,17 @@ impl<'a> wasmparser::VisitOperator<'a> for FunctionBuilder<'_> { fixed [] => [] { visit_data_drop(segment: u32) => DataDrop, visit_elem_drop(segment: u32) => ElemDrop } fixed [] => [S32] { visit_i32_const(value: i32) => Const32, visit_ref_func(function: u32) => RefFunc } fixed [] => [S64] { visit_i64_const(value: i64) => Const64 } + heap false [] => [S32] { visit_ref_null => RefNull } + heap false [S32] => [S32] { + visit_ref_test_non_null => RefTest, visit_ref_cast_non_null => RefCast, + } + heap true [S32] => [S32] { + visit_ref_test_nullable => RefTest, visit_ref_cast_nullable => RefCast, + } fixed [S32] => [S32] { - visit_i32_eqz => I32Eqz, visit_ref_is_null => RefIsNull, visit_i32_clz => I32Clz, + visit_ref_is_null => RefIsNull, visit_ref_as_non_null => RefAsNonNull, visit_ref_i31 => RefI31, + visit_i31_get_s => I31GetS, visit_i31_get_u => I31GetU, + visit_i32_eqz => I32Eqz, visit_i32_clz => I32Clz, visit_i32_ctz => I32Ctz, visit_i32_popcnt => I32Popcnt, visit_i32_extend8_s => I32Extend8S, visit_i32_extend16_s => I32Extend16S, visit_i32_trunc_f32_s => I32TruncF32S, visit_i32_trunc_f32_u => I32TruncF32U, visit_f32_convert_i32_s => F32ConvertI32S, @@ -313,6 +415,7 @@ impl<'a> wasmparser::VisitOperator<'a> for FunctionBuilder<'_> { visit_f32_ceil => F32Ceil, visit_f32_floor => F32Floor, visit_f32_trunc => F32Trunc, visit_f32_nearest => F32Nearest, visit_f32_sqrt => F32Sqrt, } + effect [S32] => [S32] { visit_any_convert_extern, visit_extern_convert_any } fixed [S64] => [S64] { visit_i64_clz => I64Clz, visit_i64_ctz => I64Ctz, visit_i64_popcnt => I64Popcnt, visit_i64_extend8_s => I64Extend8S, visit_i64_extend16_s => I64Extend16S, @@ -337,7 +440,8 @@ impl<'a> wasmparser::VisitOperator<'a> for FunctionBuilder<'_> { visit_i64_trunc_sat_f32_u => I64TruncSatF32U, } fixed [S32, S32] => [S32] { - visit_i32_eq => I32Eq, visit_i32_ne => I32Ne, visit_i32_lt_s => I32LtS, visit_i32_lt_u => I32LtU, + visit_ref_eq => RefEq, visit_i32_eq => I32Eq, visit_i32_ne => I32Ne, + visit_i32_lt_s => I32LtS, visit_i32_lt_u => I32LtU, visit_i32_gt_s => I32GtS, visit_i32_gt_u => I32GtU, visit_i32_le_s => I32LeS, visit_i32_le_u => I32LeU, visit_i32_ge_s => I32GeS, visit_i32_ge_u => I32GeU, visit_f32_eq => F32Eq, visit_f32_ne => F32Ne, visit_f32_lt => F32Lt, visit_f32_gt => F32Gt, @@ -371,19 +475,75 @@ impl<'a> wasmparser::VisitOperator<'a> for FunctionBuilder<'_> { effect [S32] => [S32] { visit_f32_reinterpret_i32, visit_i32_reinterpret_f32 } effect [S64] => [S64] { visit_f64_reinterpret_i64, visit_i64_reinterpret_f64 } terminating [] => [] { visit_unreachable => Unreachable, visit_return => Return } - global [] => [Addr] { visit_global_get(global_index: u32) => GlobalGet } memory_index [] => [Addr] { visit_memory_size(memory: u32) => MemorySize } memory_index [Addr] => [Addr] { visit_memory_grow(memory: u32) => MemoryGrow } - memory_index [Addr, S32, Addr] => [] { - visit_memory_init(data_index: u32, memory: u32) => MemoryInit, - visit_memory_fill(memory: u32) => MemoryFill, - } + memory_index [Addr, S32, Addr] => [] { visit_memory_fill(memory: u32) => MemoryFill } table [Addr] => [S32] { visit_table_get(table: u32) => TableGet } table [Addr, S32] => [] { visit_table_set(table: u32) => TableSet } table [] => [Addr] { visit_table_size(table: u32) => TableSize } table [S32, Addr] => [Addr] { visit_table_grow(table: u32) => TableGrow } table [Addr, S32, Addr] => [] { visit_table_fill(table: u32) => TableFill } table [Addr, S32, S32] => [] { visit_table_init(elem_index: u32, table: u32) => TableInit } + fixed [] => [S32] { visit_struct_new_default(type_index: u32) => StructNewDefault } + fixed [S32] => [S32] { + visit_array_new_default(type_index: u32) => ArrayNewDefault, visit_array_len => ArrayLen, + } + fixed [S32, S32] => [S32] { + visit_array_new_data(type_index: u32, data_index: u32) => ArrayNewData, + visit_array_new_elem(type_index: u32, elem_index: u32) => ArrayNewElem, + } + fixed [S32, S32, S32, S32] => [] { + visit_array_init_data(type_index: u32, data_index: u32) => ArrayInitData, + visit_array_init_elem(type_index: u32, elem_index: u32) => ArrayInitElem, + } + fixed [S32, S32, S32, S32, S32] => [] { + visit_array_copy(type_index_dst: u32, type_index_src: u32) => ArrayCopy, + } + array_field [Field, S32] => [S32] { visit_array_new(type_index: u32) => ArrayNew } + array_field [S32, S32] => [Field] { + visit_array_get(type_index: u32) => ArrayGet, visit_array_get_s(type_index: u32) => ArrayGetS, + visit_array_get_u(type_index: u32) => ArrayGetU, + } + array_field [S32, S32, Field] => [] { visit_array_set(type_index: u32) => ArraySet } + array_field [S32, S32, Field, S32] => [] { visit_array_fill(type_index: u32) => ArrayFill } + } + + fn visit_struct_new(&mut self, type_index: u32) -> Self::Output { + let field_count = self.metadata.struct_fields(type_index)?.len(); + for field_index in (0..field_count).rev() { + let size = self.metadata.struct_field(type_index, field_index as u32)?; + self.pop_expect(size)?; + } + self.push_sizes(&[ValueLane::S32])?; + self.instructions.push(Instruction::StructNew(type_index)); + Ok(()) + } + + fn visit_struct_get(&mut self, type_index: u32, field_index: u32) -> Self::Output { + self.visit_struct_get_impl(type_index, field_index, Instruction::StructGet) + } + + fn visit_struct_get_s(&mut self, type_index: u32, field_index: u32) -> Self::Output { + self.visit_struct_get_impl(type_index, field_index, Instruction::StructGetS) + } + + fn visit_struct_get_u(&mut self, type_index: u32, field_index: u32) -> Self::Output { + self.visit_struct_get_impl(type_index, field_index, Instruction::StructGetU) + } + + fn visit_struct_set(&mut self, type_index: u32, field_index: u32) -> Self::Output { + let size = self.metadata.struct_field(type_index, field_index)?; + self.emit(&[ValueLane::S32, size], &[], Instruction::StructSet(type_index, field_index)) + } + + fn visit_array_new_fixed(&mut self, type_index: u32, array_size: u32) -> Self::Output { + let size = self.metadata.array_field(type_index)?; + for _ in 0..array_size { + self.pop_expect(size)?; + } + self.push_sizes(&[ValueLane::S32])?; + self.instructions.push(Instruction::ArrayNewFixed(type_index, array_size)); + Ok(()) } fn visit_call(&mut self, function_index: u32) -> Self::Output { @@ -401,7 +561,7 @@ impl<'a> wasmparser::VisitOperator<'a> for FunctionBuilder<'_> { fn visit_call_ref(&mut self, type_index: u32) -> Self::Output { let signature = self.metadata.signature(type_index)?.clone(); let mut inputs = signature.params; - inputs.push(OperandSize::S32); + inputs.push(ValueLane::S32); self.emit(&inputs, &signature.results, Instruction::CallRef(type_index)) } @@ -426,7 +586,7 @@ impl<'a> wasmparser::VisitOperator<'a> for FunctionBuilder<'_> { fn visit_return_call_ref(&mut self, type_index: u32) -> Self::Output { let signature = self.metadata.signature(type_index)?.clone(); let mut inputs = signature.params; - inputs.push(OperandSize::S32); + inputs.push(ValueLane::S32); self.apply_effect(&inputs, &[])?; self.mark_unreachable(); self.instructions.push(Instruction::ReturnCallRef(type_index)); @@ -435,7 +595,7 @@ impl<'a> wasmparser::VisitOperator<'a> for FunctionBuilder<'_> { fn visit_global_set(&mut self, global_index: u32) -> Self::Output { let size = self.metadata.global_size(global_index)?; - let instruction = size.choose( + let instruction = size.select( Instruction::GlobalSet32(global_index), Instruction::GlobalSet64(global_index), Instruction::GlobalSet128(global_index), @@ -443,21 +603,31 @@ impl<'a> wasmparser::VisitOperator<'a> for FunctionBuilder<'_> { self.emit(&[size], &[], instruction) } + fn visit_global_get(&mut self, global_index: u32) -> Self::Output { + let size = self.metadata.global_size(global_index)?; + let instruction = size.select( + Instruction::GlobalGet32(global_index), + Instruction::GlobalGet64(global_index), + Instruction::GlobalGet128(global_index), + ); + self.emit(&[], &[size], instruction) + } + fn visit_drop(&mut self) -> Self::Output { - let size = self.operand_stack.last().copied().unwrap_or(OperandSize::S32); - let instruction = size.choose(Instruction::Drop32, Instruction::Drop64, Instruction::Drop128); + let size = self.operand_stack.last().copied().unwrap_or(ValueLane::S32); + let instruction = size.select(Instruction::Drop32, Instruction::Drop64, Instruction::Drop128); self.emit(&[size], &[], instruction) } fn visit_select(&mut self) -> Self::Output { - let size = self.operand_stack.iter().rev().nth(1).copied().unwrap_or(OperandSize::S32); - let instruction = size.choose(Instruction::Select32, Instruction::Select64, Instruction::Select128); - self.emit(&[size, size, OperandSize::S32], &[size], instruction) + let size = self.operand_stack.iter().rev().nth(1).copied().unwrap_or(ValueLane::S32); + let instruction = size.select(Instruction::Select32, Instruction::Select64, Instruction::Select128); + self.emit(&[size, size, ValueLane::S32], &[size], instruction) } fn visit_local_get(&mut self, idx: u32) -> Self::Output { let (size, local_idx) = self.local(idx)?; - let instruction = size.choose( + let instruction = size.select( Instruction::LocalGet32(local_idx), Instruction::LocalGet64(local_idx), Instruction::LocalGet128(local_idx), @@ -467,7 +637,7 @@ impl<'a> wasmparser::VisitOperator<'a> for FunctionBuilder<'_> { fn visit_local_set(&mut self, idx: u32) -> Self::Output { let (size, local_idx) = self.local(idx)?; - let instruction = size.choose( + let instruction = size.select( Instruction::LocalSet32(local_idx), Instruction::LocalSet64(local_idx), Instruction::LocalSet128(local_idx), @@ -479,21 +649,21 @@ impl<'a> wasmparser::VisitOperator<'a> for FunctionBuilder<'_> { let (size, local_idx) = self.local(idx)?; self.apply_effect(&[size], &[size])?; let src = match (size, self.instructions.last()) { - (OperandSize::S32, Some(Instruction::LocalGet32(src))) => Some(*src), - (OperandSize::S64, Some(Instruction::LocalGet64(src))) => Some(*src), - (OperandSize::S128, Some(Instruction::LocalGet128(src))) => Some(*src), + (ValueLane::S32, Some(Instruction::LocalGet32(src))) => Some(*src), + (ValueLane::S64, Some(Instruction::LocalGet64(src))) => Some(*src), + (ValueLane::S128, Some(Instruction::LocalGet128(src))) => Some(*src), _ => None, }; if let Some(src) = src { self.instructions.pop(); let instructions = match size { - OperandSize::S32 => [Instruction::LocalCopy32(src, local_idx), Instruction::LocalGet32(local_idx)], - OperandSize::S64 => [Instruction::LocalCopy64(src, local_idx), Instruction::LocalGet64(local_idx)], - OperandSize::S128 => [Instruction::LocalCopy128(src, local_idx), Instruction::LocalGet128(local_idx)], + ValueLane::S32 => [Instruction::LocalCopy32(src, local_idx), Instruction::LocalGet32(local_idx)], + ValueLane::S64 => [Instruction::LocalCopy64(src, local_idx), Instruction::LocalGet64(local_idx)], + ValueLane::S128 => [Instruction::LocalCopy128(src, local_idx), Instruction::LocalGet128(local_idx)], }; self.instructions.extend(instructions); } else { - self.instructions.push(size.choose( + self.instructions.push(size.select( Instruction::LocalTee32(local_idx), Instruction::LocalTee64(local_idx), Instruction::LocalTee128(local_idx), @@ -511,11 +681,94 @@ impl<'a> wasmparser::VisitOperator<'a> for FunctionBuilder<'_> { } fn visit_if(&mut self, ty: wasmparser::BlockType) -> Self::Output { - self.pop_expect(OperandSize::S32)?; + self.pop_expect(ValueLane::S32)?; self.instructions.push(Instruction::JumpIfZero32(0)); self.push_control(BlockKind::If, ty, Some(self.instructions.len() - 1)) } + fn visit_try_table(&mut self, try_table: wasmparser::TryTable) -> Self::Output { + let signature = self.block_signature(try_table.ty)?; + for &size in signature.params.iter().rev() { + self.pop_expect(size)?; + } + let height = self.operand_stack.len(); + let base = self.lane_counts; + let entry_unreachable = self.is_unreachable(); + + let body_jump = self.instructions.len(); + self.instructions.push(Instruction::Jump(0)); + let mut catches = Vec::with_capacity(try_table.catches.len()); + for catch in try_table.catches { + let (tag, depth, with_ref) = match catch { + wasmparser::Catch::One { tag, label } => (Some(tag), label, false), + wasmparser::Catch::OneRef { tag, label } => (Some(tag), label, true), + wasmparser::Catch::All { label } => (None, label, false), + wasmparser::Catch::AllRef { label } => (None, label, true), + }; + if let Some(tag) = tag { + self.metadata.tag_signature(tag)?; + } + let target_idx = self.get_ctx_idx(depth)?; + let target_base = self.control_stack[target_idx].base; + let landing_pad = u32::try_from(self.instructions.len()) + .map_err(|_| crate::ParseError::Other("function body is too large".into()))?; + match self.control_stack[target_idx].kind { + BlockKind::Function => self.instructions.push(Instruction::Return), + BlockKind::Loop => { + self.instructions.push(Instruction::Jump(self.control_stack[target_idx].start_ip as u32)); + } + BlockKind::Block | BlockKind::If | BlockKind::TryTable(_) => { + self.control_stack[target_idx].branch_jumps.push(self.instructions.len()); + self.control_stack[target_idx].end_reachable = true; + self.instructions.push(Instruction::Jump(0)); + } + } + catches.push(match tag { + Some(tag) => tinywasm_types::ExceptionCatch::Tag { tag, landing_pad, base: target_base, with_ref }, + None => tinywasm_types::ExceptionCatch::All { landing_pad, base: target_base, with_ref }, + }); + } + + let body_start = self.instructions.len(); + self.patch_jump(body_jump, body_start); + let handler_idx = self.data.exception_handlers.len(); + self.data.exception_handlers.push(tinywasm_types::ExceptionHandler { + start_ip: body_start as u32, + end_ip: 0, + catches: catches.into_boxed_slice(), + }); + self.push_sizes(&signature.params)?; + self.control_stack.push(ControlFrame { + kind: BlockKind::TryTable(handler_idx), + has_else: false, + start_ip: body_start, + branch_jumps: Vec::new(), + height, + base, + params: signature.params, + results: signature.results, + unreachable: entry_unreachable, + entry_unreachable, + end_reachable: false, + }); + Ok(()) + } + + fn visit_throw(&mut self, tag_index: u32) -> Self::Output { + let signature = self.metadata.tag_signature(tag_index)?.clone(); + self.apply_effect(&signature.params, &[])?; + self.instructions.push(Instruction::Throw(tag_index)); + self.mark_unreachable(); + Ok(()) + } + + fn visit_throw_ref(&mut self) -> Self::Output { + self.apply_effect(&[ValueLane::S32], &[])?; + self.instructions.push(Instruction::ThrowRef); + self.mark_unreachable(); + Ok(()) + } + fn visit_else(&mut self) -> Self::Output { let (cond_jump_ip, height, base, params, entry_unreachable) = { let ctx = self @@ -540,6 +793,9 @@ impl<'a> wasmparser::VisitOperator<'a> for FunctionBuilder<'_> { fn visit_end(&mut self) -> Self::Output { let ctx = self.control_stack.pop().ok_or_else(|| crate::ParseError::Other("end without control frame".into()))?; + if let BlockKind::TryTable(handler_idx) = ctx.kind { + self.data.exception_handlers[handler_idx].end_ip = self.instructions.len() as u32; + } if matches!(ctx.kind, BlockKind::Function) { self.instructions.push(Instruction::Return); } else { @@ -563,7 +819,7 @@ impl<'a> wasmparser::VisitOperator<'a> for FunctionBuilder<'_> { } fn visit_br_if(&mut self, depth: u32) -> Self::Output { - self.pop_expect(OperandSize::S32)?; + self.pop_expect(ValueLane::S32)?; let cond_jump_ip = self.instructions.len(); self.instructions.push(Instruction::JumpIfZero32(0)); @@ -587,7 +843,7 @@ impl<'a> wasmparser::VisitOperator<'a> for FunctionBuilder<'_> { fn visit_br_table(&mut self, targets: wasmparser::BrTable<'_>) -> Self::Output { let ts = targets.targets().collect::, wasmparser::Error>>()?; - self.pop_expect(OperandSize::S32)?; + self.pop_expect(ValueLane::S32)?; let default_depth = targets.default(); let len = ts.len() as u32; @@ -661,73 +917,86 @@ impl<'a> wasmparser::VisitOperator<'a> for FunctionBuilder<'_> { } fn visit_f32_const(&mut self, val: wasmparser::Ieee32) -> Self::Output { - self.emit(&[], &[OperandSize::S32], Instruction::Const32(val.bits() as i32)) + self.emit(&[], &[ValueLane::S32], Instruction::Const32(val.bits() as i32)) } fn visit_f64_const(&mut self, val: wasmparser::Ieee64) -> Self::Output { - self.emit(&[], &[OperandSize::S64], Instruction::Const64(val.bits() as i64)) + self.emit(&[], &[ValueLane::S64], Instruction::Const64(val.bits() as i64)) } fn visit_table_copy(&mut self, dst_table: u32, src_table: u32) -> Self::Output { let dst = self.metadata.table_size(dst_table)?; let src = self.metadata.table_size(src_table)?; - let len = if dst == OperandSize::S32 || src == OperandSize::S32 { OperandSize::S32 } else { OperandSize::S64 }; + let len = if dst == ValueLane::S32 || src == ValueLane::S32 { ValueLane::S32 } else { ValueLane::S64 }; self.emit(&[dst, src, len], &[], Instruction::TableCopy { dst_table, src_table }) } fn visit_memory_copy(&mut self, dst_mem: u32, src_mem: u32) -> Self::Output { let dst = self.metadata.memory_size(dst_mem)?; let src = self.metadata.memory_size(src_mem)?; - let len = if dst == OperandSize::S32 || src == OperandSize::S32 { OperandSize::S32 } else { OperandSize::S64 }; + let len = if dst == ValueLane::S32 || src == ValueLane::S32 { ValueLane::S32 } else { ValueLane::S64 }; self.emit(&[dst, src, len], &[], Instruction::MemoryCopy { dst_mem, src_mem }) } - // Reference Types - fn visit_ref_null(&mut self, ty: wasmparser::HeapType) -> Self::Output { - let instruction = Instruction::RefNull(convert_heap_type(ty, false)?); - self.emit(&[], &[OperandSize::S32], instruction) + fn visit_memory_init(&mut self, data_index: u32, memory: u32) -> Self::Output { + let dst = self.metadata.memory_size(memory)?; + self.emit(&[dst, ValueLane::S32, ValueLane::S32], &[], Instruction::MemoryInit(data_index, memory)) + } + + fn visit_br_on_cast( + &mut self, + relative_depth: u32, + _from_ref_type: wasmparser::RefType, + to_ref_type: wasmparser::RefType, + ) -> Self::Output { + self.emit_cast_branch(relative_depth, to_ref_type, false) } - fn visit_ref_as_non_null(&mut self) -> Self::Output { - self.emit(&[OperandSize::S32], &[OperandSize::S32], Instruction::RefAsNonNull) + fn visit_br_on_cast_fail( + &mut self, + relative_depth: u32, + _from_ref_type: wasmparser::RefType, + to_ref_type: wasmparser::RefType, + ) -> Self::Output { + self.emit_cast_branch(relative_depth, to_ref_type, true) } fn visit_br_on_null(&mut self, relative_depth: u32) -> Self::Output { - self.pop_expect(OperandSize::S32)?; + self.pop_expect(ValueLane::S32)?; let fallthrough_jump = self.instructions.len(); self.instructions.push(Instruction::JumpIfRefNonNull(0)); self.emit_dropkeep_to_label(relative_depth)?; self.emit_branch_jump_or_return(relative_depth)?; self.patch_jump(fallthrough_jump, self.instructions.len()); - self.push_sizes(&[OperandSize::S32]) + self.push_sizes(&[ValueLane::S32]) } fn visit_br_on_non_null(&mut self, relative_depth: u32) -> Self::Output { - self.pop_expect(OperandSize::S32)?; + self.pop_expect(ValueLane::S32)?; let fallthrough_jump = self.instructions.len(); self.instructions.push(Instruction::JumpIfRefNull(0)); - self.push_sizes(&[OperandSize::S32])?; + self.push_sizes(&[ValueLane::S32])?; self.emit_dropkeep_to_label(relative_depth)?; - self.pop_expect(OperandSize::S32)?; + self.pop_expect(ValueLane::S32)?; self.emit_branch_jump_or_return(relative_depth)?; self.patch_jump(fallthrough_jump, self.instructions.len()); Ok(()) } fn visit_typed_select_multi(&mut self, tys: Vec) -> Self::Output { - let sizes: Vec<_> = tys.into_iter().map(OperandSize::from).collect(); + let sizes: Vec<_> = tys.into_iter().map(value_lane).collect(); let counts = Self::value_counts(&sizes); self.emit( - &[sizes.as_slice(), sizes.as_slice(), &[OperandSize::S32]].concat(), + &[sizes.as_slice(), sizes.as_slice(), &[ValueLane::S32]].concat(), &sizes, Instruction::SelectMulti(counts), ) } fn visit_typed_select(&mut self, ty: wasmparser::ValType) -> Self::Output { - let size = OperandSize::from(ty); - let instruction = size.choose(Instruction::Select32, Instruction::Select64, Instruction::Select128); - self.emit(&[size, size, OperandSize::S32], &[size], instruction) + let size = value_lane(ty); + let instruction = size.select(Instruction::Select32, Instruction::Select64, Instruction::Select128); + self.emit(&[size, size, ValueLane::S32], &[size], instruction) } } @@ -919,8 +1188,8 @@ impl wasmparser::VisitSimdOperator<'_> for FunctionBuilder<'_> { fn visit_i8x16_shuffle(&mut self, lanes: [u8; 16]) -> Self::Output { self.emit( - &[OperandSize::S128, OperandSize::S128], - &[OperandSize::S128], + &[ValueLane::S128, ValueLane::S128], + &[ValueLane::S128], Instruction::I8x16Shuffle(self.data.v128_constants.len() as u32), )?; self.data.v128_constants.push(lanes); @@ -928,13 +1197,30 @@ impl wasmparser::VisitSimdOperator<'_> for FunctionBuilder<'_> { } fn visit_v128_const(&mut self, value: wasmparser::V128) -> Self::Output { - self.emit(&[], &[OperandSize::S128], Instruction::Const128(self.data.v128_constants.len() as u32))?; + self.emit(&[], &[ValueLane::S128], Instruction::Const128(self.data.v128_constants.len() as u32))?; self.data.v128_constants.push(*value.bytes()); Ok(()) } } impl FunctionBuilder<'_> { + fn emit_cast_branch( + &mut self, + relative_depth: u32, + target: wasmparser::RefType, + branch_on_fail: bool, + ) -> Result<()> { + self.pop_expect(ValueLane::S32)?; + let target = convert_heap_type(target.heap_type(), target.is_nullable())?; + let conditional_ip = self.instructions.len(); + self.instructions.push(Instruction::BrOnCast(0, target, branch_on_fail)); + self.push_sizes(&[ValueLane::S32])?; + self.emit_dropkeep_to_label(relative_depth)?; + self.emit_branch_jump_or_return(relative_depth)?; + self.patch_jump(conditional_ip, self.instructions.len()); + Ok(()) + } + fn is_unreachable(&self) -> bool { self.control_stack.last().is_none_or(|frame| frame.unreachable) } @@ -946,7 +1232,7 @@ impl FunctionBuilder<'_> { .ok_or_else(|| crate::ParseError::Other(alloc::format!("branch depth out of bounds: {depth}"))) } - fn local(&self, idx: u32) -> Result<(OperandSize, u16)> { + fn local(&self, idx: u32) -> Result<(ValueLane, u16)> { let size = *self .local_types .get(idx as usize) @@ -959,12 +1245,12 @@ impl FunctionBuilder<'_> { } /// Pushes logical operands while maintaining the lane counts used by `DropKeep`. - fn push_sizes(&mut self, sizes: &[OperandSize]) -> Result<()> { + fn push_sizes(&mut self, sizes: &[ValueLane]) -> Result<()> { for &size in sizes { let count = match size { - OperandSize::S32 => &mut self.lane_counts.c32, - OperandSize::S64 => &mut self.lane_counts.c64, - OperandSize::S128 => &mut self.lane_counts.c128, + ValueLane::S32 => &mut self.lane_counts.c32, + ValueLane::S64 => &mut self.lane_counts.c64, + ValueLane::S128 => &mut self.lane_counts.c128, }; *count = count .checked_add(1) @@ -975,7 +1261,7 @@ impl FunctionBuilder<'_> { } /// Pops an operand, allowing a polymorphic value at an unreachable frame base. - fn pop_expect(&mut self, expected: OperandSize) -> Result<()> { + fn pop_expect(&mut self, expected: ValueLane) -> Result<()> { let frame_height = self.control_stack.last().map_or(0, |frame| frame.height); if self.operand_stack.len() == frame_height && self.is_unreachable() { return Ok(()); @@ -988,22 +1274,22 @@ impl FunctionBuilder<'_> { return Err(crate::ParseError::Other("logical operand width mismatch".into())); } match actual { - OperandSize::S32 => self.lane_counts.c32 -= 1, - OperandSize::S64 => self.lane_counts.c64 -= 1, - OperandSize::S128 => self.lane_counts.c128 -= 1, + ValueLane::S32 => self.lane_counts.c32 -= 1, + ValueLane::S64 => self.lane_counts.c64 -= 1, + ValueLane::S128 => self.lane_counts.c128 -= 1, } Ok(()) } /// Applies a declared logical stack effect in WebAssembly operand order. - fn apply_effect(&mut self, inputs: &[OperandSize], outputs: &[OperandSize]) -> Result<()> { + fn apply_effect(&mut self, inputs: &[ValueLane], outputs: &[ValueLane]) -> Result<()> { inputs.iter().rev().try_for_each(|&size| self.pop_expect(size))?; self.push_sizes(outputs)?; Ok(()) } /// Applies an instruction's stack effect before adding it to the bytecode. - fn emit(&mut self, inputs: &[OperandSize], outputs: &[OperandSize], instruction: Instruction) -> Result<()> { + fn emit(&mut self, inputs: &[ValueLane], outputs: &[ValueLane], instruction: Instruction) -> Result<()> { self.apply_effect(inputs, outputs)?; self.instructions.push(instruction); Ok(()) @@ -1027,13 +1313,24 @@ impl FunctionBuilder<'_> { /// Enters a control frame with its parameters restored above the saved base. fn push_control(&mut self, kind: BlockKind, ty: wasmparser::BlockType, initial_jump: Option) -> Result<()> { - let signature = match ty { + let signature = self.block_signature(ty)?; + self.push_control_signature(kind, signature, initial_jump) + } + + fn block_signature(&self, ty: wasmparser::BlockType) -> Result { + Ok(match ty { wasmparser::BlockType::Empty => Signature { params: Vec::new(), results: Vec::new() }, - wasmparser::BlockType::Type(ty) => { - Signature { params: Vec::new(), results: alloc::vec![OperandSize::from(ty)] } - } + wasmparser::BlockType::Type(ty) => Signature { params: Vec::new(), results: alloc::vec![value_lane(ty)] }, wasmparser::BlockType::FuncType(idx) => self.metadata.signature(idx)?.clone(), - }; + }) + } + + fn push_control_signature( + &mut self, + kind: BlockKind, + signature: Signature, + initial_jump: Option, + ) -> Result<()> { for &size in signature.params.iter().rev() { self.pop_expect(size)?; } @@ -1072,20 +1369,21 @@ impl FunctionBuilder<'_> { | Instruction::JumpIfZero32(ip) | Instruction::JumpIfNonZero32(ip) | Instruction::JumpIfRefNull(ip) - | Instruction::JumpIfRefNonNull(ip) => { + | Instruction::JumpIfRefNonNull(ip) + | Instruction::BrOnCast(ip, _, _) => { *ip = target as u32; } _ => {} } } - fn value_counts(sizes: &[OperandSize]) -> ValueCounts { + fn value_counts(sizes: &[ValueLane]) -> ValueCounts { let mut counts = ValueCounts::default(); for size in sizes { match size { - OperandSize::S32 => counts.c32 += 1, - OperandSize::S64 => counts.c64 += 1, - OperandSize::S128 => counts.c128 += 1, + ValueLane::S32 => counts.c32 += 1, + ValueLane::S64 => counts.c64 += 1, + ValueLane::S128 => counts.c128 += 1, } } counts @@ -1108,7 +1406,7 @@ impl FunctionBuilder<'_> { match self.control_stack[ctx_idx].kind { BlockKind::Function => self.instructions.push(Instruction::Return), BlockKind::Loop => self.instructions.push(Instruction::Jump(self.control_stack[ctx_idx].start_ip as u32)), - BlockKind::Block | BlockKind::If => { + BlockKind::Block | BlockKind::If | BlockKind::TryTable(_) => { self.control_stack[ctx_idx].branch_jumps.push(self.instructions.len()); self.control_stack[ctx_idx].end_reachable = true; self.instructions.push(Instruction::Jump(0)); diff --git a/crates/tinywasm/Cargo.toml b/crates/tinywasm/Cargo.toml index aecebebd..eb5dd946 100644 --- a/crates/tinywasm/Cargo.toml +++ b/crates/tinywasm/Cargo.toml @@ -13,6 +13,7 @@ categories.workspace = true [package.metadata.docs.rs] features = [ "std", + "validate", "parser", "archive", "log", @@ -33,7 +34,6 @@ name = "test-wasm-1" [[test]] harness = false name = "test-wasm-3" -test = false [[test]] harness = false @@ -42,7 +42,6 @@ name = "test-wasm-2" [[test]] harness = false name = "test-wasm-latest" -test = false [[test]] harness = false @@ -53,10 +52,6 @@ harness = false name = "test-wasm-threads" test = false -[[test]] -harness = false -name = "test-wasm-annotations" - [[test]] harness = false name = "test-wast" @@ -66,11 +61,6 @@ test = false harness = false name = "test-wasm-custom-page-sizes" -[[test]] -harness = false -name = "test-wasm-function-references" -test = false - [[test]] harness = false name = "test-wasm-custom" @@ -78,20 +68,11 @@ name = "test-wasm-custom" [[test]] harness = false name = "test-wasm-gc" -test = false - -[[test]] -harness = false -name = "test-wasm-tail-call" [[test]] harness = false name = "test-wasm-memory64" -[[test]] -harness = false -name = "test-wasm-extended-const" - [[test]] harness = false name = "test-wasm-relaxed-simd" @@ -104,38 +85,6 @@ name = "test-wasm-simd" harness = false name = "test-wasm-wide-arithmetic" -[[test]] -harness = false -name = "test-wasm-sign-extension-op" - -[[test]] -harness = false -name = "test-wasm-nontrapping-float-to-int-conversions" - -[[test]] -harness = false -name = "test-wasm-reference-types" - -[[bench]] -harness = false -name = "argon2id" - -[[bench]] -harness = false -name = "fibonacci" - -[[bench]] -harness = false -name = "tinywasm" - -[[bench]] -harness = false -name = "tinywasm_modes" - -[[bench]] -harness = false -name = "memory_backends" - [dependencies] libm = { version = "0.2", default-features = false } log = { workspace = true, optional = true } @@ -143,12 +92,7 @@ tinywasm-parser = { workspace = true, optional = true } tinywasm-types = { workspace = true } [dev-dependencies] -criterion.workspace = true -eyre.workspace = true -owo-colors.workspace = true -serde.workspace = true -serde_json.workspace = true -tinywasm-cli = { path = "../cli", features = ["wast", "wat"] } +tinywasm-cli = { path = "../cli", features = ["tests", "wat"] } wasm-testsuite.workspace = true wat.workspace = true @@ -160,7 +104,8 @@ default = [ "log", "parallel-parser", "parser", - "std" + "std", + "validate" ] log = ["dep:log", "tinywasm-parser?/log", "tinywasm-types/log"] @@ -169,6 +114,9 @@ std = ["tinywasm-parser?/std", "tinywasm-types/std"] # support for parsing WebAssembly parser = ["dep:tinywasm-parser"] +# validate WebAssembly while parsing +validate = ["parser", "tinywasm-parser/validate"] + # parallelize function parsing/validation across threads (requires std) parallel-parser = ["parser", "tinywasm-parser?/parallel"] diff --git a/crates/tinywasm/src/engine.rs b/crates/tinywasm/src/engine.rs index 521eb806..64ef6be9 100644 --- a/crates/tinywasm/src/engine.rs +++ b/crates/tinywasm/src/engine.rs @@ -123,6 +123,9 @@ pub struct Config { /// Whether memory and stack allocation failures should trap instead of degrading into normal operation failure modes. /// Defaults to `false`. pub trap_on_oom: bool, + /// Initial number of GC heap bytes that triggers collection. + /// Defaults to 1 MiB. + pub gc_collection_threshold: usize, } impl Config { @@ -181,6 +184,12 @@ impl Config { self } + /// Set the initial GC heap collection threshold in bytes. + pub fn with_gc_collection_threshold(mut self, threshold: usize) -> Self { + self.gc_collection_threshold = threshold; + self + } + /// Get the current fuel policy pub fn fuel_policy(&self) -> FuelPolicy { self.fuel_policy @@ -206,6 +215,7 @@ impl Default for Config { fuel_policy: FuelPolicy::default(), memory_backend: MemoryBackend::default(), trap_on_oom: false, + gc_collection_threshold: 1024 * 1024, } } } diff --git a/crates/tinywasm/src/error.rs b/crates/tinywasm/src/error.rs index b5885ca3..05e36aa1 100644 --- a/crates/tinywasm/src/error.rs +++ b/crates/tinywasm/src/error.rs @@ -2,8 +2,8 @@ use alloc::boxed::Box; use alloc::string::{String, ToString}; use alloc::vec::Vec; use core::fmt::{Debug, Display}; -use tinywasm_types::FuncType; use tinywasm_types::archive::TwasmError; +use tinywasm_types::{ExnRef, FuncType}; #[cfg(feature = "parser")] pub use tinywasm_parser::ParseError; @@ -14,6 +14,9 @@ pub enum Error { /// A WebAssembly trap occurred Trap(Trap), + /// An uncaught WebAssembly exception occurred. + Exception(ExnRef), + /// A linking error occurred Linker(LinkingError), @@ -50,6 +53,7 @@ impl PartialEq for Error { fn eq(&self, other: &Self) -> bool { match (self, other) { (Self::Trap(a), Self::Trap(b)) => a == b, + (Self::Exception(a), Self::Exception(b)) => a == b, (Self::Linker(a), Self::Linker(b)) => a == b, (Self::UnsupportedFeature(a), Self::UnsupportedFeature(b)) => a == b, (Self::Other(a), Self::Other(b)) => a == b, @@ -133,6 +137,9 @@ pub enum Trap { max: usize, }, + /// An out-of-bounds GC array access occurred. + ArrayOutOfBounds, + /// A division by zero occurred DivisionByZero, @@ -169,9 +176,21 @@ pub enum Trap { /// A null reference was used where a non-null reference was required. NullReference, + /// A null struct reference was dereferenced. + NullStructReference, + + /// A null array reference was dereferenced. + NullArrayReference, + /// A null function reference was called. NullFunctionReference, + /// A null i31 reference was unwrapped. + NullI31Reference, + + /// A reference cast failed. + CastFailure, + /// Indirect call type mismatch IndirectCallTypeMismatch { /// The expected type @@ -191,6 +210,7 @@ impl Trap { Self::Unreachable => "unreachable", Self::MemoryOutOfBounds { .. } => "out of bounds memory access", Self::TableOutOfBounds { .. } => "out of bounds table access", + Self::ArrayOutOfBounds => "out of bounds array access", Self::DivisionByZero => "integer divide by zero", Self::InvalidConversionToInt => "invalid conversion to integer", Self::IntegerOverflow => "integer overflow", @@ -200,7 +220,11 @@ impl Trap { Self::UndefinedElement { .. } => "undefined element", Self::UninitializedElement { .. } => "uninitialized element", Self::NullReference => "null reference", + Self::NullStructReference => "null structure reference", + Self::NullArrayReference => "null array reference", Self::NullFunctionReference => "null function reference", + Self::NullI31Reference => "null i31 reference", + Self::CastFailure => "cast failure", Self::IndirectCallTypeMismatch { .. } => "indirect call type mismatch", Self::HostFunction(_) => "host function trap", Self::InvalidStore => "invalid store", @@ -226,18 +250,21 @@ impl LinkingError { } impl From for Error { + #[cold] fn from(value: LinkingError) -> Self { Self::Linker(value) } } impl From for Error { + #[cold] fn from(value: TwasmError) -> Self { Self::Twasm(value) } } impl From for Error { + #[cold] fn from(value: Trap) -> Self { Self::Trap(value) } @@ -254,6 +281,7 @@ impl Display for Error { Self::Twasm(err) => write!(f, "serialization error: {err}"), Self::Trap(trap) => write!(f, "trap: {trap}"), + Self::Exception(_) => write!(f, "uncaught WebAssembly exception"), Self::Linker(err) => write!(f, "linking error: {err}"), Self::InvalidLabelType => write!(f, "invalid label type"), Self::Other(message) => write!(f, "unknown error: {message}"), @@ -291,6 +319,7 @@ impl Display for Trap { Self::TableOutOfBounds { offset, len, max } => { write!(f, "out of bounds table access: offset={offset}, len={len}, max={max}") } + Self::ArrayOutOfBounds => write!(f, "out of bounds array access"), Self::DivisionByZero => write!(f, "integer divide by zero"), Self::InvalidConversionToInt => write!(f, "invalid conversion to integer"), Self::IntegerOverflow => write!(f, "integer overflow"), @@ -302,7 +331,11 @@ impl Display for Trap { write!(f, "uninitialized element: index={index}") } Self::NullReference => write!(f, "null reference"), + Self::NullStructReference => write!(f, "null structure reference"), + Self::NullArrayReference => write!(f, "null array reference"), Self::NullFunctionReference => write!(f, "null function reference"), + Self::NullI31Reference => write!(f, "null i31 reference"), + Self::CastFailure => write!(f, "cast failure"), Self::InvalidStore => write!(f, "invalid store"), #[cfg(feature = "debug")] Self::IndirectCallTypeMismatch { expected, actual } => { @@ -334,6 +367,7 @@ impl From for crate::std::io::Error { #[cfg(feature = "parser")] impl From for Error { + #[cold] fn from(value: tinywasm_parser::ParseError) -> Self { Self::Parser(value) } diff --git a/crates/tinywasm/src/func.rs b/crates/tinywasm/src/func.rs deleted file mode 100644 index b6b7b142..00000000 --- a/crates/tinywasm/src/func.rs +++ /dev/null @@ -1,851 +0,0 @@ -use crate::interpreter::stack::{CallFrame, ValueStack}; -use crate::reference::StoreItem; -use crate::{Error, FunctionInstance, InterpreterRuntime, Result, Store, Trap}; -use alloc::{borrow::Cow, boxed::Box, format, rc::Rc, vec, vec::Vec}; -use core::hint::cold_path; -use tinywasm_types::{ExternRef, FuncAddr, FuncRef, FuncType, ModuleInstanceId, TypeAddr, WasmType, WasmValue}; - -impl Function { - #[inline] - pub(crate) const fn addr(&self) -> FuncAddr { - self.item.addr - } - - /// Get this function's canonical type from its store. - /// - /// Concrete reference types are only meaningful in this store. - pub fn ty<'a>(&self, store: &'a Store) -> Result<&'a FuncType> { - self.item.validate_store(store)?; - Ok(store.state.get_func_type(self.addr())) - } - - /// Call a function (Invocation) - /// - /// See - #[inline] - pub fn call(&self, store: &mut Store, params: &[WasmValue]) -> Result> { - #[inline] - fn call_inner(func: &Function, store: &mut Store, params: &[WasmValue]) -> Result> { - let func_instance = store.state.get_func(func.addr()).clone(); - let wasm_func = match &func_instance.kind { - crate::store::FunctionKind::Host(host_func) => { - let result = host_func.clone().call(FuncContext { store, module_id: func.module_id }, params)?; - return validate_host_results(store, func_instance.type_addr, result); - } - crate::store::FunctionKind::Wasm(wasm_func) => wasm_func, - }; - - // Reset stack, push args, allocate locals, create entry frame. - store.call_stack.clear(); - store.value_stack.clear(); - store.value_stack.extend_from_wasmvalues(params)?; - let locals_base = store.value_stack.enter_locals(&wasm_func.func.params, &wasm_func.func.locals)?; - let callframe = CallFrame::new(func.addr(), locals_base, wasm_func.func.locals); - - // Execute until completion and then collect result values from the stack. - InterpreterRuntime::exec(store, callframe, 0)?; - collect_call_results(&mut store.value_stack, store.state.get_type(func_instance.type_addr)) - } - - self.item.validate_store(store)?; - validate_call_params(&store.state, store.state.get_func_type(self.addr()), params)?; - - store.enter_execution()?; - let result = call_inner(self, store, params); - store.exit_execution(); - - result - } - - /// Call a function and return a resumable execution handle. - /// - /// The returned handle keeps a mutable borrow of the [`Store`] until it - /// completes. Use [`FuncExecution::resume_with_fuel`] (or - /// `resume_with_time_budget` with `std`) to continue. - pub fn call_resumable<'store>( - &self, - store: &'store mut Store, - params: &[WasmValue], - ) -> Result> { - #[inline] - fn call_resumable_inner( - func: &Function, - store: &mut Store, - params: &[WasmValue], - ) -> Result { - let func_instance = store.state.get_func(func.addr()).clone(); - match &func_instance.kind { - crate::store::FunctionKind::Host(host_func) => { - let result = host_func.clone().call(FuncContext { store, module_id: func.module_id }, params)?; - let result = validate_host_results(store, func_instance.type_addr, result)?; - Ok(FuncExecutionState::Completed { result: Some(result) }) - } - crate::store::FunctionKind::Wasm(wasm_func) => { - store.call_stack.clear(); - store.value_stack.clear(); - store.value_stack.extend_from_wasmvalues(params)?; - let locals_base = store.value_stack.enter_locals(&wasm_func.func.params, &wasm_func.func.locals)?; - let callframe = CallFrame::new(func.addr(), locals_base, wasm_func.func.locals); - - Ok(FuncExecutionState::Running { - exec_state: ExecutionState { callframe }, - root_func_addr: func.addr(), - }) - } - } - } - - self.item.validate_store(store)?; - validate_call_params(&store.state, store.state.get_func_type(self.addr()), params)?; - - store.enter_execution()?; - let result = call_resumable_inner(self, store, params); - store.exit_execution(); - - Ok(FuncExecution { store, state: result? }) - } -} - -#[derive(Clone, PartialEq, Eq)] -/// Progress for fuel-limited function execution. -pub enum ExecProgress { - /// Execution completed and produced a result. - Completed(T), - /// Execution suspended after exhausting fuel or time budget. - Suspended, -} - -#[derive(Clone)] -#[cfg_attr(feature = "debug", derive(core::fmt::Debug))] -pub(crate) struct ExecutionState { - pub(crate) callframe: CallFrame, -} - -/// A function handle -#[derive(Clone)] -#[cfg_attr(feature = "debug", derive(core::fmt::Debug))] -pub struct Function { - pub(crate) item: StoreItem, - pub(crate) module_id: ModuleInstanceId, -} - -/// A typed function handle -#[cfg_attr(feature = "debug", derive(core::fmt::Debug))] -pub struct FunctionTyped { - /// The underlying function handle - pub func: Function, - pub(crate) marker: core::marker::PhantomData<(P, R)>, -} - -/// A host function -pub struct HostFunction { - pub(crate) func: HostFuncInner, -} - -impl HostFunction { - /// Call the function - pub fn call(&self, ctx: FuncContext<'_>, args: &[WasmValue]) -> Result> { - (self.func)(ctx, args) - } - - /// Create a new untyped host function import. - /// - /// ## Example - /// ```rust - /// # fn main() -> tinywasm::Result<()> { - /// # use tinywasm::{FuncContext, HostFunction, Imports, ModuleInstance, Store}; - /// # use tinywasm::types::{FuncType, WasmType, WasmValue}; - /// # let wasm = wat::parse_str(r#" - /// # (module - /// # (import "host" "add_one" (func $add_one (param i32) (result i32))) - /// # (func (export "call") (param i32) (result i32) - /// # local.get 0 - /// # call $add_one)) - /// # "#).expect("valid wat"); - /// # let module = tinywasm::parse_bytes(&wasm)?; - /// let mut store = Store::default(); - /// let ty = FuncType::new(&[WasmType::I32], &[WasmType::I32]); - /// let add_one = HostFunction::from_untyped(&mut store, &ty, |_ctx: FuncContext<'_>, args| { - /// let WasmValue::I32(value) = args[0] else { - /// return Err(tinywasm::Error::Other("expected i32".into())); - /// }; - /// Ok(vec![WasmValue::I32(value + 1)]) - /// }); - /// - /// let mut imports = Imports::new(); - /// imports.define("host", "add_one", add_one); - /// # let instance = ModuleInstance::instantiate(&mut store, &module, Some(imports))?; - /// # let call = instance.func::(&store, "call")?; - /// # assert_eq!(call.call(&mut store, 41)?, 42); - /// # Ok(()) - /// # } - /// ``` - pub fn from_untyped( - store: &mut Store, - ty: &FuncType, - func: impl Fn(FuncContext<'_>, &[WasmValue]) -> Result> + 'static, - ) -> Function { - let type_addr = store.register_host_type(ty); - let addr = store.add_func(FunctionInstance { - type_addr, - kind: crate::store::FunctionKind::Host(Rc::new(Self { func: Box::new(func) })), - }); - Function { item: crate::StoreItem::new(store.id(), addr), module_id: 0 } - } - - /// Create a new typed host function import. - /// - /// ## Example - /// ```rust - /// # fn main() -> tinywasm::Result<()> { - /// # use tinywasm::{HostFunction, Imports, ModuleInstance, Store}; - /// # let wasm = wat::parse_str(r#" - /// # (module - /// # (import "host" "add_one" (func $add_one (param i32) (result i32))) - /// # (func (export "call") (param i32) (result i32) - /// # local.get 0 - /// # call $add_one)) - /// # "#).expect("valid wat"); - /// # let module = tinywasm::parse_bytes(&wasm)?; - /// let mut store = Store::default(); - /// let add_one = HostFunction::from(&mut store, |_ctx, value: i32| Ok(value + 1)); - /// - /// let mut imports = Imports::new(); - /// imports.define("host", "add_one", add_one); - /// # let instance = ModuleInstance::instantiate(&mut store, &module, Some(imports))?; - /// # let call = instance.func::(&store, "call")?; - /// # assert_eq!(call.call(&mut store, 41)?, 42); - /// # Ok(()) - /// # } - /// ``` - pub fn from(store: &mut Store, func: impl Fn(FuncContext<'_>, P) -> Result + 'static) -> Function - where - P: FromWasmValues + ToWasmTypes, - R: IntoWasmValues + ToWasmTypes, - { - let inner_func = move |ctx: FuncContext<'_>, args: &[WasmValue]| -> Result> { - Ok(func(ctx, P::from_wasm_values(args)?)?.into_wasm_values()) - }; - - let ty = tinywasm_types::FuncType::new(&P::wasm_types(), &R::wasm_types()); - let type_addr = store.register_host_type(&ty); - let addr = store.add_func(FunctionInstance { - type_addr, - kind: crate::store::FunctionKind::Host(Rc::new(Self { func: Box::new(inner_func) })), - }); - Function { item: crate::StoreItem::new(store.id(), addr), module_id: 0 } - } -} - -pub(crate) type HostFuncInner = Box, &[WasmValue]) -> Result>>; - -/// The context of a host-function call -#[cfg_attr(feature = "debug", derive(core::fmt::Debug))] -pub struct FuncContext<'a> { - pub(crate) store: &'a mut crate::Store, - pub(crate) module_id: ModuleInstanceId, -} - -impl FuncContext<'_> { - /// Get the store. - pub fn store(&self) -> &crate::Store { - self.store - } - - /// Get mutable access to the store. - pub fn store_mut(&mut self) -> &mut crate::Store { - self.store - } - - /// Get the module instance. - pub fn module(&self) -> crate::ModuleInstance { - self.store - .get_module_instance(self.module_id) - .unwrap_or_else(|| unreachable!("invalid module instance id in host function context: {}", self.module_id)) - } - - /// Get a memory export. - pub fn memory(&self, name: &str) -> Result { - self.module().memory(name) - } - - /// Get any exported extern value by name. - pub fn extern_item(&self, name: &str) -> Result { - self.module().extern_item(name) - } - - /// Get a table export. - pub fn table(&self, name: &str) -> Result { - self.module().table(name) - } - - /// Get the value of a global export. - pub fn global_get(&self, name: &str) -> Result { - self.module().global_get(self.store, name) - } - - /// Get a global export. - pub fn global(&self, name: &str) -> Result { - self.module().global(name) - } - - /// Set the value of a mutable global export. - pub fn global_set(&mut self, name: &str, value: WasmValue) -> Result<()> { - self.module().global_set(self.store, name, value) - } - - /// Charge additional fuel from the currently running resumable invocation. - /// - /// This is a no-op when the current invocation is not using fuel-based - /// resumption. - pub fn charge_fuel(&mut self, fuel: u32) { - self.store.execution_fuel = self.store.execution_fuel.saturating_sub(fuel); - } - - /// Get remaining fuel for the current invocation. - /// - /// Returns `0` when fuel-based resumption is not active. - pub fn remaining_fuel(&self) -> u32 { - self.store.execution_fuel - } - - /// Call a function from within the current host-function invocation. - /// - /// This is the safe way for host functions to perform blocking reentrant - /// calls into Wasm. Unlike [`Function::call`], it preserves the active - /// invocation's stacks and resumes the host caller after the nested call - /// completes. - /// - /// Nested calls are currently blocking only. If the surrounding invocation - /// is resumed with fuel or a time budget, this method does not suspend and - /// later continue the host function in the middle of the nested call. - pub fn call_untyped(&mut self, func: &Function, args: &[WasmValue]) -> Result> { - if !self.store.execution_active { - return Err(Error::other("FuncContext::call requires an active host-function invocation")); - } - - func.item.validate_store(self.store)?; - validate_call_params(&self.store.state, self.store.state.get_func_type(func.addr()), args)?; - - let func_instance = self.store.state.get_func(func.addr()).clone(); - match func_instance.kind { - crate::store::FunctionKind::Host(host_func) => { - let result = - host_func.call(FuncContext { store: &mut *self.store, module_id: func.module_id }, args)?; - validate_host_results(self.store, func_instance.type_addr, result) - } - crate::store::FunctionKind::Wasm(wasm_func) => { - let call_stack_base = self.store.call_stack.len(); - let value_stack_base = self.store.value_stack.base(); - - self.store.value_stack.extend_from_wasmvalues(args).inspect_err(|_| { - self.store.value_stack.truncate_to_base(value_stack_base); - })?; - - let locals_base = self - .store - .value_stack - .enter_locals(&wasm_func.func.params, &wasm_func.func.locals) - .inspect_err(|_| self.store.value_stack.truncate_to_base(value_stack_base))?; - - let callframe = CallFrame::new(func.addr(), locals_base, wasm_func.func.locals); - InterpreterRuntime::exec(self.store, callframe, call_stack_base).inspect_err(|_| { - self.store.call_stack.truncate_to(call_stack_base); - self.store.value_stack.truncate_to_base(value_stack_base); - })?; - - collect_call_results(&mut self.store.value_stack, self.store.state.get_type(func_instance.type_addr)) - } - } - } - - /// Call a typed function from within the current host-function invocation. - /// - /// See [`Self::call_untyped`] for reentrancy and resumable-execution - /// limitations. - pub fn call(&mut self, func: &FunctionTyped, params: P) -> Result - where - P: IntoWasmValues, - R: FromWasmValues, - { - R::from_wasm_values(&self.call_untyped(&func.func, ¶ms.into_wasm_values())?) - } -} - -impl core::ops::Deref for FuncContext<'_> { - type Target = crate::Store; - - fn deref(&self) -> &Self::Target { - self.store - } -} - -impl core::ops::DerefMut for FuncContext<'_> { - fn deref_mut(&mut self) -> &mut Self::Target { - self.store - } -} - -impl<'a> FuncContext<'a> { - /// Create a new host function context. - pub const fn new(store: &'a mut crate::Store, module_id: ModuleInstanceId) -> Self { - Self { store, module_id } - } -} - -#[cfg(feature = "debug")] -impl core::fmt::Debug for HostFunction { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_struct("HostFunction").field("func", &"...").finish() - } -} - -/// Resumable execution for an untyped function call. -#[cfg_attr(feature = "debug", derive(core::fmt::Debug))] -pub struct FuncExecution<'store> { - store: &'store mut Store, - state: FuncExecutionState, -} - -#[cfg_attr(feature = "debug", derive(core::fmt::Debug))] -enum FuncExecutionState { - Running { exec_state: ExecutionState, root_func_addr: u32 }, - Completed { result: Option> }, -} - -/// Resumable execution for a typed function call. -#[cfg_attr(feature = "debug", derive(core::fmt::Debug))] -pub struct FuncExecutionTyped<'store, R> { - execution: FuncExecution<'store>, - marker: core::marker::PhantomData, -} - -impl<'store> FuncExecution<'store> { - fn resume( - &mut self, - run: impl FnOnce(&mut Store, CallFrame) -> Result, - ) -> Result>> { - let (callframe, root_func_addr) = match &mut self.state { - FuncExecutionState::Running { exec_state, root_func_addr } => (exec_state.callframe, *root_func_addr), - FuncExecutionState::Completed { result } => { - return match result.take() { - Some(res) => Ok(ExecProgress::Completed(res)), - None => Err(Error::other("execution already completed")), - }; - } - }; - - self.store.enter_execution()?; - let result = run(self.store, callframe); - self.store.exit_execution(); - - match result? { - crate::interpreter::ExecState::Completed => { - let result_ty = self.store.state.get_func(root_func_addr).type_addr; - self.state = FuncExecutionState::Completed { result: None }; - Ok(ExecProgress::Completed(collect_call_results( - &mut self.store.value_stack, - self.store.state.get_type(result_ty), - )?)) - } - crate::interpreter::ExecState::Suspended(callframe) => { - let FuncExecutionState::Running { exec_state, .. } = &mut self.state else { - unreachable!("invalid function execution state") - }; - exec_state.callframe = callframe; - Ok(ExecProgress::Suspended) - } - } - } - - /// Resume execution with up to `fuel` units of fuel. - /// - /// Fuel is accounted in chunks, so execution may overshoot the requested - /// fuel before returning [`ExecProgress::Suspended`] (currently the chunk size is 128 instructions between fuel checks, but this may change in the future). - /// - /// Returns [`ExecProgress::Suspended`] when fuel is exhausted, or - /// [`ExecProgress::Completed`] with the final values once the invocation - /// returns. - /// - /// Reentrant calls made by host functions through [`FuncContext::call`] are - /// currently blocking. They do not suspend and later resume the host - /// function in the middle of the nested call. - pub fn resume_with_fuel(&mut self, fuel: u32) -> Result>> { - self.resume(|store, callframe| InterpreterRuntime::exec_with_fuel(store, callframe, fuel)) - } - - #[cfg(feature = "std")] - /// Resume execution for at most `time_budget` wall-clock time. - /// - /// Time is checked periodically, so execution may overshoot the requested - /// time budget before returning [`ExecProgress::Suspended`] (currently time is checked every 128 instructions, but this may change in the future). - /// - /// Returns [`ExecProgress::Suspended`] when the budget is exhausted, or - /// [`ExecProgress::Completed`] with the final values once the invocation - /// returns. - /// - /// Reentrant calls made by host functions through [`FuncContext::call`] are - /// currently blocking. They do not suspend and later resume the host - /// function in the middle of the nested call. - pub fn resume_with_time_budget( - &mut self, - time_budget: crate::std::time::Duration, - ) -> Result>> { - self.resume(|store, callframe| InterpreterRuntime::exec_with_time_budget(store, callframe, time_budget)) - } -} - -fn validate_call_params(state: &crate::store::State, func_ty: &FuncType, params: &[WasmValue]) -> Result<()> { - if func_ty.params().len() != params.len() { - cold_path(); - return Err(Error::Other(format!( - "param count mismatch: expected {}, got {}", - func_ty.params().len(), - params.len() - ))); - } - - if !func_ty.params().iter().zip(params).all(|(ty, param)| state.value_matches_type(*param, *ty)) { - return Err(Error::other("Type mismatch")); - } - - Ok(()) -} - -pub(crate) fn validate_host_results( - store: &Store, - type_addr: TypeAddr, - result: Vec, -) -> Result> { - let expected = store.state.get_type(type_addr); - if result.len() == expected.results().len() - && result.iter().zip(expected.results()).all(|(&value, &ty)| store.state.value_matches_type(value, ty)) - { - return Ok(result); - } - - Err(Error::InvalidHostFnReturn { expected: Box::new(expected.clone()), actual: result }) -} - -fn collect_call_results(value_stack: &mut ValueStack, func_ty: &FuncType) -> Result> { - debug_assert!(value_stack.len() >= func_ty.results().len()); // m values are on the top of the stack (Ensured by validation) - let mut res: Vec<_> = value_stack.pop_types(func_ty.results().iter().rev()).collect(); // pop in reverse order since the stack is LIFO - res.reverse(); // reverse to get the original order - Ok(res) -} - -pub trait IntoWasmValues { - fn into_wasm_values(self) -> Vec; -} - -pub trait FromWasmValues: Sized { - fn from_wasm_values(values: &[WasmValue]) -> Result; -} - -impl FunctionTyped { - /// Call a typed function - pub fn call(&self, store: &mut Store, params: P) -> Result { - // Convert params into Vec - let wasm_values = params.into_wasm_values(); - - // Call the underlying WASM function - let result = self.func.call(store, &wasm_values)?; - - // Convert the Vec back to R - R::from_wasm_values(&result) - } - - /// Call a typed function and return a resumable execution handle. - /// - /// The handle keeps a mutable borrow of the [`Store`] until completion. - /// - /// ## Example - /// - /// ```rust - /// # fn main() -> tinywasm::Result<()> { - /// use tinywasm::{ExecProgress, ModuleInstance, Store}; - /// - /// let wasm = include_bytes!("../../../examples/wasm/add.wasm"); - /// let module = tinywasm::parse_bytes(wasm)?; - /// let mut store = Store::default(); - /// let instance = ModuleInstance::instantiate(&mut store, &module, None)?; - /// let add = instance.func::<(i32, i32), i32>(&store, "add")?; - /// - /// let mut execution = add.call_resumable(&mut store, (20, 22))?; - /// assert!(matches!(execution.resume_with_fuel(0)?, ExecProgress::Suspended)); - /// assert!(matches!(execution.resume_with_fuel(16)?, ExecProgress::Completed(42))); - /// # Ok(()) - /// # } - /// ``` - pub fn call_resumable<'store>(&self, store: &'store mut Store, params: P) -> Result> { - let wasm_values = params.into_wasm_values(); - let execution = self.func.call_resumable(store, &wasm_values)?; - Ok(FuncExecutionTyped { execution, marker: core::marker::PhantomData }) - } -} - -impl<'store, R: FromWasmValues> FuncExecutionTyped<'store, R> { - /// Resume typed execution with up to `fuel` units of fuel. - /// - /// Fuel is accounted in chunks, so execution may overshoot the requested - /// fuel before returning [`ExecProgress::Suspended`]. - pub fn resume_with_fuel(&mut self, fuel: u32) -> Result> { - match self.execution.resume_with_fuel(fuel)? { - ExecProgress::Completed(values) => Ok(ExecProgress::Completed(R::from_wasm_values(&values)?)), - ExecProgress::Suspended => Ok(ExecProgress::Suspended), - } - } - - #[cfg(feature = "std")] - /// Resume typed execution for at most `time_budget` wall-clock time. - /// - /// Time is checked periodically, so execution may overshoot the requested - /// time budget before returning [`ExecProgress::Suspended`]. - pub fn resume_with_time_budget(&mut self, time_budget: crate::std::time::Duration) -> Result> { - match self.execution.resume_with_time_budget(time_budget)? { - ExecProgress::Completed(values) => Ok(ExecProgress::Completed(R::from_wasm_values(&values)?)), - ExecProgress::Suspended => Ok(ExecProgress::Suspended), - } - } -} - -/// Describes the WebAssembly value types produced by a Rust value or tuple shape. -pub trait ToWasmTypes { - /// Static WebAssembly types for this shape. - /// - /// Implementations that require runtime construction may set this to `None`, - /// but must then override [`Self::wasm_types`]. - const WASM_TYPES: Option<&'static [WasmType]>; - - /// Return the flattened WebAssembly value types for this tuple shape. - fn wasm_types() -> Cow<'static, [WasmType]> { - Cow::Borrowed(Self::WASM_TYPES.expect("dynamic ToWasmTypes implementation must override wasm_types")) - } -} - -/// Describes the WebAssembly value types produced by a scalar Rust type. -pub trait ToWasmType { - /// The single WebAssembly value type for this scalar type. - const WASM_TYPE: WasmType; -} - -macro_rules! impl_scalar_wasm_traits { - ($($T:ty => $val_ty:expr),+ $(,)?) => { - $( - impl ToWasmType for $T { - const WASM_TYPE: WasmType = $val_ty; - } - - impl ToWasmTypes for $T { - const WASM_TYPES: Option<&'static [WasmType]> = Some(&[$val_ty]); - } - - impl IntoWasmValues for $T { - #[inline] - fn into_wasm_values(self) -> Vec { - vec![self.into()] - } - } - - impl FromWasmValues for $T { - #[inline] - fn from_wasm_values(values: &[WasmValue]) -> Result { - let value = *values.first().ok_or_else(|| { - core::hint::cold_path(); - Error::other("Not enough elements in &[WasmValue]") - })?; - - <$T>::try_from(value).map_err(|e| { - core::hint::cold_path(); - Error::Other(format!( - "FromWasmValues: Could not convert WasmValue to expected type: {e:?}" - )) - }) - } - } - )+ - }; -} - -macro_rules! impl_tuple_traits { - ($($T:ident),+) => { - impl<$($T),+> ToWasmTypes for ($($T,)+) - where - $($T: ToWasmType,)+ - { - const WASM_TYPES: Option<&'static [WasmType]> = Some(&[$($T::WASM_TYPE,)+]); - } - - impl<$($T),+> IntoWasmValues for ($($T,)+) - where - $($T: Into,)+ - { - #[allow(non_snake_case)] - #[inline] - fn into_wasm_values(self) -> Vec { - let ($($T,)+) = self; - vec![$($T.into(),)+] - } - } - - impl<$($T),+> FromWasmValues for ($($T,)+) - where - $($T: TryFrom,)+ - { - #[inline] - fn from_wasm_values(values: &[WasmValue]) -> Result { - let mut iter = values.iter(); - - Ok(($( - $T::try_from(*iter.next().ok_or(Error::other("Not enough values in WasmValue vector"))?) - .map_err(|e| Error::Other(format!("FromWasmValues: Could not convert WasmValue to expected type: {e:?}")))?, - )+)) - } - } - } -} - -macro_rules! impl_tuple { - ($macro:ident) => { - $macro!(T1); - $macro!(T1, T2); - $macro!(T1, T2, T3); - $macro!(T1, T2, T3, T4); - $macro!(T1, T2, T3, T4, T5); - $macro!(T1, T2, T3, T4, T5, T6); - $macro!(T1, T2, T3, T4, T5, T6, T7); - $macro!(T1, T2, T3, T4, T5, T6, T7, T8); - $macro!(T1, T2, T3, T4, T5, T6, T7, T8, T9); - $macro!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10); - $macro!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11); - $macro!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12); - }; -} - -impl_scalar_wasm_traits!( - i32 => WasmType::I32, - i64 => WasmType::I64, - f32 => WasmType::F32, - f64 => WasmType::F64, - FuncRef => WasmType::Ref(tinywasm_types::RefType::FUNCREF), - ExternRef => WasmType::Ref(tinywasm_types::RefType::EXTERNREF), -); -impl_tuple!(impl_tuple_traits); - -/// A helper type for using tuples of arbitrary number of elements as function parameters or results, -/// by concatenating the Wasm types of each element. -/// -/// This is useful when a function signature exceeds tuple arity 12. `tinywasm` only implements -/// direct tuple conversions up to arity 12, but `WasmTupleChain` lets you describe longer -/// signatures by combining smaller tuples at the type level. -/// -/// ## Example -/// ```rust -/// # fn main() -> tinywasm::Result<()> { -/// # use tinywasm::{ModuleInstance, Store, WasmTupleChain}; -/// # let wasm = wat::parse_str(r#" -/// # (module -/// # (func (export "echo13") -/// # (param i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32) -/// # (result i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32) -/// # local.get 0 -/// # local.get 1 -/// # local.get 2 -/// # local.get 3 -/// # local.get 4 -/// # local.get 5 -/// # local.get 6 -/// # local.get 7 -/// # local.get 8 -/// # local.get 9 -/// # local.get 10 -/// # local.get 11 -/// # local.get 12) -/// # ) -/// # "#).expect("valid wat"); -/// # let module = tinywasm::parse_bytes(&wasm)?; -/// # let mut store = Store::default(); -/// # let instance = ModuleInstance::instantiate(&mut store, &module, None)?; -/// -/// type Params = -/// WasmTupleChain<(i32, i32, i32, i32, i32, i32), (i32, i32, i32, i32, i32, i32, i32)>; -/// type Results = -/// WasmTupleChain<(i32, i32, i32, i32, i32, i32), (i32, i32, i32, i32, i32, i32, i32)>; -/// -/// let echo13 = instance.func::(&store, "echo13")?; -/// let result = echo13.call(&mut store, ((1, 2, 3, 4, 5, 6), (7, 8, 9, 10, 11, 12, 13)).into())?; -/// assert_eq!(result.into_inner(), ((1, 2, 3, 4, 5, 6), (7, 8, 9, 10, 11, 12, 13))); -/// # Ok(()) -/// # } -/// ``` -#[derive(Default)] -pub struct WasmTupleChain(T1, T2); - -impl WasmTupleChain { - /// Create a new concatenated tuple wrapper. - pub const fn new(left: T1, right: T2) -> Self { - Self(left, right) - } - - /// Split the wrapper back into its two component values. - pub fn into_inner(self) -> (T1, T2) { - (self.0, self.1) - } -} - -impl From<(T1, T2)> for WasmTupleChain { - fn from((left, right): (T1, T2)) -> Self { - Self::new(left, right) - } -} - -impl ToWasmTypes for WasmTupleChain { - const WASM_TYPES: Option<&'static [WasmType]> = None; - - #[inline] - fn wasm_types() -> Cow<'static, [WasmType]> { - let mut types = Vec::new(); - types.extend_from_slice(&T1::wasm_types()); - types.extend_from_slice(&T2::wasm_types()); - Cow::Owned(types) - } -} - -impl IntoWasmValues for WasmTupleChain { - #[inline] - fn into_wasm_values(self) -> Vec { - let (left, right) = self.into_inner(); - let mut values = Vec::new(); - values.extend(left.into_wasm_values()); - values.extend(right.into_wasm_values()); - values - } -} - -impl FromWasmValues for WasmTupleChain { - #[inline] - fn from_wasm_values(values: &[WasmValue]) -> Result { - let left_len = T1::wasm_types().len(); - let left = T1::from_wasm_values(&values[..values.len().min(left_len)])?; - let right = T2::from_wasm_values(values.get(left_len..).unwrap_or(&[]))?; - Ok(Self::new(left, right)) - } -} - -impl ToWasmTypes for () { - const WASM_TYPES: Option<&'static [WasmType]> = Some(&[]); -} - -impl IntoWasmValues for () { - #[inline] - fn into_wasm_values(self) -> Vec { - vec![] - } -} - -impl FromWasmValues for () { - #[inline] - fn from_wasm_values(_values: &[WasmValue]) -> Result { - Ok(()) - } -} diff --git a/crates/tinywasm/src/func/context.rs b/crates/tinywasm/src/func/context.rs new file mode 100644 index 00000000..95a2cec8 --- /dev/null +++ b/crates/tinywasm/src/func/context.rs @@ -0,0 +1,151 @@ +use alloc::vec::Vec; +use tinywasm_types::{ModuleInstanceId, WasmValue}; + +use crate::{Error, FromWasmValues, Function, FunctionTyped, IntoWasmValues, Result}; + +/// The context of a host-function call +#[cfg_attr(feature = "debug", derive(core::fmt::Debug))] +pub struct FuncContext<'a> { + pub(crate) store: &'a mut crate::Store, + pub(crate) module_id: ModuleInstanceId, +} + +impl FuncContext<'_> { + /// Get the store. + pub fn store(&self) -> &crate::Store { + self.store + } + + /// Get mutable access to the store. + pub fn store_mut(&mut self) -> &mut crate::Store { + self.store + } + + /// Get the module instance. + pub fn module(&self) -> crate::ModuleInstance { + self.store + .get_module_instance(self.module_id) + .unwrap_or_else(|| unreachable!("invalid module instance id in host function context: {}", self.module_id)) + .clone() + } + + /// Get a memory export. + pub fn memory(&self, name: &str) -> Result { + self.module().memory(name) + } + + /// Get any exported extern value by name. + pub fn extern_item(&self, name: &str) -> Result { + self.module().extern_item(name) + } + + /// Get a table export. + pub fn table(&self, name: &str) -> Result { + self.module().table(name) + } + + /// Get the value of a global export. + pub fn global_get(&self, name: &str) -> Result { + self.module().global_get(self.store, name) + } + + /// Get a global export. + pub fn global(&self, name: &str) -> Result { + self.module().global(name) + } + + /// Set the value of a mutable global export. + pub fn global_set(&mut self, name: &str, value: WasmValue) -> Result<()> { + self.module().global_set(self.store, name, value) + } + + /// Charge additional fuel from the currently running resumable invocation. + /// + /// This is a no-op when the current invocation is not using fuel-based + /// resumption. + pub fn charge_fuel(&mut self, fuel: u32) { + self.store.execution_fuel = self.store.execution_fuel.saturating_sub(fuel); + } + + /// Get remaining fuel for the current invocation. + /// + /// Returns `0` when fuel-based resumption is not active. + pub fn remaining_fuel(&self) -> u32 { + self.store.execution_fuel + } + + /// Call a function from within the current host-function invocation. + /// + /// This is the safe way for host functions to perform blocking reentrant + /// calls into Wasm. Unlike [`Function::call`], it preserves the active + /// invocation's stacks and resumes the host caller after the nested call + /// completes. + /// + /// Nested calls are currently blocking only. If the surrounding invocation + /// is resumed with fuel or a time budget, this method does not suspend and + /// later continue the host function in the middle of the nested call. + pub fn call_untyped(&mut self, func: &Function, args: &[WasmValue]) -> Result> { + if !self.store.execution_active { + return Err(Error::other("FuncContext::call requires an active host-function invocation")); + } + + func.item.validate_store(self.store)?; + func.validate_params(self.store, args)?; + + let call_stack_base = self.store.call_stack.len(); + let value_stack_base = self.store.value_stack.base(); + func.call_untyped(self.store, args, call_stack_base, value_stack_base) + } + + /// Call a typed function from within the current host-function invocation. + /// + /// See [`Self::call_untyped`] for reentrancy and resumable-execution + /// limitations. + pub fn call(&mut self, func: &FunctionTyped, params: P) -> Result + where + P: IntoWasmValues, + R: FromWasmValues, + { + if !self.store.execution_active { + return Err(Error::other("FuncContext::call requires an active host-function invocation")); + } + func.func.item.validate_store(self.store)?; + let func_instance = self.store.state.get_func(func.func.addr()).clone(); + if matches!(&func_instance.kind, crate::store::FunctionKind::Host(host) if host.typed_callback().is_none()) { + let params = params.into_wasm_values().collect::>(); + let results = self.call_untyped(&func.func, ¶ms)?; + let mut values = results.into_iter(); + let result = R::from_wasm_values(&mut values)?; + return if values.next().is_none() { + Ok(result) + } else { + Err(Error::other("typed conversion did not consume all WebAssembly values")) + }; + } + + let call_stack_base = self.store.call_stack.len(); + let value_stack_base = self.store.value_stack.base(); + func.func.call_typed(self.store, &func_instance, params.into_wasm_values(), call_stack_base, value_stack_base) + } +} + +impl core::ops::Deref for FuncContext<'_> { + type Target = crate::Store; + + fn deref(&self) -> &Self::Target { + self.store + } +} + +impl core::ops::DerefMut for FuncContext<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.store + } +} + +impl<'a> FuncContext<'a> { + /// Create a new host function context. + pub const fn new(store: &'a mut crate::Store, module_id: ModuleInstanceId) -> Self { + Self { store, module_id } + } +} diff --git a/crates/tinywasm/src/func/host.rs b/crates/tinywasm/src/func/host.rs new file mode 100644 index 00000000..27c55737 --- /dev/null +++ b/crates/tinywasm/src/func/host.rs @@ -0,0 +1,261 @@ +use alloc::{boxed::Box, sync::Arc, vec::Vec}; +use tinywasm_types::{FuncType, ModuleInstanceId, TypeAddr, WasmType, WasmValue}; + +use super::{FromWasmValues, FuncContext, IntoWasmValues, ToWasmTypes}; +use crate::{Function, FunctionInstance, Result, Store}; + +/// A reusable host function definition. +#[derive(Clone)] +pub struct HostFunction(Arc); + +impl HostFunction { + /// Instantiates the function with an already registered canonical type. + pub(crate) fn instantiate_registered(&self, store: &mut Store, type_addr: TypeAddr) -> Function { + let addr = store.add_func(FunctionInstance { + type_addr, + gc: store.state.func_gc_metadata(type_addr), + kind: crate::store::FunctionKind::Host(self.clone()), + }); + Function { item: crate::StoreItem::new(store.id(), addr), module_id: 0 } + } + + /// Resolves the importing module's types without allocating a function instance. + pub(crate) fn resolve_import_type(&self, type_addrs: &[TypeAddr]) -> Result { + let mut types = self.0.ty.params().iter().chain(self.0.ty.results()); + if types.all(|ty| !matches!(ty, WasmType::Ref(ty) if ty.is_concrete())) { + return Ok(self.0.ty.clone()); + } + let resolve = |ty: WasmType| -> Result { + let WasmType::Ref(ref_ty) = ty else { return Ok(ty) }; + let Some(module_addr) = ref_ty.type_index() else { return Ok(ty) }; + let canonical = *type_addrs + .get(module_addr as usize) + .ok_or_else(|| crate::Error::other("host function signature contains an invalid concrete type"))?; + Ok(WasmType::Ref(tinywasm_types::RefType::new_concrete(ref_ty.is_nullable(), canonical))) + }; + let params = self.0.ty.params().iter().copied().map(resolve).collect::>>()?; + let results = self.0.ty.results().iter().copied().map(resolve).collect::>>()?; + Ok(FuncType::new(¶ms, &results)) + } + + /// Calls the host function through its untyped value interface. + pub(crate) fn call_values( + &self, + store: &mut Store, + module_id: ModuleInstanceId, + type_addr: TypeAddr, + args: &[WasmValue], + ) -> Result> { + let result = match &self.0.callback { + HostCallback::Untyped(func) => func(FuncContext { store, module_id }, args), + HostCallback::Typed(func) => func.call(FuncContext { store, module_id }, args), + }?; + let expected = store.state.get_canonical_func_type(type_addr); + if result.len() == expected.results().len() + && result.iter().zip(expected.results()).all(|(&value, &ty)| store.state.value_matches_type(value, ty)) + { + Ok(result) + } else { + Err(crate::Error::InvalidHostFnReturn { expected: Box::new(expected.clone()), actual: result }) + } + } + + /// Returns the allocation-free typed callback when one is available. + pub(crate) fn typed_callback(&self) -> Option<&dyn TypedHostCallback> { + match &self.0.callback { + HostCallback::Untyped(_) => None, + HostCallback::Typed(func) => Some(&**func), + } + } + + /// Create a directly callable, store-owned function from this definition. + /// + /// The returned [`Function`] can only be used with `store`. + /// + /// Host functions intended as module imports usually do not need to be + /// instantiated manually. Pass the reusable definition to + /// [`crate::Imports::define`] instead. TinyWasm will then match any GC + /// reference types to the module that imports the function. + /// + /// # Errors + /// + /// Returns an error if the signature uses a store-specific reference type + /// that is not registered in `store`. + /// + /// ## Example + /// + /// ```rust + /// # fn main() -> tinywasm::Result<()> { + /// use tinywasm::types::WasmValue; + /// use tinywasm::{HostFunction, Store}; + /// + /// let mut store = Store::default(); + /// let add_one = HostFunction::from(|_ctx, value: i32| Ok(value + 1)); + /// let function = add_one.instantiate(&mut store)?; + /// + /// assert_eq!(function.call(&mut store, &[WasmValue::I32(41)])?, [WasmValue::I32(42)]); + /// # Ok(()) + /// # } + /// ``` + pub fn instantiate(&self, store: &mut Store) -> Result { + if self + .0 + .ty + .params() + .iter() + .chain(self.0.ty.results()) + .filter_map(|ty| match ty { + WasmType::Ref(ty) => ty.type_index(), + _ => None, + }) + .any(|type_addr| type_addr as usize >= store.state.canonical_types.len()) + { + return Err(crate::Error::other("host function signature contains a concrete type from another store")); + } + let type_addr = store.register_host_type(&self.0.ty); + Ok(self.instantiate_registered(store, type_addr)) + } + + /// Create a new untyped host function. + /// + /// To call Wasm from inside the callback, use [`FuncContext::call`] or + /// [`FuncContext::call_untyped`]. + /// + /// ## Example + /// ```rust + /// # fn main() -> tinywasm::Result<()> { + /// # use tinywasm::{FuncContext, HostFunction, Imports, ModuleInstance, Store}; + /// # use tinywasm::types::{FuncType, WasmType, WasmValue}; + /// # let wasm = wat::parse_str(r#" + /// # (module + /// # (import "host" "add_one" (func $add_one (param i32) (result i32))) + /// # (func (export "call") (param i32) (result i32) + /// # local.get 0 + /// # call $add_one)) + /// # "#).expect("valid wat"); + /// # let module = tinywasm::parse_bytes(&wasm)?; + /// let mut store = Store::default(); + /// let ty = FuncType::new(&[WasmType::I32], &[WasmType::I32]); + /// let add_one = HostFunction::from_untyped(&ty, |_ctx: FuncContext<'_>, args| { + /// let WasmValue::I32(value) = args[0] else { + /// return Err(tinywasm::Error::Other("expected i32".into())); + /// }; + /// Ok(vec![WasmValue::I32(value + 1)]) + /// }); + /// + /// let mut imports = Imports::new(); + /// imports.define("host", "add_one", add_one); + /// # let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports))?; + /// # let call = instance.func::(&store, "call")?; + /// # assert_eq!(call.call(&mut store, 41)?, 42); + /// # Ok(()) + /// # } + /// ``` + pub fn from_untyped( + ty: &FuncType, + func: impl Fn(FuncContext<'_>, &[WasmValue]) -> Result> + Send + Sync + 'static, + ) -> Self { + Self(Arc::new(HostFunctionInner { ty: ty.clone(), callback: HostCallback::Untyped(Box::new(func)) })) + } + + /// Create a new typed host function. + /// + /// To call Wasm from inside the callback, use [`FuncContext::call`] or + /// [`FuncContext::call_untyped`]. + /// + /// ## Example + /// ```rust + /// # fn main() -> tinywasm::Result<()> { + /// # use tinywasm::{HostFunction, Imports, ModuleInstance, Store}; + /// # let wasm = wat::parse_str(r#" + /// # (module + /// # (import "host" "add_one" (func $add_one (param i32) (result i32))) + /// # (func (export "call") (param i32) (result i32) + /// # local.get 0 + /// # call $add_one)) + /// # "#).expect("valid wat"); + /// # let module = tinywasm::parse_bytes(&wasm)?; + /// let mut store = Store::default(); + /// let add_one = HostFunction::from(|_ctx, value: i32| Ok(value + 1)); + /// + /// let mut imports = Imports::new(); + /// imports.define("host", "add_one", add_one); + /// # let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports))?; + /// # let call = instance.func::(&store, "call")?; + /// # assert_eq!(call.call(&mut store, 41)?, 42); + /// # Ok(()) + /// # } + /// ``` + pub fn from(func: impl Fn(FuncContext<'_>, P) -> Result + Send + Sync + 'static) -> Self + where + P: FromWasmValues + ToWasmTypes + 'static, + R: IntoWasmValues + ToWasmTypes + 'static, + { + let ty = FuncType::new(&P::wasm_types(), &R::wasm_types()); + let func = TypedHostCallbackImpl { func, marker: core::marker::PhantomData }; + Self(Arc::new(HostFunctionInner { ty, callback: HostCallback::Typed(Box::new(func)) })) + } +} + +struct HostFunctionInner { + ty: FuncType, + callback: HostCallback, +} + +enum HostCallback { + Untyped(Box), + Typed(Box), +} + +type UntypedHostCallback = dyn Fn(FuncContext<'_>, &[WasmValue]) -> Result> + Send + Sync; + +pub(crate) trait TypedHostCallback: Send + Sync { + fn call(&self, ctx: FuncContext<'_>, args: &[WasmValue]) -> Result>; + fn call_stack(&self, store: &mut Store, module_id: ModuleInstanceId, type_addr: TypeAddr) -> Result<()>; +} + +struct TypedHostCallbackImpl { + func: F, + marker: core::marker::PhantomData R>, +} + +impl TypedHostCallback for TypedHostCallbackImpl +where + F: Fn(FuncContext<'_>, P) -> Result + Send + Sync, + P: FromWasmValues, + R: IntoWasmValues, +{ + fn call(&self, ctx: FuncContext<'_>, args: &[WasmValue]) -> Result> { + let mut values = args.iter().copied(); + let params = P::from_wasm_values(&mut values)?; + if values.next().is_some() { + return Err(crate::Error::other("typed conversion did not consume all WebAssembly values")); + } + Ok((self.func)(ctx, params)?.into_wasm_values().collect()) + } + + fn call_stack(&self, store: &mut Store, module_id: ModuleInstanceId, type_addr: TypeAddr) -> Result<()> { + let params = store.state.get_canonical_func_type(type_addr).params(); + let base = store.value_stack.base_before(params.iter().collect()); + let mut values = store.value_stack.wasm_values(&store.state, params, base, true); + let params = P::from_wasm_values(&mut values).and_then(|params| { + if values.next().is_some() { + Err(crate::Error::other("typed conversion did not consume all WebAssembly values")) + } else { + Ok(params) + } + }); + drop(values); + store.value_stack.truncate_to_base(base); + let params = params?; + let result = (self.func)(FuncContext { store, module_id }, params)?; + store.push_typed_values::(type_addr, result.into_wasm_values(), base) + } +} + +#[cfg(feature = "debug")] +impl core::fmt::Debug for HostFunction { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_struct("HostFunction").field("ty", &self.0.ty).finish_non_exhaustive() + } +} diff --git a/crates/tinywasm/src/func/mod.rs b/crates/tinywasm/src/func/mod.rs new file mode 100644 index 00000000..34a19389 --- /dev/null +++ b/crates/tinywasm/src/func/mod.rs @@ -0,0 +1,201 @@ +use crate::interpreter::stack::{CallFrame, StackBase}; +use crate::reference::StoreItem; +use crate::{Error, FunctionInstance, InterpreterRuntime, Result, Store}; +use alloc::{format, vec::Vec}; +use core::hint::cold_path; +use tinywasm_types::{FuncAddr, FuncType, ModuleInstanceId, WasmValue}; + +mod context; +mod host; +mod resume; +mod values; +pub use context::FuncContext; +pub use host::HostFunction; +pub use resume::{ExecProgress, FuncExecution, FuncExecutionTyped}; +#[allow(deprecated)] +pub use values::WasmTupleChain; +pub use values::{FromWasmValues, IntoWasmValues, ToWasmType, ToWasmTypes}; + +/// A function handle +#[derive(Clone)] +#[cfg_attr(feature = "debug", derive(core::fmt::Debug))] +pub struct Function { + pub(crate) item: StoreItem, + pub(crate) module_id: ModuleInstanceId, +} + +impl Function { + #[inline] + /// Returns the function's address in its store. + pub(crate) const fn addr(&self) -> FuncAddr { + self.item.addr + } + + /// Get this function's canonical type from its store. + /// + /// Concrete reference types are only meaningful in this store. + pub fn ty<'a>(&self, store: &'a Store) -> Result<&'a FuncType> { + self.item.validate_store(store)?; + Ok(store.state.get_func_type(self.addr())) + } + + /// Call a function (Invocation) + /// + /// See + #[inline] + pub fn call(&self, store: &mut Store, params: &[WasmValue]) -> Result> { + self.item.validate_store(store)?; + self.validate_params(store, params)?; + + store.enter_execution()?; + store.call_stack.clear(); + store.value_stack.clear(); + let result = self.call_untyped(store, params, 0, StackBase::default()); + store.exit_execution(); + result + } + + fn validate_params(&self, store: &Store, params: &[WasmValue]) -> Result<()> { + let func_ty = store.state.get_func_type(self.addr()); + if func_ty.params().len() != params.len() { + cold_path(); + return Err(Error::Other(format!( + "param count mismatch: expected {}, got {}", + func_ty.params().len(), + params.len() + ))); + } + + if !func_ty.params().iter().zip(params).all(|(ty, param)| store.state.value_matches_type(*param, *ty)) { + cold_path(); + #[cfg(feature = "debug")] + return Err(Error::Other(format!( + "param type mismatch: expected {:?}, got {:?}", + func_ty.params(), + params.iter().map(|v| v.ty()).collect::>() + ))); + #[cfg(not(feature = "debug"))] + return Err(Error::Other("param type mismatch".into())); + } + Ok(()) + } + + #[inline] + fn call_untyped( + &self, + store: &mut Store, + params: &[WasmValue], + call_stack_base: u32, + value_stack_base: StackBase, + ) -> Result> { + let instance = store.state.get_func(self.addr()); + let type_addr = instance.type_addr; + let results_may_gc = instance.gc.results; + let result = match &instance.kind { + crate::store::FunctionKind::Host(host) => { + let host = host.clone(); + host.call_values(store, self.module_id, type_addr, params) + } + crate::store::FunctionKind::Wasm(wasm) => { + let wasm_params = wasm.func.params; + let wasm_locals = wasm.func.locals; + let locals_base = + store.value_stack.enter_wasm_call(params, wasm_params, wasm_locals, value_stack_base)?; + let callframe = CallFrame::new(self.addr(), locals_base, wasm_locals); + InterpreterRuntime::exec(store, callframe, call_stack_base).inspect_err(|_| { + store.call_stack.truncate_to(call_stack_base); + store.value_stack.truncate_to_base(value_stack_base); + })?; + let result_types = store.state.get_canonical_func_type(type_addr).results(); + Ok(store.value_stack.pop_wasmvalues(&store.state, result_types)) + } + }; + if results_may_gc && let Ok(values) = &result { + store.state.pin_host_values(values); + } + result + } + + fn prepare_typed( + &self, + store: &mut Store, + instance: &FunctionInstance, + params: impl Iterator, + stack_base: StackBase, + ) -> Result> { + store.push_typed_values::(instance.type_addr, params, stack_base)?; + match &instance.kind { + crate::store::FunctionKind::Wasm(wasm) => { + let locals_base = store + .value_stack + .enter_locals(&wasm.func.params, &wasm.func.locals) + .inspect_err(|_| store.value_stack.truncate_to_base(stack_base))?; + Ok(Some(CallFrame::new(self.addr(), locals_base, wasm.func.locals))) + } + crate::store::FunctionKind::Host(host) => { + host.typed_callback() + .expect("typed host function") + .call_stack(store, self.module_id, instance.type_addr) + .inspect_err(|_| store.value_stack.truncate_to_base(stack_base))?; + Ok(None) + } + } + } + + #[inline] + fn call_typed( + &self, + store: &mut Store, + instance: &FunctionInstance, + params: impl Iterator, + call_stack_base: u32, + value_stack_base: StackBase, + ) -> Result { + if let Some(callframe) = self.prepare_typed(store, instance, params, value_stack_base)? { + InterpreterRuntime::exec(store, callframe, call_stack_base).inspect_err(|_| { + store.call_stack.truncate_to(call_stack_base); + store.value_stack.truncate_to_base(value_stack_base); + })?; + } + store.take_typed_results(instance.type_addr, value_stack_base, instance.gc.results) + } +} + +/// A typed function handle. +/// +/// Parameter and result tuples are supported up to arity 20. Use +/// [`crate::ModuleInstance::func_untyped`] for larger signatures. +#[cfg_attr(feature = "debug", derive(core::fmt::Debug))] +pub struct FunctionTyped { + /// The underlying function handle + pub func: Function, + pub(crate) marker: core::marker::PhantomData<(P, R)>, +} + +impl FunctionTyped { + /// Call a typed function + pub fn call(&self, store: &mut Store, params: P) -> Result { + self.func.item.validate_store(store)?; + let func = store.state.get_func(self.func.addr()).clone(); + if matches!(&func.kind, crate::store::FunctionKind::Host(host) if host.typed_callback().is_none()) { + let params = params.into_wasm_values().collect::>(); + let result = self.func.call(store, ¶ms)?; + let mut values = result.into_iter(); + let result = R::from_wasm_values(&mut values)?; + return if values.next().is_none() { + Ok(result) + } else { + Err(Error::other("typed conversion did not consume all WebAssembly values")) + }; + } + + store.enter_execution()?; + let result: Result = { + store.call_stack.clear(); + store.value_stack.clear(); + self.func.call_typed(store, &func, params.into_wasm_values(), 0, StackBase::default()) + }; + store.exit_execution(); + result + } +} diff --git a/crates/tinywasm/src/func/resume.rs b/crates/tinywasm/src/func/resume.rs new file mode 100644 index 00000000..892aa05b --- /dev/null +++ b/crates/tinywasm/src/func/resume.rs @@ -0,0 +1,275 @@ +use alloc::vec::Vec; +use tinywasm_types::{FuncAddr, TypeAddr, WasmValue}; + +use super::{FromWasmValues, Function, FunctionTyped, IntoWasmValues}; +use crate::interpreter::stack::{CallFrame, StackBase}; +use crate::{Error, InterpreterRuntime, Result, Store}; + +#[derive(Clone, PartialEq, Eq)] +/// Progress for fuel-limited function execution. +pub enum ExecProgress { + /// Execution completed and produced a result. + Completed(T), + /// Execution suspended after exhausting fuel or time budget. + Suspended, +} + +/// Resumable execution for an untyped function call. +#[cfg_attr(feature = "debug", derive(core::fmt::Debug))] +pub struct FuncExecution<'store> { + store: &'store mut Store, + state: FuncExecutionState, +} + +#[cfg_attr(feature = "debug", derive(core::fmt::Debug))] +enum FuncExecutionState { + Running { callframe: CallFrame, root_func_addr: FuncAddr }, + Completed(Option), +} + +#[cfg_attr(feature = "debug", derive(core::fmt::Debug))] +enum CallResult { + Stack { type_addr: TypeAddr, pin_refs: bool }, + Values(Vec), +} + +/// Resumable execution for a typed function call. +#[cfg_attr(feature = "debug", derive(core::fmt::Debug))] +pub struct FuncExecutionTyped<'store, R> { + execution: FuncExecution<'store>, + marker: core::marker::PhantomData, +} + +impl Function { + /// Call a function and return a resumable execution handle. + /// + /// The returned handle keeps a mutable borrow of the [`Store`] until it + /// completes. Use [`FuncExecution::resume_with_fuel`] (or + /// `resume_with_time_budget` with `std`) to continue. + pub fn call_resumable<'store>( + &self, + store: &'store mut Store, + params: &[WasmValue], + ) -> Result> { + self.item.validate_store(store)?; + self.validate_params(store, params)?; + let results_may_gc = store.state.get_func(self.addr()).gc.results; + + store.enter_execution()?; + let result: Result = (|| { + let func_instance = store.state.get_func(self.addr()).clone(); + match &func_instance.kind { + crate::store::FunctionKind::Host(host_func) => { + let result = + host_func.clone().call_values(store, self.module_id, func_instance.type_addr, params)?; + Ok(FuncExecutionState::Completed(Some(CallResult::Values(result)))) + } + crate::store::FunctionKind::Wasm(wasm_func) => { + store.call_stack.clear(); + store.value_stack.clear(); + let locals = wasm_func.func.locals; + let locals_base = store.value_stack.enter_wasm_call( + params, + wasm_func.func.params, + locals, + StackBase::default(), + )?; + let callframe = CallFrame::new(self.addr(), locals_base, locals); + + Ok(FuncExecutionState::Running { callframe, root_func_addr: self.addr() }) + } + } + })(); + store.exit_execution(); + + let state = result?; + if results_may_gc && let FuncExecutionState::Completed(Some(CallResult::Values(values))) = &state { + store.state.pin_host_values(values); + } + Ok(FuncExecution { store, state }) + } +} + +impl<'store> FuncExecution<'store> { + fn resume_raw( + &mut self, + run: impl FnOnce(&mut Store, CallFrame) -> Result, + ) -> Result> { + let (callframe, root_func_addr) = match &mut self.state { + FuncExecutionState::Running { callframe, root_func_addr } => (*callframe, *root_func_addr), + FuncExecutionState::Completed(result) => { + return result + .take() + .map(ExecProgress::Completed) + .ok_or_else(|| Error::other("execution already completed")); + } + }; + + self.store.enter_execution()?; + let result = run(self.store, callframe); + self.store.exit_execution(); + + let result = match result { + Ok(result) => result, + Err(error) => { + self.store.call_stack.clear(); + self.store.value_stack.clear(); + self.state = FuncExecutionState::Completed(None); + return Err(error); + } + }; + + match result { + crate::interpreter::ExecState::Completed => { + let func = self.store.state.get_func(root_func_addr); + let result_ty = func.type_addr; + let results_may_gc = func.gc.results; + self.state = FuncExecutionState::Completed(None); + Ok(ExecProgress::Completed(CallResult::Stack { type_addr: result_ty, pin_refs: results_may_gc })) + } + crate::interpreter::ExecState::Suspended(callframe) => { + let FuncExecutionState::Running { callframe: current, .. } = &mut self.state else { + unreachable!("invalid function execution state") + }; + *current = callframe; + Ok(ExecProgress::Suspended) + } + } + } + + fn resume( + &mut self, + run: impl FnOnce(&mut Store, CallFrame) -> Result, + ) -> Result>> { + match self.resume_raw(run)? { + ExecProgress::Completed(CallResult::Stack { type_addr, pin_refs }) => { + let types = self.store.state.get_canonical_func_type(type_addr).results(); + let values = self.store.value_stack.pop_wasmvalues(&self.store.state, types); + if pin_refs { + self.store.state.pin_host_values(&values); + } + Ok(ExecProgress::Completed(values)) + } + ExecProgress::Completed(CallResult::Values(values)) => Ok(ExecProgress::Completed(values)), + ExecProgress::Suspended => Ok(ExecProgress::Suspended), + } + } + + /// Resume execution with up to `fuel` units of fuel. + /// + /// Fuel is accounted in chunks, so execution may overshoot the requested + /// fuel before returning [`ExecProgress::Suspended`] (currently the chunk size is 128 instructions between fuel checks, but this may change in the future). + /// + /// Returns [`ExecProgress::Suspended`] when fuel is exhausted, or + /// [`ExecProgress::Completed`] with the final values once the invocation + /// returns. + /// + /// Reentrant calls made by host functions through [`crate::FuncContext::call`] are + /// currently blocking. They do not suspend and later resume the host + /// function in the middle of the nested call. + pub fn resume_with_fuel(&mut self, fuel: u32) -> Result>> { + self.resume(|store, callframe| InterpreterRuntime::exec_with_fuel(store, callframe, fuel)) + } + + #[cfg(feature = "std")] + /// Resume execution for at most `time_budget` wall-clock time. + /// + /// Time is checked periodically, so execution may overshoot the requested + /// time budget before returning [`ExecProgress::Suspended`] (currently time + /// is checked every 128 instructions, but this may change in the future). + /// + /// Reentrant calls made by host functions through [`crate::FuncContext::call`] + /// are blocking and do not suspend in the middle of the host callback. + pub fn resume_with_time_budget( + &mut self, + time_budget: crate::std::time::Duration, + ) -> Result>> { + self.resume(|store, callframe| InterpreterRuntime::exec_with_time_budget(store, callframe, time_budget)) + } +} + +impl FunctionTyped { + /// Call a typed function and return a resumable execution handle. + /// + /// The handle keeps a mutable borrow of the [`Store`] until completion. + /// + /// ## Example + /// + /// ```rust + /// # fn main() -> tinywasm::Result<()> { + /// use tinywasm::{ExecProgress, ModuleInstance, Store}; + /// + /// let wasm = include_bytes!("../../../../examples/wasm/add.wasm"); + /// let module = tinywasm::parse_bytes(wasm)?; + /// let mut store = Store::default(); + /// let instance = ModuleInstance::instantiate(&mut store, &module, None)?; + /// let add = instance.func::<(i32, i32), i32>(&store, "add")?; + /// + /// let mut execution = add.call_resumable(&mut store, (20, 22))?; + /// assert!(matches!(execution.resume_with_fuel(0)?, ExecProgress::Suspended)); + /// assert!(matches!(execution.resume_with_fuel(16)?, ExecProgress::Completed(42))); + /// # Ok(()) + /// # } + /// ``` + pub fn call_resumable<'store>(&self, store: &'store mut Store, params: P) -> Result> { + self.func.item.validate_store(store)?; + let func = store.state.get_func(self.func.addr()).clone(); + if matches!(&func.kind, crate::store::FunctionKind::Host(host) if host.typed_callback().is_none()) { + let params = params.into_wasm_values().collect::>(); + let execution = self.func.call_resumable(store, ¶ms)?; + return Ok(FuncExecutionTyped { execution, marker: core::marker::PhantomData }); + } + + store.enter_execution()?; + let result: Result = (|| { + store.call_stack.clear(); + store.value_stack.clear(); + match self.func.prepare_typed(store, &func, params.into_wasm_values(), StackBase::default())? { + Some(callframe) => Ok(FuncExecutionState::Running { callframe, root_func_addr: self.func.addr() }), + None => Ok(FuncExecutionState::Completed(Some(CallResult::Stack { + type_addr: func.type_addr, + pin_refs: func.gc.results, + }))), + } + })(); + store.exit_execution(); + let execution = FuncExecution { store, state: result? }; + Ok(FuncExecutionTyped { execution, marker: core::marker::PhantomData }) + } +} + +impl<'store, R: FromWasmValues> FuncExecutionTyped<'store, R> { + fn resume( + &mut self, + run: impl FnOnce(&mut Store, CallFrame) -> Result, + ) -> Result> { + match self.execution.resume_raw(run)? { + ExecProgress::Completed(CallResult::Stack { type_addr, pin_refs }) => Ok(ExecProgress::Completed( + self.execution.store.take_typed_results(type_addr, StackBase::default(), pin_refs)?, + )), + ExecProgress::Completed(CallResult::Values(values)) => { + let mut values = values.into_iter(); + let result = R::from_wasm_values(&mut values)?; + if values.next().is_some() { + return Err(Error::other("typed conversion did not consume all WebAssembly values")); + } + Ok(ExecProgress::Completed(result)) + } + ExecProgress::Suspended => Ok(ExecProgress::Suspended), + } + } + + /// Resume typed execution with up to `fuel` units of fuel. + pub fn resume_with_fuel(&mut self, fuel: u32) -> Result> { + self.resume(|store, callframe| InterpreterRuntime::exec_with_fuel(store, callframe, fuel)) + } + + #[cfg(feature = "std")] + /// Resume typed execution for at most `time_budget` wall-clock time. + /// + /// Time is checked periodically, so execution may overshoot the requested + /// time budget before returning [`ExecProgress::Suspended`]. + pub fn resume_with_time_budget(&mut self, time_budget: crate::std::time::Duration) -> Result> { + self.resume(|store, callframe| InterpreterRuntime::exec_with_time_budget(store, callframe, time_budget)) + } +} diff --git a/crates/tinywasm/src/func/values.rs b/crates/tinywasm/src/func/values.rs new file mode 100644 index 00000000..319b4939 --- /dev/null +++ b/crates/tinywasm/src/func/values.rs @@ -0,0 +1,201 @@ +use crate::{Error, Result}; +use alloc::{borrow::Cow, vec::Vec}; +use tinywasm_types::{ExternRef, FuncRef, WasmType, WasmValue}; + +/// Convert a Rust value or tuple into WebAssembly values. +pub trait IntoWasmValues { + /// Return the flattened WebAssembly values. + fn into_wasm_values(self) -> impl Iterator; +} + +/// Convert WebAssembly values into a Rust value or tuple. +pub trait FromWasmValues: Sized { + /// Read this value from a flattened WebAssembly value iterator. + fn from_wasm_values(values: &mut impl Iterator) -> Result; +} + +/// Describes the WebAssembly value types produced by a Rust value or tuple shape. +pub trait ToWasmTypes { + /// Static WebAssembly types for this shape. + /// + /// Implementations that require runtime construction may set this to `None`, + /// but must then override [`Self::wasm_types`]. + const WASM_TYPES: Option<&'static [WasmType]>; + + /// Return the flattened WebAssembly value types for this tuple shape. + fn wasm_types() -> Cow<'static, [WasmType]> { + Cow::Borrowed(Self::WASM_TYPES.expect("dynamic ToWasmTypes implementation must override wasm_types")) + } +} + +/// Describes the WebAssembly value types produced by a scalar Rust type. +pub trait ToWasmType { + /// The single WebAssembly value type for this scalar type. + const WASM_TYPE: WasmType; +} + +fn next_value>(values: &mut impl Iterator) -> Result { + let value = values.next().ok_or_else(|| { + core::hint::cold_path(); + Error::other("not enough WebAssembly values") + })?; + T::try_from(value).map_err(|_| { + core::hint::cold_path(); + Error::other("WebAssembly value does not match the expected type") + }) +} + +macro_rules! impl_scalar_wasm_traits { + ($($T:ty => $val_ty:expr),+ $(,)?) => { + $( + impl ToWasmType for $T { + const WASM_TYPE: WasmType = $val_ty; + } + + impl ToWasmTypes for $T { + const WASM_TYPES: Option<&'static [WasmType]> = Some(&[$val_ty]); + } + + impl IntoWasmValues for $T { + #[inline] + fn into_wasm_values(self) -> impl Iterator { + core::iter::once(self.into()) + } + } + + impl FromWasmValues for $T { + #[inline] + fn from_wasm_values(values: &mut impl Iterator) -> Result { + next_value(values) + } + } + )+ + }; +} + +macro_rules! impl_tuple_traits { + (@next $head:ident, $($tail:ident),+) => { + impl_tuple_traits!($($tail),+); + }; + (@next $head:ident) => {}; + ($($T:ident),+) => { + impl<$($T),+> ToWasmTypes for ($($T,)+) + where + $($T: ToWasmType,)+ + { + const WASM_TYPES: Option<&'static [WasmType]> = Some(&[$($T::WASM_TYPE,)+]); + } + + impl<$($T),+> IntoWasmValues for ($($T,)+) + where + $($T: Into,)+ + { + #[allow(non_snake_case)] + #[inline] + fn into_wasm_values(self) -> impl Iterator { + let ($($T,)+) = self; + [$($T.into(),)+].into_iter() + } + } + + impl<$($T),+> FromWasmValues for ($($T,)+) + where + $($T: TryFrom,)+ + { + #[inline] + fn from_wasm_values(values: &mut impl Iterator) -> Result { + Ok(($(next_value::<$T>(values)?,)+)) + } + } + + impl_tuple_traits!(@next $($T),+); + } +} + +impl_scalar_wasm_traits!( + i32 => WasmType::I32, + i64 => WasmType::I64, + f32 => WasmType::F32, + f64 => WasmType::F64, + FuncRef => WasmType::Ref(tinywasm_types::RefType::FUNCREF), + ExternRef => WasmType::Ref(tinywasm_types::RefType::EXTERNREF), +); +impl_tuple_traits!(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20); + +/// Concatenates two typed parameter or result groups. +/// +/// Direct tuple conversions are supported up to arity 20. Use untyped functions +/// for larger signatures. +#[deprecated(note = "direct tuples are supported up to arity 20, use untyped functions for larger signatures")] +#[derive(Default)] +pub struct WasmTupleChain(T1, T2); + +#[allow(deprecated)] +impl WasmTupleChain { + /// Create a new concatenated tuple wrapper. + pub const fn new(left: T1, right: T2) -> Self { + Self(left, right) + } + + /// Split the wrapper back into its two component values. + pub fn into_inner(self) -> (T1, T2) { + (self.0, self.1) + } +} + +#[allow(deprecated)] +impl From<(T1, T2)> for WasmTupleChain { + fn from((left, right): (T1, T2)) -> Self { + Self::new(left, right) + } +} + +#[allow(deprecated)] +impl ToWasmTypes for WasmTupleChain { + const WASM_TYPES: Option<&'static [WasmType]> = None; + + #[inline] + fn wasm_types() -> Cow<'static, [WasmType]> { + let mut types = Vec::new(); + types.extend_from_slice(&T1::wasm_types()); + types.extend_from_slice(&T2::wasm_types()); + Cow::Owned(types) + } +} + +#[allow(deprecated)] +impl IntoWasmValues for WasmTupleChain { + #[inline] + fn into_wasm_values(self) -> impl Iterator { + let (left, right) = self.into_inner(); + left.into_wasm_values().chain(right.into_wasm_values()) + } +} + +#[allow(deprecated)] +impl FromWasmValues for WasmTupleChain { + #[inline] + fn from_wasm_values(values: &mut impl Iterator) -> Result { + let left = T1::from_wasm_values(values)?; + let right = T2::from_wasm_values(values)?; + Ok(Self::new(left, right)) + } +} + +impl ToWasmTypes for () { + const WASM_TYPES: Option<&'static [WasmType]> = Some(&[]); +} + +impl IntoWasmValues for () { + #[inline] + fn into_wasm_values(self) -> impl Iterator { + core::iter::empty() + } +} + +impl FromWasmValues for () { + #[inline] + fn from_wasm_values(_values: &mut impl Iterator) -> Result { + Ok(()) + } +} diff --git a/crates/tinywasm/src/imports.rs b/crates/tinywasm/src/imports.rs index 887d1073..eb2351b9 100644 --- a/crates/tinywasm/src/imports.rs +++ b/crates/tinywasm/src/imports.rs @@ -1,14 +1,12 @@ use alloc::collections::BTreeMap; use alloc::string::{String, ToString}; use alloc::vec::Vec; -use core::fmt::Debug; -use core::hint::cold_path; -use crate::{Function, Global, LinkingError, Memory, Result, Table}; +use crate::{Function, Global, HostFunction, LinkingError, Memory, Result, Table, Tag}; use tinywasm_types::*; #[derive(Clone)] -#[cfg_attr(feature = "debug", derive(Debug))] +#[cfg_attr(feature = "debug", derive(core::fmt::Debug))] #[non_exhaustive] /// An external import value. pub enum Extern { @@ -20,43 +18,31 @@ pub enum Extern { Memory(Memory), /// A function import. Function(Function), + /// A reusable host function definition. + HostFunction(HostFunction), + /// A tag instance. + Tag(Tag), } -impl From for Extern { - fn from(value: Global) -> Self { - Self::Global(value) - } -} - -impl From for Extern { - fn from(value: Table) -> Self { - Self::Table(value) - } -} - -impl From for Extern { - fn from(value: Memory) -> Self { - Self::Memory(value) - } -} - -impl From for Extern { - fn from(value: Function) -> Self { - Self::Function(value) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)] -/// Name of an import -pub struct ExternName { - module: String, - name: String, +macro_rules! impl_conv { + ($($ty:ty => $variant:ident),* $(,)?) => { + $( + impl From<$ty> for Extern { + fn from(value: $ty) -> Self { + Self::$variant(value) + } + } + )* + }; } -impl From<&Import> for ExternName { - fn from(import: &Import) -> Self { - Self { module: import.module.to_string(), name: import.name.to_string() } - } +impl_conv! { + Global => Global, + Table => Table, + Memory => Memory, + Function => Function, + HostFunction => HostFunction, + Tag => Tag, } /// Imports for a module instance @@ -75,7 +61,7 @@ impl From<&Import> for ExternName { /// # let my_other_instance = ModuleInstance::instantiate(&mut store, &module, None)?; /// let mut imports = Imports::new(); /// -/// let print_i32 = HostFunction::from(&mut store, |_ctx: tinywasm::FuncContext<'_>, arg: i32| { +/// let print_i32 = HostFunction::from(|_ctx: tinywasm::FuncContext<'_>, arg: i32| { /// log::debug!("print_i32: {}", arg); /// Ok(()) /// }); @@ -101,11 +87,13 @@ impl From<&Import> for ExternName { /// # Ok(()) /// # } /// ``` -/// Now, the imports object can be passed to [`crate::ModuleInstance::instantiate`]. +/// Host function definitions are store-independent, so the imports object can be borrowed by +/// [`crate::ModuleInstance::instantiate`] for multiple stores. +/// TinyWasm also matches GC reference types to each module when it links an imported host function. #[derive(Default, Clone)] #[cfg_attr(feature = "debug", derive(Debug))] pub struct Imports { - externs: BTreeMap, + externs: BTreeMap>, modules: BTreeMap, } @@ -114,6 +102,7 @@ pub(crate) struct ResolvedImports { pub(crate) tables: Vec, pub(crate) memories: Vec, pub(crate) funcs: Vec, + pub(crate) tags: Vec, } impl Imports { @@ -124,7 +113,9 @@ impl Imports { /// Merge two import sets pub fn merge(mut self, other: Self) -> Self { - self.externs.extend(other.externs); + for (module, externs) in other.externs { + self.externs.entry(module).or_default().extend(externs); + } self.modules.extend(other.modules); self } @@ -138,65 +129,68 @@ impl Imports { } /// Define an import value. + /// + /// A [`Function`], [`Global`], [`Table`], [`Memory`], or [`Tag`] handle belongs to + /// one store and can only be imported into that store. A [`HostFunction`] + /// is a reusable definition and can be imported into multiple stores. + /// + /// ## Example + /// + /// ```rust + /// # fn main() -> tinywasm::Result<()> { + /// use tinywasm::{HostFunction, Imports, ModuleInstance, Store}; + /// + /// let wasm = wat::parse_str( + /// r#" + /// (module + /// (import "host" "answer" (func $answer (result i32))) + /// (export "answer" (func $answer))) + /// "#, + /// ) + /// .expect("valid wat"); + /// let module = tinywasm::parse_bytes(&wasm)?; + /// let mut imports = Imports::new(); + /// imports.define("host", "answer", HostFunction::from(|_ctx, ()| Ok(42_i32))); + /// + /// let mut first_store = Store::default(); + /// let first = ModuleInstance::instantiate(&mut first_store, &module, Some(&imports))?; + /// assert_eq!(first.func::<(), i32>(&first_store, "answer")?.call(&mut first_store, ())?, 42); + /// + /// let mut second_store = Store::default(); + /// let second = ModuleInstance::instantiate(&mut second_store, &module, Some(&imports))?; + /// assert_eq!(second.func::<(), i32>(&second_store, "answer")?.call(&mut second_store, ())?, 42); + /// # Ok(()) + /// # } + /// ``` pub fn define(&mut self, module: &str, name: &str, value: impl Into) -> &mut Self { - let name = ExternName { module: module.to_string(), name: name.to_string() }; - self.externs.insert(name, value.into()); + self.externs.entry(module.to_string()).or_default().insert(name.to_string(), value.into()); self } - pub(crate) fn take_defined(&self, import: &Import) -> Option { - let name = ExternName::from(import); - self.externs.get(&name).cloned() + /// Returns an explicitly defined import without cloning its handle. + pub(crate) fn defined(&self, import: &Import) -> Option<&Extern> { + self.externs.get(import.module.as_ref())?.get(import.name.as_ref()) } fn compare_types(import: &Import, actual: &T, expected: &T) -> Result<()> { if expected != actual { - cold_path(); - return Err(LinkingError::incompatible_import_type(import).into()); + return cold!(Err(LinkingError::incompatible_import_type(import).into())); } Ok(()) } - fn ref_subtype(actual: RefType, expected: RefType) -> bool { - if actual.is_nullable() && !expected.is_nullable() { - return false; - } - match (actual.type_index(), expected.type_index()) { - (Some(actual), Some(expected)) => actual == expected, - (Some(_), None) => matches!(expected.abstract_heap_type(), Some(AbstractHeapType::Func)), - (None, Some(_)) => matches!(actual.abstract_heap_type(), Some(AbstractHeapType::NoFunc)), - (None, None) => { - actual.abstract_heap_type() == expected.abstract_heap_type() - || actual.is_func() && matches!(expected.abstract_heap_type(), Some(AbstractHeapType::Func)) - || actual.is_extern() && matches!(expected.abstract_heap_type(), Some(AbstractHeapType::Extern)) - || actual.is_exn() && matches!(expected.abstract_heap_type(), Some(AbstractHeapType::Exn)) - } - } - } - - fn value_subtype(actual: WasmType, expected: WasmType) -> bool { - match (actual, expected) { - (WasmType::Ref(actual), WasmType::Ref(expected)) => Self::ref_subtype(actual, expected), - _ => actual == expected, - } - } - fn compare_table_types(import: &Import, actual: &TableType, expected: &TableType) -> Result<()> { Self::compare_types(import, &actual.arch(), &expected.arch())?; - if !Self::ref_subtype(actual.element_type, expected.element_type) - || !Self::ref_subtype(expected.element_type, actual.element_type) - { + if actual.element_type != expected.element_type { return Err(LinkingError::incompatible_import_type(import).into()); } if actual.size_initial < expected.size_initial { - cold_path(); - return Err(LinkingError::incompatible_import_type(import).into()); + return cold!(Err(LinkingError::incompatible_import_type(import).into())); } match expected.size_max { Some(expected_max) if actual.size_max.is_none_or(|actual_max| actual_max > expected_max) => { - cold_path(); - Err(LinkingError::incompatible_import_type(import).into()) + cold!(Err(LinkingError::incompatible_import_type(import).into())) } _ => Ok(()), } @@ -232,12 +226,13 @@ impl Imports { module: &Module, type_addrs: &[TypeAddr], ) -> Result { - let (global_count, table_count, mem_count, func_count) = - module.imports.iter().fold((0, 0, 0, 0), |(g, t, m, f), import| match import.kind { - ImportKind::Global(_) => (g + 1, t, m, f), - ImportKind::Table(_) => (g, t + 1, m, f), - ImportKind::Memory(_) => (g, t, m + 1, f), - ImportKind::Function(_) => (g, t, m, f + 1), + let (global_count, table_count, mem_count, func_count, tag_count) = + module.imports.iter().fold((0, 0, 0, 0, 0), |(g, t, m, f, e), import| match import.kind { + ImportKind::Global(_) => (g + 1, t, m, f, e), + ImportKind::Table(_) => (g, t + 1, m, f, e), + ImportKind::Memory(_) => (g, t, m + 1, f, e), + ImportKind::Function(_) => (g, t, m, f + 1, e), + ImportKind::Tag(_) => (g, t, m, f, e + 1), }); let mut imports = ResolvedImports { @@ -245,41 +240,68 @@ impl Imports { tables: Vec::with_capacity(table_count + module.tables.len()), memories: Vec::with_capacity(mem_count + module.memory_types.len()), funcs: Vec::with_capacity(func_count + module.funcs.len()), + tags: Vec::with_capacity(tag_count + module.tags.len()), }; for import in &*module.imports { - let (val, func_handle) = if let Some(defined) = self.take_defined(import) { + let val = if let Some(defined) = self.defined(import) { match defined { - Extern::Global(global) => (ExternVal::Global(global.0.addr), None), - Extern::Table(table) => (ExternVal::Table(table.0.addr), None), - Extern::Memory(memory) => (ExternVal::Memory(memory.0.addr), None), - Extern::Function(func) => (ExternVal::Func(func.addr()), Some(func)), + Extern::Global(global) => { + global.0.validate_store(store)?; + ExternVal::Global(global.0.addr) + } + Extern::Table(table) => { + table.0.validate_store(store)?; + ExternVal::Table(table.0.addr) + } + Extern::Memory(memory) => { + memory.0.validate_store(store)?; + ExternVal::Memory(memory.0.addr) + } + Extern::Function(func) => { + func.item.validate_store(store)?; + ExternVal::Func(func.addr()) + } + Extern::HostFunction(func) => { + let ImportKind::Function(type_idx) = import.kind else { + return cold!(Err(LinkingError::incompatible_import_type(import).into())); + }; + let expected_type_addr = type_addrs + .get(type_idx as usize) + .ok_or_else(|| LinkingError::incompatible_import_type(import))?; + let actual_ty = func.resolve_import_type(type_addrs)?; + let actual_type_addr = store.register_host_type(&actual_ty); + if !store.state.type_addr_is_subtype(actual_type_addr, *expected_type_addr) { + return cold!(Err(LinkingError::incompatible_import_type(import).into())); + } + ExternVal::Func(func.instantiate_registered(store, actual_type_addr).addr()) + } + Extern::Tag(tag) => { + tag.0.validate_store(store)?; + ExternVal::Tag(tag.0.addr) + } } } else { - let name = ExternName::from(import); - let Some(instance) = self.modules.get(&name.module) else { - cold_path(); - return Err(LinkingError::unknown_import(import).into()); + let Some(instance) = self.modules.get(import.module.as_ref()) else { + return cold!(Err(LinkingError::unknown_import(import).into())); }; instance.validate_store(store)?; - (instance.export_addr(&import.name).ok_or_else(|| LinkingError::unknown_import(import))?, None) + instance.export_addr(&import.name).ok_or_else(|| LinkingError::unknown_import(import))? }; if val.kind() != (&import.kind).into() { - cold_path(); - return Err(LinkingError::incompatible_import_type(import).into()); + return cold!(Err(LinkingError::incompatible_import_type(import).into())); } match (val, &import.kind) { (ExternVal::Global(global_addr), ImportKind::Global(ty)) => { - let global = store.state.get_global(global_addr); + let global_ty = store.state.globals.ty(global_addr); let expected = ty.with_ty(crate::store::canonicalize_value_type(ty.ty, type_addrs)); - let compatible = global.ty.mutable == ty.mutable - && Self::value_subtype(global.ty.ty, expected.ty) - && (!ty.mutable || Self::value_subtype(expected.ty, global.ty.ty)); + let compatible = global_ty.mutable == ty.mutable + && store.state.value_type_is_subtype(global_ty.ty, expected.ty) + && (!ty.mutable || store.state.value_type_is_subtype(expected.ty, global_ty.ty)); if !compatible { - cold_path(); - return Err(LinkingError::incompatible_import_type(import).into()); + return cold!(Err(LinkingError::incompatible_import_type(import).into())); } imports.globals.push(global_addr); } @@ -303,15 +325,21 @@ impl Imports { (ExternVal::Func(func_addr), ImportKind::Function(ty)) => { let expected_type_addr = type_addrs.get(*ty as usize).ok_or_else(|| LinkingError::incompatible_import_type(import))?; - if let Some(func) = &func_handle { - func.item.validate_store(store)?; - } - if store.state.get_func(func_addr).type_addr != *expected_type_addr { - cold_path(); - return Err(LinkingError::incompatible_import_type(import).into()); + if !store.state.type_addr_is_subtype(store.state.get_func(func_addr).type_addr, *expected_type_addr) + { + return cold!(Err(LinkingError::incompatible_import_type(import).into())); } imports.funcs.push(func_addr); } + (ExternVal::Tag(tag_addr), ImportKind::Tag(ty)) => { + let expected_type_addr = type_addrs + .get(ty.type_idx as usize) + .ok_or_else(|| LinkingError::incompatible_import_type(import))?; + if store.state.get_tag(tag_addr).type_addr != *expected_type_addr { + return cold!(Err(LinkingError::incompatible_import_type(import).into())); + } + imports.tags.push(tag_addr); + } _ => unreachable!("import kind checked above"), } } diff --git a/crates/tinywasm/src/instance.rs b/crates/tinywasm/src/instance.rs index ecfbacbf..f45b7184 100644 --- a/crates/tinywasm/src/instance.rs +++ b/crates/tinywasm/src/instance.rs @@ -4,7 +4,7 @@ use tinywasm_types::*; use crate::func::{FromWasmValues, IntoWasmValues, ToWasmTypes}; use crate::store::MemoryInstance; -use crate::{Error, Function, FunctionTyped, Global, Imports, Memory, Result, Store, StoreItem, Table, Trap}; +use crate::{Error, Function, FunctionTyped, Global, Imports, Memory, Result, Store, StoreItem, Table, Tag, Trap}; /// A typed view over an exported extern value. pub enum ExternItem { @@ -16,6 +16,8 @@ pub enum ExternItem { Table(Table), /// Exported global reference. Global(Global), + /// Exported tag reference. + Tag(Tag), } /// An instantiated WebAssembly module @@ -45,13 +47,14 @@ pub struct ModuleInstance(Rc); #[cfg_attr(feature = "debug", derive(Debug))] struct ModuleInstanceInner { - store_id: usize, + store_id: u32, id: ModuleInstanceId, type_addrs: Box<[TypeAddr]>, func_addrs: Box<[FuncAddr]>, table_addrs: Box<[TableAddr]>, mem_addrs: Box<[MemAddr]>, global_addrs: Box<[GlobalAddr]>, + tag_addrs: Box<[TagAddr]>, elem_addrs: Box<[ElemAddr]>, data_addrs: Box<[DataAddr]>, func_start: Option, @@ -100,11 +103,15 @@ impl ModuleInstance { *self.0.global_addrs.get(addr as usize).unwrap_or_else(|| unreachable!("invalid global address: {addr}")) } + #[inline] + pub(crate) fn resolve_tag_addr(&self, addr: TagAddr) -> TagAddr { + *self.0.tag_addrs.get(addr as usize).unwrap_or_else(|| unreachable!("invalid tag address: {addr}")) + } + #[inline] pub(crate) fn validate_store(&self, store: &Store) -> Result<()> { if self.0.store_id != store.id() { - cold_path(); - return Err(Trap::InvalidStore.into()); + return cold!(Err(Trap::InvalidStore.into())); } Ok(()) } @@ -117,7 +124,7 @@ impl ModuleInstance { /// Instantiate the module in the given store /// /// See - pub fn instantiate(store: &mut Store, module: &Module, imports: Option) -> Result { + pub fn instantiate(store: &mut Store, module: &Module, imports: Option<&Imports>) -> Result { let instance = ModuleInstance::instantiate_no_start(store, module, imports)?; let _ = instance.start(store)?; Ok(instance) @@ -150,15 +157,14 @@ impl ModuleInstance { /// ``` /// /// See - pub fn instantiate_no_start(store: &mut Store, module: &Module, imports: Option) -> Result { - let type_addrs = store.register_module_types(&module.func_types); + pub fn instantiate_no_start(store: &mut Store, module: &Module, imports: Option<&Imports>) -> Result { + let type_addrs = store.register_module_types(&module.types); let id = store.next_module_instance_id(); - let mut addrs = imports.unwrap_or_default().link(store, module, &type_addrs)?; - let local_type_addrs = module.func_type_idxs[addrs.funcs.len()..] - .iter() - .map(|&addr| type_addrs[addr as usize]) - .collect::>(); - addrs.funcs.extend(store.init_funcs(&module.funcs, id, &local_type_addrs)); + let default_imports = Imports::default(); + let mut addrs = imports.unwrap_or(&default_imports).link(store, module, &type_addrs)?; + let imported_funcs = addrs.funcs.len(); + addrs.funcs.extend(store.init_funcs(&module.funcs, id, &module.func_type_idxs[imported_funcs..], &type_addrs)); + addrs.tags.extend(store.init_tags(&module.tags, &type_addrs)); match module.local_memory_allocation { LocalMemoryAllocation::Skip => { #[cfg(feature = "guest-debug")] @@ -175,9 +181,9 @@ impl ModuleInstance { store.init_globals(&mut addrs.globals, &module.globals, &addrs.funcs, &type_addrs)?; addrs.tables.extend(store.init_tables(&module.tables, &addrs.globals, &addrs.funcs, &type_addrs)?); let (elem_addrs, elem_trapped) = - store.init_elements(&addrs.tables, &addrs.funcs, &addrs.globals, &module.elements)?; + store.init_elements(&addrs.tables, &addrs.funcs, &addrs.globals, &module.elements, &type_addrs)?; let (data_addrs, data_trapped) = - store.init_data(&addrs.memories, &addrs.globals, &addrs.funcs, &module.data)?; + store.init_data(&addrs.memories, &addrs.globals, &addrs.funcs, &module.data, &type_addrs)?; let instance = ModuleInstanceInner { store_id: store.id(), @@ -187,6 +193,7 @@ impl ModuleInstance { table_addrs: addrs.tables.into_boxed_slice(), mem_addrs: addrs.memories.into_boxed_slice(), global_addrs: addrs.globals.into_boxed_slice(), + tag_addrs: addrs.tags.into_boxed_slice(), elem_addrs, data_addrs, func_start: module.start_func, @@ -197,8 +204,7 @@ impl ModuleInstance { store.add_instance(instance.clone()); if let Some(trap) = elem_trapped.or(data_trapped) { - cold_path(); - return Err(trap.into()); + return cold!(Err(trap.into())); } Ok(instance) } @@ -211,6 +217,7 @@ impl ModuleInstance { ExternalKind::Table => self.0.table_addrs.get(export.index as usize)?, ExternalKind::Memory => self.0.mem_addrs.get(export.index as usize)?, ExternalKind::Global => self.0.global_addrs.get(export.index as usize)?, + ExternalKind::Tag => self.0.tag_addrs.get(export.index as usize)?, }; Some(ExternVal::new(export.kind, *addr)) } @@ -246,13 +253,10 @@ impl ModuleInstance { pub fn exports(&self) -> impl Iterator + '_ { self.0.exports.iter().map(move |export| { let item = match export.kind { - ExternalKind::Func => { - let func_addr = self.resolve_func_addr(export.index); - ExternItem::Func(Function { - item: StoreItem::new(self.0.store_id, func_addr), - module_id: self.id(), - }) - } + ExternalKind::Func => ExternItem::Func(Function { + item: StoreItem::new(self.0.store_id, self.resolve_func_addr(export.index)), + module_id: self.id(), + }), ExternalKind::Table => { ExternItem::Table(Table(StoreItem::new(self.0.store_id, self.resolve_table_addr(export.index)))) } @@ -262,6 +266,9 @@ impl ModuleInstance { ExternalKind::Global => { ExternItem::Global(Global(StoreItem::new(self.0.store_id, self.resolve_global_addr(export.index)))) } + ExternalKind::Tag => { + ExternItem::Tag(Tag(StoreItem::new(self.0.store_id, self.resolve_tag_addr(export.index)))) + } }; (export.name.as_ref(), item) @@ -314,6 +321,7 @@ impl ModuleInstance { ExternVal::Memory(addr) => Ok(ExternItem::Memory(Memory(StoreItem::new(self.0.store_id, addr)))), ExternVal::Table(addr) => Ok(ExternItem::Table(Table(StoreItem::new(self.0.store_id, addr)))), ExternVal::Global(addr) => Ok(ExternItem::Global(Global(StoreItem::new(self.0.store_id, addr)))), + ExternVal::Tag(addr) => Ok(ExternItem::Tag(Tag(StoreItem::new(self.0.store_id, addr)))), } } @@ -346,8 +354,7 @@ impl ModuleInstance { self.validate_store(store)?; let ExternVal::Func(func_addr) = self.require_export(name)? else { - cold_path(); - return Err(Error::Other(format!("Export is not a function: {name}"))); + return cold!(Err(Error::Other(format!("Export is not a function: {name}")))); }; Ok(Function { item: StoreItem::new(self.0.store_id, func_addr), module_id: self.id() }) @@ -364,7 +371,6 @@ impl ModuleInstance { pub fn func_by_index(&self, store: &Store, func_index: FuncAddr) -> Result { self.validate_store(store)?; let func_addr = Self::index_addr(&self.0.func_addrs, func_index, "function")?; - Ok(Function { item: StoreItem::new(self.0.store_id, func_addr), module_id: self.id() }) } @@ -392,8 +398,7 @@ impl ModuleInstance { /// /// For untyped access, see [`Self::func_untyped`] and [`Self::extern_item`]. /// - /// For signatures that exceed tuple arity 12, see [`crate::WasmTupleChain`], which can be used - /// directly as `instance.func::, _>(...)`. + /// Tuples are supported up to arity 20. Use [`Self::func_untyped`] for larger signatures. pub fn func( &self, store: &Store, @@ -402,8 +407,7 @@ impl ModuleInstance { self.validate_store(store)?; let ExternVal::Func(func_addr) = self.require_export(name)? else { - cold_path(); - return Err(Error::Other(format!("Export is not a function: {name}"))); + return cold!(Err(Error::Other(format!("Export is not a function: {name}")))); }; let func = Function { item: StoreItem::new(self.0.store_id, func_addr), module_id: self.id() }; @@ -456,8 +460,7 @@ impl ModuleInstance { match self.require_export(name)? { ExternVal::Memory(mem_addr) => Ok(Memory(StoreItem::new(self.0.store_id, mem_addr))), _ => { - cold_path(); - Err(Error::Other(format!("Export is not a memory: {name}"))) + cold!(Err(Error::Other(format!("Export is not a memory: {name}")))) } } } @@ -494,6 +497,21 @@ impl ModuleInstance { Ok(Table(StoreItem::new(self.0.store_id, Self::index_addr(&self.0.table_addrs, table_index, "table")?))) } + /// Get a tag export by name. + pub fn tag(&self, name: &str) -> Result { + match self.require_export(name)? { + ExternVal::Tag(tag_addr) => Ok(Tag(StoreItem::new(self.0.store_id, tag_addr))), + _ => Err(Error::Other(format!("Export is not a tag: {name}"))), + } + } + + /// Get a tag by its module-local index. + #[cfg_attr(docsrs, doc(cfg(feature = "guest-debug")))] + #[cfg(feature = "guest-debug")] + pub fn tag_by_index(&self, tag_index: TagAddr) -> Result { + Ok(Tag(StoreItem::new(self.0.store_id, Self::index_addr(&self.0.tag_addrs, tag_index, "tag")?))) + } + /// Get the value of a global export by name. pub fn global_get(&self, store: &Store, name: &str) -> Result { self.global(name)?.get(store) diff --git a/crates/tinywasm/src/interpreter/executor.rs b/crates/tinywasm/src/interpreter/executor.rs index 5e78daeb..0bdb4748 100644 --- a/crates/tinywasm/src/interpreter/executor.rs +++ b/crates/tinywasm/src/interpreter/executor.rs @@ -5,18 +5,17 @@ use core::hint::cold_path; use super::no_std_floats::NoStdFloatExt; use alloc::boxed::Box; -use alloc::rc::Rc; use alloc::vec::Vec; use alloc::sync::Arc; -use interpreter::stack::{CallFrame, ValueStack}; +use interpreter::stack::CallFrame; use tinywasm_types::*; use super::ExecState; use super::num_helpers::*; use super::values::*; use crate::engine::FuelPolicy; -use crate::func::{FuncContext, HostFunction}; +use crate::func::HostFunction; use crate::interpreter::Value128; use crate::*; @@ -31,22 +30,12 @@ pub(crate) struct Executor<'store, const BUDGETED: bool> { } impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { - #[inline(always)] - fn pop_memory_addr( - stack: &mut ValueStack, - mem: &MemoryInstance, - offset: u64, - ) -> Result { - if mem.is_64bit() { - mem.effective_addr_64::(::stack_pop(stack) as u64, offset) - } else { - mem.effective_addr_32::(::stack_pop(stack) as u32, offset) - } - } - pub(crate) fn new(store: &'store mut Store, cf: CallFrame, call_stack_base: u32) -> Self { let wasm_func = store.state.get_wasm_func(cf.func_addr); - let module = store.get_module_instance_internal(wasm_func.owner); + let module = store + .get_module_instance(wasm_func.owner) + .unwrap_or_else(|| unreachable!("invalid module instance")) + .clone(); Self { module, cf, func: wasm_func.func.clone(), store, call_stack_base } } @@ -63,15 +52,9 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { } #[inline(always)] - fn exec(&mut self) -> Result, Trap> { - macro_rules! stack_op { - (unary $ty:ty, |$v:ident| $expr:expr) => { - stack_op!(unary $ty => $ty, |$v| $expr) - }; - (binary $ty:ty, |$lhs:ident, $rhs:ident| $expr:expr) => { - stack_op!(binary $ty => $ty, |$lhs, $rhs| $expr) - }; - (binary try $ty:ty, |$lhs:ident, $rhs:ident| $expr:expr) => {{ + fn exec(&mut self, instr_ptr: usize) -> Result> { + macro_rules! exec_op { + (binary_fallible $ty:ty, |$lhs:ident, $rhs:ident| $expr:expr) => {{ let $rhs = <$ty>::stack_pop(&mut self.store.value_stack); let $lhs = <$ty>::stack_pop(&mut self.store.value_stack); <$ty>::stack_push(&mut self.store.value_stack, $expr?)?; @@ -85,28 +68,25 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { let $lhs = <$from>::stack_pop(&mut self.store.value_stack); <$to>::stack_push(&mut self.store.value_stack, $expr)?; }}; - (binary_into2 $from:ty => $to:ty, |$lhs:ident, $rhs:ident| $expr:expr) => {{ + (binary_two_results $from:ty => $to:ty, |$lhs:ident, $rhs:ident| $expr:expr) => {{ let $rhs = <$from>::stack_pop(&mut self.store.value_stack); let $lhs = <$from>::stack_pop(&mut self.store.value_stack); let out = $expr; <$to>::stack_push(&mut self.store.value_stack, out.0)?; <$to>::stack_push(&mut self.store.value_stack, out.1)?; }}; - (binary $lhs_ty:ty, $rhs_ty:ty, |$lhs:ident, $rhs:ident| $expr:expr) => { - stack_op!(binary $lhs_ty, $rhs_ty => $rhs_ty, |$lhs, $rhs| $expr) - }; - (binary $lhs_ty:ty, $rhs_ty:ty => $res:ty, |$lhs:ident, $rhs:ident| $expr:expr) => {{ + (binary_mixed $lhs_ty:ty, $rhs_ty:ty => $res:ty, |$lhs:ident, $rhs:ident| $expr:expr) => {{ let $rhs = <$rhs_ty>::stack_pop(&mut self.store.value_stack); let $lhs = <$lhs_ty>::stack_pop(&mut self.store.value_stack); <$res>::stack_push(&mut self.store.value_stack, $expr)?; }}; - (ternary $ty:ty, |$a:ident, $b:ident, $c:ident| $expr:expr) => {{ - let $c = <$ty>::stack_pop(&mut self.store.value_stack); - let $b = <$ty>::stack_pop(&mut self.store.value_stack); - let $a = <$ty>::stack_pop(&mut self.store.value_stack); - <$ty>::stack_push(&mut self.store.value_stack, $expr)?; + (ternary $from:ty => $to:ty, |$a:ident, $b:ident, $c:ident| $expr:expr) => {{ + let $c = <$from>::stack_pop(&mut self.store.value_stack); + let $b = <$from>::stack_pop(&mut self.store.value_stack); + let $a = <$from>::stack_pop(&mut self.store.value_stack); + <$to>::stack_push(&mut self.store.value_stack, $expr)?; }}; - (quaternary_into2 $from:ty => $to:ty, |$a:ident, $b:ident, $c:ident, $d:ident| $expr:expr) => {{ + (quaternary_two_results $from:ty => $to:ty, |$a:ident, $b:ident, $c:ident, $d:ident| $expr:expr) => {{ let $d = <$from>::stack_pop(&mut self.store.value_stack); let $c = <$from>::stack_pop(&mut self.store.value_stack); let $b = <$from>::stack_pop(&mut self.store.value_stack); @@ -123,104 +103,105 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { let val = <$ty>::stack_peek(&self.store.value_stack); <$ty>::local_set(&mut self.store.value_stack, &self.cf, *$local_index, val); }}; - } - - macro_rules! exec_binop { - (32, $op:expr, $lhs:expr, $rhs:expr) => { - exec_binop!(@scalar i32, u32, f32, $op, $lhs, $rhs) - }; - (64, $op:expr, $lhs:expr, $rhs:expr) => { - exec_binop!(@scalar i64, u64, f64, $op, $lhs, $rhs) - }; - (@scalar $signed:ty, $unsigned:ty, $float:ty, $op:expr, $lhs:expr, $rhs:expr) => {{ - match $op { - BinOp::IAdd => $lhs.wrapping_add($rhs), - BinOp::ISub => $lhs.wrapping_sub($rhs), - BinOp::IMul => $lhs.wrapping_mul($rhs), - BinOp::IAnd => $lhs & $rhs, - BinOp::IOr => $lhs | $rhs, - BinOp::IXor => $lhs ^ $rhs, - BinOp::IShl => (($lhs as $signed).wrapping_shl($rhs as u32)) as $unsigned, - BinOp::IShrS => (($lhs as $signed).wrapping_shr($rhs as u32)) as $unsigned, - BinOp::IShrU => $lhs.wrapping_shr($rhs as u32), - BinOp::IRotl => (($lhs as $signed).rotate_left($rhs as u32)) as $unsigned, - BinOp::IRotr => (($lhs as $signed).rotate_right($rhs as u32)) as $unsigned, - BinOp::FAdd => (<$float>::from_bits($lhs) + <$float>::from_bits($rhs)).to_bits(), - BinOp::FSub => (<$float>::from_bits($lhs) - <$float>::from_bits($rhs)).to_bits(), - BinOp::FMul => (<$float>::from_bits($lhs) * <$float>::from_bits($rhs)).to_bits(), - BinOp::FDiv => (<$float>::from_bits($lhs) / <$float>::from_bits($rhs)).to_bits(), - BinOp::FMin => <$float>::from_bits($lhs).tw_minimum(<$float>::from_bits($rhs)).to_bits(), - BinOp::FMax => <$float>::from_bits($lhs).tw_maximum(<$float>::from_bits($rhs)).to_bits(), - BinOp::FCopysign => <$float>::from_bits($lhs).copysign(<$float>::from_bits($rhs)).to_bits(), - } + (global_get $ty:ty, $global_index:expr) => {{ + let addr = self.module.resolve_global_addr(*$global_index); + let value = <$ty>::global_get(&self.store.state.globals, addr); + <$ty>::stack_push(&mut self.store.value_stack, value)?; }}; - (128, $op:expr, $lhs:expr, $rhs:expr) => {{ - match $op { - BinOp128::And => $lhs.v128_and($rhs), - BinOp128::AndNot => $lhs.v128_andnot($rhs), - BinOp128::Or => $lhs.v128_or($rhs), - BinOp128::Xor => $lhs.v128_xor($rhs), - BinOp128::I64x2Add => $lhs.i64x2_add($rhs), - BinOp128::I64x2Mul => $lhs.i64x2_mul($rhs), - } + (global_set $ty:ty, $global_index:expr) => {{ + let addr = self.module.resolve_global_addr(*$global_index); + let value = <$ty>::stack_pop(&mut self.store.value_stack); + <$ty>::global_set(&mut self.store.state.globals, addr, value); }}; - } - - macro_rules! binop { - (local_local $vt:ty, $width:tt, $op:ident, $a:ident, $b:ident) => {{ + (global_tee $ty:ty, $global_index:expr) => {{ + let addr = self.module.resolve_global_addr(*$global_index); + let value = <$ty>::stack_peek(&self.store.value_stack); + <$ty>::global_set(&mut self.store.state.globals, addr, value); + }}; + (binop_local_local $vt:ty, $exec:ident, $op:ident, $a:ident, $b:ident) => {{ let lhs = <$vt>::local_get(&self.store.value_stack, &self.cf, *$a); let rhs = <$vt>::local_get(&self.store.value_stack, &self.cf, *$b); - self.store.value_stack.push(exec_binop!($width, *$op, lhs, rhs))?; + <$vt>::stack_push(&mut self.store.value_stack, $exec(*$op, lhs, rhs))?; }}; - (local_local_set $vt:ty, $width:tt, $op:ident, $a:ident, $b:ident, $dst:ident) => {{ + (binop_local_local_set $vt:ty, $exec:ident, $op:ident, $a:ident, $b:ident, $dst:ident) => {{ let lhs = <$vt>::local_get(&self.store.value_stack, &self.cf, *$a); let rhs = <$vt>::local_get(&self.store.value_stack, &self.cf, *$b); - let value = exec_binop!($width, *$op, lhs, rhs); + let value = $exec(*$op, lhs, rhs); <$vt>::local_set(&mut self.store.value_stack, &self.cf, *$dst, value); }}; - (local_local_tee $vt:ty, $width:tt, $op:ident, $a:ident, $b:ident, $dst:ident) => {{ + (binop_local_local_tee $vt:ty, $exec:ident, $op:ident, $a:ident, $b:ident, $dst:ident) => {{ let lhs = <$vt>::local_get(&self.store.value_stack, &self.cf, *$a); let rhs = <$vt>::local_get(&self.store.value_stack, &self.cf, *$b); - let value = exec_binop!($width, *$op, lhs, rhs); + let value = $exec(*$op, lhs, rhs); <$vt>::local_set(&mut self.store.value_stack, &self.cf, *$dst, value); - self.store.value_stack.push(value)?; + <$vt>::stack_push(&mut self.store.value_stack, value)?; }}; - (local_const $vt:ty, $width:tt, $op:ident, $local:ident, $rhs:expr) => {{ + (cmp_local_local $vt:ty, $cmp:ident, $op:ident, $a:ident, $b:ident) => {{ + let lhs = <$vt>::local_get(&self.store.value_stack, &self.cf, *$a); + let rhs = <$vt>::local_get(&self.store.value_stack, &self.cf, *$b); + self.store.value_stack.push(i32::from($cmp(lhs, rhs, *$op)))?; + }}; + (binop_local_const $vt:ty, $exec:ident, $op:ident, $local:ident, $rhs:expr) => {{ let lhs = <$vt>::local_get(&self.store.value_stack, &self.cf, *$local); - self.store.value_stack.push(exec_binop!($width, *$op, lhs, $rhs))?; + <$vt>::stack_push(&mut self.store.value_stack, $exec(*$op, lhs, $rhs))?; }}; - (local_const_set $vt:ty, $width:tt, $op:ident, $local:ident, $rhs:expr, $dst:ident) => {{ + (binop_local_const_set $vt:ty, $exec:ident, $op:ident, $local:ident, $rhs:expr, $dst:ident) => {{ let lhs = <$vt>::local_get(&self.store.value_stack, &self.cf, *$local); - let value = exec_binop!($width, *$op, lhs, $rhs); + let value = $exec(*$op, lhs, $rhs); <$vt>::local_set(&mut self.store.value_stack, &self.cf, *$dst, value); }}; - (local_const_tee $vt:ty, $width:tt, $op:ident, $local:ident, $rhs:expr, $dst:ident) => {{ + (binop_local_const_tee $vt:ty, $exec:ident, $op:ident, $local:ident, $rhs:expr, $dst:ident) => {{ let lhs = <$vt>::local_get(&self.store.value_stack, &self.cf, *$local); - let value = exec_binop!($width, *$op, lhs, $rhs); + let value = $exec(*$op, lhs, $rhs); <$vt>::local_set(&mut self.store.value_stack, &self.cf, *$dst, value); - self.store.value_stack.push(value)?; + <$vt>::stack_push(&mut self.store.value_stack, value)?; }}; - (stack_global $vt:ident, $width:tt, $op:ident, $global:ident) => {{ - let TinyWasmValue::$vt(global_val) = - self.store.state.get_global_val(self.module.resolve_global_addr(*$global)) - else { - unreachable!("expected global to be Value32") - }; + (binop_global_const $vt:ty, $exec:ident, $op:ident, $global:ident, $rhs:expr) => {{ + let lhs = <$vt>::global_get(&self.store.state.globals, self.module.resolve_global_addr(*$global)); + self.store.value_stack.push($exec(*$op, lhs, $rhs))?; + }}; + (binop_stack_global $vt:ty, $exec:ident, $op:ident, $global:ident) => {{ + let global_val = + <$vt>::global_get(&self.store.state.globals, self.module.resolve_global_addr(*$global)); let stack_val = <$vt>::stack_pop(&mut self.store.value_stack); - self.store.value_stack.push(exec_binop!($width, *$op, stack_val, global_val))?; + self.store.value_stack.push($exec(*$op, stack_val, global_val))?; + }}; + (binop_stack_local $vt:ty, $exec:ident, $op:ident, $local:ident) => {{ + let rhs = <$vt>::local_get(&self.store.value_stack, &self.cf, *$local); + let lhs = <$vt>::stack_pop(&mut self.store.value_stack); + <$vt>::stack_push(&mut self.store.value_stack, $exec(*$op, lhs, rhs))?; + }}; + (binop_stack_local_set $vt:ty, $exec:ident, $op:ident, $local:ident, $dst:ident) => {{ + let rhs = <$vt>::local_get(&self.store.value_stack, &self.cf, *$local); + let lhs = <$vt>::stack_pop(&mut self.store.value_stack); + let value = $exec(*$op, lhs, rhs); + <$vt>::local_set(&mut self.store.value_stack, &self.cf, *$dst, value); + }}; + (binop_stack_local_tee $vt:ty, $exec:ident, $op:ident, $local:ident, $dst:ident) => {{ + let rhs = <$vt>::local_get(&self.store.value_stack, &self.cf, *$local); + let lhs = <$vt>::stack_pop(&mut self.store.value_stack); + let value = $exec(*$op, lhs, rhs); + <$vt>::local_set(&mut self.store.value_stack, &self.cf, *$dst, value); + <$vt>::stack_push(&mut self.store.value_stack, value)?; + }}; + (binop_acc_local $ty:ty, $acc:ident, $mul:expr, $add:expr) => {{ + let rhs = <$ty>::stack_pop(&mut self.store.value_stack); + let lhs = <$ty>::stack_pop(&mut self.store.value_stack); + let value = ($mul)(lhs, rhs); + <$ty>::local_update(&mut self.store.value_stack, &self.cf, *$acc, |acc| ($add)(value, acc)); }}; } use tinywasm_types::Instruction::*; #[rustfmt::skip] - match &self.func.instructions[self.cf.instr_ptr] { - Unreachable => { cold_path(); return Err(Trap::Unreachable) }, + match &self.func.instructions[instr_ptr] { + Unreachable => { cold_path(); return Err(Trap::Unreachable.into()) }, Drop32 => { _ = Value32::stack_pop(&mut self.store.value_stack)}, Drop64 => { _ = Value64::stack_pop(&mut self.store.value_stack)}, Drop128 => { _ = Value128::stack_pop(&mut self.store.value_stack)}, - Select32 => Value32::stack_select(&mut self.store.value_stack)?, - Select64 => Value64::stack_select(&mut self.store.value_stack)?, - Select128 => Value128::stack_select(&mut self.store.value_stack)?, + Select32 => Value32::stack_select(&mut self.store.value_stack), + Select64 => Value64::stack_select(&mut self.store.value_stack), + Select128 => Value128::stack_select(&mut self.store.value_stack), SelectMulti(counts) => self.store.value_stack.select_multi(*counts), Call(v) => { self.exec_call_direct(*v)?; return Ok(None); } CallSelf => { self.exec_call_self()?; return Ok(None); } @@ -230,23 +211,86 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { ReturnCallSelf => { self.exec_return_call_self()?; return Ok(None); } ReturnCallIndirect(ty, table) => { if self.exec_call_indirect::(*ty, *table)? { return Ok(Some(())); } return Ok(None); } ReturnCallRef(ty) => { if self.exec_call_ref::(*ty)? { return Ok(Some(())); } return Ok(None); } + Throw(tag) => { self.exec_throw(*tag)?; return Ok(None); } + ThrowRef => { self.exec_throw_ref()?; return Ok(None); } Jump(ip) => { self.cf.instr_ptr = *ip as usize; return Ok(None); } - JumpIfZero32(ip) => if self.exec_jump_zero_32(*ip) { return Ok(None) }, - JumpIfNonZero32(ip) => if self.exec_jump_non_zero_32(*ip) { return Ok(None) }, - JumpIfZero64(ip) => if self.exec_jump_zero_64(*ip) { return Ok(None) }, - JumpIfNonZero64(ip) => if self.exec_jump_non_zero_64(*ip) { return Ok(None) }, - JumpIfRefNull(ip) => if self.exec_jump_ref_null(*ip) { return Ok(None) }, - JumpIfRefNonNull(ip) => if self.exec_jump_ref_non_null(*ip) { return Ok(None) }, - JumpIfLocalZero32 { target_ip, local } => if self.exec_jump_local_zero_32(*target_ip, *local) { return Ok(None) }, - JumpIfLocalNonZero32 { target_ip, local } => if self.exec_jump_local_non_zero_32(*target_ip, *local) { return Ok(None) }, - JumpIfLocalZero64 { target_ip, local } => if self.exec_jump_local_zero_64(*target_ip, *local) { return Ok(None) }, - JumpIfLocalNonZero64 { target_ip, local } => if self.exec_jump_local_non_zero_64(*target_ip, *local) { return Ok(None) }, - JumpCmpStackConst32 { target_ip, imm, op } => if self.exec_jump_cmp_stack_const_32(*target_ip, *imm, *op) { return Ok(None) }, - JumpCmpStackConst64 { target_ip, imm, op } => if self.exec_jump_cmp_stack_const_64(*target_ip, *imm, *op) { return Ok(None) }, - JumpCmpLocalConst32 { target_ip, local, imm, op } => if self.exec_jump_cmp_local_const_32(*target_ip, *local, *imm, *op) { return Ok(None) }, - JumpCmpLocalConst64 { target_ip, local, imm, op } => if self.exec_jump_cmp_local_const_64(*target_ip, *local, *imm, *op) { return Ok(None) }, - JumpCmpLocalLocal32 { target_ip, left, right, op } => if self.exec_jump_cmp_local_local_32(*target_ip, *left, *right, *op) { return Ok(None) }, - JumpCmpLocalLocal64 { target_ip, left, right, op } => if self.exec_jump_cmp_local_local_64(*target_ip, *left, *right, *op) { return Ok(None) }, + JumpIfZero32(ip) => if Self::exec_jump_if(&mut self.cf, *ip, |_| i32::stack_pop(&mut self.store.value_stack) == 0) { return Ok(None) }, + JumpIfNonZero32(ip) => if Self::exec_jump_if(&mut self.cf, *ip, |_| i32::stack_pop(&mut self.store.value_stack) != 0) { return Ok(None) }, + JumpIfZero64(ip) => if Self::exec_jump_if(&mut self.cf, *ip, |_| i64::stack_pop(&mut self.store.value_stack) == 0) { return Ok(None) }, + JumpIfNonZero64(ip) => if Self::exec_jump_if(&mut self.cf, *ip, |_| i64::stack_pop(&mut self.store.value_stack) != 0) { return Ok(None) }, + JumpIfRefNull(ip) => if Self::exec_jump_if(&mut self.cf, *ip, |_| { + let is_null = ValueRef::stack_peek(&self.store.value_stack).is_null(); + if is_null { ValueRef::stack_pop(&mut self.store.value_stack); } + is_null + }) { return Ok(None) }, + JumpIfRefNonNull(ip) => if Self::exec_jump_if(&mut self.cf, *ip, |_| { + let is_non_null = !ValueRef::stack_peek(&self.store.value_stack).is_null(); + if !is_non_null { ValueRef::stack_pop(&mut self.store.value_stack); } + is_non_null + }) { return Ok(None) }, + BrOnCast(ip, ty, on_fail) => if self.exec_ref_matches(*ty) == *on_fail { self.cf.instr_ptr = *ip as usize; return Ok(None); }, + JumpIfLocalZero32 { target_ip, local } => if Self::exec_jump_if(&mut self.cf, *target_ip, |cf| Value32::local_get(&self.store.value_stack, cf, *local) == 0) { return Ok(None) }, + JumpIfLocalNonZero32 { target_ip, local } => if Self::exec_jump_if(&mut self.cf, *target_ip, |cf| Value32::local_get(&self.store.value_stack, cf, *local) != 0) { return Ok(None) }, + JumpIfLocalZero64 { target_ip, local } => if Self::exec_jump_if(&mut self.cf, *target_ip, |cf| Value64::local_get(&self.store.value_stack, cf, *local) == 0) { return Ok(None) }, + JumpIfLocalNonZero64 { target_ip, local } => if Self::exec_jump_if(&mut self.cf, *target_ip, |cf| Value64::local_get(&self.store.value_stack, cf, *local) != 0) { return Ok(None) }, + JumpCmpStackConst32 { target_ip, imm, op } => if Self::exec_jump_if(&mut self.cf, *target_ip, |_| cmp_i32(i32::stack_pop(&mut self.store.value_stack), *imm, *op)) { return Ok(None) }, + JumpCmpStackConst64 { target_ip, imm, op } => if Self::exec_jump_if(&mut self.cf, *target_ip, |_| cmp_i64(i64::stack_pop(&mut self.store.value_stack), *imm, *op)) { return Ok(None) }, + JumpCmpStackLocal32 { target_ip, local, op } => if Self::exec_jump_if(&mut self.cf, *target_ip, |cf| { + let lhs = i32::stack_pop(&mut self.store.value_stack); + cmp_i32(lhs, i32::local_get(&self.store.value_stack, cf, *local), *op) + }) { return Ok(None) }, + JumpCmpStackLocal64 { target_ip, local, op } => if Self::exec_jump_if(&mut self.cf, *target_ip, |cf| { + let lhs = i64::stack_pop(&mut self.store.value_stack); + cmp_i64(lhs, i64::local_get(&self.store.value_stack, cf, *local), *op) + }) { return Ok(None) }, + BinOpLocalConstJump32 { target_ip, local, imm, op, on_zero } => if Self::exec_jump_if(&mut self.cf, *target_ip, |cf| { + let value = exec_binop_32(*op, i32::local_get(&self.store.value_stack, cf, *local) as u32, *imm as u32) as i32; + i32::local_set(&mut self.store.value_stack, cf, *local, value); + (value == 0) == *on_zero + }) { return Ok(None) }, + BinOpLocalConstJumpCmpLocal32 { target_ip, local, imm, binop, right, cmp } => if Self::exec_jump_if(&mut self.cf, *target_ip, |cf| { + let lhs = exec_binop_32(*binop, i32::local_get(&self.store.value_stack, cf, *local) as u32, *imm as u32) as i32; + i32::local_set(&mut self.store.value_stack, cf, *local, lhs); + cmp_i32(lhs, i32::local_get(&self.store.value_stack, cf, *right), *cmp) + }) { return Ok(None) }, + BinOpStackConstTeeLocalJump32 { target_ip, local, imm, op, on_zero } => { + let value = exec_binop_32(*op, i32::stack_pop(&mut self.store.value_stack) as u32, *imm as u32) as i32; + i32::local_set(&mut self.store.value_stack, &self.cf, *local, value); + i32::stack_push(&mut self.store.value_stack, value)?; + if Self::exec_jump_if(&mut self.cf, *target_ip, |_| (value == 0) == *on_zero) { return Ok(None) } + }, + BinOpGlobalConstJump32 { target_ip, global, imm, op, on_zero } => if Self::exec_jump_if(&mut self.cf, *target_ip, |_| { + let global = self.module.resolve_global_addr(*global); + let value = exec_binop_32(*op, i32::global_get(&self.store.state.globals, global) as u32, *imm as u32) as i32; + i32::global_set(&mut self.store.state.globals, global, value); + (value == 0) == *on_zero + }) { return Ok(None) }, + IncLocalJump32 { target_ip, local, delta, on_zero } => if Self::exec_jump_if(&mut self.cf, *target_ip, |cf| { + let value = i32::local_get(&self.store.value_stack, cf, *local).wrapping_add(*delta); + i32::local_set(&mut self.store.value_stack, cf, *local, value); + (value == 0) == *on_zero + }) { return Ok(None) }, + IncStackTeeLocalJump32 { target_ip, local, delta, on_zero } => { + let value = i32::stack_pop(&mut self.store.value_stack).wrapping_add(*delta); + i32::local_set(&mut self.store.value_stack, &self.cf, *local, value); + i32::stack_push(&mut self.store.value_stack, value)?; + if Self::exec_jump_if(&mut self.cf, *target_ip, |_| (value == 0) == *on_zero) { return Ok(None) } + }, + IncGlobalJump32 { target_ip, global, delta, on_zero } => if Self::exec_jump_if(&mut self.cf, *target_ip, |_| { + let global = self.module.resolve_global_addr(*global); + let value = i32::global_get(&self.store.state.globals, global).wrapping_add(*delta); + i32::global_set(&mut self.store.state.globals, global, value); + (value == 0) == *on_zero + }) { return Ok(None) }, + IncLocalJumpCmpLocal32 { target_ip, local, delta, right, op } => if Self::exec_jump_if(&mut self.cf, *target_ip, |cf| { + let lhs = i32::local_get(&self.store.value_stack, cf, *local).wrapping_add(*delta); + i32::local_set(&mut self.store.value_stack, cf, *local, lhs); + cmp_i32(lhs, i32::local_get(&self.store.value_stack, cf, *right), *op) + }) { return Ok(None) }, + JumpCmpLocalConst32 { target_ip, local, imm, op } => if Self::exec_jump_if(&mut self.cf, *target_ip, |cf| cmp_i32(i32::local_get(&self.store.value_stack, cf, *local), *imm, *op)) { return Ok(None) }, + JumpCmpLocalConst64 { target_ip, local, imm, op } => if Self::exec_jump_if(&mut self.cf, *target_ip, |cf| cmp_i64(i64::local_get(&self.store.value_stack, cf, *local), i64::from(*imm), *op)) { return Ok(None) }, + JumpCmpLocalLocal32 { target_ip, left, right, op } => if Self::exec_jump_if(&mut self.cf, *target_ip, |cf| cmp_i32(i32::local_get(&self.store.value_stack, cf, *left), i32::local_get(&self.store.value_stack, cf, *right), *op)) { return Ok(None) }, + JumpCmpLocalLocal64 { target_ip, left, right, op } => if Self::exec_jump_if(&mut self.cf, *target_ip, |cf| cmp_i64(i64::local_get(&self.store.value_stack, cf, *left), i64::local_get(&self.store.value_stack, cf, *right), *op)) { return Ok(None) }, DropKeep(drop_keep) => self.exec_drop_keep(*drop_keep), BranchTable(default_ip, start, len) => { self.exec_branch_table(*default_ip, *start, *len); return Ok(None); } Return => { if self.exec_return() { return Ok(Some(())); } return Ok(None); } @@ -257,42 +301,50 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { LocalGet32(local_index) => self.store.value_stack.push(Value32::local_get(&self.store.value_stack, &self.cf, *local_index))?, LocalGet64(local_index) => self.store.value_stack.push(Value64::local_get(&self.store.value_stack, &self.cf, *local_index))?, LocalGet128(local_index) => self.store.value_stack.push(Value128::local_get(&self.store.value_stack, &self.cf, *local_index))?, - LocalSet32(local_index) => stack_op!(local_set_pop Value32, local_index), - LocalSet64(local_index) => stack_op!(local_set_pop Value64, local_index), - LocalSet128(local_index) => stack_op!(local_set_pop Value128, local_index), + LocalSet32(local_index) => exec_op!(local_set_pop Value32, local_index), + LocalSet64(local_index) => exec_op!(local_set_pop Value64, local_index), + LocalSet128(local_index) => exec_op!(local_set_pop Value128, local_index), LocalCopy32(from, to) => Value32::local_copy(&mut self.store.value_stack, &self.cf, *from, *to), LocalCopy64(from, to) => Value64::local_copy(&mut self.store.value_stack, &self.cf, *from, *to), LocalCopy128(from, to) => Value128::local_copy(&mut self.store.value_stack, &self.cf, *from, *to), - AddConst32(c) => stack_op!(unary i32, |v| v.wrapping_add(*c)), - AddConst64(c) => stack_op!(unary i64, |v| v.wrapping_add(*c)), + AddConst32(c) => exec_op!(unary i32 => i32, |v| v.wrapping_add(*c)), + AddConst64(c) => exec_op!(unary i64 => i64, |v| v.wrapping_add(*c)), IncLocal32(local_index, delta) => i32::local_update(&mut self.store.value_stack, &self.cf, *local_index, |v| v.wrapping_add(*delta)), IncLocal64(local_index, delta) => i64::local_update(&mut self.store.value_stack, &self.cf, *local_index, |v| v.wrapping_add(*delta )), - I32Add3 => stack_op!(ternary i32, |a, b, c| a.wrapping_add(b).wrapping_add(c)), - I64Add3 => stack_op!(ternary i64, |a, b, c| a.wrapping_add(b).wrapping_add(c)), - MulAccLocal32(acc) => self.exec_binop_acc_local::(*acc, |a, b| a.wrapping_mul(b), |a, b| a.wrapping_add(b)), - MulAccLocal64(acc) => self.exec_binop_acc_local::(*acc, |a, b| a.wrapping_mul(b), |a, b| a.wrapping_add(b)), - FMulAccLocal32(acc) => self.exec_binop_acc_local::(*acc, |a, b| a * b, |a, b| a + b), - FMulAccLocal64(acc) => self.exec_binop_acc_local::(*acc, |a, b| a * b, |a, b| a + b), - BinOpLocalLocal32(op, a, b) => binop!(local_local Value32, 32, op, a, b), - BinOpLocalLocal64(op, a, b) => binop!(local_local Value64, 64, op, a, b), - BinOpLocalLocal128(op, a, b) => binop!(local_local Value128, 128, op, a, b), - BinOpLocalLocalSet32(op, a, b, dst) => binop!(local_local_set Value32, 32, op, a, b, dst), - BinOpLocalLocalSet64(op, a, b, dst) => binop!(local_local_set Value64, 64, op, a, b, dst), - BinOpLocalLocalSet128(op, a, b, dst) => binop!(local_local_set Value128, 128, op, a, b, dst), - BinOpLocalLocalTee32(op, a, b, dst) => binop!(local_local_tee Value32, 32, op, a, b, dst), - BinOpLocalLocalTee64(op, a, b, dst) => binop!(local_local_tee Value64, 64, op, a, b, dst), - BinOpLocalLocalTee128(op, a, b, dst) => binop!(local_local_tee Value128, 128, op, a, b, dst), - BinOpLocalConst32(op, local_index, c) => binop!(local_const Value32, 32, op, local_index, *c as u32), - BinOpLocalConst64(op, local_index, c) => binop!(local_const Value64, 64, op, local_index, *c as u64), - BinOpLocalConst128(op, local_index, c) => binop!(local_const Value128, 128, op, local_index, Value128(self.func.data.v128_const(*c))), - BinOpLocalConstSet32(op, local_index, c, dst) => binop!(local_const_set Value32, 32, op, local_index, *c as u32, dst), - BinOpLocalConstSet64(op, local_index, c, dst) => binop!(local_const_set Value64, 64, op, local_index, *c as u64, dst), - BinOpLocalConstSet128(op, local_index, c, dst) => binop!(local_const_set Value128, 128, op, local_index, Value128(self.func.data.v128_const(*c)), dst), - BinOpLocalConstTee32(op, local_index, c, dst) => binop!(local_const_tee Value32, 32, op, local_index, *c as u32, dst), - BinOpLocalConstTee64(op, local_index, c, dst) => binop!(local_const_tee Value64, 64, op, local_index, *c as u64, dst), - BinOpLocalConstTee128(op, local_index, c, dst) => binop!(local_const_tee Value128, 128, op, local_index, Value128(self.func.data.v128_const(*c)), dst), - BinOpStackGlobal32(op, global_index) => binop!(stack_global Value32, 32, op, global_index), - BinOpStackGlobal64(op, global_index) => binop!(stack_global Value64, 64, op, global_index), + I32Add3 => exec_op!(ternary i32 => i32, |a, b, c| a.wrapping_add(b).wrapping_add(c)), + I64Add3 => exec_op!(ternary i64 => i64, |a, b, c| a.wrapping_add(b).wrapping_add(c)), + MulAccLocal32(acc) => exec_op!(binop_acc_local i32, acc, |a: i32, b| a.wrapping_mul(b), |a: i32, b| a.wrapping_add(b)), + MulAccLocal64(acc) => exec_op!(binop_acc_local i64, acc, |a: i64, b| a.wrapping_mul(b), |a: i64, b| a.wrapping_add(b)), + FMulAccLocal32(acc) => exec_op!(binop_acc_local f32, acc, |a: f32, b| a * b, |a: f32, b| a + b), + FMulAccLocal64(acc) => exec_op!(binop_acc_local f64, acc, |a: f64, b| a * b, |a: f64, b| a + b), + BinOpLocalLocal32(op, a, b) => exec_op!(binop_local_local Value32, exec_binop_32, op, a, b), + BinOpLocalLocal64(op, a, b) => exec_op!(binop_local_local Value64, exec_binop_64, op, a, b), + BinOpLocalLocal128(op, a, b) => exec_op!(binop_local_local Value128, exec_binop_128, op, a, b), + CmpLocalLocal32(op, a, b) => exec_op!(cmp_local_local i32, cmp_i32, op, a, b), + CmpLocalLocal64(op, a, b) => exec_op!(cmp_local_local i64, cmp_i64, op, a, b), + BinOpLocalLocalSet32(op, a, b, dst) => exec_op!(binop_local_local_set Value32, exec_binop_32, op, a, b, dst), + BinOpLocalLocalSet64(op, a, b, dst) => exec_op!(binop_local_local_set Value64, exec_binop_64, op, a, b, dst), + BinOpLocalLocalSet128(op, a, b, dst) => exec_op!(binop_local_local_set Value128, exec_binop_128, op, a, b, dst), + BinOpLocalLocalTee32(op, a, b, dst) => exec_op!(binop_local_local_tee Value32, exec_binop_32, op, a, b, dst), + BinOpLocalLocalTee64(op, a, b, dst) => exec_op!(binop_local_local_tee Value64, exec_binop_64, op, a, b, dst), + BinOpLocalLocalTee128(op, a, b, dst) => exec_op!(binop_local_local_tee Value128, exec_binop_128, op, a, b, dst), + BinOpLocalConst32(op, local_index, c) => exec_op!(binop_local_const Value32, exec_binop_32, op, local_index, *c as u32), + BinOpLocalConst64(op, local_index, c) => exec_op!(binop_local_const Value64, exec_binop_64, op, local_index, *c as u64), + BinOpLocalConst128(op, local_index, c) => exec_op!(binop_local_const Value128, exec_binop_128, op, local_index, Value128(self.func.data.v128_const(*c))), + BinOpGlobalConst32(op, global_index, c) => exec_op!(binop_global_const Value32, exec_binop_32, op, global_index, *c as u32), + BinOpGlobalConst64(op, global_index, c) => exec_op!(binop_global_const Value64, exec_binop_64, op, global_index, *c as u64), + BinOpGlobalConst128(op, global_index, c) => exec_op!(binop_global_const Value128, exec_binop_128, op, global_index, Value128(self.func.data.v128_const(*c))), + BinOpLocalConstSet32(op, local_index, c, dst) => exec_op!(binop_local_const_set Value32, exec_binop_32, op, local_index, *c as u32, dst), + BinOpLocalConstSet64(op, local_index, c, dst) => exec_op!(binop_local_const_set Value64, exec_binop_64, op, local_index, *c as u64, dst), + BinOpLocalConstSet128(op, local_index, c, dst) => exec_op!(binop_local_const_set Value128, exec_binop_128, op, local_index, Value128(self.func.data.v128_const(*c)), dst), + BinOpLocalConstTee32(op, local_index, c, dst) => exec_op!(binop_local_const_tee Value32, exec_binop_32, op, local_index, *c as u32, dst), + BinOpLocalConstTee64(op, local_index, c, dst) => exec_op!(binop_local_const_tee Value64, exec_binop_64, op, local_index, *c as u64, dst), + BinOpLocalConstTee128(op, local_index, c, dst) => exec_op!(binop_local_const_tee Value128, exec_binop_128, op, local_index, Value128(self.func.data.v128_const(*c)), dst), + BinOpStackGlobal32(op, global_index) => exec_op!(binop_stack_global Value32, exec_binop_32, op, global_index), + BinOpStackGlobal64(op, global_index) => exec_op!(binop_stack_global Value64, exec_binop_64, op, global_index), + BinOpStackLocal32(op, local) => exec_op!(binop_stack_local Value32, exec_binop_32, op, local), + BinOpStackLocalSet32(op, local, dst) => exec_op!(binop_stack_local_set Value32, exec_binop_32, op, local, dst), + BinOpStackLocalTee32(op, local, dst) => exec_op!(binop_stack_local_tee Value32, exec_binop_32, op, local, dst), SetLocalConst32(local_index, c) => i32::local_set(&mut self.store.value_stack, &self.cf, *local_index, *c), SetLocalConst64(local_index, c) => i64::local_set(&mut self.store.value_stack, &self.cf, *local_index, *c), SetLocalConst128(local_index, c) => Value128::local_set(&mut self.store.value_stack, &self.cf, *local_index, Value128(self.func.data.v128_const(*c))), @@ -301,153 +353,173 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { StoreLocalLocal32(m, addr_local, value_local) => self.exec_store_local_local::(*m, *addr_local, *value_local)?, StoreLocalLocal64(m, addr_local, value_local) => self.exec_store_local_local::(*m, *addr_local, *value_local)?, StoreLocalLocal128(m, addr_local, value_local) => self.exec_store_local_local::(*m, *addr_local, *value_local)?, - LoadLocal32(m, addr_local) => self.store.value_stack.push(self.exec_load_local_value::(*m, *addr_local)?)?, - LoadLocal64(m, addr_local) => self.store.value_stack.push(self.exec_load_local_value::(*m, *addr_local)?)?, - LoadLocal8S32(m, addr_local) => { - let value = self.exec_load_local_value::(*m, *addr_local)?; - self.store.value_stack.push(i32::from(value))?; - } - LoadLocal8U32(m, addr_local) => { - let value = self.exec_load_local_value::(*m, *addr_local)?; - self.store.value_stack.push(i32::from(value))?; - } - LoadLocal16S32(m, addr_local) => { - let value = self.exec_load_local_value::(*m, *addr_local)?; - self.store.value_stack.push(i32::from(value))?; - } - LoadLocal16U32(m, addr_local) => { - let value = self.exec_load_local_value::(*m, *addr_local)?; - self.store.value_stack.push(i32::from(value))?; - } - LoadLocalTee32(m, addr_local, dst_local) => self.exec_load_local_tee::(*m, *addr_local, *dst_local, |v| v)?, - LoadLocalSet32(m, addr_local, dst_local) => self.exec_load_local_set::(*m, *addr_local, *dst_local, |v| v)?, - LoadLocalTee8S32(m, addr_local, dst_local) => self.exec_load_local_tee::(*m, *addr_local, *dst_local, i32::from)?, - LoadLocalTee8U32(m, addr_local, dst_local) => self.exec_load_local_tee::(*m, *addr_local, *dst_local, i32::from)?, - LoadLocalTee16S32(m, addr_local, dst_local) => self.exec_load_local_tee::(*m, *addr_local, *dst_local, i32::from)?, - LoadLocalTee16U32(m, addr_local, dst_local) => self.exec_load_local_tee::(*m, *addr_local, *dst_local, i32::from)?, - LoadLocalSet8S32(m, addr_local, dst_local) => self.exec_load_local_set::(*m, *addr_local, *dst_local, i32::from)?, - LoadLocalSet8U32(m, addr_local, dst_local) => self.exec_load_local_set::(*m, *addr_local, *dst_local, i32::from)?, - LoadLocalSet16S32(m, addr_local, dst_local) => self.exec_load_local_set::(*m, *addr_local, *dst_local, i32::from)?, - LoadLocalSet16U32(m, addr_local, dst_local) => self.exec_load_local_set::(*m, *addr_local, *dst_local, i32::from)?, - LoadLocalTee128(m, addr_local, dst_local) => self.exec_load_local_tee::(*m, *addr_local, *dst_local, |v| v)?, - LoadLocalSet128(m, addr_local, dst_local) => self.exec_load_local_set::(*m, *addr_local, *dst_local, |v| v)?, - AndConstTee32(c, local_index) => { stack_op!(unary i32, |v| v & *c); stack_op!(local_tee i32, local_index); } - SubConstTee32(c, local_index) => { stack_op!(unary i32, |v| v.wrapping_sub(*c)); stack_op!(local_tee i32, local_index); } - AndConstTee64(c, local_index) => { stack_op!(unary i64, |v| v & *c); stack_op!(local_tee i64, local_index); } - SubConstTee64(c, local_index) => { stack_op!(unary i64, |v| v.wrapping_sub(*c)); stack_op!(local_tee i64, local_index); } - LocalTee32(local_index) => stack_op!(local_tee Value32, local_index), - LocalTee64(local_index) => stack_op!(local_tee Value64, local_index), - LocalTee128(local_index) => stack_op!(local_tee Value128, local_index), - GlobalGet(global_index) => self.exec_global_get(*global_index)?, - GlobalSet32(global_index) => self.exec_global_set_32(*global_index), - GlobalSet64(global_index) => self.exec_global_set::(*global_index), - GlobalSet128(global_index) => self.exec_global_set::(*global_index), - Const32(val) => self.exec_const(*val)?, - Const64(val) => self.exec_const(*val)?, - I64Eqz => stack_op!(unary i64 => i32, |v| i32::from(v == 0)), - I32Eqz => stack_op!(unary i32, |v| i32::from(v == 0)), - I32Eq => stack_op!(binary i32, |a, b| i32::from(a == b)), - I64Eq => stack_op!(binary i64 => i32, |a, b| i32::from(a == b)), - F32Eq => stack_op!(binary f32 => i32, |a, b| i32::from(a == b)), - F64Eq => stack_op!(binary f64 => i32, |a, b| i32::from(a == b)), - I32Ne => stack_op!(binary i32, |a, b| i32::from(a != b)), - I64Ne => stack_op!(binary i64 => i32, |a, b| i32::from(a != b)), - F32Ne => stack_op!(binary f32 => i32, |a, b| i32::from(a != b)), - F64Ne => stack_op!(binary f64 => i32, |a, b| i32::from(a != b)), - I32LtS => stack_op!(binary i32, |a, b| i32::from(a < b)), - I64LtS => stack_op!(binary i64 => i32, |a, b| i32::from(a < b)), - I32LtU => stack_op!(binary u32 => i32, |a, b| i32::from(a < b)), - I64LtU => stack_op!(binary u64 => i32, |a, b| i32::from(a < b)), - F32Lt => stack_op!(binary f32 => i32, |a, b| i32::from(a < b)), - F64Lt => stack_op!(binary f64 => i32, |a, b| i32::from(a < b)), - I32LeS => stack_op!(binary i32, |a, b| i32::from(a <= b)), - I64LeS => stack_op!(binary i64 => i32, |a, b| i32::from(a <= b)), - I32LeU => stack_op!(binary u32 => i32, |a, b| i32::from(a <= b)), - I64LeU => stack_op!(binary u64 => i32, |a, b| i32::from(a <= b)), - F32Le => stack_op!(binary f32 => i32, |a, b| i32::from(a <= b)), - F64Le => stack_op!(binary f64 => i32, |a, b| i32::from(a <= b)), - I32GeS => stack_op!(binary i32, |a, b| i32::from(a >= b)), - I64GeS => stack_op!(binary i64 => i32, |a, b| i32::from(a >= b)), - I32GeU => stack_op!(binary u32 => i32, |a, b| i32::from(a >= b)), - I64GeU => stack_op!(binary u64 => i32, |a, b| i32::from(a >= b)), - F32Ge => stack_op!(binary f32 => i32, |a, b| i32::from(a >= b)), - F64Ge => stack_op!(binary f64 => i32, |a, b| i32::from(a >= b)), - I32GtS => stack_op!(binary i32, |a, b| i32::from(a > b)), - I64GtS => stack_op!(binary i64 => i32, |a, b| i32::from(a > b)), - I32GtU => stack_op!(binary u32 => i32, |a, b| i32::from(a > b)), - I64GtU => stack_op!(binary u64 => i32, |a, b| i32::from(a > b)), - F32Gt => stack_op!(binary f32 => i32, |a, b| i32::from(a > b)), - F64Gt => stack_op!(binary f64 => i32, |a, b| i32::from(a > b)), - I32Add => stack_op!(binary i32, |a, b| a.wrapping_add(b)), - I64Add => stack_op!(binary i64, |a, b| a.wrapping_add(b)), - F32Add => stack_op!(binary f32, |a, b| a + b), - F64Add => stack_op!(binary f64, |a, b| a + b), - I32Sub => stack_op!(binary i32, |a, b| a.wrapping_sub(b)), - I64Sub => stack_op!(binary i64, |a, b| a.wrapping_sub(b)), - F32Sub => stack_op!(binary f32, |a, b| a - b), - F64Sub => stack_op!(binary f64, |a, b| a - b), - F32Div => stack_op!(binary f32, |a, b| a / b), - F64Div => stack_op!(binary f64, |a, b| a / b), - I32Mul => stack_op!(binary i32, |a, b| a.wrapping_mul(b)), - I64Mul => stack_op!(binary i64, |a, b| a.wrapping_mul(b)), - F32Mul => stack_op!(binary f32, |a, b| a * b), - F64Mul => stack_op!(binary f64, |a, b| a * b), - I32DivS => stack_op!(binary try i32, |a, b| a.tw_checked_div(b)), - I64DivS => stack_op!(binary try i64, |a, b| a.tw_checked_div(b)), - I32DivU => stack_op!(binary try u32, |a, b| a.checked_div(b).ok_or(Trap::DivisionByZero)), - I64DivU => stack_op!(binary try u64, |a, b| a.checked_div(b).ok_or(Trap::DivisionByZero)), - I32RemS => stack_op!(binary try i32, |a, b| a.tw_checked_wrapping_rem(b)), - I64RemS => stack_op!(binary try i64, |a, b| a.tw_checked_wrapping_rem(b)), - I32RemU => stack_op!(binary try u32, |a, b| a.tw_checked_wrapping_rem(b)), - I64RemU => stack_op!(binary try u64, |a, b| a.tw_checked_wrapping_rem(b)), - I32And => stack_op!(binary i32, |a, b| a & b), - I64And => stack_op!(binary i64, |a, b| a & b), - I32Or => stack_op!(binary i32, |a, b| a | b), - I64Or => stack_op!(binary i64, |a, b| a | b), - I32Xor => stack_op!(binary i32, |a, b| a ^ b), - I64Xor => stack_op!(binary i64, |a, b| a ^ b), - I32Shl => stack_op!(binary i32, |a, b| a.wrapping_shl(b as u32)), - I64Shl => stack_op!(binary i64, |a, b| a.wrapping_shl(b as u32)), - I32ShrS => stack_op!(binary i32, |a, b| a.wrapping_shr(b as u32)), - I64ShrS => stack_op!(binary i64, |a, b| a.wrapping_shr(b as u32)), - I32ShrU => stack_op!(binary u32, |a, b| a.wrapping_shr(b)), - I64ShrU => stack_op!(binary u64, |a, b| a.wrapping_shr(b as u32)), - I32Rotl => stack_op!(binary i32, |a, b| a.rotate_left(b as u32)), - I64Rotl => stack_op!(binary i64, |a, b| a.rotate_left(b as u32)), - I32Rotr => stack_op!(binary i32, |a, b| a.rotate_right(b as u32)), - I64Rotr => stack_op!(binary i64, |a, b| a.rotate_right(b as u32)), - I64Add128 => stack_op!(quaternary_into2 i64 => i64, |a_lo, a_hi, b_lo, b_hi| { + LoadLocal32(m, addr_local) => self.exec_load_local::(*m, *addr_local, 0, |v| v)?, + LoadLocal64(m, addr_local) => self.exec_load_local::(*m, *addr_local, 0, |v| v)?, + LoadLocal8S32(m, addr_local) => self.exec_load_local::(*m, *addr_local, 0, i32::from)?, + LoadLocal8U32(m, addr_local) => self.exec_load_local::(*m, *addr_local, 0, i32::from)?, + LoadLocal16S32(m, addr_local) => self.exec_load_local::(*m, *addr_local, 0, i32::from)?, + LoadLocal16U32(m, addr_local) => self.exec_load_local::(*m, *addr_local, 0, i32::from)?, + LoadLocalTee32(m, addr_local, dst_local) => self.exec_load_local::(*m, *addr_local, *dst_local, |v| v)?, + LoadLocalSet32(m, addr_local, dst_local) => self.exec_load_local::(*m, *addr_local, *dst_local, |v| v)?, + LoadLocalTee8S32(m, addr_local, dst_local) => self.exec_load_local::(*m, *addr_local, *dst_local, i32::from)?, + LoadLocalTee8U32(m, addr_local, dst_local) => self.exec_load_local::(*m, *addr_local, *dst_local, i32::from)?, + LoadLocalTee16S32(m, addr_local, dst_local) => self.exec_load_local::(*m, *addr_local, *dst_local, i32::from)?, + LoadLocalTee16U32(m, addr_local, dst_local) => self.exec_load_local::(*m, *addr_local, *dst_local, i32::from)?, + LoadLocalSet8S32(m, addr_local, dst_local) => self.exec_load_local::(*m, *addr_local, *dst_local, i32::from)?, + LoadLocalSet8U32(m, addr_local, dst_local) => self.exec_load_local::(*m, *addr_local, *dst_local, i32::from)?, + LoadLocalSet16S32(m, addr_local, dst_local) => self.exec_load_local::(*m, *addr_local, *dst_local, i32::from)?, + LoadLocalSet16U32(m, addr_local, dst_local) => self.exec_load_local::(*m, *addr_local, *dst_local, i32::from)?, + LoadLocalTee128(m, addr_local, dst_local) => self.exec_load_local::(*m, *addr_local, *dst_local, |v| v)?, + LoadLocalSet128(m, addr_local, dst_local) => self.exec_load_local::(*m, *addr_local, *dst_local, |v| v)?, + AndConstTee32(c, local_index) => { exec_op!(unary i32 => i32, |v| v & *c); exec_op!(local_tee i32, local_index); } + SubConstTee32(c, local_index) => { exec_op!(unary i32 => i32, |v| v.wrapping_sub(*c)); exec_op!(local_tee i32, local_index); } + AndConstTee64(c, local_index) => { exec_op!(unary i64 => i64, |v| v & *c); exec_op!(local_tee i64, local_index); } + SubConstTee64(c, local_index) => { exec_op!(unary i64 => i64, |v| v.wrapping_sub(*c)); exec_op!(local_tee i64, local_index); } + LocalTee32(local_index) => exec_op!(local_tee Value32, local_index), + LocalTee64(local_index) => exec_op!(local_tee Value64, local_index), + LocalTee128(local_index) => exec_op!(local_tee Value128, local_index), + GlobalGet32(global_index) => exec_op!(global_get Value32, global_index), + GlobalGet64(global_index) => exec_op!(global_get Value64, global_index), + GlobalGet128(global_index) => exec_op!(global_get Value128, global_index), + GlobalSet32(global_index) => exec_op!(global_set Value32, global_index), + GlobalSet64(global_index) => exec_op!(global_set Value64, global_index), + GlobalSet128(global_index) => exec_op!(global_set Value128, global_index), + GlobalTee32(global_index) => exec_op!(global_tee Value32, global_index), + GlobalTee64(global_index) => exec_op!(global_tee Value64, global_index), + GlobalTee128(global_index) => exec_op!(global_tee Value128, global_index), + Const32(val) => i32::stack_push(&mut self.store.value_stack, *val)?, + Const64(val) => i64::stack_push(&mut self.store.value_stack, *val)?, + I64Eqz => exec_op!(unary i64 => i32, |v| i32::from(v == 0)), + I32Eqz => exec_op!(unary i32 => i32, |v| i32::from(v == 0)), + I32Eq => exec_op!(binary i32 => i32, |a, b| i32::from(a == b)), + I64Eq => exec_op!(binary i64 => i32, |a, b| i32::from(a == b)), + F32Eq => exec_op!(binary f32 => i32, |a, b| i32::from(a == b)), + F64Eq => exec_op!(binary f64 => i32, |a, b| i32::from(a == b)), + I32Ne => exec_op!(binary i32 => i32, |a, b| i32::from(a != b)), + I64Ne => exec_op!(binary i64 => i32, |a, b| i32::from(a != b)), + F32Ne => exec_op!(binary f32 => i32, |a, b| i32::from(a != b)), + F64Ne => exec_op!(binary f64 => i32, |a, b| i32::from(a != b)), + I32LtS => exec_op!(binary i32 => i32, |a, b| i32::from(a < b)), + I64LtS => exec_op!(binary i64 => i32, |a, b| i32::from(a < b)), + I32LtU => exec_op!(binary u32 => i32, |a, b| i32::from(a < b)), + I64LtU => exec_op!(binary u64 => i32, |a, b| i32::from(a < b)), + F32Lt => exec_op!(binary f32 => i32, |a, b| i32::from(a < b)), + F64Lt => exec_op!(binary f64 => i32, |a, b| i32::from(a < b)), + I32LeS => exec_op!(binary i32 => i32, |a, b| i32::from(a <= b)), + I64LeS => exec_op!(binary i64 => i32, |a, b| i32::from(a <= b)), + I32LeU => exec_op!(binary u32 => i32, |a, b| i32::from(a <= b)), + I64LeU => exec_op!(binary u64 => i32, |a, b| i32::from(a <= b)), + F32Le => exec_op!(binary f32 => i32, |a, b| i32::from(a <= b)), + F64Le => exec_op!(binary f64 => i32, |a, b| i32::from(a <= b)), + I32GeS => exec_op!(binary i32 => i32, |a, b| i32::from(a >= b)), + I64GeS => exec_op!(binary i64 => i32, |a, b| i32::from(a >= b)), + I32GeU => exec_op!(binary u32 => i32, |a, b| i32::from(a >= b)), + I64GeU => exec_op!(binary u64 => i32, |a, b| i32::from(a >= b)), + F32Ge => exec_op!(binary f32 => i32, |a, b| i32::from(a >= b)), + F64Ge => exec_op!(binary f64 => i32, |a, b| i32::from(a >= b)), + I32GtS => exec_op!(binary i32 => i32, |a, b| i32::from(a > b)), + I64GtS => exec_op!(binary i64 => i32, |a, b| i32::from(a > b)), + I32GtU => exec_op!(binary u32 => i32, |a, b| i32::from(a > b)), + I64GtU => exec_op!(binary u64 => i32, |a, b| i32::from(a > b)), + F32Gt => exec_op!(binary f32 => i32, |a, b| i32::from(a > b)), + F64Gt => exec_op!(binary f64 => i32, |a, b| i32::from(a > b)), + I32Add => exec_op!(binary i32 => i32, |a, b| a.wrapping_add(b)), + I64Add => exec_op!(binary i64 => i64, |a, b| a.wrapping_add(b)), + F32Add => exec_op!(binary f32 => f32, |a, b| a + b), + F64Add => exec_op!(binary f64 => f64, |a, b| a + b), + I32Sub => exec_op!(binary i32 => i32, |a, b| a.wrapping_sub(b)), + I64Sub => exec_op!(binary i64 => i64, |a, b| a.wrapping_sub(b)), + F32Sub => exec_op!(binary f32 => f32, |a, b| a - b), + F64Sub => exec_op!(binary f64 => f64, |a, b| a - b), + F32Div => exec_op!(binary f32 => f32, |a, b| a / b), + F64Div => exec_op!(binary f64 => f64, |a, b| a / b), + I32Mul => exec_op!(binary i32 => i32, |a, b| a.wrapping_mul(b)), + I64Mul => exec_op!(binary i64 => i64, |a, b| a.wrapping_mul(b)), + F32Mul => exec_op!(binary f32 => f32, |a, b| a * b), + F64Mul => exec_op!(binary f64 => f64, |a, b| a * b), + I32DivS => exec_op!(binary_fallible i32, |a, b| a.tw_checked_div(b)), + I64DivS => exec_op!(binary_fallible i64, |a, b| a.tw_checked_div(b)), + I32DivU => exec_op!(binary_fallible u32, |a, b| a.checked_div(b).ok_or(Trap::DivisionByZero)), + I64DivU => exec_op!(binary_fallible u64, |a, b| a.checked_div(b).ok_or(Trap::DivisionByZero)), + I32RemS => exec_op!(binary_fallible i32, |a, b| a.tw_checked_wrapping_rem(b)), + I64RemS => exec_op!(binary_fallible i64, |a, b| a.tw_checked_wrapping_rem(b)), + I32RemU => exec_op!(binary_fallible u32, |a, b| a.tw_checked_wrapping_rem(b)), + I64RemU => exec_op!(binary_fallible u64, |a, b| a.tw_checked_wrapping_rem(b)), + I32And => exec_op!(binary i32 => i32, |a, b| a & b), + I64And => exec_op!(binary i64 => i64, |a, b| a & b), + I32Or => exec_op!(binary i32 => i32, |a, b| a | b), + I64Or => exec_op!(binary i64 => i64, |a, b| a | b), + I32Xor => exec_op!(binary i32 => i32, |a, b| a ^ b), + I64Xor => exec_op!(binary i64 => i64, |a, b| a ^ b), + I32Shl => exec_op!(binary i32 => i32, |a, b| a.wrapping_shl(b as u32)), + I64Shl => exec_op!(binary i64 => i64, |a, b| a.wrapping_shl(b as u32)), + I32ShrS => exec_op!(binary i32 => i32, |a, b| a.wrapping_shr(b as u32)), + I64ShrS => exec_op!(binary i64 => i64, |a, b| a.wrapping_shr(b as u32)), + I32ShrU => exec_op!(binary u32 => u32, |a, b| a.wrapping_shr(b)), + I64ShrU => exec_op!(binary u64 => u64, |a, b| a.wrapping_shr(b as u32)), + I32Rotl => exec_op!(binary i32 => i32, |a, b| a.rotate_left(b as u32)), + I64Rotl => exec_op!(binary i64 => i64, |a, b| a.rotate_left(b as u32)), + I32Rotr => exec_op!(binary i32 => i32, |a, b| a.rotate_right(b as u32)), + I64Rotr => exec_op!(binary i64 => i64, |a, b| a.rotate_right(b as u32)), + I64Add128 => exec_op!(quaternary_two_results i64 => i64, |a_lo, a_hi, b_lo, b_hi| { let lo = a_lo.wrapping_add(b_lo); let carry = u64::from((lo as u64) < (a_lo as u64)); let hi = a_hi.wrapping_add(b_hi).wrapping_add(carry as i64); (lo, hi) }), - I64Sub128 => stack_op!(quaternary_into2 i64 => i64, |a_lo, a_hi, b_lo, b_hi| { + I64Sub128 => exec_op!(quaternary_two_results i64 => i64, |a_lo, a_hi, b_lo, b_hi| { let lo = a_lo.wrapping_sub(b_lo); let borrow = u64::from((a_lo as u64) < (b_lo as u64)); let hi = a_hi.wrapping_sub(b_hi).wrapping_sub(borrow as i64); (lo, hi) }), - I64MulWideS => stack_op!(binary_into2 i64 => i64, |a, b| { + I64MulWideS => exec_op!(binary_two_results i64 => i64, |a, b| { let product = (a as i128).wrapping_mul(b as i128); (product as i64, (product >> 64) as i64) }), - I64MulWideU => stack_op!(binary_into2 i64 => i64, |a, b| { + I64MulWideU => exec_op!(binary_two_results i64 => i64, |a, b| { let product = (a as u64 as u128).wrapping_mul(b as u64 as u128); (product as u64 as i64, (product >> 64) as u64 as i64) }), - I32Clz => stack_op!(unary i32, |v| v.leading_zeros() as i32), - I64Clz => stack_op!(unary i64, |v| i64::from(v.leading_zeros())), - I32Ctz => stack_op!(unary i32, |v| v.trailing_zeros() as i32), - I64Ctz => stack_op!(unary i64, |v| i64::from(v.trailing_zeros())), - I32Popcnt => stack_op!(unary i32, |v| v.count_ones() as i32), - I64Popcnt => stack_op!(unary i64, |v| i64::from(v.count_ones())), + I32Clz => exec_op!(unary i32 => i32, |v| v.leading_zeros() as i32), + I64Clz => exec_op!(unary i64 => i64, |v| i64::from(v.leading_zeros())), + I32Ctz => exec_op!(unary i32 => i32, |v| v.trailing_zeros() as i32), + I64Ctz => exec_op!(unary i64 => i64, |v| i64::from(v.trailing_zeros())), + I32Popcnt => exec_op!(unary i32 => i32, |v| v.count_ones() as i32), + I64Popcnt => exec_op!(unary i64 => i64, |v| i64::from(v.count_ones())), // Reference types - RefFunc(func_idx) => self.exec_const(ValueRef::from_addr(Some(self.module.resolve_func_addr(*func_idx))))?, - RefNull(_) => self.exec_const(ValueRef::NULL)?, + RefFunc(func_idx) => ValueRef::stack_push(&mut self.store.value_stack, ValueRef::from_category_addr(self.module.resolve_func_addr(*func_idx)))?, + RefNull(_) => ValueRef::stack_push(&mut self.store.value_stack, ValueRef::NULL)?, RefIsNull => self.exec_ref_is_null()?, RefAsNonNull => self.exec_ref_as_non_null()?, + RefI31 => exec_op!(unary i32 => ValueRef, |v| ValueRef::from_i31(v)), + I31GetS => self.exec_i31_get(true)?, + I31GetU => self.exec_i31_get(false)?, + RefEq => exec_op!(binary ValueRef => i32, |a, b| i32::from(a == b)), + RefTest(ty) => self.exec_ref_test(*ty)?, + RefCast(ty) => self.exec_ref_cast(*ty)?, + // GC objects + StructNew(ty) => self.exec_struct_new(*ty, false)?, + StructNewDefault(ty) => self.exec_struct_new(*ty, true)?, + StructGet(ty, field) => self.exec_struct_get(*ty, *field, None)?, + StructGetS(ty, field) => self.exec_struct_get(*ty, *field, Some(true))?, + StructGetU(ty, field) => self.exec_struct_get(*ty, *field, Some(false))?, + StructSet(ty, field) => self.exec_struct_set(*ty, *field)?, + ArrayNew(ty) => self.exec_array_new(*ty, false)?, + ArrayNewDefault(ty) => self.exec_array_new(*ty, true)?, + ArrayNewFixed(ty, len) => self.exec_array_new_fixed(*ty, *len)?, + ArrayNewData(ty, data) => self.exec_array_new_data(*ty, *data)?, + ArrayNewElem(ty, elem) => self.exec_array_new_elem(*ty, *elem)?, + ArrayGet(ty) => self.exec_array_get(*ty, None)?, + ArrayGetS(ty) => self.exec_array_get(*ty, Some(true))?, + ArrayGetU(ty) => self.exec_array_get(*ty, Some(false))?, + ArraySet(ty) => self.exec_array_set(*ty)?, + ArrayLen => self.exec_array_len()?, + ArrayFill(ty) => self.exec_array_fill(*ty)?, + ArrayCopy(dst, src) => self.exec_array_copy(*dst, *src)?, + ArrayInitData(ty, data) => self.exec_array_init_data(*ty, *data)?, + ArrayInitElem(ty, elem) => self.exec_array_init_elem(*ty, *elem)?, MemorySize(addr) => self.exec_memory_size(*addr)?, MemoryGrow(addr) => self.exec_memory_grow(*addr)?, @@ -496,46 +568,46 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { I64Load32U(m) => self.exec_mem_load::(m.mem_addr(), m.offset(), i64::from)?, // Numeric conversion operations - F32ConvertI32S => stack_op!(unary i32 => f32, |v| v as f32), - F32ConvertI64S => stack_op!(unary i64 => f32, |v| v as f32), - F64ConvertI32S => stack_op!(unary i32 => f64, |v| f64::from(v)), - F64ConvertI64S => stack_op!(unary i64 => f64, |v| v as f64), - F32ConvertI32U => stack_op!(unary u32 => f32, |v| v as f32), - F32ConvertI64U => stack_op!(unary u64 => f32, |v| v as f32), - F64ConvertI32U => stack_op!(unary u32 => f64, |v| f64::from(v)), - F64ConvertI64U => stack_op!(unary u64 => f64, |v| v as f64), + F32ConvertI32S => exec_op!(unary i32 => f32, |v| v as f32), + F32ConvertI64S => exec_op!(unary i64 => f32, |v| v as f32), + F64ConvertI32S => exec_op!(unary i32 => f64, |v| f64::from(v)), + F64ConvertI64S => exec_op!(unary i64 => f64, |v| v as f64), + F32ConvertI32U => exec_op!(unary u32 => f32, |v| v as f32), + F32ConvertI64U => exec_op!(unary u64 => f32, |v| v as f32), + F64ConvertI32U => exec_op!(unary u32 => f64, |v| f64::from(v)), + F64ConvertI64U => exec_op!(unary u64 => f64, |v| v as f64), // Sign-extension operations - I32Extend8S => stack_op!(unary i32, |v| i32::from(v as i8)), - I32Extend16S => stack_op!(unary i32, |v| i32::from(v as i16)), - I64Extend8S => stack_op!(unary i64, |v| i64::from(v as i8)), - I64Extend16S => stack_op!(unary i64, |v| i64::from(v as i16)), - I64Extend32S => stack_op!(unary i64, |v| i64::from(v as i32)), - I64ExtendI32U => stack_op!(unary u32 => i64, |v| i64::from(v)), - I64ExtendI32S => stack_op!(unary i32 => i64, |v| i64::from(v)), - I32WrapI64 => stack_op!(unary i64 => i32, |v| v as i32), - F32DemoteF64 => stack_op!(unary f64 => f32, |v| v as f32), - F64PromoteF32 => stack_op!(unary f32 => f64, |v| f64::from(v)), - F32Abs => stack_op!(unary f32, |v| v.abs()), - F64Abs => stack_op!(unary f64, |v| v.abs()), - F32Neg => stack_op!(unary f32, |v| -v), - F64Neg => stack_op!(unary f64, |v| -v), - F32Ceil => stack_op!(unary f32, |v| v.ceil()), - F64Ceil => stack_op!(unary f64, |v| v.ceil()), - F32Floor => stack_op!(unary f32, |v| v.floor()), - F64Floor => stack_op!(unary f64, |v| v.floor()), - F32Trunc => stack_op!(unary f32, |v| v.trunc()), - F64Trunc => stack_op!(unary f64, |v| v.trunc()), - F32Nearest => stack_op!(unary f32, |v| v.tw_nearest()), - F64Nearest => stack_op!(unary f64, |v| v.tw_nearest()), - F32Sqrt => stack_op!(unary f32, |v| v.sqrt()), - F64Sqrt => stack_op!(unary f64, |v| v.sqrt()), - F32Min => stack_op!(binary f32, |a, b| a.tw_minimum(b)), - F64Min => stack_op!(binary f64, |a, b| a.tw_minimum(b)), - F32Max => stack_op!(binary f32, |a, b| a.tw_maximum(b)), - F64Max => stack_op!(binary f64, |a, b| a.tw_maximum(b)), - F32Copysign => stack_op!(binary f32, |a, b| a.copysign(b)), - F64Copysign => stack_op!(binary f64, |a, b| a.copysign(b)), + I32Extend8S => exec_op!(unary i32 => i32, |v| i32::from(v as i8)), + I32Extend16S => exec_op!(unary i32 => i32, |v| i32::from(v as i16)), + I64Extend8S => exec_op!(unary i64 => i64, |v| i64::from(v as i8)), + I64Extend16S => exec_op!(unary i64 => i64, |v| i64::from(v as i16)), + I64Extend32S => exec_op!(unary i64 => i64, |v| i64::from(v as i32)), + I64ExtendI32U => exec_op!(unary u32 => i64, |v| i64::from(v)), + I64ExtendI32S => exec_op!(unary i32 => i64, |v| i64::from(v)), + I32WrapI64 => exec_op!(unary i64 => i32, |v| v as i32), + F32DemoteF64 => exec_op!(unary f64 => f32, |v| v as f32), + F64PromoteF32 => exec_op!(unary f32 => f64, |v| f64::from(v)), + F32Abs => exec_op!(unary f32 => f32, |v| v.abs()), + F64Abs => exec_op!(unary f64 => f64, |v| v.abs()), + F32Neg => exec_op!(unary f32 => f32, |v| -v), + F64Neg => exec_op!(unary f64 => f64, |v| -v), + F32Ceil => exec_op!(unary f32 => f32, |v| v.ceil()), + F64Ceil => exec_op!(unary f64 => f64, |v| v.ceil()), + F32Floor => exec_op!(unary f32 => f32, |v| v.floor()), + F64Floor => exec_op!(unary f64 => f64, |v| v.floor()), + F32Trunc => exec_op!(unary f32 => f32, |v| v.trunc()), + F64Trunc => exec_op!(unary f64 => f64, |v| v.trunc()), + F32Nearest => exec_op!(unary f32 => f32, |v| v.tw_nearest()), + F64Nearest => exec_op!(unary f64 => f64, |v| v.tw_nearest()), + F32Sqrt => exec_op!(unary f32 => f32, |v| v.sqrt()), + F64Sqrt => exec_op!(unary f64 => f64, |v| v.sqrt()), + F32Min => exec_op!(binary f32 => f32, |a, b| a.tw_minimum(b)), + F64Min => exec_op!(binary f64 => f64, |a, b| a.tw_minimum(b)), + F32Max => exec_op!(binary f32 => f32, |a, b| a.tw_maximum(b)), + F64Max => exec_op!(binary f64 => f64, |a, b| a.tw_maximum(b)), + F32Copysign => exec_op!(binary f32 => f32, |a, b| a.copysign(b)), + F64Copysign => exec_op!(binary f64 => f64, |a, b| a.copysign(b)), I32TruncF32S => checked_conv_float!(f32, i32, self), I32TruncF64S => checked_conv_float!(f64, i32, self), I32TruncF32U => checked_conv_float!(f32, u32, i32, self), @@ -546,25 +618,25 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { I64TruncF64U => checked_conv_float!(f64, u64, i64, self), // Non-trapping float-to-int conversions - I32TruncSatF32S => stack_op!(unary f32 => i32, |v| v.trunc() as i32), - I32TruncSatF32U => stack_op!(unary f32 => u32, |v| v.trunc() as u32), - I32TruncSatF64S => stack_op!(unary f64 => i32, |v| v.trunc() as i32), - I32TruncSatF64U => stack_op!(unary f64 => u32, |v| v.trunc() as u32), - I64TruncSatF32S => stack_op!(unary f32 => i64, |v| v.trunc() as i64), - I64TruncSatF32U => stack_op!(unary f32 => u64, |v| v.trunc() as u64), - I64TruncSatF64S => stack_op!(unary f64 => i64, |v| v.trunc() as i64), - I64TruncSatF64U => stack_op!(unary f64 => u64, |v| v.trunc() as u64), + I32TruncSatF32S => exec_op!(unary f32 => i32, |v| v.trunc() as i32), + I32TruncSatF32U => exec_op!(unary f32 => u32, |v| v.trunc() as u32), + I32TruncSatF64S => exec_op!(unary f64 => i32, |v| v.trunc() as i32), + I32TruncSatF64U => exec_op!(unary f64 => u32, |v| v.trunc() as u32), + I64TruncSatF32S => exec_op!(unary f32 => i64, |v| v.trunc() as i64), + I64TruncSatF32U => exec_op!(unary f32 => u64, |v| v.trunc() as u64), + I64TruncSatF64S => exec_op!(unary f64 => i64, |v| v.trunc() as i64), + I64TruncSatF64U => exec_op!(unary f64 => u64, |v| v.trunc() as u64), // SIMD extension - V128Not => stack_op!(unary Value128, |v| v.v128_not()), - V128And => stack_op!(binary Value128, |a, b| a.v128_and(b)), - V128AndNot => stack_op!(binary Value128, |a, b| a.v128_andnot(b)), - V128Or => stack_op!(binary Value128, |a, b| a.v128_or(b)), - V128Xor => stack_op!(binary Value128, |a, b| a.v128_xor(b)), - V128Bitselect => stack_op!(ternary Value128, |a, b, c| Value128::v128_bitselect(a, b, c)), - V128AnyTrue => stack_op!(unary Value128 => i32, |v| v.v128_any_true() as i32), - I8x16Swizzle => stack_op!(binary Value128, |a, s| a.i8x16_swizzle(s)), - I8x16RelaxedSwizzle => stack_op!(binary Value128, |a, s| a.i8x16_relaxed_swizzle(s)), + V128Not => exec_op!(unary Value128 => Value128, |v| v.v128_not()), + V128And => exec_op!(binary Value128 => Value128, |a, b| a.v128_and(b)), + V128AndNot => exec_op!(binary Value128 => Value128, |a, b| a.v128_andnot(b)), + V128Or => exec_op!(binary Value128 => Value128, |a, b| a.v128_or(b)), + V128Xor => exec_op!(binary Value128 => Value128, |a, b| a.v128_xor(b)), + V128Bitselect => exec_op!(ternary Value128 => Value128, |a, b, c| Value128::v128_bitselect(a, b, c)), + V128AnyTrue => exec_op!(unary Value128 => i32, |v| v.v128_any_true() as i32), + I8x16Swizzle => exec_op!(binary Value128 => Value128, |a, s| a.i8x16_swizzle(s)), + I8x16RelaxedSwizzle => exec_op!(binary Value128 => Value128, |a, s| a.i8x16_relaxed_swizzle(s)), V128Load(arg) => self.exec_mem_load::(arg.mem_addr(), arg.offset(), |v| v)?, V128Load8x8S(arg) => self.exec_mem_load::(arg.mem_addr(), arg.offset(), |v| Value128::v128_load8x8_s(v.to_le_bytes()))?, V128Load8x8U(arg) => self.exec_mem_load::(arg.mem_addr(), arg.offset(), |v| Value128::v128_load8x8_u(v.to_le_bytes()))?, @@ -583,348 +655,251 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { V128Store64Lane(arg, lane) => self.exec_mem_store_lane::(arg.mem_addr(), arg.offset(), *lane)?, V128Load32Zero(arg) => self.exec_mem_load::(arg.mem_addr(), arg.offset(), |v| Value128::from_i32x4([v, 0, 0, 0]))?, V128Load64Zero(arg) => self.exec_mem_load::(arg.mem_addr(), arg.offset(), |v| Value128::from_i64x2([v, 0]))?, - Const128(arg) => self.exec_const(Value128(self.func.data.v128_const(*arg)))?, - I8x16ExtractLaneS(lane) => stack_op!(unary Value128 => i32, |v| v.extract_lane_i8(*lane) as i32), - I8x16ExtractLaneU(lane) => stack_op!(unary Value128 => i32, |v| v.extract_lane_u8(*lane) as i32), - I16x8ExtractLaneS(lane) => stack_op!(unary Value128 => i32, |v| v.extract_lane_i16(*lane) as i32), - I16x8ExtractLaneU(lane) => stack_op!(unary Value128 => i32, |v| v.extract_lane_u16(*lane) as i32), - I32x4ExtractLane(lane) => stack_op!(unary Value128 => i32, |v| v.extract_lane_i32(*lane)), - I64x2ExtractLane(lane) => stack_op!(unary Value128 => i64, |v| v.extract_lane_i64(*lane)), - F32x4ExtractLane(lane) => stack_op!(unary Value128 => f32, |v| v.extract_lane_f32(*lane)), - F64x2ExtractLane(lane) => stack_op!(unary Value128 => f64, |v| v.extract_lane_f64(*lane)), + Const128(arg) => Value128::stack_push(&mut self.store.value_stack, Value128(self.func.data.v128_const(*arg)))?, + I8x16ExtractLaneS(lane) => exec_op!(unary Value128 => i32, |v| v.extract_lane_i8(*lane) as i32), + I8x16ExtractLaneU(lane) => exec_op!(unary Value128 => i32, |v| v.extract_lane_u8(*lane) as i32), + I16x8ExtractLaneS(lane) => exec_op!(unary Value128 => i32, |v| v.extract_lane_i16(*lane) as i32), + I16x8ExtractLaneU(lane) => exec_op!(unary Value128 => i32, |v| v.extract_lane_u16(*lane) as i32), + I32x4ExtractLane(lane) => exec_op!(unary Value128 => i32, |v| v.extract_lane_i32(*lane)), + I64x2ExtractLane(lane) => exec_op!(unary Value128 => i64, |v| v.extract_lane_i64(*lane)), + F32x4ExtractLane(lane) => exec_op!(unary Value128 => f32, |v| v.extract_lane_f32(*lane)), + F64x2ExtractLane(lane) => exec_op!(unary Value128 => f64, |v| v.extract_lane_f64(*lane)), V128Load8Lane(arg, lane) => self.exec_mem_load_lane::(arg.mem_addr(), arg.offset(), *lane)?, V128Load16Lane(arg, lane) => self.exec_mem_load_lane::(arg.mem_addr(), arg.offset(), *lane)?, V128Load32Lane(arg, lane) => self.exec_mem_load_lane::(arg.mem_addr(), arg.offset(), *lane)?, V128Load64Lane(arg, lane) => self.exec_mem_load_lane::(arg.mem_addr(), arg.offset(), *lane)?, - I8x16ReplaceLane(lane) => stack_op!(binary i32, Value128, |value, vec| vec.i8x16_replace_lane(*lane, value as i8)), - I16x8ReplaceLane(lane) => stack_op!(binary i32, Value128, |value, vec| vec.i16x8_replace_lane(*lane, value as i16)), - I32x4ReplaceLane(lane) => stack_op!(binary i32, Value128, |value, vec| vec.i32x4_replace_lane(*lane, value)), - I64x2ReplaceLane(lane) => stack_op!(binary i64, Value128, |value, vec| vec.i64x2_replace_lane(*lane, value)), - F32x4ReplaceLane(lane) => stack_op!(binary f32, Value128, |value, vec| vec.f32x4_replace_lane(*lane, value)), - F64x2ReplaceLane(lane) => stack_op!(binary f64, Value128, |value, vec| vec.f64x2_replace_lane(*lane, value)), - I8x16Splat => stack_op!(unary i32 => Value128, |v| Value128::splat_i8(v as i8)), - I16x8Splat => stack_op!(unary i32 => Value128, |v| Value128::splat_i16(v as i16)), - I32x4Splat => stack_op!(unary i32 => Value128, |v| Value128::splat_i32(v)), - I64x2Splat => stack_op!(unary i64 => Value128, |v| Value128::splat_i64(v)), - F32x4Splat => stack_op!(unary f32 => Value128, |v| Value128::splat_f32(v)), - F64x2Splat => stack_op!(unary f64 => Value128, |v| Value128::splat_f64(v)), - I8x16Eq => stack_op!(binary Value128, |a, b| a.i8x16_eq(b)), - I16x8Eq => stack_op!(binary Value128, |a, b| a.i16x8_eq(b)), - I32x4Eq => stack_op!(binary Value128, |a, b| a.i32x4_eq(b)), - I64x2Eq => stack_op!(binary Value128, |a, b| a.i64x2_eq(b)), - F32x4Eq => stack_op!(binary Value128, |a, b| a.f32x4_eq(b)), - F64x2Eq => stack_op!(binary Value128, |a, b| a.f64x2_eq(b)), - I8x16Ne => stack_op!(binary Value128, |a, b| a.i8x16_ne(b)), - I16x8Ne => stack_op!(binary Value128, |a, b| a.i16x8_ne(b)), - I32x4Ne => stack_op!(binary Value128, |a, b| a.i32x4_ne(b)), - I64x2Ne => stack_op!(binary Value128, |a, b| a.i64x2_ne(b)), - F32x4Ne => stack_op!(binary Value128, |a, b| a.f32x4_ne(b)), - F64x2Ne => stack_op!(binary Value128, |a, b| a.f64x2_ne(b)), - I8x16LtS => stack_op!(binary Value128, |a, b| a.i8x16_lt_s(b)), - I16x8LtS => stack_op!(binary Value128, |a, b| a.i16x8_lt_s(b)), - I32x4LtS => stack_op!(binary Value128, |a, b| a.i32x4_lt_s(b)), - I64x2LtS => stack_op!(binary Value128, |a, b| a.i64x2_lt_s(b)), - I8x16LtU => stack_op!(binary Value128, |a, b| a.i8x16_lt_u(b)), - I16x8LtU => stack_op!(binary Value128, |a, b| a.i16x8_lt_u(b)), - I32x4LtU => stack_op!(binary Value128, |a, b| a.i32x4_lt_u(b)), - F32x4Lt => stack_op!(binary Value128, |a, b| a.f32x4_lt(b)), - F64x2Lt => stack_op!(binary Value128, |a, b| a.f64x2_lt(b)), - F32x4Gt => stack_op!(binary Value128, |a, b| a.f32x4_gt(b)), - F64x2Gt => stack_op!(binary Value128, |a, b| a.f64x2_gt(b)), - I8x16GtS => stack_op!(binary Value128, |a, b| a.i8x16_gt_s(b)), - I16x8GtS => stack_op!(binary Value128, |a, b| a.i16x8_gt_s(b)), - I32x4GtS => stack_op!(binary Value128, |a, b| a.i32x4_gt_s(b)), - I64x2GtS => stack_op!(binary Value128, |a, b| a.i64x2_gt_s(b)), - I64x2LeS => stack_op!(binary Value128, |a, b| a.i64x2_le_s(b)), - F32x4Le => stack_op!(binary Value128, |a, b| a.f32x4_le(b)), - F64x2Le => stack_op!(binary Value128, |a, b| a.f64x2_le(b)), - I8x16GtU => stack_op!(binary Value128, |a, b| a.i8x16_gt_u(b)), - I16x8GtU => stack_op!(binary Value128, |a, b| a.i16x8_gt_u(b)), - I32x4GtU => stack_op!(binary Value128, |a, b| a.i32x4_gt_u(b)), - F32x4Ge => stack_op!(binary Value128, |a, b| a.f32x4_ge(b)), - F64x2Ge => stack_op!(binary Value128, |a, b| a.f64x2_ge(b)), - I8x16LeS => stack_op!(binary Value128, |a, b| a.i8x16_le_s(b)), - I16x8LeS => stack_op!(binary Value128, |a, b| a.i16x8_le_s(b)), - I32x4LeS => stack_op!(binary Value128, |a, b| a.i32x4_le_s(b)), - I8x16LeU => stack_op!(binary Value128, |a, b| a.i8x16_le_u(b)), - I16x8LeU => stack_op!(binary Value128, |a, b| a.i16x8_le_u(b)), - I32x4LeU => stack_op!(binary Value128, |a, b| a.i32x4_le_u(b)), - I8x16GeS => stack_op!(binary Value128, |a, b| a.i8x16_ge_s(b)), - I16x8GeS => stack_op!(binary Value128, |a, b| a.i16x8_ge_s(b)), - I32x4GeS => stack_op!(binary Value128, |a, b| a.i32x4_ge_s(b)), - I64x2GeS => stack_op!(binary Value128, |a, b| a.i64x2_ge_s(b)), - I8x16GeU => stack_op!(binary Value128, |a, b| a.i8x16_ge_u(b)), - I16x8GeU => stack_op!(binary Value128, |a, b| a.i16x8_ge_u(b)), - I32x4GeU => stack_op!(binary Value128, |a, b| a.i32x4_ge_u(b)), - I8x16Abs => stack_op!(unary Value128, |a| a.i8x16_abs()), - I16x8Abs => stack_op!(unary Value128, |a| a.i16x8_abs()), - I32x4Abs => stack_op!(unary Value128, |a| a.i32x4_abs()), - I64x2Abs => stack_op!(unary Value128, |a| a.i64x2_abs()), - I8x16Neg => stack_op!(unary Value128, |a| a.i8x16_neg()), - I16x8Neg => stack_op!(unary Value128, |a| a.i16x8_neg()), - I32x4Neg => stack_op!(unary Value128, |a| a.i32x4_neg()), - I64x2Neg => stack_op!(unary Value128, |a| a.i64x2_neg()), - I8x16AllTrue => stack_op!(unary Value128 => i32, |v| v.i8x16_all_true() as i32), - I16x8AllTrue => stack_op!(unary Value128 => i32, |v| v.i16x8_all_true() as i32), - I32x4AllTrue => stack_op!(unary Value128 => i32, |v| v.i32x4_all_true() as i32), - I64x2AllTrue => stack_op!(unary Value128 => i32, |v| v.i64x2_all_true() as i32), - I8x16Bitmask => stack_op!(unary Value128 => i32, |v| v.i8x16_bitmask() as i32), - I16x8Bitmask => stack_op!(unary Value128 => i32, |v| v.i16x8_bitmask() as i32), - I32x4Bitmask => stack_op!(unary Value128 => i32, |v| v.i32x4_bitmask() as i32), - I64x2Bitmask => stack_op!(unary Value128 => i32, |v| v.i64x2_bitmask() as i32), - I8x16Shl => stack_op!(binary i32, Value128, |a, b| b.i8x16_shl(a as u32)), - I16x8Shl => stack_op!(binary i32, Value128, |a, b| b.i16x8_shl(a as u32)), - I32x4Shl => stack_op!(binary i32, Value128, |a, b| b.i32x4_shl(a as u32)), - I64x2Shl => stack_op!(binary i32, Value128, |a, b| b.i64x2_shl(a as u32)), - I8x16ShrS => stack_op!(binary i32, Value128, |a, b| b.i8x16_shr_s(a as u32)), - I16x8ShrS => stack_op!(binary i32, Value128, |a, b| b.i16x8_shr_s(a as u32)), - I32x4ShrS => stack_op!(binary i32, Value128, |a, b| b.i32x4_shr_s(a as u32)), - I64x2ShrS => stack_op!(binary i32, Value128, |a, b| b.i64x2_shr_s(a as u32)), - I8x16ShrU => stack_op!(binary i32, Value128, |a, b| b.i8x16_shr_u(a as u32)), - I16x8ShrU => stack_op!(binary i32, Value128, |a, b| b.i16x8_shr_u(a as u32)), - I32x4ShrU => stack_op!(binary i32, Value128, |a, b| b.i32x4_shr_u(a as u32)), - I64x2ShrU => stack_op!(binary i32, Value128, |a, b| b.i64x2_shr_u(a as u32)), - I8x16Add => stack_op!(binary Value128, |a, b| a.i8x16_add(b)), - I16x8Add => stack_op!(binary Value128, |a, b| a.i16x8_add(b)), - I32x4Add => stack_op!(binary Value128, |a, b| a.i32x4_add(b)), - I64x2Add => stack_op!(binary Value128, |a, b| a.i64x2_add(b)), - I8x16Sub => stack_op!(binary Value128, |a, b| a.i8x16_sub(b)), - I16x8Sub => stack_op!(binary Value128, |a, b| a.i16x8_sub(b)), - I32x4Sub => stack_op!(binary Value128, |a, b| a.i32x4_sub(b)), - I64x2Sub => stack_op!(binary Value128, |a, b| a.i64x2_sub(b)), - I8x16MinS => stack_op!(binary Value128, |a, b| a.i8x16_min_s(b)), - I16x8MinS => stack_op!(binary Value128, |a, b| a.i16x8_min_s(b)), - I32x4MinS => stack_op!(binary Value128, |a, b| a.i32x4_min_s(b)), - I8x16MinU => stack_op!(binary Value128, |a, b| a.i8x16_min_u(b)), - I16x8MinU => stack_op!(binary Value128, |a, b| a.i16x8_min_u(b)), - I32x4MinU => stack_op!(binary Value128, |a, b| a.i32x4_min_u(b)), - I8x16MaxS => stack_op!(binary Value128, |a, b| a.i8x16_max_s(b)), - I16x8MaxS => stack_op!(binary Value128, |a, b| a.i16x8_max_s(b)), - I32x4MaxS => stack_op!(binary Value128, |a, b| a.i32x4_max_s(b)), - I8x16MaxU => stack_op!(binary Value128, |a, b| a.i8x16_max_u(b)), - I16x8MaxU => stack_op!(binary Value128, |a, b| a.i16x8_max_u(b)), - I32x4MaxU => stack_op!(binary Value128, |a, b| a.i32x4_max_u(b)), - I64x2Mul => stack_op!(binary Value128, |a, b| a.i64x2_mul(b)), - I16x8Mul => stack_op!(binary Value128, |a, b| a.i16x8_mul(b)), - I32x4Mul => stack_op!(binary Value128, |a, b| a.i32x4_mul(b)), - I8x16NarrowI16x8S => stack_op!(binary Value128, |a, b| Value128::i8x16_narrow_i16x8_s(a, b)), - I8x16NarrowI16x8U => stack_op!(binary Value128, |a, b| Value128::i8x16_narrow_i16x8_u(a, b)), - I16x8NarrowI32x4S => stack_op!(binary Value128, |a, b| Value128::i16x8_narrow_i32x4_s(a, b)), - I16x8NarrowI32x4U => stack_op!(binary Value128, |a, b| Value128::i16x8_narrow_i32x4_u(a, b)), - I8x16AddSatS => stack_op!(binary Value128, |a, b| a.i8x16_add_sat_s(b)), - I16x8AddSatS => stack_op!(binary Value128, |a, b| a.i16x8_add_sat_s(b)), - I8x16AddSatU => stack_op!(binary Value128, |a, b| a.i8x16_add_sat_u(b)), - I16x8AddSatU => stack_op!(binary Value128, |a, b| a.i16x8_add_sat_u(b)), - I8x16SubSatS => stack_op!(binary Value128, |a, b| a.i8x16_sub_sat_s(b)), - I16x8SubSatS => stack_op!(binary Value128, |a, b| a.i16x8_sub_sat_s(b)), - I8x16SubSatU => stack_op!(binary Value128, |a, b| a.i8x16_sub_sat_u(b)), - I16x8SubSatU => stack_op!(binary Value128, |a, b| a.i16x8_sub_sat_u(b)), - I8x16AvgrU => stack_op!(binary Value128, |a, b| a.i8x16_avgr_u(b)), - I16x8AvgrU => stack_op!(binary Value128, |a, b| a.i16x8_avgr_u(b)), - I16x8ExtAddPairwiseI8x16S => stack_op!(unary Value128, |a| a.i16x8_extadd_pairwise_i8x16_s()), - I16x8ExtAddPairwiseI8x16U => stack_op!(unary Value128, |a| a.i16x8_extadd_pairwise_i8x16_u()), - I32x4ExtAddPairwiseI16x8S => stack_op!(unary Value128, |a| a.i32x4_extadd_pairwise_i16x8_s()), - I32x4ExtAddPairwiseI16x8U => stack_op!(unary Value128, |a| a.i32x4_extadd_pairwise_i16x8_u()), - I16x8ExtMulLowI8x16S => stack_op!(binary Value128, |a, b| a.i16x8_extmul_low_i8x16_s(b)), - I16x8ExtMulLowI8x16U => stack_op!(binary Value128, |a, b| a.i16x8_extmul_low_i8x16_u(b)), - I16x8ExtMulHighI8x16S => stack_op!(binary Value128, |a, b| a.i16x8_extmul_high_i8x16_s(b)), - I16x8ExtMulHighI8x16U => stack_op!(binary Value128, |a, b| a.i16x8_extmul_high_i8x16_u(b)), - I32x4ExtMulLowI16x8S => stack_op!(binary Value128, |a, b| a.i32x4_extmul_low_i16x8_s(b)), - I32x4ExtMulLowI16x8U => stack_op!(binary Value128, |a, b| a.i32x4_extmul_low_i16x8_u(b)), - I32x4ExtMulHighI16x8S => stack_op!(binary Value128, |a, b| a.i32x4_extmul_high_i16x8_s(b)), - I32x4ExtMulHighI16x8U => stack_op!(binary Value128, |a, b| a.i32x4_extmul_high_i16x8_u(b)), - I64x2ExtMulLowI32x4S => stack_op!(binary Value128, |a, b| a.i64x2_extmul_low_i32x4_s(b)), - I64x2ExtMulLowI32x4U => stack_op!(binary Value128, |a, b| a.i64x2_extmul_low_i32x4_u(b)), - I64x2ExtMulHighI32x4S => stack_op!(binary Value128, |a, b| a.i64x2_extmul_high_i32x4_s(b)), - I64x2ExtMulHighI32x4U => stack_op!(binary Value128, |a, b| a.i64x2_extmul_high_i32x4_u(b)), - I16x8ExtendLowI8x16S => stack_op!(unary Value128, |a| a.i16x8_extend_low_i8x16_s()), - I16x8ExtendLowI8x16U => stack_op!(unary Value128, |a| a.i16x8_extend_low_i8x16_u()), - I16x8ExtendHighI8x16S => stack_op!(unary Value128, |a| a.i16x8_extend_high_i8x16_s()), - I16x8ExtendHighI8x16U => stack_op!(unary Value128, |a| a.i16x8_extend_high_i8x16_u()), - I32x4ExtendLowI16x8S => stack_op!(unary Value128, |a| a.i32x4_extend_low_i16x8_s()), - I32x4ExtendLowI16x8U => stack_op!(unary Value128, |a| a.i32x4_extend_low_i16x8_u()), - I32x4ExtendHighI16x8S => stack_op!(unary Value128, |a| a.i32x4_extend_high_i16x8_s()), - I32x4ExtendHighI16x8U => stack_op!(unary Value128, |a| a.i32x4_extend_high_i16x8_u()), - I64x2ExtendLowI32x4S => stack_op!(unary Value128, |a| a.i64x2_extend_low_i32x4_s()), - I64x2ExtendLowI32x4U => stack_op!(unary Value128, |a| a.i64x2_extend_low_i32x4_u()), - I64x2ExtendHighI32x4S => stack_op!(unary Value128, |a| a.i64x2_extend_high_i32x4_s()), - I64x2ExtendHighI32x4U => stack_op!(unary Value128, |a| a.i64x2_extend_high_i32x4_u()), - I8x16Popcnt => stack_op!(unary Value128, |v| v.i8x16_popcnt()), - I8x16Shuffle(idx) => stack_op!(binary Value128, |a, b| Value128::i8x16_shuffle(a, b, Value128(self.func.data.v128_const(*idx)))), - I16x8Q15MulrSatS => stack_op!(binary Value128, |a, b| a.i16x8_q15mulr_sat_s(b)), - I32x4DotI16x8S => stack_op!(binary Value128, |a, b| a.i32x4_dot_i16x8_s(b)), - I8x16RelaxedLaneselect => stack_op!(ternary Value128, |a, b, c| Value128::i8x16_relaxed_laneselect(a, b, c)), - I16x8RelaxedLaneselect => stack_op!(ternary Value128, |a, b, c| Value128::i16x8_relaxed_laneselect(a, b, c)), - I32x4RelaxedLaneselect => stack_op!(ternary Value128, |a, b, c| Value128::i32x4_relaxed_laneselect(a, b, c)), - I64x2RelaxedLaneselect => stack_op!(ternary Value128, |a, b, c| Value128::i64x2_relaxed_laneselect(a, b, c)), - I16x8RelaxedQ15mulrS => stack_op!(binary Value128, |a, b| a.i16x8_relaxed_q15mulr_s(b)), - I16x8RelaxedDotI8x16I7x16S => stack_op!(binary Value128, |a, b| a.i16x8_relaxed_dot_i8x16_i7x16_s(b)), - I32x4RelaxedDotI8x16I7x16AddS => stack_op!(ternary Value128, |a, b, c| a.i32x4_relaxed_dot_i8x16_i7x16_add_s(b, c)), - F32x4Ceil => stack_op!(unary Value128, |v| v.f32x4_ceil()), - F64x2Ceil => stack_op!(unary Value128, |v| v.f64x2_ceil()), - F32x4Floor => stack_op!(unary Value128, |v| v.f32x4_floor()), - F64x2Floor => stack_op!(unary Value128, |v| v.f64x2_floor()), - F32x4Trunc => stack_op!(unary Value128, |v| v.f32x4_trunc()), - F64x2Trunc => stack_op!(unary Value128, |v| v.f64x2_trunc()), - F32x4Nearest => stack_op!(unary Value128, |v| v.f32x4_nearest()), - F64x2Nearest => stack_op!(unary Value128, |v| v.f64x2_nearest()), - F32x4Abs => stack_op!(unary Value128, |v| v.f32x4_abs()), - F64x2Abs => stack_op!(unary Value128, |v| v.f64x2_abs()), - F32x4Neg => stack_op!(unary Value128, |v| v.f32x4_neg()), - F64x2Neg => stack_op!(unary Value128, |v| v.f64x2_neg()), - F32x4Sqrt => stack_op!(unary Value128, |v| v.f32x4_sqrt()), - F64x2Sqrt => stack_op!(unary Value128, |v| v.f64x2_sqrt()), - F32x4Add => stack_op!(binary Value128, |a, b| a.f32x4_add(b)), - F64x2Add => stack_op!(binary Value128, |a, b| a.f64x2_add(b)), - F32x4Sub => stack_op!(binary Value128, |a, b| a.f32x4_sub(b)), - F64x2Sub => stack_op!(binary Value128, |a, b| a.f64x2_sub(b)), - F32x4Mul => stack_op!(binary Value128, |a, b| a.f32x4_mul(b)), - F64x2Mul => stack_op!(binary Value128, |a, b| a.f64x2_mul(b)), - F32x4Div => stack_op!(binary Value128, |a, b| a.f32x4_div(b)), - F64x2Div => stack_op!(binary Value128, |a, b| a.f64x2_div(b)), - F32x4Min => stack_op!(binary Value128, |a, b| a.f32x4_min(b)), - F64x2Min => stack_op!(binary Value128, |a, b| a.f64x2_min(b)), - F32x4Max => stack_op!(binary Value128, |a, b| a.f32x4_max(b)), - F64x2Max => stack_op!(binary Value128, |a, b| a.f64x2_max(b)), - F32x4PMin => stack_op!(binary Value128, |a, b| a.f32x4_pmin(b)), - F32x4PMax => stack_op!(binary Value128, |a, b| a.f32x4_pmax(b)), - F64x2PMin => stack_op!(binary Value128, |a, b| a.f64x2_pmin(b)), - F64x2PMax => stack_op!(binary Value128, |a, b| a.f64x2_pmax(b)), - F32x4RelaxedMadd => stack_op!(ternary Value128, |a, b, c| a.f32x4_relaxed_madd(b, c)), - F32x4RelaxedNmadd => stack_op!(ternary Value128, |a, b, c| a.f32x4_relaxed_nmadd(b, c)), - F64x2RelaxedMadd => stack_op!(ternary Value128, |a, b, c| a.f64x2_relaxed_madd(b, c)), - F64x2RelaxedNmadd => stack_op!(ternary Value128, |a, b, c| a.f64x2_relaxed_nmadd(b, c)), - F32x4RelaxedMin => stack_op!(binary Value128, |a, b| a.f32x4_relaxed_min(b)), - F32x4RelaxedMax => stack_op!(binary Value128, |a, b| a.f32x4_relaxed_max(b)), - F64x2RelaxedMin => stack_op!(binary Value128, |a, b| a.f64x2_relaxed_min(b)), - F64x2RelaxedMax => stack_op!(binary Value128, |a, b| a.f64x2_relaxed_max(b)), - I32x4TruncSatF32x4S => stack_op!(unary Value128, |v| v.i32x4_trunc_sat_f32x4_s()), - I32x4TruncSatF32x4U => stack_op!(unary Value128, |v| v.i32x4_trunc_sat_f32x4_u()), - F32x4ConvertI32x4S => stack_op!(unary Value128, |v| v.f32x4_convert_i32x4_s()), - F32x4ConvertI32x4U => stack_op!(unary Value128, |v| v.f32x4_convert_i32x4_u()), - F64x2ConvertLowI32x4S => stack_op!(unary Value128, |v| v.f64x2_convert_low_i32x4_s()), - F64x2ConvertLowI32x4U => stack_op!(unary Value128, |v| v.f64x2_convert_low_i32x4_u()), - F32x4DemoteF64x2Zero => stack_op!(unary Value128, |v| v.f32x4_demote_f64x2_zero()), - F64x2PromoteLowF32x4 => stack_op!(unary Value128, |v| v.f64x2_promote_low_f32x4()), - I32x4TruncSatF64x2SZero => stack_op!(unary Value128, |v| v.i32x4_trunc_sat_f64x2_s_zero()), - I32x4TruncSatF64x2UZero => stack_op!(unary Value128, |v| v.i32x4_trunc_sat_f64x2_u_zero()), - I32x4RelaxedTruncF32x4S => stack_op!(unary Value128, |v| v.i32x4_relaxed_trunc_f32x4_s()), - I32x4RelaxedTruncF32x4U => stack_op!(unary Value128, |v| v.i32x4_relaxed_trunc_f32x4_u()), - I32x4RelaxedTruncF64x2SZero => stack_op!(unary Value128, |v| v.i32x4_relaxed_trunc_f64x2_s_zero()), - I32x4RelaxedTruncF64x2UZero => stack_op!(unary Value128, |v| v.i32x4_relaxed_trunc_f64x2_u_zero()), + I8x16ReplaceLane(lane) => exec_op!(binary_mixed i32, Value128 => Value128, |value, vec| vec.i8x16_replace_lane(*lane, value as i8)), + I16x8ReplaceLane(lane) => exec_op!(binary_mixed i32, Value128 => Value128, |value, vec| vec.i16x8_replace_lane(*lane, value as i16)), + I32x4ReplaceLane(lane) => exec_op!(binary_mixed i32, Value128 => Value128, |value, vec| vec.i32x4_replace_lane(*lane, value)), + I64x2ReplaceLane(lane) => exec_op!(binary_mixed i64, Value128 => Value128, |value, vec| vec.i64x2_replace_lane(*lane, value)), + F32x4ReplaceLane(lane) => exec_op!(binary_mixed f32, Value128 => Value128, |value, vec| vec.f32x4_replace_lane(*lane, value)), + F64x2ReplaceLane(lane) => exec_op!(binary_mixed f64, Value128 => Value128, |value, vec| vec.f64x2_replace_lane(*lane, value)), + I8x16Splat => exec_op!(unary i32 => Value128, |v| Value128::splat_i8(v as i8)), + I16x8Splat => exec_op!(unary i32 => Value128, |v| Value128::splat_i16(v as i16)), + I32x4Splat => exec_op!(unary i32 => Value128, |v| Value128::splat_i32(v)), + I64x2Splat => exec_op!(unary i64 => Value128, |v| Value128::splat_i64(v)), + F32x4Splat => exec_op!(unary f32 => Value128, |v| Value128::splat_f32(v)), + F64x2Splat => exec_op!(unary f64 => Value128, |v| Value128::splat_f64(v)), + I8x16Eq => exec_op!(binary Value128 => Value128, |a, b| a.i8x16_eq(b)), + I16x8Eq => exec_op!(binary Value128 => Value128, |a, b| a.i16x8_eq(b)), + I32x4Eq => exec_op!(binary Value128 => Value128, |a, b| a.i32x4_eq(b)), + I64x2Eq => exec_op!(binary Value128 => Value128, |a, b| a.i64x2_eq(b)), + F32x4Eq => exec_op!(binary Value128 => Value128, |a, b| a.f32x4_eq(b)), + F64x2Eq => exec_op!(binary Value128 => Value128, |a, b| a.f64x2_eq(b)), + I8x16Ne => exec_op!(binary Value128 => Value128, |a, b| a.i8x16_ne(b)), + I16x8Ne => exec_op!(binary Value128 => Value128, |a, b| a.i16x8_ne(b)), + I32x4Ne => exec_op!(binary Value128 => Value128, |a, b| a.i32x4_ne(b)), + I64x2Ne => exec_op!(binary Value128 => Value128, |a, b| a.i64x2_ne(b)), + F32x4Ne => exec_op!(binary Value128 => Value128, |a, b| a.f32x4_ne(b)), + F64x2Ne => exec_op!(binary Value128 => Value128, |a, b| a.f64x2_ne(b)), + I8x16LtS => exec_op!(binary Value128 => Value128, |a, b| a.i8x16_lt_s(b)), + I16x8LtS => exec_op!(binary Value128 => Value128, |a, b| a.i16x8_lt_s(b)), + I32x4LtS => exec_op!(binary Value128 => Value128, |a, b| a.i32x4_lt_s(b)), + I64x2LtS => exec_op!(binary Value128 => Value128, |a, b| a.i64x2_lt_s(b)), + I8x16LtU => exec_op!(binary Value128 => Value128, |a, b| a.i8x16_lt_u(b)), + I16x8LtU => exec_op!(binary Value128 => Value128, |a, b| a.i16x8_lt_u(b)), + I32x4LtU => exec_op!(binary Value128 => Value128, |a, b| a.i32x4_lt_u(b)), + F32x4Lt => exec_op!(binary Value128 => Value128, |a, b| a.f32x4_lt(b)), + F64x2Lt => exec_op!(binary Value128 => Value128, |a, b| a.f64x2_lt(b)), + F32x4Gt => exec_op!(binary Value128 => Value128, |a, b| a.f32x4_gt(b)), + F64x2Gt => exec_op!(binary Value128 => Value128, |a, b| a.f64x2_gt(b)), + I8x16GtS => exec_op!(binary Value128 => Value128, |a, b| a.i8x16_gt_s(b)), + I16x8GtS => exec_op!(binary Value128 => Value128, |a, b| a.i16x8_gt_s(b)), + I32x4GtS => exec_op!(binary Value128 => Value128, |a, b| a.i32x4_gt_s(b)), + I64x2GtS => exec_op!(binary Value128 => Value128, |a, b| a.i64x2_gt_s(b)), + I64x2LeS => exec_op!(binary Value128 => Value128, |a, b| a.i64x2_le_s(b)), + F32x4Le => exec_op!(binary Value128 => Value128, |a, b| a.f32x4_le(b)), + F64x2Le => exec_op!(binary Value128 => Value128, |a, b| a.f64x2_le(b)), + I8x16GtU => exec_op!(binary Value128 => Value128, |a, b| a.i8x16_gt_u(b)), + I16x8GtU => exec_op!(binary Value128 => Value128, |a, b| a.i16x8_gt_u(b)), + I32x4GtU => exec_op!(binary Value128 => Value128, |a, b| a.i32x4_gt_u(b)), + F32x4Ge => exec_op!(binary Value128 => Value128, |a, b| a.f32x4_ge(b)), + F64x2Ge => exec_op!(binary Value128 => Value128, |a, b| a.f64x2_ge(b)), + I8x16LeS => exec_op!(binary Value128 => Value128, |a, b| a.i8x16_le_s(b)), + I16x8LeS => exec_op!(binary Value128 => Value128, |a, b| a.i16x8_le_s(b)), + I32x4LeS => exec_op!(binary Value128 => Value128, |a, b| a.i32x4_le_s(b)), + I8x16LeU => exec_op!(binary Value128 => Value128, |a, b| a.i8x16_le_u(b)), + I16x8LeU => exec_op!(binary Value128 => Value128, |a, b| a.i16x8_le_u(b)), + I32x4LeU => exec_op!(binary Value128 => Value128, |a, b| a.i32x4_le_u(b)), + I8x16GeS => exec_op!(binary Value128 => Value128, |a, b| a.i8x16_ge_s(b)), + I16x8GeS => exec_op!(binary Value128 => Value128, |a, b| a.i16x8_ge_s(b)), + I32x4GeS => exec_op!(binary Value128 => Value128, |a, b| a.i32x4_ge_s(b)), + I64x2GeS => exec_op!(binary Value128 => Value128, |a, b| a.i64x2_ge_s(b)), + I8x16GeU => exec_op!(binary Value128 => Value128, |a, b| a.i8x16_ge_u(b)), + I16x8GeU => exec_op!(binary Value128 => Value128, |a, b| a.i16x8_ge_u(b)), + I32x4GeU => exec_op!(binary Value128 => Value128, |a, b| a.i32x4_ge_u(b)), + I8x16Abs => exec_op!(unary Value128 => Value128, |a| a.i8x16_abs()), + I16x8Abs => exec_op!(unary Value128 => Value128, |a| a.i16x8_abs()), + I32x4Abs => exec_op!(unary Value128 => Value128, |a| a.i32x4_abs()), + I64x2Abs => exec_op!(unary Value128 => Value128, |a| a.i64x2_abs()), + I8x16Neg => exec_op!(unary Value128 => Value128, |a| a.i8x16_neg()), + I16x8Neg => exec_op!(unary Value128 => Value128, |a| a.i16x8_neg()), + I32x4Neg => exec_op!(unary Value128 => Value128, |a| a.i32x4_neg()), + I64x2Neg => exec_op!(unary Value128 => Value128, |a| a.i64x2_neg()), + I8x16AllTrue => exec_op!(unary Value128 => i32, |v| v.i8x16_all_true() as i32), + I16x8AllTrue => exec_op!(unary Value128 => i32, |v| v.i16x8_all_true() as i32), + I32x4AllTrue => exec_op!(unary Value128 => i32, |v| v.i32x4_all_true() as i32), + I64x2AllTrue => exec_op!(unary Value128 => i32, |v| v.i64x2_all_true() as i32), + I8x16Bitmask => exec_op!(unary Value128 => i32, |v| v.i8x16_bitmask() as i32), + I16x8Bitmask => exec_op!(unary Value128 => i32, |v| v.i16x8_bitmask() as i32), + I32x4Bitmask => exec_op!(unary Value128 => i32, |v| v.i32x4_bitmask() as i32), + I64x2Bitmask => exec_op!(unary Value128 => i32, |v| v.i64x2_bitmask() as i32), + I8x16Shl => exec_op!(binary_mixed i32, Value128 => Value128, |a, b| b.i8x16_shl(a as u32)), + I16x8Shl => exec_op!(binary_mixed i32, Value128 => Value128, |a, b| b.i16x8_shl(a as u32)), + I32x4Shl => exec_op!(binary_mixed i32, Value128 => Value128, |a, b| b.i32x4_shl(a as u32)), + I64x2Shl => exec_op!(binary_mixed i32, Value128 => Value128, |a, b| b.i64x2_shl(a as u32)), + I8x16ShrS => exec_op!(binary_mixed i32, Value128 => Value128, |a, b| b.i8x16_shr_s(a as u32)), + I16x8ShrS => exec_op!(binary_mixed i32, Value128 => Value128, |a, b| b.i16x8_shr_s(a as u32)), + I32x4ShrS => exec_op!(binary_mixed i32, Value128 => Value128, |a, b| b.i32x4_shr_s(a as u32)), + I64x2ShrS => exec_op!(binary_mixed i32, Value128 => Value128, |a, b| b.i64x2_shr_s(a as u32)), + I8x16ShrU => exec_op!(binary_mixed i32, Value128 => Value128, |a, b| b.i8x16_shr_u(a as u32)), + I16x8ShrU => exec_op!(binary_mixed i32, Value128 => Value128, |a, b| b.i16x8_shr_u(a as u32)), + I32x4ShrU => exec_op!(binary_mixed i32, Value128 => Value128, |a, b| b.i32x4_shr_u(a as u32)), + I64x2ShrU => exec_op!(binary_mixed i32, Value128 => Value128, |a, b| b.i64x2_shr_u(a as u32)), + I8x16Add => exec_op!(binary Value128 => Value128, |a, b| a.i8x16_add(b)), + I16x8Add => exec_op!(binary Value128 => Value128, |a, b| a.i16x8_add(b)), + I32x4Add => exec_op!(binary Value128 => Value128, |a, b| a.i32x4_add(b)), + I64x2Add => exec_op!(binary Value128 => Value128, |a, b| a.i64x2_add(b)), + I8x16Sub => exec_op!(binary Value128 => Value128, |a, b| a.i8x16_sub(b)), + I16x8Sub => exec_op!(binary Value128 => Value128, |a, b| a.i16x8_sub(b)), + I32x4Sub => exec_op!(binary Value128 => Value128, |a, b| a.i32x4_sub(b)), + I64x2Sub => exec_op!(binary Value128 => Value128, |a, b| a.i64x2_sub(b)), + I8x16MinS => exec_op!(binary Value128 => Value128, |a, b| a.i8x16_min_s(b)), + I16x8MinS => exec_op!(binary Value128 => Value128, |a, b| a.i16x8_min_s(b)), + I32x4MinS => exec_op!(binary Value128 => Value128, |a, b| a.i32x4_min_s(b)), + I8x16MinU => exec_op!(binary Value128 => Value128, |a, b| a.i8x16_min_u(b)), + I16x8MinU => exec_op!(binary Value128 => Value128, |a, b| a.i16x8_min_u(b)), + I32x4MinU => exec_op!(binary Value128 => Value128, |a, b| a.i32x4_min_u(b)), + I8x16MaxS => exec_op!(binary Value128 => Value128, |a, b| a.i8x16_max_s(b)), + I16x8MaxS => exec_op!(binary Value128 => Value128, |a, b| a.i16x8_max_s(b)), + I32x4MaxS => exec_op!(binary Value128 => Value128, |a, b| a.i32x4_max_s(b)), + I8x16MaxU => exec_op!(binary Value128 => Value128, |a, b| a.i8x16_max_u(b)), + I16x8MaxU => exec_op!(binary Value128 => Value128, |a, b| a.i16x8_max_u(b)), + I32x4MaxU => exec_op!(binary Value128 => Value128, |a, b| a.i32x4_max_u(b)), + I64x2Mul => exec_op!(binary Value128 => Value128, |a, b| a.i64x2_mul(b)), + I16x8Mul => exec_op!(binary Value128 => Value128, |a, b| a.i16x8_mul(b)), + I32x4Mul => exec_op!(binary Value128 => Value128, |a, b| a.i32x4_mul(b)), + I8x16NarrowI16x8S => exec_op!(binary Value128 => Value128, |a, b| Value128::i8x16_narrow_i16x8_s(a, b)), + I8x16NarrowI16x8U => exec_op!(binary Value128 => Value128, |a, b| Value128::i8x16_narrow_i16x8_u(a, b)), + I16x8NarrowI32x4S => exec_op!(binary Value128 => Value128, |a, b| Value128::i16x8_narrow_i32x4_s(a, b)), + I16x8NarrowI32x4U => exec_op!(binary Value128 => Value128, |a, b| Value128::i16x8_narrow_i32x4_u(a, b)), + I8x16AddSatS => exec_op!(binary Value128 => Value128, |a, b| a.i8x16_add_sat_s(b)), + I16x8AddSatS => exec_op!(binary Value128 => Value128, |a, b| a.i16x8_add_sat_s(b)), + I8x16AddSatU => exec_op!(binary Value128 => Value128, |a, b| a.i8x16_add_sat_u(b)), + I16x8AddSatU => exec_op!(binary Value128 => Value128, |a, b| a.i16x8_add_sat_u(b)), + I8x16SubSatS => exec_op!(binary Value128 => Value128, |a, b| a.i8x16_sub_sat_s(b)), + I16x8SubSatS => exec_op!(binary Value128 => Value128, |a, b| a.i16x8_sub_sat_s(b)), + I8x16SubSatU => exec_op!(binary Value128 => Value128, |a, b| a.i8x16_sub_sat_u(b)), + I16x8SubSatU => exec_op!(binary Value128 => Value128, |a, b| a.i16x8_sub_sat_u(b)), + I8x16AvgrU => exec_op!(binary Value128 => Value128, |a, b| a.i8x16_avgr_u(b)), + I16x8AvgrU => exec_op!(binary Value128 => Value128, |a, b| a.i16x8_avgr_u(b)), + I16x8ExtAddPairwiseI8x16S => exec_op!(unary Value128 => Value128, |a| a.i16x8_extadd_pairwise_i8x16_s()), + I16x8ExtAddPairwiseI8x16U => exec_op!(unary Value128 => Value128, |a| a.i16x8_extadd_pairwise_i8x16_u()), + I32x4ExtAddPairwiseI16x8S => exec_op!(unary Value128 => Value128, |a| a.i32x4_extadd_pairwise_i16x8_s()), + I32x4ExtAddPairwiseI16x8U => exec_op!(unary Value128 => Value128, |a| a.i32x4_extadd_pairwise_i16x8_u()), + I16x8ExtMulLowI8x16S => exec_op!(binary Value128 => Value128, |a, b| a.i16x8_extmul_low_i8x16_s(b)), + I16x8ExtMulLowI8x16U => exec_op!(binary Value128 => Value128, |a, b| a.i16x8_extmul_low_i8x16_u(b)), + I16x8ExtMulHighI8x16S => exec_op!(binary Value128 => Value128, |a, b| a.i16x8_extmul_high_i8x16_s(b)), + I16x8ExtMulHighI8x16U => exec_op!(binary Value128 => Value128, |a, b| a.i16x8_extmul_high_i8x16_u(b)), + I32x4ExtMulLowI16x8S => exec_op!(binary Value128 => Value128, |a, b| a.i32x4_extmul_low_i16x8_s(b)), + I32x4ExtMulLowI16x8U => exec_op!(binary Value128 => Value128, |a, b| a.i32x4_extmul_low_i16x8_u(b)), + I32x4ExtMulHighI16x8S => exec_op!(binary Value128 => Value128, |a, b| a.i32x4_extmul_high_i16x8_s(b)), + I32x4ExtMulHighI16x8U => exec_op!(binary Value128 => Value128, |a, b| a.i32x4_extmul_high_i16x8_u(b)), + I64x2ExtMulLowI32x4S => exec_op!(binary Value128 => Value128, |a, b| a.i64x2_extmul_low_i32x4_s(b)), + I64x2ExtMulLowI32x4U => exec_op!(binary Value128 => Value128, |a, b| a.i64x2_extmul_low_i32x4_u(b)), + I64x2ExtMulHighI32x4S => exec_op!(binary Value128 => Value128, |a, b| a.i64x2_extmul_high_i32x4_s(b)), + I64x2ExtMulHighI32x4U => exec_op!(binary Value128 => Value128, |a, b| a.i64x2_extmul_high_i32x4_u(b)), + I16x8ExtendLowI8x16S => exec_op!(unary Value128 => Value128, |a| a.i16x8_extend_low_i8x16_s()), + I16x8ExtendLowI8x16U => exec_op!(unary Value128 => Value128, |a| a.i16x8_extend_low_i8x16_u()), + I16x8ExtendHighI8x16S => exec_op!(unary Value128 => Value128, |a| a.i16x8_extend_high_i8x16_s()), + I16x8ExtendHighI8x16U => exec_op!(unary Value128 => Value128, |a| a.i16x8_extend_high_i8x16_u()), + I32x4ExtendLowI16x8S => exec_op!(unary Value128 => Value128, |a| a.i32x4_extend_low_i16x8_s()), + I32x4ExtendLowI16x8U => exec_op!(unary Value128 => Value128, |a| a.i32x4_extend_low_i16x8_u()), + I32x4ExtendHighI16x8S => exec_op!(unary Value128 => Value128, |a| a.i32x4_extend_high_i16x8_s()), + I32x4ExtendHighI16x8U => exec_op!(unary Value128 => Value128, |a| a.i32x4_extend_high_i16x8_u()), + I64x2ExtendLowI32x4S => exec_op!(unary Value128 => Value128, |a| a.i64x2_extend_low_i32x4_s()), + I64x2ExtendLowI32x4U => exec_op!(unary Value128 => Value128, |a| a.i64x2_extend_low_i32x4_u()), + I64x2ExtendHighI32x4S => exec_op!(unary Value128 => Value128, |a| a.i64x2_extend_high_i32x4_s()), + I64x2ExtendHighI32x4U => exec_op!(unary Value128 => Value128, |a| a.i64x2_extend_high_i32x4_u()), + I8x16Popcnt => exec_op!(unary Value128 => Value128, |v| v.i8x16_popcnt()), + I8x16Shuffle(idx) => exec_op!(binary Value128 => Value128, |a, b| Value128::i8x16_shuffle(a, b, Value128(self.func.data.v128_const(*idx)))), + I16x8Q15MulrSatS => exec_op!(binary Value128 => Value128, |a, b| a.i16x8_q15mulr_sat_s(b)), + I32x4DotI16x8S => exec_op!(binary Value128 => Value128, |a, b| a.i32x4_dot_i16x8_s(b)), + I8x16RelaxedLaneselect => exec_op!(ternary Value128 => Value128, |a, b, c| Value128::i8x16_relaxed_laneselect(a, b, c)), + I16x8RelaxedLaneselect => exec_op!(ternary Value128 => Value128, |a, b, c| Value128::i16x8_relaxed_laneselect(a, b, c)), + I32x4RelaxedLaneselect => exec_op!(ternary Value128 => Value128, |a, b, c| Value128::i32x4_relaxed_laneselect(a, b, c)), + I64x2RelaxedLaneselect => exec_op!(ternary Value128 => Value128, |a, b, c| Value128::i64x2_relaxed_laneselect(a, b, c)), + I16x8RelaxedQ15mulrS => exec_op!(binary Value128 => Value128, |a, b| a.i16x8_relaxed_q15mulr_s(b)), + I16x8RelaxedDotI8x16I7x16S => exec_op!(binary Value128 => Value128, |a, b| a.i16x8_relaxed_dot_i8x16_i7x16_s(b)), + I32x4RelaxedDotI8x16I7x16AddS => exec_op!(ternary Value128 => Value128, |a, b, c| a.i32x4_relaxed_dot_i8x16_i7x16_add_s(b, c)), + F32x4Ceil => exec_op!(unary Value128 => Value128, |v| v.f32x4_ceil()), + F64x2Ceil => exec_op!(unary Value128 => Value128, |v| v.f64x2_ceil()), + F32x4Floor => exec_op!(unary Value128 => Value128, |v| v.f32x4_floor()), + F64x2Floor => exec_op!(unary Value128 => Value128, |v| v.f64x2_floor()), + F32x4Trunc => exec_op!(unary Value128 => Value128, |v| v.f32x4_trunc()), + F64x2Trunc => exec_op!(unary Value128 => Value128, |v| v.f64x2_trunc()), + F32x4Nearest => exec_op!(unary Value128 => Value128, |v| v.f32x4_nearest()), + F64x2Nearest => exec_op!(unary Value128 => Value128, |v| v.f64x2_nearest()), + F32x4Abs => exec_op!(unary Value128 => Value128, |v| v.f32x4_abs()), + F64x2Abs => exec_op!(unary Value128 => Value128, |v| v.f64x2_abs()), + F32x4Neg => exec_op!(unary Value128 => Value128, |v| v.f32x4_neg()), + F64x2Neg => exec_op!(unary Value128 => Value128, |v| v.f64x2_neg()), + F32x4Sqrt => exec_op!(unary Value128 => Value128, |v| v.f32x4_sqrt()), + F64x2Sqrt => exec_op!(unary Value128 => Value128, |v| v.f64x2_sqrt()), + F32x4Add => exec_op!(binary Value128 => Value128, |a, b| a.f32x4_add(b)), + F64x2Add => exec_op!(binary Value128 => Value128, |a, b| a.f64x2_add(b)), + F32x4Sub => exec_op!(binary Value128 => Value128, |a, b| a.f32x4_sub(b)), + F64x2Sub => exec_op!(binary Value128 => Value128, |a, b| a.f64x2_sub(b)), + F32x4Mul => exec_op!(binary Value128 => Value128, |a, b| a.f32x4_mul(b)), + F64x2Mul => exec_op!(binary Value128 => Value128, |a, b| a.f64x2_mul(b)), + F32x4Div => exec_op!(binary Value128 => Value128, |a, b| a.f32x4_div(b)), + F64x2Div => exec_op!(binary Value128 => Value128, |a, b| a.f64x2_div(b)), + F32x4Min => exec_op!(binary Value128 => Value128, |a, b| a.f32x4_min(b)), + F64x2Min => exec_op!(binary Value128 => Value128, |a, b| a.f64x2_min(b)), + F32x4Max => exec_op!(binary Value128 => Value128, |a, b| a.f32x4_max(b)), + F64x2Max => exec_op!(binary Value128 => Value128, |a, b| a.f64x2_max(b)), + F32x4PMin => exec_op!(binary Value128 => Value128, |a, b| a.f32x4_pmin(b)), + F32x4PMax => exec_op!(binary Value128 => Value128, |a, b| a.f32x4_pmax(b)), + F64x2PMin => exec_op!(binary Value128 => Value128, |a, b| a.f64x2_pmin(b)), + F64x2PMax => exec_op!(binary Value128 => Value128, |a, b| a.f64x2_pmax(b)), + F32x4RelaxedMadd => exec_op!(ternary Value128 => Value128, |a, b, c| a.f32x4_relaxed_madd(b, c)), + F32x4RelaxedNmadd => exec_op!(ternary Value128 => Value128, |a, b, c| a.f32x4_relaxed_nmadd(b, c)), + F64x2RelaxedMadd => exec_op!(ternary Value128 => Value128, |a, b, c| a.f64x2_relaxed_madd(b, c)), + F64x2RelaxedNmadd => exec_op!(ternary Value128 => Value128, |a, b, c| a.f64x2_relaxed_nmadd(b, c)), + F32x4RelaxedMin => exec_op!(binary Value128 => Value128, |a, b| a.f32x4_relaxed_min(b)), + F32x4RelaxedMax => exec_op!(binary Value128 => Value128, |a, b| a.f32x4_relaxed_max(b)), + F64x2RelaxedMin => exec_op!(binary Value128 => Value128, |a, b| a.f64x2_relaxed_min(b)), + F64x2RelaxedMax => exec_op!(binary Value128 => Value128, |a, b| a.f64x2_relaxed_max(b)), + I32x4TruncSatF32x4S => exec_op!(unary Value128 => Value128, |v| v.i32x4_trunc_sat_f32x4_s()), + I32x4TruncSatF32x4U => exec_op!(unary Value128 => Value128, |v| v.i32x4_trunc_sat_f32x4_u()), + F32x4ConvertI32x4S => exec_op!(unary Value128 => Value128, |v| v.f32x4_convert_i32x4_s()), + F32x4ConvertI32x4U => exec_op!(unary Value128 => Value128, |v| v.f32x4_convert_i32x4_u()), + F64x2ConvertLowI32x4S => exec_op!(unary Value128 => Value128, |v| v.f64x2_convert_low_i32x4_s()), + F64x2ConvertLowI32x4U => exec_op!(unary Value128 => Value128, |v| v.f64x2_convert_low_i32x4_u()), + F32x4DemoteF64x2Zero => exec_op!(unary Value128 => Value128, |v| v.f32x4_demote_f64x2_zero()), + F64x2PromoteLowF32x4 => exec_op!(unary Value128 => Value128, |v| v.f64x2_promote_low_f32x4()), + I32x4TruncSatF64x2SZero => exec_op!(unary Value128 => Value128, |v| v.i32x4_trunc_sat_f64x2_s_zero()), + I32x4TruncSatF64x2UZero => exec_op!(unary Value128 => Value128, |v| v.i32x4_trunc_sat_f64x2_u_zero()), + I32x4RelaxedTruncF32x4S => exec_op!(unary Value128 => Value128, |v| v.i32x4_relaxed_trunc_f32x4_s()), + I32x4RelaxedTruncF32x4U => exec_op!(unary Value128 => Value128, |v| v.i32x4_relaxed_trunc_f32x4_u()), + I32x4RelaxedTruncF64x2SZero => exec_op!(unary Value128 => Value128, |v| v.i32x4_relaxed_trunc_f64x2_s_zero()), + I32x4RelaxedTruncF64x2UZero => exec_op!(unary Value128 => Value128, |v| v.i32x4_relaxed_trunc_f64x2_u_zero()), }; - self.cf.instr_ptr += 1; + self.cf.instr_ptr = instr_ptr + 1; Ok(None) } #[inline(always)] - fn jump_if(&mut self, condition: bool, ip: u32) -> bool { + fn exec_jump_if(cf: &mut CallFrame, target_ip: u32, condition: impl FnOnce(&CallFrame) -> bool) -> bool { + let condition = condition(cf); if condition { - self.cf.instr_ptr = ip as usize; + cf.instr_ptr = target_ip as usize; } condition } - #[inline(always)] - fn exec_jump_zero_32(&mut self, target_ip: u32) -> bool { - let cond = ::stack_pop(&mut self.store.value_stack) == 0; - self.jump_if(cond, target_ip) - } - - #[inline(always)] - fn exec_jump_non_zero_32(&mut self, target_ip: u32) -> bool { - let cond = ::stack_pop(&mut self.store.value_stack) != 0; - self.jump_if(cond, target_ip) - } - - #[inline(always)] - fn exec_jump_zero_64(&mut self, target_ip: u32) -> bool { - let cond = ::stack_pop(&mut self.store.value_stack) == 0; - self.jump_if(cond, target_ip) - } - - #[inline(always)] - fn exec_jump_non_zero_64(&mut self, target_ip: u32) -> bool { - let cond = ::stack_pop(&mut self.store.value_stack) != 0; - self.jump_if(cond, target_ip) - } - - #[inline(always)] - fn exec_jump_ref_null(&mut self, target_ip: u32) -> bool { - let is_null = ValueRef::stack_peek(&self.store.value_stack).is_null(); - if is_null { - ValueRef::stack_pop(&mut self.store.value_stack); - } - self.jump_if(is_null, target_ip) - } - - #[inline(always)] - fn exec_jump_ref_non_null(&mut self, target_ip: u32) -> bool { - let is_non_null = !ValueRef::stack_peek(&self.store.value_stack).is_null(); - if !is_non_null { - ValueRef::stack_pop(&mut self.store.value_stack); - } - self.jump_if(is_non_null, target_ip) - } - - #[inline(always)] - fn exec_jump_local_zero_32(&mut self, target_ip: u32, local: LocalAddr) -> bool { - self.jump_if(Value32::local_get(&self.store.value_stack, &self.cf, local) == 0, target_ip) - } - - #[inline(always)] - fn exec_jump_local_non_zero_32(&mut self, target_ip: u32, local: LocalAddr) -> bool { - self.jump_if(Value32::local_get(&self.store.value_stack, &self.cf, local) != 0, target_ip) - } - - #[inline(always)] - fn exec_jump_local_zero_64(&mut self, target_ip: u32, local: LocalAddr) -> bool { - self.jump_if(Value64::local_get(&self.store.value_stack, &self.cf, local) == 0, target_ip) - } - - #[inline(always)] - fn exec_jump_local_non_zero_64(&mut self, target_ip: u32, local: LocalAddr) -> bool { - self.jump_if(Value64::local_get(&self.store.value_stack, &self.cf, local) != 0, target_ip) - } - - #[inline(always)] - fn exec_jump_cmp_stack_const_32(&mut self, target_ip: u32, imm: i32, op: CmpOp) -> bool { - let condition = cmp_i32(::stack_pop(&mut self.store.value_stack), imm, op); - self.jump_if(condition, target_ip) - } - - #[inline(always)] - fn exec_jump_cmp_stack_const_64(&mut self, target_ip: u32, imm: i64, op: CmpOp) -> bool { - let condition = cmp_i64(::stack_pop(&mut self.store.value_stack), imm, op); - self.jump_if(condition, target_ip) - } - - #[inline(always)] - fn exec_jump_cmp_local_const_32(&mut self, target_ip: u32, local: LocalAddr, imm: i32, op: CmpOp) -> bool { - self.jump_if(cmp_i32(i32::local_get(&self.store.value_stack, &self.cf, local), imm, op), target_ip) - } - - #[inline(always)] - fn exec_jump_cmp_local_const_64(&mut self, target_ip: u32, local: LocalAddr, imm: i32, op: CmpOp) -> bool { - self.jump_if(cmp_i64(i64::local_get(&self.store.value_stack, &self.cf, local), i64::from(imm), op), target_ip) - } - - #[inline(always)] - fn exec_jump_cmp_local_local_32(&mut self, target_ip: u32, left: LocalAddr, right: LocalAddr, op: CmpOp) -> bool { - let lhs = i32::local_get(&self.store.value_stack, &self.cf, left); - let rhs = i32::local_get(&self.store.value_stack, &self.cf, right); - self.jump_if(cmp_i32(lhs, rhs, op), target_ip) - } - - #[inline(always)] - fn exec_jump_cmp_local_local_64(&mut self, target_ip: u32, left: LocalAddr, right: LocalAddr, op: CmpOp) -> bool { - let lhs = i64::local_get(&self.store.value_stack, &self.cf, left); - let rhs = i64::local_get(&self.store.value_stack, &self.cf, right); - self.jump_if(cmp_i64(lhs, rhs, op), target_ip) - } - fn exec_branch_table(&mut self, default_ip: u32, start: u32, len: u32) { let idx = ::stack_pop(&mut self.store.value_stack); let target_ip = if idx >= 0 && (idx as u32) < len { @@ -944,6 +919,130 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { self.store.value_stack.truncate_keep_counts(base, drop_keep.keep); } + fn create_exception(&mut self, tag_index: TagAddr) -> Result { + let tag_addr = self.module.resolve_tag_addr(tag_index); + let type_addr = self.store.state.get_tag(tag_addr).type_addr; + let addr = cold_err!(u32::try_from(self.store.state.exceptions.len())).map_err(|_| Trap::OutOfMemory)?; + cold_err!(self.store.state.exceptions.try_reserve(1)).map_err(|_| Trap::OutOfMemory)?; + let params = self.store.state.get_canonical_func_type(type_addr).params(); + let mut payload = Vec::new(); + cold_err!(payload.try_reserve_exact(params.len())).map_err(|_| Trap::OutOfMemory)?; + let value_stack = &mut self.store.value_stack; + for &ty in params.iter().rev() { + payload.push(match ty { + WasmType::I32 | WasmType::F32 => TinyWasmValue::Value32(Value32::stack_pop(value_stack)), + WasmType::I64 | WasmType::F64 => TinyWasmValue::Value64(Value64::stack_pop(value_stack)), + WasmType::V128 => TinyWasmValue::Value128(Value128::stack_pop(value_stack)), + WasmType::Ref(_) => TinyWasmValue::ValueRef(ValueRef::stack_pop(value_stack)), + }); + } + payload.reverse(); + let exception = crate::store::ExceptionInstance { tag_addr, payload: payload.into_boxed_slice() }; + self.store.state.exceptions.push(exception); + Ok(addr) + } + + fn exec_throw(&mut self, tag_index: TagAddr) -> Result<()> { + let exception = self.create_exception(tag_index)?; + let outcome = match self.dispatch_exception(exception) { + Ok(outcome) => outcome, + Err(trap) => { + _ = self.store.state.exceptions.pop(); + return Err(trap.into()); + } + }; + match outcome { + Some(catch) if !catch.with_ref() => { + debug_assert_eq!(self.store.state.exceptions.len() - 1, exception as usize); + _ = self.store.state.exceptions.pop(); + Ok(()) + } + Some(_) => Ok(()), + None => Err(Error::Exception(ExnRef::new(exception))), + } + } + + fn exec_throw_ref(&mut self) -> Result<()> { + let exception = ValueRef::stack_pop(&mut self.store.value_stack); + let exception = exception + .addr() + .filter(|addr| self.store.state.exceptions.get(*addr as usize).is_some()) + .ok_or(Trap::NullReference)?; + match self.dispatch_exception(exception)? { + Some(_) => Ok(()), + None => Err(Error::Exception(ExnRef::new(exception))), + } + } + + fn matching_catch(&self, protected_ip: usize, tag_addr: TagAddr) -> Option { + let handlers = &self.func.data.exception_handlers; + let end = handlers.partition_point(|handler| handler.start_ip as usize <= protected_ip); + handlers[..end] + .iter() + .rev() + .filter(|handler| protected_ip < handler.end_ip as usize) + .flat_map(|handler| handler.catches.iter().copied()) + .find(|catch| match catch { + ExceptionCatch::Tag { tag, .. } => self.module.resolve_tag_addr(*tag) == tag_addr, + ExceptionCatch::All { .. } => true, + }) + } + + fn switch_to_frame(&mut self, frame: CallFrame) { + let previous = core::mem::replace(&mut self.cf, frame); + if previous.func_addr == self.cf.func_addr { + return; + } + + let wasm_func = self.store.state.get_wasm_func(self.cf.func_addr); + self.func = wasm_func.func.clone(); + if wasm_func.owner != self.module.id() { + self.module = self + .store + .get_module_instance(wasm_func.owner) + .unwrap_or_else(|| unreachable!("invalid module instance")) + .clone(); + } + } + + fn dispatch_exception(&mut self, exception_addr: ExnAddr) -> Result, Trap> { + let tag_addr = self.store.state.exceptions[exception_addr as usize].tag_addr; + let mut protected_ip = self.cf.instr_ptr; + loop { + if let Some(catch) = self.matching_catch(protected_ip, tag_addr) { + let (landing_pad, base, with_ref, include_payload) = match catch { + ExceptionCatch::Tag { landing_pad, base, with_ref, .. } => (landing_pad, base, with_ref, true), + ExceptionCatch::All { landing_pad, base, with_ref } => (landing_pad, base, with_ref, false), + }; + let stack_base = self.cf.stack_base(); + let target = interpreter::stack::StackBase { + s32: stack_base.s32 + base.c32 as u32, + s64: stack_base.s64 + base.c64 as u32, + s128: stack_base.s128 + base.c128 as u32, + }; + self.store.value_stack.truncate_to_base(target); + if include_payload { + let Store { state, value_stack, .. } = self.store; + for value in state.exceptions[exception_addr as usize].payload.iter().copied() { + value_stack.push_dyn(value)?; + } + } + if with_ref { + self.store.value_stack.push(ValueRef::from_category_addr(exception_addr))?; + } + self.cf.instr_ptr = landing_pad as usize; + return Ok(Some(catch)); + } + + self.store.value_stack.truncate_to_base(self.cf.locals_base); + let Some(caller) = self.store.call_stack.pop_frame(self.call_stack_base) else { + return Ok(None); + }; + self.switch_to_frame(caller); + protected_ip = self.cf.instr_ptr.checked_sub(1).unwrap_or_else(|| unreachable!("invalid caller IP")); + } + } + fn exec_call(&mut self, wasm_func: WasmFunctionInstance, func_addr: FuncAddr) -> Result<(), Trap> { if !Arc::ptr_eq(&self.func, &wasm_func.func) { self.func = wasm_func.func.clone(); @@ -951,14 +1050,17 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { let Ok(locals_base) = self.store.value_stack.enter_locals(&wasm_func.func.params, &wasm_func.func.locals) else { - cold_path(); - return Err(Trap::CallStackOverflow); + return cold!(Err(Trap::CallStackOverflow)); }; self.store.call_stack.push(self.cf)?; self.cf = CallFrame::new(func_addr, locals_base, wasm_func.func.locals); if wasm_func.owner != self.module.id() { - self.module = self.store.get_module_instance_internal(wasm_func.owner); + self.module = self + .store + .get_module_instance(wasm_func.owner) + .unwrap_or_else(|| unreachable!("invalid module instance")) + .clone(); } Ok(()) @@ -972,12 +1074,15 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { self.store.value_stack.truncate_keep_counts(self.cf.locals_base, wasm_func.func.params); let Ok(locals_base) = self.store.value_stack.enter_locals(&wasm_func.func.params, &wasm_func.func.locals) else { - cold_path(); - return Err(Trap::CallStackOverflow); + return cold!(Err(Trap::CallStackOverflow)); }; self.cf = CallFrame::new(func_addr, locals_base, wasm_func.func.locals); if wasm_func.owner != self.module.id() { - self.module = self.store.get_module_instance_internal(wasm_func.owner); + self.module = self + .store + .get_module_instance(wasm_func.owner) + .unwrap_or_else(|| unreachable!("invalid module instance")) + .clone(); } Ok(()) @@ -985,22 +1090,37 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { fn exec_call_host( &mut self, - host_func: Rc, + host_func: HostFunction, type_addr: TypeAddr, + params_may_gc: bool, ) -> Result { - let ty = self.store.state.get_type(type_addr); - let mut params = self.store.value_stack.pop_types(ty.params().iter().rev()).collect::>(); - params.reverse(); - let result = host_func.call(FuncContext { store: self.store, module_id: self.module.id() }, ¶ms); - let res = match result.and_then(|result| crate::func::validate_host_results(self.store, type_addr, result)) { - Ok(res) => res, - Err(err) => { - cold_path(); - return Err(Trap::HostFunction(Box::new(err))); + if let Some(host_func) = host_func.typed_callback() { + cold_err!(host_func.call_stack(self.store, self.module.id(), type_addr)) + .map_err(|error| Trap::HostFunction(Box::new(error)))?; + if TAIL { + return Ok(self.exec_return()); } - }; + self.cf.instr_ptr += 1; + return Ok(false); + } + + let param_types = self.store.state.get_canonical_func_type(type_addr).params(); + let mut params = core::mem::take(&mut self.store.host_params); + debug_assert!(params.is_empty()); + cold_err!(params.try_reserve_exact(param_types.len())).map_err(|_| Trap::OutOfMemory)?; + for &ty in param_types.iter().rev() { + params.push(self.store.value_stack.pop_wasmvalue(&self.store.state, ty)); + } + params.reverse(); + if params_may_gc { + self.store.state.pin_host_values(¶ms); + } + let result = host_func.call_values(self.store, self.module.id(), type_addr, ¶ms); + params.clear(); + self.store.host_params = params; + let res = cold_err!(result).map_err(|error| Trap::HostFunction(Box::new(error)))?; - self.store.value_stack.extend_from_wasmvalues(&res)?; + self.store.value_stack.extend_wasmvalues(res.iter().copied())?; if TAIL { Ok(self.exec_return()) } else { @@ -1016,7 +1136,7 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { match func.kind { crate::store::FunctionKind::Wasm(wasm_func) => self.exec_call(wasm_func, addr), crate::store::FunctionKind::Host(host_func) => { - self.exec_call_host::(host_func, func.type_addr)?; + self.exec_call_host::(host_func, func.type_addr, func.gc.params)?; Ok(()) } } @@ -1031,19 +1151,19 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { self.exec_return_call(wasm_func, addr)?; Ok(false) } - crate::store::FunctionKind::Host(host_func) => self.exec_call_host::(host_func, func.type_addr), + crate::store::FunctionKind::Host(host_func) => { + self.exec_call_host::(host_func, func.type_addr, func.gc.params) + } } } fn exec_call_self(&mut self) -> Result<(), Trap> { self.charge_call_fuel(FUEL_COST_CALL_TOTAL); - - self.store.call_stack.push(self.cf)?; let Ok(locals_base) = self.store.value_stack.enter_locals(&self.func.params, &self.func.locals) else { - cold_path(); - return Err(Trap::CallStackOverflow); + return cold!(Err(Trap::CallStackOverflow)); }; - self.cf = CallFrame::new(self.cf.func_addr, locals_base, self.func.locals); + let new = CallFrame::new(self.cf.func_addr, locals_base, self.func.locals); + self.store.call_stack.push(core::mem::replace(&mut self.cf, new))?; Ok(()) } @@ -1053,8 +1173,7 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { self.store.value_stack.truncate_keep_counts(self.cf.locals_base, self.func.params); let Ok(locals_base) = self.store.value_stack.enter_locals(&self.func.params, &self.func.locals) else { - cold_path(); - return Err(Trap::CallStackOverflow); + return cold!(Err(Trap::CallStackOverflow)); }; self.cf = CallFrame::new(self.cf.func_addr, locals_base, self.func.locals); Ok(()) @@ -1071,16 +1190,13 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { let table_addr = self.module.resolve_table_addr(table_addr); let table_idx = self.pop_table_operand(self.store.state.get_table(table_addr).kind.arch())?; let table = self.store.state.get_table(table_addr); - debug_assert!(table.kind.element_type.is_func(), "table is not of type funcref"); let Ok(table) = table.get(table_idx) else { - cold_path(); - return Err(Trap::UndefinedElement { index: table_idx }); + return cold!(Err(Trap::UndefinedElement { index: table_idx })); }; let Some(func_ref) = table.addr() else { - cold_path(); - return Err(Trap::UninitializedElement { index: table_idx }); + return cold!(Err(Trap::UninitializedElement { index: table_idx })); }; self.exec_typed_call::(func_ref, self.module.resolve_type_addr(type_addr)) @@ -1092,12 +1208,11 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { expected_type_addr: TypeAddr, ) -> Result { let func = self.store.state.get_func(func_addr).clone(); - if func.type_addr != expected_type_addr { - cold_path(); - return Err(Trap::IndirectCallTypeMismatch { - actual: Box::new(self.store.state.get_type(func.type_addr).clone()), - expected: Box::new(self.store.state.get_type(expected_type_addr).clone()), - }); + if !self.store.state.type_addr_is_subtype(func.type_addr, expected_type_addr) { + return cold!(Err(Trap::IndirectCallTypeMismatch { + actual: Box::new(self.store.state.get_canonical_func_type(func.type_addr).clone()), + expected: Box::new(self.store.state.get_canonical_func_type(expected_type_addr).clone()), + })); } match func.kind { crate::store::FunctionKind::Wasm(wasm_func) => match IS_RETURN_CALL { @@ -1105,7 +1220,7 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { false => self.exec_call(wasm_func, func_addr), }, crate::store::FunctionKind::Host(host_func) => { - return self.exec_call_host::(host_func, func.type_addr); + return self.exec_call_host::(host_func, func.type_addr, func.gc.params); } }?; Ok(false) @@ -1115,8 +1230,7 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { self.charge_call_fuel(FUEL_COST_CALL_TOTAL); let func_ref = ValueRef::stack_pop(&mut self.store.value_stack); let Some(func_addr) = func_ref.addr() else { - cold_path(); - return Err(Trap::NullFunctionReference); + return cold!(Err(Trap::NullFunctionReference)); }; self.exec_typed_call::(func_addr, self.module.resolve_type_addr(type_addr)) @@ -1134,14 +1248,9 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { }; if caller.func_addr == self.cf.func_addr { self.cf = caller; - return false; - } - let wasm_func = self.store.state.get_wasm_func(caller.func_addr); - self.func = wasm_func.func.clone(); - if wasm_func.owner != self.module.id() { - self.module = self.store.get_module_instance_internal(wasm_func.owner); + } else { + self.switch_to_frame(caller); } - self.cf = caller; false } @@ -1175,18 +1284,30 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { self.finish_return() } + #[inline(always)] fn exec_store_local_local, const N: usize>( &mut self, memarg: MemoryArg, addr_local: u8, value_local: u8, ) -> Result<(), Trap> { - let base = u32::local_get(&self.store.value_stack, &self.cf, u16::from(addr_local)); let value = T::local_get(&self.store.value_stack, &self.cf, u16::from(value_local)); - let mem = self.store.state.get_mem_mut(self.module.resolve_mem_addr(memarg.mem_addr())); - let addr = mem.effective_addr_32::(base, memarg.offset())?; - value.store_at(&mut *mem.inner, addr)?; - Ok(()) + let mem_addr = self.module.resolve_mem_addr(memarg.mem_addr()); + let mem = self.store.state.get_mem(mem_addr); + let addr = if mem.is_64bit() { + let base = u64::local_get(&self.store.value_stack, &self.cf, u16::from(addr_local)); + let base = cold_err!(usize::try_from(base).map_err(|_| Trap::MemoryOutOfBounds { + offset: usize::MAX, + len: N, + max: mem.inner.len(), + }))?; + mem.effective_addr::(base, memarg.offset())? + } else { + let base = u32::local_get(&self.store.value_stack, &self.cf, u16::from(addr_local)); + mem.effective_addr::(base as usize, memarg.offset())? + }; + let mem = self.store.state.get_mem_mut(mem_addr); + value.store_at(&mut *mem.inner, addr) } #[inline(always)] @@ -1200,10 +1321,15 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { let mem = self.store.state.get_mem(mem_addr); let addr = if mem.is_64bit() { let base = i64::local_get(&self.store.value_stack, &self.cf, u16::from(addr_local)) as u64; - mem.effective_addr_64::(base, memarg.offset())? + let base = cold_err!(usize::try_from(base).map_err(|_| Trap::MemoryOutOfBounds { + offset: usize::MAX, + len: N, + max: mem.inner.len(), + }))?; + mem.effective_addr::(base, memarg.offset())? } else { let base = u32::local_get(&self.store.value_stack, &self.cf, u16::from(addr_local)); - mem.effective_addr_32::(base, memarg.offset())? + mem.effective_addr::(base as usize, memarg.offset())? }; let mem = self.store.state.get_mem_mut(mem_addr); @@ -1222,26 +1348,17 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { let rhs = T::stack_pop(&mut self.store.value_stack); let lhs = T::stack_pop(&mut self.store.value_stack); let acc = T::stack_pop(&mut self.store.value_stack); - let addr = i32::stack_pop(&mut self.store.value_stack); let fma = acc + lhs * rhs; - let mem = self.store.state.get_mem_mut(self.module.resolve_mem_addr(m.mem_addr())); - let addr = mem.effective_addr_32::(addr as u32, m.offset())?; + let mem_addr = self.module.resolve_mem_addr(m.mem_addr()); + let mem = self.store.state.get_mem(mem_addr); + let base = self.store.value_stack.pop_memory_operand(mem.kind.arch())?; + let addr = mem.effective_addr::(base, m.offset())?; + let mem = self.store.state.get_mem_mut(mem_addr); fma.store_at(&mut *mem.inner, addr)?; Ok(()) } #[inline(always)] - fn exec_binop_acc_local(&mut self, acc: LocalAddr, mul: M, add: A) - where - T: InternalValue, - M: Fn(T, T) -> T, - A: Fn(T, T) -> T, - { - let rhs = T::stack_pop(&mut self.store.value_stack); - let lhs = T::stack_pop(&mut self.store.value_stack); - T::local_update(&mut self.store.value_stack, &self.cf, acc, |v| add(mul(lhs, rhs), v)); - } - fn exec_load_local_value, const N: usize>( &self, memarg: MemoryArg, @@ -1250,21 +1367,27 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { let mem = self.store.state.get_mem(self.module.resolve_mem_addr(memarg.mem_addr())); let addr = if mem.is_64bit() { let base = i64::local_get(&self.store.value_stack, &self.cf, u16::from(addr_local)) as u64; - mem.effective_addr_64::(base, memarg.offset())? + let base = cold_err!(usize::try_from(base).map_err(|_| Trap::MemoryOutOfBounds { + offset: usize::MAX, + len: N, + max: mem.inner.len(), + }))?; + mem.effective_addr::(base, memarg.offset())? } else { let base = u32::local_get(&self.store.value_stack, &self.cf, u16::from(addr_local)); - mem.effective_addr_32::(base, memarg.offset())? + mem.effective_addr::(base as usize, memarg.offset())? }; - match T::load_at(&*mem.inner, addr) { - Ok(res) => Ok(res), - Err(err) => { - cold_path(); - Err(err) - } - } + T::load_at(&*mem.inner, addr) } - fn exec_load_local_tee, const N: usize, TARGET: InternalValue>( + #[inline(always)] + fn exec_load_local< + LOAD: MemValue, + const N: usize, + TARGET: InternalValue, + const SET_LOCAL: bool, + const TEE: bool, + >( &mut self, memarg: MemoryArg, addr_local: u8, @@ -1272,58 +1395,252 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { cast: impl Fn(LOAD) -> TARGET, ) -> Result<(), Trap> { let value = cast(self.exec_load_local_value::(memarg, addr_local)?); - TARGET::local_set(&mut self.store.value_stack, &self.cf, u16::from(dst_local), value); - self.store.value_stack.push(value)?; + if SET_LOCAL { + TARGET::local_set(&mut self.store.value_stack, &self.cf, u16::from(dst_local), value); + } + if !SET_LOCAL || TEE { + TARGET::stack_push(&mut self.store.value_stack, value)?; + } Ok(()) } - fn exec_load_local_set, const N: usize, TARGET: InternalValue>( - &mut self, - memarg: MemoryArg, - addr_local: u8, - dst_local: u8, - cast: impl Fn(LOAD) -> TARGET, - ) -> Result<(), Trap> { - let value = cast(self.exec_load_local_value::(memarg, addr_local)?); - TARGET::local_set(&mut self.store.value_stack, &self.cf, u16::from(dst_local), value); + fn exec_ref_is_null(&mut self) -> Result<(), Trap> { + let is_null = i32::from(::stack_pop(&mut self.store.value_stack).is_null()); + self.store.value_stack.push::(is_null) + } + + fn exec_ref_as_non_null(&mut self) -> Result<(), Trap> { + if ValueRef::stack_peek(&self.store.value_stack).is_null() { + return cold!(Err(Trap::NullReference)); + } Ok(()) } - fn exec_global_get(&mut self, global_index: u32) -> Result<(), Trap> { - self.store.value_stack.push_dyn(self.store.state.get_global_val(self.module.resolve_global_addr(global_index))) + fn canonical_ref_type(&self, ty: RefType) -> RefType { + let Some(type_index) = ty.type_index() else { return ty }; + RefType::new_concrete(ty.is_nullable(), self.module.resolve_type_addr(type_index)) + } + + fn exec_ref_matches(&self, ty: RefType) -> bool { + let value = ValueRef::stack_peek(&self.store.value_stack); + self.store.state.value_ref_matches(value, self.canonical_ref_type(ty)) + } + + fn exec_ref_test(&mut self, ty: RefType) -> Result<(), Trap> { + let value = ValueRef::stack_pop(&mut self.store.value_stack); + let matches = self.store.state.value_ref_matches(value, self.canonical_ref_type(ty)); + self.store.value_stack.push(i32::from(matches)) } - fn exec_global_set(&mut self, global_index: u32) { - let global_addr = self.module.resolve_global_addr(global_index); - let value = ::stack_pop(&mut self.store.value_stack).into(); - self.store.state.set_global_val(global_addr, value); + fn exec_ref_cast(&self, ty: RefType) -> Result<(), Trap> { + if !self.exec_ref_matches(ty) { + return cold!(Err(Trap::CastFailure)); + } + Ok(()) } - fn exec_global_set_32(&mut self, global_index: u32) { - let global_addr = self.module.resolve_global_addr(global_index); - let raw = ::stack_pop(&mut self.store.value_stack); - let value = match self.store.state.get_global(global_addr).ty.ty { - WasmType::I32 | WasmType::F32 => TinyWasmValue::Value32(raw), - WasmType::Ref(_) => TinyWasmValue::ValueRef(ValueRef::from_raw(raw)), - WasmType::I64 | WasmType::F64 | WasmType::V128 => unreachable!("invalid global.set.32 target type"), + fn exec_i31_get(&mut self, signed: bool) -> Result<(), Trap> { + let value = ValueRef::stack_pop(&mut self.store.value_stack); + if value.is_null() { + return cold!(Err(Trap::NullI31Reference)); + } + let value = if signed { + value.i31_s().expect("validated i31.get operand") + } else { + value.i31_u().expect("validated i31.get operand") as i32 }; - self.store.state.set_global_val(global_addr, value); + self.store.value_stack.push(value) } - fn exec_const(&mut self, val: T) -> Result<(), Trap> { - self.store.value_stack.push(val) + fn push_gc_object(&mut self, type_addr: TypeAddr, values: Vec) -> Result<(), Trap> { + let roots = (&self.store.value_stack.stack_32).into_iter().copied(); + let reference = self.store.state.alloc_gc_object(type_addr, values, roots)?; + self.store.value_stack.push(reference) } - fn exec_ref_is_null(&mut self) -> Result<(), Trap> { - let is_null = i32::from(::stack_pop(&mut self.store.value_stack).is_null()); - self.store.value_stack.push::(is_null) + fn exec_struct_new(&mut self, type_index: TypeAddr, default: bool) -> Result<(), Trap> { + let type_addr = self.module.resolve_type_addr(type_index); + let fields = &self.store.state.get_type(type_addr).as_struct().expect("validated struct.new type").fields; + let mut values = Vec::new(); + cold_err!(values.try_reserve_exact(fields.len())).map_err(|_| Trap::OutOfMemory)?; + if default { + values.extend(fields.iter().map(|field| default_value(field.storage))); + } else { + for field in fields.iter().rev() { + values.push(pop_value(&mut self.store.value_stack, field.storage)); + } + values.reverse(); + } + self.push_gc_object(type_addr, values) + } + + fn exec_struct_get(&mut self, type_index: TypeAddr, field_index: u32, signed: Option) -> Result<(), Trap> { + let reference = ValueRef::stack_pop(&mut self.store.value_stack); + let type_addr = self.module.resolve_type_addr(type_index); + let storage = self.store.state.get_type(type_addr).as_struct().expect("validated struct.get type").fields + [field_index as usize] + .storage; + let object = self.store.state.gc_object(reference, type_addr)?; + let value = *object.values.get(field_index as usize).expect("validated struct field index"); + push_value(&mut self.store.value_stack, value, storage, signed) + } + + fn exec_struct_set(&mut self, type_index: TypeAddr, field_index: u32) -> Result<(), Trap> { + let type_addr = self.module.resolve_type_addr(type_index); + let storage = self.store.state.get_type(type_addr).as_struct().expect("validated struct.set type").fields + [field_index as usize] + .storage; + let value = pop_value(&mut self.store.value_stack, storage); + let reference = ValueRef::stack_pop(&mut self.store.value_stack); + self.store.state.gc_object(reference, type_addr)?; + self.store.state.gc.set(reference, field_index as usize, value).expect("live struct field"); + Ok(()) } - fn exec_ref_as_non_null(&mut self) -> Result<(), Trap> { - if ValueRef::stack_peek(&self.store.value_stack).is_null() { - cold_path(); - return Err(Trap::NullReference); + fn exec_array_new(&mut self, type_index: TypeAddr, default: bool) -> Result<(), Trap> { + let type_addr = self.module.resolve_type_addr(type_index); + let storage = self.store.state.get_type(type_addr).as_array().expect("validated array.new type").field.storage; + let len = u32::stack_pop(&mut self.store.value_stack) as usize; + let value = if default { default_value(storage) } else { pop_value(&mut self.store.value_stack, storage) }; + let mut values = Vec::new(); + cold_err!(values.try_reserve_exact(len)).map_err(|_| Trap::OutOfMemory)?; + values.resize(len, value); + self.push_gc_object(type_addr, values) + } + + fn exec_array_new_fixed(&mut self, type_index: TypeAddr, len: u32) -> Result<(), Trap> { + let type_addr = self.module.resolve_type_addr(type_index); + let storage = + self.store.state.get_type(type_addr).as_array().expect("validated array.new_fixed type").field.storage; + let len = len as usize; + let mut values = Vec::new(); + cold_err!(values.try_reserve_exact(len)).map_err(|_| Trap::OutOfMemory)?; + for _ in 0..len { + values.push(pop_value(&mut self.store.value_stack, storage)); + } + values.reverse(); + self.push_gc_object(type_addr, values) + } + + fn exec_array_get(&mut self, type_index: TypeAddr, signed: Option) -> Result<(), Trap> { + let index = u32::stack_pop(&mut self.store.value_stack) as usize; + let reference = ValueRef::stack_pop(&mut self.store.value_stack); + let type_addr = self.module.resolve_type_addr(type_index); + let storage = self.store.state.get_type(type_addr).as_array().expect("validated array.get type").field.storage; + let object = self.store.state.gc_object(reference, type_addr)?; + let value = *object.values.get(index).ok_or(Trap::ArrayOutOfBounds)?; + push_value(&mut self.store.value_stack, value, storage, signed) + } + + fn exec_array_set(&mut self, type_index: TypeAddr) -> Result<(), Trap> { + let type_addr = self.module.resolve_type_addr(type_index); + let storage = self.store.state.get_type(type_addr).as_array().expect("validated array.set type").field.storage; + let value = pop_value(&mut self.store.value_stack, storage); + let index = u32::stack_pop(&mut self.store.value_stack) as usize; + let reference = ValueRef::stack_pop(&mut self.store.value_stack); + self.store.state.gc_object(reference, type_addr)?; + self.store.state.gc.set(reference, index, value).ok_or(Trap::ArrayOutOfBounds) + } + + fn exec_array_len(&mut self) -> Result<(), Trap> { + let reference = ValueRef::stack_pop(&mut self.store.value_stack); + if reference.is_null() { + return Err(Trap::NullArrayReference); + } + let object = self.store.state.gc.get(reference).ok_or(Trap::Other("invalid GC reference"))?; + if self.store.state.get_type(object.type_addr).as_array().is_none() { + return Err(Trap::Other("GC reference is not an array")); } + self.store.value_stack.push(object.values.len() as i32) + } + + fn exec_array_fill(&mut self, type_index: TypeAddr) -> Result<(), Trap> { + let type_addr = self.module.resolve_type_addr(type_index); + let storage = self.store.state.get_type(type_addr).as_array().expect("validated array.fill type").field.storage; + let len = u32::stack_pop(&mut self.store.value_stack) as usize; + let value = pop_value(&mut self.store.value_stack, storage); + let index = u32::stack_pop(&mut self.store.value_stack) as usize; + let reference = ValueRef::stack_pop(&mut self.store.value_stack); + let object = self.store.state.gc_object(reference, type_addr)?; + let end = index.checked_add(len).filter(|end| *end <= object.values.len()).ok_or(Trap::ArrayOutOfBounds)?; + self.store.state.gc.fill(reference, index..end, value).expect("live array range"); + Ok(()) + } + + fn exec_array_copy(&mut self, dst_type: TypeAddr, src_type: TypeAddr) -> Result<(), Trap> { + let len = u32::stack_pop(&mut self.store.value_stack) as usize; + let src_index = u32::stack_pop(&mut self.store.value_stack) as usize; + let src = ValueRef::stack_pop(&mut self.store.value_stack); + let dst_index = u32::stack_pop(&mut self.store.value_stack) as usize; + let dst = ValueRef::stack_pop(&mut self.store.value_stack); + let dst_type = self.module.resolve_type_addr(dst_type); + let src_type = self.module.resolve_type_addr(src_type); + let dst_len = self.store.state.gc_object(dst, dst_type)?.values.len(); + let src_object = self.store.state.gc_object(src, src_type)?; + let src_end = + src_index.checked_add(len).filter(|end| *end <= src_object.values.len()).ok_or(Trap::ArrayOutOfBounds)?; + dst_index.checked_add(len).filter(|end| *end <= dst_len).ok_or(Trap::ArrayOutOfBounds)?; + if src == dst { + self.store.state.gc.copy_within(dst, src_index..src_end, dst_index).expect("live array range"); + return Ok(()); + } + self.store.state.gc.copy_between(src, src_index..src_end, dst, dst_index).expect("live array ranges"); + Ok(()) + } + + fn exec_array_new_data(&mut self, type_index: TypeAddr, data_index: DataAddr) -> Result<(), Trap> { + let len = u32::stack_pop(&mut self.store.value_stack) as usize; + let src = u32::stack_pop(&mut self.store.value_stack) as usize; + let type_addr = self.module.resolve_type_addr(type_index); + let storage = self.store.state.get_type(type_addr).as_array().expect("validated array type").field.storage; + let data_addr = self.module.resolve_data_addr(data_index); + let data = self.store.state.data[data_addr as usize].data.as_deref().unwrap_or(&[]); + let values = decode_data(storage, data, src, len)?; + self.push_gc_object(type_addr, values) + } + + fn exec_array_new_elem(&mut self, type_index: TypeAddr, elem_index: ElemAddr) -> Result<(), Trap> { + let len = u32::stack_pop(&mut self.store.value_stack) as usize; + let src = u32::stack_pop(&mut self.store.value_stack) as usize; + let elem_addr = self.module.resolve_elem_addr(elem_index); + let items = self.store.state.elements[elem_addr as usize].items_range(src, len)?; + let mut values = Vec::new(); + cold_err!(values.try_reserve_exact(len)).map_err(|_| Trap::OutOfMemory)?; + values.extend(items.iter().copied().map(TinyWasmValue::ValueRef)); + let type_addr = self.module.resolve_type_addr(type_index); + self.push_gc_object(type_addr, values) + } + + fn exec_array_init_data(&mut self, type_index: TypeAddr, data_index: DataAddr) -> Result<(), Trap> { + let len = u32::stack_pop(&mut self.store.value_stack) as usize; + let src = u32::stack_pop(&mut self.store.value_stack) as usize; + let dst = u32::stack_pop(&mut self.store.value_stack) as usize; + let reference = ValueRef::stack_pop(&mut self.store.value_stack); + let type_addr = self.module.resolve_type_addr(type_index); + let storage = self.store.state.get_type(type_addr).as_array().expect("validated array type").field.storage; + let object_len = self.store.state.gc_object(reference, type_addr)?.values.len(); + dst.checked_add(len).filter(|end| *end <= object_len).ok_or(Trap::ArrayOutOfBounds)?; + let data = + self.store.state.data[self.module.resolve_data_addr(data_index) as usize].data.as_deref().unwrap_or(&[]); + let values = decode_data(storage, data, src, len)?; + self.store.state.gc.set_slice(reference, dst, &values).expect("live array range"); + Ok(()) + } + + fn exec_array_init_elem(&mut self, type_index: TypeAddr, elem_index: ElemAddr) -> Result<(), Trap> { + let len = u32::stack_pop(&mut self.store.value_stack) as usize; + let src = u32::stack_pop(&mut self.store.value_stack) as usize; + let dst = u32::stack_pop(&mut self.store.value_stack) as usize; + let reference = ValueRef::stack_pop(&mut self.store.value_stack); + let type_addr = self.module.resolve_type_addr(type_index); + let object_len = self.store.state.gc_object(reference, type_addr)?.values.len(); + dst.checked_add(len).filter(|end| *end <= object_len).ok_or(Trap::ArrayOutOfBounds)?; + let items = + self.store.state.elements[self.module.resolve_elem_addr(elem_index) as usize].items_range(src, len)?; + let mut values = Vec::new(); + cold_err!(values.try_reserve_exact(len)).map_err(|_| Trap::OutOfMemory)?; + values.extend(items.iter().copied().map(TinyWasmValue::ValueRef)); + self.store.state.gc.set_slice(reference, dst, &values).expect("live array range"); Ok(()) } @@ -1353,60 +1670,69 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { } fn exec_memory_copy(&mut self, dst_mem: u32, src_mem: u32) -> Result<(), Trap> { - let size = i32::stack_pop(&mut self.store.value_stack); - let src = i32::stack_pop(&mut self.store.value_stack); - let dst = i32::stack_pop(&mut self.store.value_stack); let dst_mem_addr = self.module.resolve_mem_addr(dst_mem); + let src_mem_addr = self.module.resolve_mem_addr(src_mem); + let dst_arch = self.store.state.get_mem(dst_mem_addr).kind.arch(); + let src_arch = self.store.state.get_mem(src_mem_addr).kind.arch(); + let len_arch = + if dst_arch == MemoryArch::I32 || src_arch == MemoryArch::I32 { MemoryArch::I32 } else { MemoryArch::I64 }; + let size = self.store.value_stack.pop_memory_operand(len_arch)?; + let src = self.store.value_stack.pop_memory_operand(src_arch)?; + let dst = self.store.value_stack.pop_memory_operand(dst_arch)?; if dst_mem == src_mem { // copy within the same memory let mem = self.store.state.get_mem_mut(dst_mem_addr); - mem.copy_within(dst as usize, src as usize, size as usize)?; + mem.copy_within(dst, src, size)?; } else { // copy between two memories - let src_mem_addr = self.module.resolve_mem_addr(src_mem); let (dst_memory, src_memory) = self.store.state.get_mems_mut(dst_mem_addr, src_mem_addr); - dst_memory.copy_from_memory(dst as usize, src_memory, src as usize, size as usize)?; + dst_memory.copy_from_memory(dst, src_memory, src, size)?; } Ok(()) } fn exec_memory_fill(&mut self, addr: u32) -> Result<(), Trap> { - let size = i32::stack_pop(&mut self.store.value_stack); + let mem_addr = self.module.resolve_mem_addr(addr); + let arch = self.store.state.get_mem(mem_addr).kind.arch(); + let size = self.store.value_stack.pop_memory_operand(arch)?; let val = i32::stack_pop(&mut self.store.value_stack); - let dst = i32::stack_pop(&mut self.store.value_stack); - self.exec_memory_fill_impl(addr, dst, val as u8, size) + let dst = self.store.value_stack.pop_memory_operand(arch)?; + self.exec_memory_fill_impl(mem_addr, dst, val as u8, size) } fn exec_memory_fill_imm(&mut self, addr: u32, val: u8, size: i32) -> Result<(), Trap> { - let dst = i32::stack_pop(&mut self.store.value_stack); - self.exec_memory_fill_impl(addr, dst, val, size) + let mem_addr = self.module.resolve_mem_addr(addr); + let arch = self.store.state.get_mem(mem_addr).kind.arch(); + let dst = self.store.value_stack.pop_memory_operand(arch)?; + self.exec_memory_fill_impl(mem_addr, dst, val, size as u32 as usize) } - fn exec_memory_fill_impl(&mut self, addr: u32, dst: i32, val: u8, size: i32) -> Result<(), Trap> { - let mem = self.store.state.get_mem_mut(self.module.resolve_mem_addr(addr)); - if mem.inner.fill(dst as usize, size as usize, val).is_none() { - cold_path(); - return Err(Trap::MemoryOutOfBounds { - offset: dst as usize, - len: size as usize, - max: self.store.state.get_mem(self.module.resolve_mem_addr(addr)).inner.len(), - }); + fn exec_memory_fill_impl(&mut self, mem_addr: MemAddr, dst: usize, val: u8, size: usize) -> Result<(), Trap> { + let mem = self.store.state.get_mem_mut(mem_addr); + let max = mem.inner.len(); + if mem.inner.fill(dst, size, val)?.is_none() { + return cold!(Err(Trap::MemoryOutOfBounds { offset: dst, len: size, max })); } Ok(()) } fn exec_memory_init(&mut self, data_index: u32, mem_index: u32) -> Result<(), Trap> { - let size = i32::stack_pop(&mut self.store.value_stack); - let offset = i32::stack_pop(&mut self.store.value_stack); - let dst = i32::stack_pop(&mut self.store.value_stack); + let size = u32::stack_pop(&mut self.store.value_stack) as usize; + let offset = u32::stack_pop(&mut self.store.value_stack) as usize; + let mem_addr = self.module.resolve_mem_addr(mem_index); + let arch = self.store.state.get_mem(mem_addr).kind.arch(); + let dst = self.store.value_stack.pop_memory_operand(arch)?; let data = &self.store.state.data[self.module.resolve_data_addr(data_index) as usize]; - let mem = &mut self.store.state.memories[self.module.resolve_mem_addr(mem_index) as usize]; + let mem = &mut self.store.state.memories[mem_addr as usize]; let data_len = data.data.as_ref().map_or(0, |d| d.len()); - if ((size + offset) as usize > data_len) || ((dst + size) as usize > mem.inner.len()) { - cold_path(); - return Err(Trap::MemoryOutOfBounds { offset: offset as usize, len: size as usize, max: data_len }); + let mem_len = mem.inner.len(); + if offset.checked_add(size).is_none_or(|end| end > data_len) { + return cold!(Err(Trap::MemoryOutOfBounds { offset, len: size, max: data_len })); + } + if dst.checked_add(size).is_none_or(|end| end > mem_len) { + return cold!(Err(Trap::MemoryOutOfBounds { offset: dst, len: size, max: mem_len })); } if size == 0 { @@ -1414,13 +1740,11 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { } let Some(data) = &data.data else { - cold_path(); - return Err(Trap::MemoryOutOfBounds { offset: 0, len: 0, max: 0 }); + return cold!(Err(Trap::MemoryOutOfBounds { offset: 0, len: 0, max: 0 })); }; - if mem.inner.write_all(dst as usize, &data[offset as usize..((offset + size) as usize)]).is_none() { - cold_path(); - return Err(Trap::MemoryOutOfBounds { offset: dst as usize, len: size as usize, max: mem.inner.len() }); + if mem.inner.write_all(dst, &data[offset..offset + size])?.is_none() { + return cold!(Err(Trap::MemoryOutOfBounds { offset: dst, len: size, max: mem_len })); } Ok(()) } @@ -1453,7 +1777,8 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { lane: u8, ) -> Result<(), Trap> { let mem = self.store.state.get_mem(self.module.resolve_mem_addr(mem_addr)); - let addr = Self::pop_memory_addr::(&mut self.store.value_stack, mem, offset)?; + let base = self.store.value_stack.pop_memory_operand(mem.kind.arch())?; + let addr = mem.effective_addr::(base, offset)?; let val = LOAD::load_at(&*mem.inner, addr)?; let offset = lane as usize * LOAD_SIZE; let mut imm = ::stack_pop(&mut self.store.value_stack).to_mem_bytes(); @@ -1470,18 +1795,10 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { cast: impl Fn(LOAD) -> TARGET, ) -> Result<(), Trap> { let mem = self.store.state.get_mem(self.module.resolve_mem_addr(mem_addr)); - let addr = Self::pop_memory_addr::(&mut self.store.value_stack, mem, offset)?; - - match LOAD::load_at(&*mem.inner, addr) { - Ok(val) => { - self.store.value_stack.push(cast(val))?; - Ok(()) - } - Err(e) => { - cold_path(); - Err(e) - } - } + let base = self.store.value_stack.pop_memory_operand(mem.kind.arch())?; + let addr = mem.effective_addr::(base, offset)?; + let value = cold_err!(LOAD::load_at(&*mem.inner, addr))?; + self.store.value_stack.push(cast(value)) } fn exec_mem_store_lane + Copy, const N: usize>( @@ -1496,15 +1813,12 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { val_bytes.copy_from_slice(&bytes[lane_offset..lane_offset + N]); let val = U::from_mem_bytes(val_bytes); let mem_addr = self.module.resolve_mem_addr(mem_addr); + let mem = self.store.state.get_mem(mem_addr); + let base = self.store.value_stack.pop_memory_operand(mem.kind.arch())?; + let addr = mem.effective_addr::(base, offset)?; let mem = self.store.state.get_mem_mut(mem_addr); - let addr = Self::pop_memory_addr::(&mut self.store.value_stack, mem, offset)?; - match val.store_at(&mut *mem.inner, addr) { - Ok(()) => Ok(()), - Err(e) => { - cold_path(); - Err(e) - } - } + cold_err!(val.store_at(&mut *mem.inner, addr))?; + Ok(()) } fn exec_mem_store, const N: usize>( @@ -1517,22 +1831,19 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { let val = cast(val); let mem_addr = self.module.resolve_mem_addr(mem_addr); + let mem = self.store.state.get_mem(mem_addr); + let base = self.store.value_stack.pop_memory_operand(mem.kind.arch())?; + let addr = mem.effective_addr::(base, offset)?; let mem = self.store.state.get_mem_mut(mem_addr); - let addr = Self::pop_memory_addr::(&mut self.store.value_stack, mem, offset)?; - match val.store_at(&mut *mem.inner, addr) { - Ok(()) => Ok(()), - Err(e) => { - cold_path(); - Err(e) - } - } + cold_err!(val.store_at(&mut *mem.inner, addr))?; + Ok(()) } fn exec_table_get(&mut self, table_index: u32) -> Result<(), Trap> { let table_addr = self.module.resolve_table_addr(table_index); let idx = self.pop_table_operand(self.store.state.get_table(table_addr).kind.arch())?; - let v = self.store.state.get_table(table_addr).get_wasm_val(idx)?; - self.store.value_stack.push_dyn(v.into()) + let value = *self.store.state.get_table(table_addr).get(idx)?; + self.store.value_stack.push(value) } fn exec_table_set(&mut self, table_index: u32) -> Result<(), Trap> { @@ -1540,7 +1851,7 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { let table_addr = self.module.resolve_table_addr(table_index); let idx = self.pop_table_operand(self.store.state.get_table(table_addr).kind.arch())?; let table = self.store.state.get_table_mut(table_addr); - table.set(idx, val.addr().into()) + table.set(idx, val) } fn exec_table_size(&mut self, table_index: u32) -> Result<(), Trap> { @@ -1558,19 +1869,11 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { let dst = self.pop_table_operand(self.store.state.get_table(table_addr).kind.arch())?; // d let elem_addr = self.module.resolve_elem_addr(elem_index) as usize; let elem = self.store.state.elements.get(elem_addr).ok_or_else(|| Trap::Other("element not found"))?; - let items = elem.items.as_deref().unwrap_or(&[]); - let Some(end) = offset.checked_add(size) else { - cold_path(); - return Err(Trap::TableOutOfBounds { offset, len: size, max: items.len() }); - }; - if end > items.len() { - cold_path(); - return Err(Trap::TableOutOfBounds { offset, len: size, max: items.len() }); - } + let items = elem.items_range(offset, size)?; let table = self.store.state.tables.get_mut(table_addr as usize).ok_or_else(|| Trap::Other("table not found"))?; - table.init(dst, &items[offset..end]) + table.init(dst, items) } fn exec_table_grow(&mut self, table_index: u32) -> Result<(), Trap> { @@ -1580,7 +1883,7 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { let val = ::stack_pop(&mut self.store.value_stack); let table = self.store.state.get_table_mut(table_addr); let sz = table.size(); - let result = table.grow(n, val.addr().into()); + let result = table.grow(n, val); match (arch, result) { (MemoryArch::I32, Ok(())) => self.store.value_stack.push(sz as i32), (MemoryArch::I32, Err(_)) => self.store.value_stack.push(-1_i32), @@ -1595,7 +1898,7 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { let n = self.pop_table_operand(arch)?; let val = ::stack_pop(&mut self.store.value_stack); let i = self.pop_table_operand(arch)?; - self.store.state.get_table_mut(table_addr).fill(i, n, val.addr().into()) + self.store.state.get_table_mut(table_addr).fill(i, n, val) } fn pop_table_operand(&mut self, arch: MemoryArch) -> Result { @@ -1603,16 +1906,20 @@ impl<'store, const BUDGETED: bool> Executor<'store, BUDGETED> { MemoryArch::I32 => ::stack_pop(&mut self.store.value_stack) as u32 as u64, MemoryArch::I64 => ::stack_pop(&mut self.store.value_stack) as u64, }; - usize::try_from(value).map_err(|_| Trap::TableOutOfBounds { offset: usize::MAX, len: 1, max: usize::MAX }) + cold_err!(usize::try_from(value).map_err(|_| Trap::TableOutOfBounds { + offset: usize::MAX, + len: 1, + max: usize::MAX, + })) } } impl<'store> Executor<'store, false> { #[inline(always)] - pub(crate) fn run_to_completion(&mut self) -> Result<(), Trap> { + pub(crate) fn run_to_completion(mut self) -> Result<()> { // ideally we use `loop_match` / `become` once thats stabilized loop { - if self.exec()?.is_some() { + if self.exec(self.cf.instr_ptr)?.is_some() { return Ok(()); } } @@ -1620,7 +1927,7 @@ impl<'store> Executor<'store, false> { #[cfg(feature = "std")] #[inline(always)] - pub(crate) fn run_with_time_budget(&mut self, time_budget: core::time::Duration) -> Result { + pub(crate) fn run_with_time_budget(mut self, time_budget: core::time::Duration) -> Result { use crate::std::time::Instant; let start = Instant::now(); if time_budget.is_zero() { @@ -1629,7 +1936,7 @@ impl<'store> Executor<'store, false> { loop { for _ in 0..128 { - if self.exec()?.is_some() { + if self.exec(self.cf.instr_ptr)?.is_some() { return Ok(ExecState::Completed); } } @@ -1643,7 +1950,7 @@ impl<'store> Executor<'store, false> { impl<'store> Executor<'store, true> { #[inline(always)] - pub(crate) fn run_with_fuel(&mut self, fuel: u32) -> Result { + pub(crate) fn run_with_fuel(mut self, fuel: u32) -> Result { self.store.execution_fuel = fuel; if self.store.execution_fuel == 0 { return Ok(ExecState::Suspended(self.cf)); @@ -1651,7 +1958,7 @@ impl<'store> Executor<'store, true> { loop { for _ in 0..128 { - if self.exec()?.is_some() { + if self.exec(self.cf.instr_ptr)?.is_some() { return Ok(ExecState::Completed); } } @@ -1695,3 +2002,60 @@ fn cmp_i64(lhs: i64, rhs: i64, op: CmpOp) -> bool { CmpOp::GeU => (lhs as u64) >= (rhs as u64), } } + +fn exec_binop_32(op: BinOp, lhs: u32, rhs: u32) -> u32 { + match op { + BinOp::IAdd => lhs.wrapping_add(rhs), + BinOp::ISub => lhs.wrapping_sub(rhs), + BinOp::IMul => lhs.wrapping_mul(rhs), + BinOp::IAnd => lhs & rhs, + BinOp::IOr => lhs | rhs, + BinOp::IXor => lhs ^ rhs, + BinOp::IShl => (lhs as i32).wrapping_shl(rhs) as u32, + BinOp::IShrS => (lhs as i32).wrapping_shr(rhs) as u32, + BinOp::IShrU => lhs.wrapping_shr(rhs), + BinOp::IRotl => (lhs as i32).rotate_left(rhs) as u32, + BinOp::IRotr => (lhs as i32).rotate_right(rhs) as u32, + BinOp::FAdd => (f32::from_bits(lhs) + f32::from_bits(rhs)).to_bits(), + BinOp::FSub => (f32::from_bits(lhs) - f32::from_bits(rhs)).to_bits(), + BinOp::FMul => (f32::from_bits(lhs) * f32::from_bits(rhs)).to_bits(), + BinOp::FDiv => (f32::from_bits(lhs) / f32::from_bits(rhs)).to_bits(), + BinOp::FMin => f32::from_bits(lhs).tw_minimum(f32::from_bits(rhs)).to_bits(), + BinOp::FMax => f32::from_bits(lhs).tw_maximum(f32::from_bits(rhs)).to_bits(), + BinOp::FCopysign => f32::from_bits(lhs).copysign(f32::from_bits(rhs)).to_bits(), + } +} + +fn exec_binop_64(op: BinOp, lhs: u64, rhs: u64) -> u64 { + match op { + BinOp::IAdd => lhs.wrapping_add(rhs), + BinOp::ISub => lhs.wrapping_sub(rhs), + BinOp::IMul => lhs.wrapping_mul(rhs), + BinOp::IAnd => lhs & rhs, + BinOp::IOr => lhs | rhs, + BinOp::IXor => lhs ^ rhs, + BinOp::IShl => (lhs as i64).wrapping_shl(rhs as u32) as u64, + BinOp::IShrS => (lhs as i64).wrapping_shr(rhs as u32) as u64, + BinOp::IShrU => lhs.wrapping_shr(rhs as u32), + BinOp::IRotl => (lhs as i64).rotate_left(rhs as u32) as u64, + BinOp::IRotr => (lhs as i64).rotate_right(rhs as u32) as u64, + BinOp::FAdd => (f64::from_bits(lhs) + f64::from_bits(rhs)).to_bits(), + BinOp::FSub => (f64::from_bits(lhs) - f64::from_bits(rhs)).to_bits(), + BinOp::FMul => (f64::from_bits(lhs) * f64::from_bits(rhs)).to_bits(), + BinOp::FDiv => (f64::from_bits(lhs) / f64::from_bits(rhs)).to_bits(), + BinOp::FMin => f64::from_bits(lhs).tw_minimum(f64::from_bits(rhs)).to_bits(), + BinOp::FMax => f64::from_bits(lhs).tw_maximum(f64::from_bits(rhs)).to_bits(), + BinOp::FCopysign => f64::from_bits(lhs).copysign(f64::from_bits(rhs)).to_bits(), + } +} + +fn exec_binop_128(op: BinOp128, lhs: Value128, rhs: Value128) -> Value128 { + match op { + BinOp128::And => lhs.v128_and(rhs), + BinOp128::AndNot => lhs.v128_andnot(rhs), + BinOp128::Or => lhs.v128_or(rhs), + BinOp128::Xor => lhs.v128_xor(rhs), + BinOp128::I64x2Add => lhs.i64x2_add(rhs), + BinOp128::I64x2Mul => lhs.i64x2_mul(rhs), + } +} diff --git a/crates/tinywasm/src/interpreter/mod.rs b/crates/tinywasm/src/interpreter/mod.rs index cece36a1..6e6912c3 100644 --- a/crates/tinywasm/src/interpreter/mod.rs +++ b/crates/tinywasm/src/interpreter/mod.rs @@ -7,7 +7,7 @@ pub(crate) mod values; #[cfg(not(feature = "std"))] mod no_std_floats; -use crate::{Result, Store, Trap, interpreter::stack::CallFrame}; +use crate::{Result, Store, interpreter::stack::CallFrame}; pub(crate) use simd::*; pub(crate) use values::*; @@ -26,11 +26,11 @@ pub(crate) enum ExecState { pub(crate) struct InterpreterRuntime; impl InterpreterRuntime { - pub(crate) fn exec(store: &mut Store, cf: CallFrame, call_stack_base: u32) -> Result<(), Trap> { + pub(crate) fn exec(store: &mut Store, cf: CallFrame, call_stack_base: u32) -> Result<()> { executor::Executor::::new(store, cf, call_stack_base).run_to_completion() } - pub(crate) fn exec_with_fuel(store: &mut Store, cf: CallFrame, fuel: u32) -> Result { + pub(crate) fn exec_with_fuel(store: &mut Store, cf: CallFrame, fuel: u32) -> Result { executor::Executor::::new(store, cf, 0).run_with_fuel(fuel) } @@ -39,7 +39,7 @@ impl InterpreterRuntime { store: &mut Store, cf: CallFrame, time_budget: core::time::Duration, - ) -> Result { + ) -> Result { executor::Executor::::new(store, cf, 0).run_with_time_budget(time_budget) } } diff --git a/crates/tinywasm/src/interpreter/num_helpers.rs b/crates/tinywasm/src/interpreter/num_helpers.rs index 7b3807ed..783b10b7 100644 --- a/crates/tinywasm/src/interpreter/num_helpers.rs +++ b/crates/tinywasm/src/interpreter/num_helpers.rs @@ -32,11 +32,11 @@ macro_rules! checked_conv_float { let (min, max) = float_min_max!($from, $intermediate); if v.is_nan() { core::hint::cold_path(); - return Err(crate::Trap::InvalidConversionToInt); + return Err(crate::Trap::InvalidConversionToInt.into()); } if v <= min || v >= max { core::hint::cold_path(); - return Err(crate::Trap::IntegerOverflow); + return Err(crate::Trap::IntegerOverflow.into()); } $self.store.value_stack.push::<$to>((v as $intermediate as $to).into())?; }}; diff --git a/crates/tinywasm/src/interpreter/simd/mod.rs b/crates/tinywasm/src/interpreter/simd/mod.rs index b05769c3..406c0c7d 100644 --- a/crates/tinywasm/src/interpreter/simd/mod.rs +++ b/crates/tinywasm/src/interpreter/simd/mod.rs @@ -3,8 +3,6 @@ #[macro_use] mod macros; mod instructions; -#[cfg(test)] -mod tests; mod utils; #[cfg(target_arch = "wasm32")] @@ -13,8 +11,9 @@ use core::arch::wasm32 as wasm; use core::arch::wasm64 as wasm; #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[repr(transparent)] /// A 128-bit SIMD value -pub struct Value128(pub(super) [u8; 16]); +pub struct Value128(pub(crate) [u8; 16]); impl From<[u8; 16]> for Value128 { fn from(bytes: [u8; 16]) -> Self { @@ -24,12 +23,12 @@ impl From<[u8; 16]> for Value128 { impl Value128 { #[inline(always)] - pub fn from_le_bytes(bytes: [u8; 16]) -> Self { + pub const fn from_le_bytes(bytes: [u8; 16]) -> Self { Self(bytes) } #[inline(always)] - pub fn to_le_bytes(self) -> [u8; 16] { + pub const fn to_le_bytes(self) -> [u8; 16] { self.0 } } diff --git a/crates/tinywasm/src/interpreter/simd/tests.rs b/crates/tinywasm/src/interpreter/simd/tests.rs deleted file mode 100644 index 7400f6b1..00000000 --- a/crates/tinywasm/src/interpreter/simd/tests.rs +++ /dev/null @@ -1,60 +0,0 @@ -use super::Value128; - -fn ref_swizzle(a: [u8; 16], idx: [u8; 16]) -> [u8; 16] { - let mut out = [0u8; 16]; - for i in 0..16 { - let j = idx[i]; - out[i] = if j < 16 { a[(j & 0x0f) as usize] } else { 0 }; - } - out -} - -fn ref_shuffle(a: [u8; 16], b: [u8; 16], idx: [u8; 16]) -> [u8; 16] { - let mut out = [0u8; 16]; - for i in 0..16 { - let j = idx[i] & 31; - out[i] = if j < 16 { a[j as usize] } else { b[(j & 0x0f) as usize] }; - } - out -} - -#[test] -fn swizzle_matches_reference() { - let a = [0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]; - - for seed in 0u32..512 { - let mut s = [0u8; 16]; - let mut x = seed.wrapping_mul(0x9e37_79b9).wrapping_add(0x7f4a_7c15); - for byte in &mut s { - x ^= x << 13; - x ^= x >> 17; - x ^= x << 5; - *byte = (x & 0xff) as u8; - } - - let got = Value128(a).i8x16_swizzle(Value128(s)); - let expected = ref_swizzle(a, s).into(); - assert_eq!(got, expected, "seed={seed}"); - } -} - -#[test] -fn shuffle_matches_reference() { - let a = [0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f]; - let b = [0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf]; - - for seed in 0u32..512 { - let mut idx = [0u8; 16]; - let mut x = seed.wrapping_mul(0x85eb_ca6b).wrapping_add(0xc2b2_ae35); - for byte in &mut idx { - x ^= x << 13; - x ^= x >> 17; - x ^= x << 5; - *byte = (x & 0xff) as u8; - } - - let got = Value128::i8x16_shuffle(Value128(a), Value128(b), Value128(idx)); - let expected = ref_shuffle(a, b, idx).into(); - assert_eq!(got, expected, "seed={seed}"); - } -} diff --git a/crates/tinywasm/src/interpreter/stack/call_stack.rs b/crates/tinywasm/src/interpreter/stack/call_stack.rs index 62f1f8e9..f4e25958 100644 --- a/crates/tinywasm/src/interpreter/stack/call_stack.rs +++ b/crates/tinywasm/src/interpreter/stack/call_stack.rs @@ -1,5 +1,4 @@ use crate::{Result, Trap}; -use core::hint::cold_path; use alloc::vec::Vec; use tinywasm_types::{FuncAddr, ValueCounts}; @@ -51,14 +50,12 @@ impl CallStack { } if required_len > self.max_size || !self.dynamic { - cold_path(); - return Err(Trap::CallStackOverflow); + return cold!(Err(Trap::CallStackOverflow)); } let target_capacity = required_len.max(self.stack.capacity().max(1).saturating_mul(2)).min(self.max_size); let Ok(()) = self.stack.try_reserve(target_capacity.saturating_sub(self.stack.len())) else { - cold_path(); - return Err(Trap::CallStackOverflow); + return cold!(Err(Trap::CallStackOverflow)); }; Ok(()) } diff --git a/crates/tinywasm/src/interpreter/stack/value_stack.rs b/crates/tinywasm/src/interpreter/stack/value_stack.rs index 853c5e43..43c214a2 100644 --- a/crates/tinywasm/src/interpreter/stack/value_stack.rs +++ b/crates/tinywasm/src/interpreter/stack/value_stack.rs @@ -1,6 +1,6 @@ -use alloc::vec::Vec; +use alloc::{vec, vec::Vec}; use core::hint::cold_path; -use tinywasm_types::{ValueCounts, WasmType, WasmValue}; +use tinywasm_types::{MemoryArch, ValueCounts, WasmType, WasmValue}; use super::StackBase; use crate::engine::{Config, StackConfig}; @@ -8,12 +8,76 @@ use crate::interpreter::*; use crate::{Result, Trap}; #[cfg_attr(feature = "debug", derive(Debug))] +/// Physical value lanes used by the interpreter. +/// +/// Guest values should normally be accessed through +/// [`InternalValue`] so their stack and global representation stays consistent. pub(crate) struct ValueStack { pub(crate) stack_32: Stack, pub(crate) stack_64: Stack, pub(crate) stack_128: Stack, } +struct WasmValues<'a> { + stack: &'a ValueStack, + state: &'a crate::store::State, + types: core::slice::Iter<'a, WasmType>, + index: StackBase, + pin_refs: bool, +} + +impl Iterator for WasmValues<'_> { + type Item = WasmValue; + + fn next(&mut self) -> Option { + let value = match *self.types.next()? { + WasmType::I32 => { + let value = *self.stack.stack_32.get(self.index.s32 as usize) as i32; + self.index.s32 += 1; + WasmValue::I32(value) + } + WasmType::I64 => { + let value = *self.stack.stack_64.get(self.index.s64 as usize) as i64; + self.index.s64 += 1; + WasmValue::I64(value) + } + WasmType::F32 => { + let value = f32::from_bits(*self.stack.stack_32.get(self.index.s32 as usize)); + self.index.s32 += 1; + WasmValue::F32(value) + } + WasmType::F64 => { + let value = f64::from_bits(*self.stack.stack_64.get(self.index.s64 as usize)); + self.index.s64 += 1; + WasmValue::F64(value) + } + WasmType::Ref(ty) => { + let value = + self.state.to_ref_value(ValueRef::from_raw(*self.stack.stack_32.get(self.index.s32 as usize)), ty); + self.index.s32 += 1; + WasmValue::Ref(value) + } + WasmType::V128 => { + let value = self.stack.stack_128.get(self.index.s128 as usize).0; + self.index.s128 += 1; + WasmValue::V128(value) + } + }; + if self.pin_refs + && let WasmValue::Ref(value) = value + { + self.state.pin_host_ref(value); + } + Some(value) + } + + fn size_hint(&self) -> (usize, Option) { + self.types.size_hint() + } +} + +impl ExactSizeIterator for WasmValues<'_> {} + #[cfg_attr(feature = "debug", derive(Debug))] pub(crate) struct Stack { data: Vec, @@ -38,8 +102,7 @@ impl Stack { #[inline(always)] pub(crate) fn push(&mut self, value: T) -> Result<(), Trap> { if !self.ensure_capacity_for(self.data.len() + 1) { - cold_path(); - return Err(Trap::ValueStackOverflow); + return cold!(Err(Trap::ValueStackOverflow)); } self.data.push(value); @@ -165,6 +228,15 @@ impl Stack { self.data.truncate(len - count); } } + +impl<'a, T: Copy + Default> IntoIterator for &'a Stack { + type Item = &'a T; + type IntoIter = core::slice::Iter<'a, T>; + + fn into_iter(self) -> Self::IntoIter { + self.data.iter() + } +} impl ValueStack { pub(crate) fn new(config: &Config) -> Self { Self { @@ -189,6 +261,15 @@ impl ValueStack { } } + pub(crate) fn base_before(&self, counts: ValueCounts) -> StackBase { + let base = self.base(); + StackBase { + s32: base.s32 - counts.c32 as u32, + s64: base.s64 - counts.c64 as u32, + s128: base.s128 - counts.c128 as u32, + } + } + #[inline(always)] pub(crate) fn len(&self) -> usize { self.stack_32.len() + self.stack_64.len() + self.stack_128.len() @@ -199,6 +280,24 @@ impl ValueStack { T::stack_push(self, value) } + #[inline(always)] + pub(crate) fn pop_memory_operand(&mut self, arch: MemoryArch) -> Result { + match arch { + MemoryArch::I32 => Ok(self.stack_32.pop() as usize), + MemoryArch::I64 => { + let value = self.stack_64.pop(); + #[cfg(target_pointer_width = "64")] + return Ok(value as usize); + #[cfg(not(target_pointer_width = "64"))] + return cold_err!(usize::try_from(value).map_err(|_| Trap::MemoryOutOfBounds { + offset: usize::MAX, + len: 0, + max: usize::MAX, + })); + } + } + } + #[inline] pub(crate) fn select_multi(&mut self, counts: ValueCounts) { let condition = i32::stack_pop(self) != 0; @@ -207,13 +306,6 @@ impl ValueStack { self.stack_128.select_many(counts.c128 as usize, condition); } - pub(crate) fn pop_types<'a>( - &'a mut self, - val_types: impl IntoIterator, - ) -> impl core::iter::Iterator { - val_types.into_iter().map(|val_type| self.pop_wasmvalue(*val_type)) - } - #[inline(always)] pub(crate) fn enter_locals(&mut self, params: &ValueCounts, locals: &ValueCounts) -> Result { let locals_base32 = self.stack_32.enter_locals(params.c32 as usize, locals.c32 as usize)?; @@ -222,6 +314,19 @@ impl ValueStack { Ok(StackBase { s32: locals_base32, s64: locals_base64, s128: locals_base128 }) } + #[inline] + /// Pushes call arguments and allocates the function's local lanes. + pub(crate) fn enter_wasm_call( + &mut self, + values: &[WasmValue], + params: ValueCounts, + locals: ValueCounts, + base: StackBase, + ) -> Result { + self.extend_wasmvalues(values.iter().copied()).inspect_err(|_| self.truncate_to_base(base))?; + self.enter_locals(¶ms, &locals).inspect_err(|_| self.truncate_to_base(base)) + } + #[inline(always)] pub(crate) fn truncate_keep_counts(&mut self, base: StackBase, keep: ValueCounts) { self.stack_32.truncate_keep(base.s32 as usize, keep.c32 as usize); @@ -238,36 +343,53 @@ impl ValueStack { pub(crate) fn push_dyn(&mut self, value: TinyWasmValue) -> Result<(), Trap> { match value { - TinyWasmValue::Value32(v) => self.stack_32.push(v)?, - TinyWasmValue::Value64(v) => self.stack_64.push(v)?, - TinyWasmValue::Value128(v) => self.stack_128.push(v)?, - TinyWasmValue::ValueRef(v) => self.stack_32.push(v.raw())?, + TinyWasmValue::Value32(v) => self.stack_32.push(v), + TinyWasmValue::Value64(v) => self.stack_64.push(v), + TinyWasmValue::Value128(v) => self.stack_128.push(v), + TinyWasmValue::ValueRef(v) => self.stack_32.push(v.raw()), } - Ok(()) } - pub(crate) fn pop_wasmvalue(&mut self, val_type: WasmType) -> WasmValue { + pub(crate) fn pop_wasmvalue(&mut self, state: &crate::store::State, val_type: WasmType) -> WasmValue { match val_type { - WasmType::I32 => WasmValue::I32(i32::stack_pop(self)), - WasmType::I64 => WasmValue::I64(i64::stack_pop(self)), - WasmType::F32 => WasmValue::F32(f32::stack_pop(self)), - WasmType::F64 => WasmValue::F64(f64::stack_pop(self)), - WasmType::Ref(_) => TinyWasmValue::ValueRef(ValueRef::stack_pop(self)).attach_type(val_type).unwrap(), - WasmType::V128 => WasmValue::V128(Value128::stack_pop(self).0), + WasmType::I32 => WasmValue::I32(self.stack_32.pop() as i32), + WasmType::I64 => WasmValue::I64(self.stack_64.pop() as i64), + WasmType::F32 => WasmValue::F32(f32::from_bits(self.stack_32.pop())), + WasmType::F64 => WasmValue::F64(f64::from_bits(self.stack_64.pop())), + WasmType::Ref(ty) => WasmValue::Ref(state.to_ref_value(ValueRef::from_raw(self.stack_32.pop()), ty)), + WasmType::V128 => WasmValue::V128(self.stack_128.pop().0), } } - pub(crate) fn extend_from_wasmvalues(&mut self, values: &[WasmValue]) -> Result<(), Trap> { + /// Pops values in their logical WebAssembly order. + pub(crate) fn pop_wasmvalues(&mut self, state: &crate::store::State, types: &[WasmType]) -> Vec { + debug_assert!(self.len() >= types.len()); + let mut values = vec![WasmValue::I32(0); types.len()]; + for (index, &ty) in types.iter().enumerate().rev() { + values[index] = self.pop_wasmvalue(state, ty); + } + values + } + + pub(crate) fn wasm_values<'a>( + &'a self, + state: &'a crate::store::State, + types: &'a [WasmType], + index: StackBase, + pin_refs: bool, + ) -> impl ExactSizeIterator + 'a { + WasmValues { stack: self, state, types: types.iter(), index, pin_refs } + } + + pub(crate) fn extend_wasmvalues(&mut self, values: impl Iterator) -> Result<(), Trap> { for value in values { match value { - WasmValue::I32(v) => self.stack_32.push(*v as u32)?, - WasmValue::I64(v) => self.stack_64.push(*v as u64)?, + WasmValue::I32(v) => self.stack_32.push(v as u32)?, + WasmValue::I64(v) => self.stack_64.push(v as u64)?, WasmValue::F32(v) => self.stack_32.push(v.to_bits())?, WasmValue::F64(v) => self.stack_64.push(v.to_bits())?, - WasmValue::Ref(v) => { - self.stack_32.push(TinyWasmValue::from(WasmValue::Ref(*v)).as_ref().unwrap().raw())? - } - WasmValue::V128(v) => self.stack_128.push((*v).into())?, + WasmValue::Ref(v) => self.stack_32.push(ValueRef::from(v).raw())?, + WasmValue::V128(v) => self.stack_128.push(v.into())?, } } Ok(()) diff --git a/crates/tinywasm/src/interpreter/values.rs b/crates/tinywasm/src/interpreter/values.rs index be2e0a14..e2cbef01 100644 --- a/crates/tinywasm/src/interpreter/values.rs +++ b/crates/tinywasm/src/interpreter/values.rs @@ -1,15 +1,18 @@ use super::stack::{CallFrame, ValueStack}; +use crate::store::Globals; use crate::{Result, interpreter::simd::Value128}; -use tinywasm_types::{AnyRef, ExnRef, ExternRef, FuncRef, LocalAddr, RefValue, WasmType, WasmValue}; +use tinywasm_types::{GlobalAddr, LocalAddr, RefValue, WasmValue}; pub(crate) type Value32 = u32; pub(crate) type Value64 = u64; #[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// Packed internal representation of a WebAssembly reference. +/// +/// Unlike the public [`RefValue`], this stores no explicit reference category. +/// Converting it back therefore requires the value's canonical reference type. pub(crate) struct ValueRef(u32); -// TODO(wasm3): GC references need a tagged representation; this raw address is only sufficient for current refs. - impl Default for ValueRef { fn default() -> Self { Self::NULL @@ -17,7 +20,8 @@ impl Default for ValueRef { } impl ValueRef { - pub(crate) const NULL: Self = Self(u32::MAX); + const HOST_ANY_TAG: u32 = 1 << 30; + pub(crate) const NULL: Self = Self(0); #[inline] pub(crate) const fn from_raw(raw: u32) -> Self { @@ -25,16 +29,24 @@ impl ValueRef { } #[inline] - pub(crate) const fn from_addr(addr: Option) -> Self { - match addr { - Some(addr) => Self(addr), - None => Self::NULL, - } + pub(crate) const fn from_category_addr(addr: u32) -> Self { + let Some(raw) = addr.checked_add(1) else { panic!("reference address does not fit in the runtime encoding") }; + let Some(raw) = raw.checked_mul(2) else { panic!("reference address does not fit in the runtime encoding") }; + Self(raw) + } + + #[inline] + pub(crate) const fn from_i31(value: i32) -> Self { + Self(((value as u32) << 1) | 1) + } + + pub(crate) const fn is_host_any(self) -> bool { + matches!(self.addr(), Some(addr) if addr & Self::HOST_ANY_TAG != 0) } #[inline] pub(crate) const fn addr(self) -> Option { - if self.is_null() { None } else { Some(self.0) } + if self.is_null() || self.is_i31() { None } else { Some(self.0 / 2 - 1) } } #[inline] @@ -43,84 +55,49 @@ impl ValueRef { } #[inline] - pub(crate) const fn raw(self) -> u32 { - self.0 + pub(crate) const fn is_i31(self) -> bool { + self.0 & 1 != 0 } -} -#[allow(private_interfaces)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -/// A untyped WebAssembly value -pub enum TinyWasmValue { - /// A 32-bit value - Value32(Value32), - /// A 64-bit value - Value64(Value64), - /// A 128-bit value - Value128(Value128), - /// A reference value - ValueRef(ValueRef), -} - -impl TinyWasmValue { - /// Converts the value to a 32-bit value (returns None if the value is not a 32-bit value) - pub fn as_32(self) -> Option { - match self { - Self::Value32(v) => Some(v), - _ => None, - } + #[inline] + pub(crate) const fn i31_s(self) -> Option { + if self.is_i31() { Some((self.0 as i32) >> 1) } else { None } } - /// Converts the value to a 64-bit value (returns None if the value is not a 64-bit value) - pub fn as_64(self) -> Option { - match self { - Self::Value64(v) => Some(v), - _ => None, - } + #[inline] + pub(crate) const fn i31_u(self) -> Option { + if self.is_i31() { Some(self.0 >> 1) } else { None } } - /// Converts the value to a 128-bit value (returns None if the value is not a 128-bit value) - pub fn as_128(self) -> Option { - match self { - Self::Value128(v) => Some(v), - _ => None, - } + #[inline] + pub(crate) const fn raw(self) -> u32 { + self.0 } +} - /// Converts the value to a reference value (returns None if the value is not a reference value) - #[allow(private_interfaces, dead_code)] - pub fn as_ref(self) -> Option { - match self { - Self::ValueRef(v) => Some(v), - _ => None, +impl From for ValueRef { + fn from(value: RefValue) -> Self { + match value { + RefValue::Null => Self::NULL, + RefValue::Func(value) => Self::from_category_addr(value.addr()), + RefValue::Extern(value) => Self::from_raw(value.raw()), + RefValue::Exn(value) => Self::from_category_addr(value.addr()), + RefValue::Any(value) => Self::from_raw(value.raw()), } } +} - /// Attaches a type to the value (panics if the size of the value is not the same as the type) - pub fn attach_type(self, ty: WasmType) -> Option { - match (self, ty) { - (Self::Value32(v), WasmType::I32) => Some(WasmValue::I32(v as i32)), - (Self::Value64(v), WasmType::I64) => Some(WasmValue::I64(v as i64)), - (Self::Value32(v), WasmType::F32) => Some(WasmValue::F32(f32::from_bits(v))), - (Self::Value64(v), WasmType::F64) => Some(WasmValue::F64(f64::from_bits(v))), - (Self::ValueRef(v), WasmType::Ref(_)) if v.is_null() => Some(RefValue::Null.into()), - (Self::ValueRef(v), WasmType::Ref(ty)) if ty.is_func() => { - Some(WasmValue::Ref(RefValue::Func(FuncRef::new(v.raw())))) - } - (Self::ValueRef(v), WasmType::Ref(ty)) if ty.is_extern() => { - Some(WasmValue::Ref(RefValue::Extern(ExternRef::new(v.raw())))) - } - (Self::ValueRef(v), WasmType::Ref(ty)) if ty.is_exn() => { - Some(WasmValue::Ref(RefValue::Exn(ExnRef::new(v.raw())))) - } - (Self::ValueRef(v), WasmType::Ref(_)) => Some(WasmValue::Ref(RefValue::Any(AnyRef::from_raw(v.raw())))), - (Self::Value128(v), WasmType::V128) => Some(WasmValue::V128(v.0)), - (_, WasmType::I32 | WasmType::F32) => None, - (_, WasmType::I64 | WasmType::F64) => None, - (_, WasmType::Ref(_)) => None, - (_, WasmType::V128) => None, - } - } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// An untyped internal WebAssembly value. +pub(crate) enum TinyWasmValue { + /// A 32-bit value. + Value32(Value32), + /// A 64-bit value. + Value64(Value64), + /// A 128-bit value. + Value128(Value128), + /// A reference value. + ValueRef(ValueRef), } impl From<&WasmValue> for TinyWasmValue { @@ -130,7 +107,7 @@ impl From<&WasmValue> for TinyWasmValue { WasmValue::I64(v) => Self::Value64(*v as u64), WasmValue::F32(v) => Self::Value32(v.to_bits()), WasmValue::F64(v) => Self::Value64(v.to_bits()), - WasmValue::Ref(value) => Self::ValueRef(ValueRef::from_addr(value.raw())), + WasmValue::Ref(value) => Self::ValueRef((*value).into()), WasmValue::V128(v) => Self::Value128((*v).into()), } } @@ -153,21 +130,24 @@ mod sealed { pub trait Sealed {} } -pub(crate) trait InternalValue: sealed::Sealed + Into + Copy + Default { +/// Typed access to values in their physical [`ValueStack`] and [`Globals`] lanes. +pub(crate) trait InternalValue: sealed::Sealed + Copy + Default { fn stack_push(stack: &mut ValueStack, value: Self) -> Result<(), crate::Trap>; fn stack_pop(stack: &mut ValueStack) -> Self; fn stack_peek(stack: &ValueStack) -> Self; - fn stack_select(stack: &mut ValueStack) -> Result<(), crate::Trap>; + fn stack_select(stack: &mut ValueStack); fn local_get(stack: &ValueStack, frame: &CallFrame, index: LocalAddr) -> Self; fn local_set(stack: &mut ValueStack, frame: &CallFrame, index: LocalAddr, value: Self); fn local_update(stack: &mut ValueStack, frame: &CallFrame, index: LocalAddr, f: impl FnOnce(Self) -> Self); fn local_copy(stack: &mut ValueStack, frame: &CallFrame, from: LocalAddr, to: LocalAddr); + fn global_get(globals: &Globals, addr: GlobalAddr) -> Self; + fn global_set(globals: &mut Globals, addr: GlobalAddr, value: Self); } macro_rules! impl_internalvalue { ( $( - $variant:ident, $stack:ident, $stack_base:ident, $outer:ty, + $variant:ident, $stack:ident, $stack_base:ident, $global_get:ident, $global_set:ident, $outer:ty, |$to_value_v:ident| $to_value:expr, |$to_stack_v:ident| $to_stack:expr, |$from_stack_v:ident| $from_stack:expr @@ -188,10 +168,7 @@ macro_rules! impl_internalvalue { #[inline(always)] fn stack_push(stack: &mut ValueStack, value: Self) -> Result<(), crate::Trap> { let $to_stack_v = value; - if let Err(e) = stack.$stack.push($to_stack) { - core::hint::cold_path(); - return Err(e); - } + cold_err!(stack.$stack.push($to_stack))?; Ok(()) } @@ -222,6 +199,18 @@ macro_rules! impl_internalvalue { stack.$stack.copy(base + from as usize, base + to as usize); } + #[inline(always)] + fn global_get(globals: &Globals, addr: GlobalAddr) -> Self { + let $from_stack_v = globals.$global_get(addr); + $from_stack + } + + #[inline(always)] + fn global_set(globals: &mut Globals, addr: GlobalAddr, value: Self) { + let $to_stack_v = value; + globals.$global_set(addr, $to_stack); + } + #[inline(always)] fn stack_pop(stack: &mut ValueStack) -> Self { let $from_stack_v = stack.$stack.pop(); @@ -235,16 +224,14 @@ macro_rules! impl_internalvalue { } #[inline(always)] - fn stack_select(stack: &mut ValueStack) -> Result<(), crate::Trap> { + fn stack_select(stack: &mut ValueStack) { let cond = stack.stack_32.pop() as i32; let val2 = stack.$stack.pop(); if cond == 0 { - Self::stack_pop(stack); - stack.$stack.push(val2)?; + let val1 = stack.$stack.len() - 1; + stack.$stack.set(val1, val2); } - - Ok(()) } } )* @@ -252,12 +239,12 @@ macro_rules! impl_internalvalue { } impl_internalvalue! { - Value32, stack_32, s32, u32, |v| v, |v| v, |v| v - Value64, stack_64, s64, u64, |v| v, |v| v, |v| v - Value32, stack_32, s32, i32, |v| v as u32, |v| v as u32, |v| v as i32 - Value64, stack_64, s64, i64, |v| v as u64, |v| v as u64, |v| v as i64 - Value32, stack_32, s32, f32, |v| f32::to_bits(v), |v| f32::to_bits(v), |v| f32::from_bits(v) - Value64, stack_64, s64, f64, |v| f64::to_bits(v), |v| f64::to_bits(v), |v| f64::from_bits(v) - ValueRef, stack_32, s32, ValueRef, |v| v, |v| v.raw(), |v| ValueRef(v) - Value128, stack_128, s128, Value128, |v| v, |v| v, |v| v + Value32, stack_32, s32, get_32, set_32, u32, |v| v, |v| v, |v| v + Value64, stack_64, s64, get_64, set_64, u64, |v| v, |v| v, |v| v + Value32, stack_32, s32, get_32, set_32, i32, |v| v as u32, |v| v as u32, |v| v as i32 + Value64, stack_64, s64, get_64, set_64, i64, |v| v as u64, |v| v as u64, |v| v as i64 + Value32, stack_32, s32, get_32, set_32, f32, |v| f32::to_bits(v), |v| f32::to_bits(v), |v| f32::from_bits(v) + Value64, stack_64, s64, get_64, set_64, f64, |v| f64::to_bits(v), |v| f64::to_bits(v), |v| f64::from_bits(v) + ValueRef, stack_32, s32, get_32, set_32, ValueRef, |v| v, |v| v.raw(), |v| ValueRef(v) + Value128, stack_128, s128, get_128, set_128, Value128, |v| v, |v| v, |v| v } diff --git a/crates/tinywasm/src/lib.rs b/crates/tinywasm/src/lib.rs index 908f33be..e941430b 100644 --- a/crates/tinywasm/src/lib.rs +++ b/crates/tinywasm/src/lib.rs @@ -7,12 +7,12 @@ #![cfg_attr(not(feature = "simd-x86"), forbid(unsafe_code))] #![cfg_attr(feature = "simd-x86", deny(unsafe_code))] -//! `tinywasm` provides a small, portable WebAssembly interpreter with support for -//! the WebAssembly MVP, WebAssembly 2.0, and a growing set of newer proposals. -//! It also supports the [Lime1](https://github.com/WebAssembly/tool-conventions/blob/main/Lime.md#lime1) -//! interoperability target. -//! It is designed to stay lightweight while still being practical to embed in -//! applications, tools, and `no_std + alloc` environments. +//! A small and portable WebAssembly interpreter. +//! +//! `tinywasm` passes the full WebAssembly 3.0 core testsuite and supports the +//! [Lime1](https://github.com/WebAssembly/tool-conventions/blob/main/Lime.md#lime1) +//! interoperability target. It is designed for embedding in applications, tools, +//! and `no_std + alloc` environments. #![cfg_attr(docsrs, feature(doc_cfg))] //! @@ -64,10 +64,11 @@ //! - **`std`:** Enables `std` and parsing from files and streams. Enabled by default. //! - **`log`:** Enables integration with the `log` crate. Enabled by default. //! - **`parser`:** Enables `tinywasm-parser` and top-level parse helpers. Enabled by default. +//! - **`validate`:** Enables WebAssembly validation while parsing. Enabled by default and configurable through `ParserOptions`. //! - **`archive`:** Enables serialization and deserialization of the internal `twasm` format. Enabled by default. //! - **`canonicalize-nans`:** Canonicalizes NaN values. Enabled by default. //! - **`debug`:** Derives `Debug` for runtime types. Enabled by default. -//! - **`parallel-parser`:** Parallelizes function parsing and validation when `std` is enabled. Enabled by default. +//! - **`parallel-parser`:** Parallelizes function parsing when `std` is enabled. Enabled by default. //! - **`guest-debug`:** Exposes module-internal by-index inspection APIs (`*_by_index`). //! - **`simd-x86`:** Enables x86-specific SIMD intrinsics and uses `unsafe` internally. //! @@ -78,10 +79,12 @@ //! //! To provide 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. +//! tags, and other modules to be linked into the module when it is instantiated. //! //! See the [`Imports`] documentation for more information. +#[macro_use] +mod macros; mod std; extern crate alloc; @@ -104,9 +107,11 @@ pub(crate) mod log { mod error; pub use error::*; +#[allow(deprecated)] +pub use func::WasmTupleChain; pub use func::{ - ExecProgress, FuncContext, FuncExecution, FuncExecutionTyped, Function, FunctionTyped, HostFunction, ToWasmTypes, - WasmTupleChain, + ExecProgress, FromWasmValues, FuncContext, FuncExecution, FuncExecutionTyped, Function, FunctionTyped, + HostFunction, IntoWasmValues, ToWasmType, ToWasmTypes, }; pub use imports::*; pub use instance::{ExternItem, ModuleInstance}; diff --git a/crates/tinywasm/src/macros.rs b/crates/tinywasm/src/macros.rs new file mode 100644 index 00000000..351ae886 --- /dev/null +++ b/crates/tinywasm/src/macros.rs @@ -0,0 +1,18 @@ +macro_rules! cold { + ($value:expr) => {{ + core::hint::cold_path(); + $value + }}; +} + +// Mark the caller's error path cold. This makes a significant difference in interpreter benchmarks, +// while doing it inside map_err or inspect_err is unreliable: +// https://internals.rust-lang.org/t/err-automatic-hint-cold-path/24404 +macro_rules! cold_err { + ($result:expr) => { + match $result { + Ok(value) => Ok(value), + Err(error) => cold!(Err(error)), + } + }; +} diff --git a/crates/tinywasm/src/reference.rs b/crates/tinywasm/src/reference.rs index dc547365..f5d9c23a 100644 --- a/crates/tinywasm/src/reference.rs +++ b/crates/tinywasm/src/reference.rs @@ -5,20 +5,24 @@ use alloc::format; use alloc::string::{String, ToString}; use alloc::vec::Vec; -use crate::store::{GlobalInstance, TableElement, TableInstance}; +use crate::interpreter::ValueRef; +use crate::store::TableInstance; use crate::{Error, MemoryInstance, Result, Store, Trap}; -use tinywasm_types::{Addr, GlobalAddr, GlobalType, MemAddr, MemoryType, TableAddr, TableType, WasmType, WasmValue}; +use tinywasm_types::{ + Addr, FuncType, GlobalType, MemAddr, MemoryType, TableAddr, TableType, TagAddr, WasmType, WasmValue, +}; #[derive(Clone, Copy, PartialEq, Eq, Hash)] #[cfg_attr(feature = "debug", derive(Debug))] pub(crate) struct StoreItem { - pub(crate) store_id: usize, + pub(crate) store_id: u32, pub(crate) addr: Addr, } impl StoreItem { #[inline] - pub(crate) const fn new(store_id: usize, addr: Addr) -> Self { + /// Creates a handle for an address owned by a store. + pub(crate) const fn new(store_id: u32, addr: Addr) -> Self { Self { store_id, addr } } @@ -64,6 +68,11 @@ pub struct Table(pub(crate) StoreItem); #[cfg_attr(feature = "debug", derive(Debug))] pub struct Global(pub(crate) StoreItem); +/// A tag instance in a store. +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub struct Tag(pub(crate) StoreItem); + /// A cursor over a [`Memory`] instance. /// /// Available with the `std` feature enabled. @@ -74,11 +83,7 @@ pub struct MemoryCursor<'a> { } #[cfg(feature = "std")] -impl<'a> MemoryCursor<'a> { - fn new(memory: &'a mut MemoryInstance, position: u64) -> Self { - Self { memory, position } - } - +impl MemoryCursor<'_> { fn offset(&self) -> crate::std::io::Result { usize::try_from(self.position).map_err(|_| { crate::std::io::Error::new(crate::std::io::ErrorKind::InvalidInput, "cursor position exceeds usize") @@ -117,7 +122,7 @@ impl crate::std::io::Read for MemoryCursor<'_> { impl crate::std::io::Write for MemoryCursor<'_> { fn write(&mut self, buf: &[u8]) -> crate::std::io::Result { let offset = self.offset()?; - let written = self.memory.inner.write(offset, buf); + let written = self.memory.inner.write(offset, buf).map_err(Error::from)?; self.advance(written)?; Ok(written) } @@ -198,7 +203,7 @@ impl Memory { /// Available with the `std` feature enabled. #[cfg(feature = "std")] pub fn cursor_at<'a>(&self, store: &'a mut Store, position: u64) -> Result> { - Ok(MemoryCursor::new(self.instance_mut(store)?, position)) + Ok(MemoryCursor { memory: self.instance_mut(store)?, position }) } #[inline] @@ -237,7 +242,7 @@ impl Memory { /// Depending on the configured backend, this may return fewer bytes than requested even when /// more space is available. Use [`Self::copy_from_slice`] when you need the full slice written. pub fn write(&self, store: &mut Store, offset: usize, src: &[u8]) -> Result { - Ok(self.instance_mut(store)?.inner.write(offset, src)) + Ok(self.instance_mut(store)?.inner.write(offset, src)?) } /// Reads exactly `dst.len()` bytes from memory. @@ -276,14 +281,14 @@ impl Memory { /// Fill a slice of memory with a value. pub fn fill(&self, store: &mut Store, offset: usize, len: usize, val: u8) -> Result<()> { - self.instance_mut(store)?.inner.fill(offset, len, val).ok_or_else(|| { + self.instance_mut(store)?.inner.fill(offset, len, val)?.ok_or_else(|| { Error::Trap(crate::Trap::MemoryOutOfBounds { offset, len, max: self.instance(store).unwrap().inner.len() }) }) } /// Copies a full slice into memory. pub fn copy_from_slice(&self, store: &mut Store, offset: usize, data: &[u8]) -> Result<()> { - self.instance_mut(store)?.inner.write_all(offset, data).ok_or_else(|| { + self.instance_mut(store)?.inner.write_all(offset, data)?.ok_or_else(|| { Error::Trap(crate::Trap::MemoryOutOfBounds { offset, len: data.len(), @@ -346,12 +351,14 @@ fn table_value_to_element( state: &crate::store::State, element_type: tinywasm_types::RefType, value: WasmValue, -) -> Result { - if !state.value_matches_type(value, WasmType::Ref(element_type)) { +) -> Result { + let WasmValue::Ref(value) = value else { + return Err(Trap::Other("invalid table value type")); + }; + if !state.value_matches_type(WasmValue::Ref(value), WasmType::Ref(element_type)) { return Err(Trap::Other("invalid table value type")); } - let WasmValue::Ref(value) = value else { unreachable!() }; - Ok(TableElement::from(value.raw())) + Ok(value.into()) } impl Table { @@ -362,7 +369,7 @@ impl Table { } let init = table_value_to_element(&store.state, ty.element_type, init).map_err(Error::from)?; let addr = store.state.tables.len() as TableAddr; - store.state.tables.push(TableInstance::new_with_init(ty, init)?); + store.state.tables.push(TableInstance::new(ty, init)?); Ok(Self(StoreItem::new(store.id(), addr))) } @@ -372,12 +379,6 @@ impl Table { Ok(store.state.get_table(self.0.addr)) } - #[inline] - fn instance_mut<'a>(&self, store: &'a mut Store) -> Result<&'a mut TableInstance, Trap> { - self.0.validate_store(store)?; - Ok(store.state.get_table_mut(self.0.addr)) - } - /// Get the type of the table. pub fn ty(&self, store: &Store) -> Result { Ok(self.instance(store)?.kind) @@ -390,20 +391,29 @@ impl Table { /// Get a table element as a wasm reference value. pub fn get(&self, store: &Store, index: TableAddr) -> Result { - Ok(self.instance(store)?.get_wasm_val(index as usize)?) + let table = self.instance(store)?; + let value = store.state.to_ref_value(*table.get(index as usize)?, table.kind.element_type); + store.state.pin_host_ref(value); + Ok(WasmValue::Ref(value)) } /// Load a range of table elements and iterate over wasm reference values. - pub fn load(&self, store: &Store, offset: usize, len: usize) -> Result> { + pub fn load<'a>( + &self, + store: &'a Store, + offset: usize, + len: usize, + ) -> Result + 'a> { let table = self.instance(store)?; let element_type = table.kind.element_type; let elements = table.load(offset, len)?; - Ok(elements - .iter() - .copied() - .map(move |element| element.to_wasm_value(element_type)) - .collect::>() - .into_iter()) + let may_contain_gc = store.state.type_may_contain_gc(&WasmType::Ref(element_type)); + Ok(elements.iter().copied().map(move |value| { + if may_contain_gc { + store.state.gc.pin(value); + } + WasmValue::Ref(store.state.to_ref_value(value, element_type)) + })) } /// Set a table element. @@ -416,7 +426,8 @@ impl Table { /// Copy elements within the same table. pub fn copy_within(&self, store: &mut Store, src: usize, dst: usize, len: usize) -> Result<(), Trap> { - self.instance_mut(store)?.copy_within(dst, src, len) + self.0.validate_store(store)?; + store.state.get_table_mut(self.0.addr).copy_within(dst, src, len) } /// Grow the table and return the previous size. @@ -441,42 +452,48 @@ impl Global { cold_path(); return Err(Error::Other("invalid global value type".to_string())); } - let addr = store.state.globals.len() as GlobalAddr; - store.state.globals.push(GlobalInstance::new(ty, value.into())); + let addr = store.state.globals.push(ty, value.into()); Ok(Self(StoreItem::new(store.id(), addr))) } - #[inline] - fn instance<'a>(&self, store: &'a Store) -> Result<&'a GlobalInstance> { - self.0.validate_store(store)?; - Ok(store.state.get_global(self.0.addr)) - } - /// Get the type of the global. pub fn ty(&self, store: &Store) -> Result { - Ok(self.instance(store)?.ty) + self.0.validate_store(store)?; + Ok(store.state.globals.ty(self.0.addr)) } /// Get the current value of the global. pub fn get(&self, store: &Store) -> Result { - let global = self.instance(store)?; - let value = global.value.attach_type(global.ty.ty); - Ok(value.unwrap_or_else(|| unreachable!("Global value type does not match global type, this is a bug"))) + self.0.validate_store(store)?; + let value = store.state.get_global_wasmvalue(self.0.addr); + if let WasmValue::Ref(value) = value { + store.state.pin_host_ref(value); + } + Ok(value) } /// Set the current value of the global. pub fn set(&self, store: &mut Store, value: WasmValue) -> Result<()> { self.0.validate_store(store)?; - let global = store.state.get_global(self.0.addr); - if !global.ty.mutable { - cold_path(); - return Err(Error::Other("global is immutable".to_string())); - } - if !store.state.value_matches_type(value, global.ty.ty) { - cold_path(); - return Err(Error::Other("invalid global value type".to_string())); + store.state.set_global_wasmvalue(self.0.addr, value) + } +} + +impl Tag { + /// Create a new exception tag in the given store. + pub fn new(store: &mut Store, ty: FuncType) -> Result { + if !ty.results().is_empty() { + return Err(Error::other("tag types must not have results")); } - store.state.get_global_mut(self.0.addr).value = value.into(); - Ok(()) + let type_addr = store.register_host_type(&ty); + let addr = store.state.tags.len() as TagAddr; + store.state.tags.push(crate::store::TagInstance { type_addr }); + Ok(Self(StoreItem::new(store.id(), addr))) + } + + /// Get the payload type of the tag. + pub fn ty<'a>(&self, store: &'a Store) -> Result<&'a FuncType> { + self.0.validate_store(store)?; + Ok(store.state.get_canonical_func_type(store.state.get_tag(self.0.addr).type_addr)) } } diff --git a/crates/tinywasm/src/store/const_expr.rs b/crates/tinywasm/src/store/const_expr.rs new file mode 100644 index 00000000..2c847a73 --- /dev/null +++ b/crates/tinywasm/src/store/const_expr.rs @@ -0,0 +1,222 @@ +use alloc::{format, vec::Vec}; +use tinywasm_types::*; + +use super::{State, default_value}; +use crate::interpreter::{TinyWasmValue, ValueRef}; +use crate::{Error, Result, Trap}; + +fn resolve(items: &[T], index: u32, kind: &str) -> Result { + items.get(index as usize).copied().ok_or_else(|| Error::Other(format!("{kind} {index} not found"))) +} + +fn pop_value(stack: &mut Vec, storage: StorageType) -> Result { + let value = stack.pop().ok_or_else(|| Error::other("const stack underflow"))?; + match (storage, value) { + (StorageType::I8, TinyWasmValue::Value32(value)) => Ok(TinyWasmValue::Value32(value as u8 as u32)), + (StorageType::I16, TinyWasmValue::Value32(value)) => Ok(TinyWasmValue::Value32(value as u16 as u32)), + (StorageType::Value(WasmType::I32 | WasmType::F32), value @ TinyWasmValue::Value32(_)) + | (StorageType::Value(WasmType::I64 | WasmType::F64), value @ TinyWasmValue::Value64(_)) + | (StorageType::Value(WasmType::V128), value @ TinyWasmValue::Value128(_)) + | (StorageType::Value(WasmType::Ref(_)), value @ TinyWasmValue::ValueRef(_)) => Ok(value), + _ => Err(Error::other("type mismatch in GC constant")), + } +} + +fn value_ref(value: &TinyWasmValue) -> Option { + match value { + TinyWasmValue::ValueRef(value) => Some(*value), + _ => None, + } +} + +fn alloc_object( + state: &mut State, + stack: &mut Vec, + type_addr: TypeAddr, + values: Vec, +) -> Result<()> { + let roots = stack.iter().filter_map(value_ref).map(ValueRef::raw); + let reference = state.alloc_gc_object(type_addr, values, roots)?; + stack.push(TinyWasmValue::ValueRef(reference)); + Ok(()) +} + +#[inline] +pub(super) fn eval_const( + state: &mut State, + instructions: &[ConstInstruction], + global_addrs: &[GlobalAddr], + func_addrs: &[FuncAddr], + type_addrs: &[TypeAddr], +) -> Result { + use ConstInstruction::*; + + if let [instruction] = instructions { + match instruction { + I32Const(value) => return Ok(TinyWasmValue::Value32(*value as u32)), + I64Const(value) => return Ok(TinyWasmValue::Value64(*value as u64)), + F32Const(value) => return Ok(TinyWasmValue::Value32(value.to_bits())), + F64Const(value) => return Ok(TinyWasmValue::Value64(value.to_bits())), + V128Const(value) => return Ok(TinyWasmValue::Value128((*value).into())), + GlobalGet32(index) => { + return Ok(TinyWasmValue::Value32(state.globals.get_32(resolve(global_addrs, *index, "global")?))); + } + GlobalGet64(index) => { + return Ok(TinyWasmValue::Value64(state.globals.get_64(resolve(global_addrs, *index, "global")?))); + } + GlobalGet128(index) => { + return Ok(TinyWasmValue::Value128(state.globals.get_128(resolve(global_addrs, *index, "global")?))); + } + GlobalGetRef(index) => { + let value = state.globals.get_32(resolve(global_addrs, *index, "global")?); + return Ok(TinyWasmValue::ValueRef(ValueRef::from_raw(value))); + } + Ref(RefValue::Null) => return Ok(TinyWasmValue::ValueRef(ValueRef::NULL)), + Ref(RefValue::Func(func)) => { + let addr = resolve(func_addrs, func.addr(), "function")?; + return Ok(TinyWasmValue::ValueRef(ValueRef::from_category_addr(addr))); + } + _ => {} + } + } + + let mut stack = Vec::new(); + for instruction in instructions { + match instruction { + I32Const(value) => stack.push(TinyWasmValue::Value32(*value as u32)), + I64Const(value) => stack.push(TinyWasmValue::Value64(*value as u64)), + F32Const(value) => stack.push(TinyWasmValue::Value32(value.to_bits())), + F64Const(value) => stack.push(TinyWasmValue::Value64(value.to_bits())), + V128Const(value) => stack.push(TinyWasmValue::Value128((*value).into())), + GlobalGet32(index) => { + stack.push(TinyWasmValue::Value32(state.globals.get_32(resolve(global_addrs, *index, "global")?))); + } + GlobalGet64(index) => { + stack.push(TinyWasmValue::Value64(state.globals.get_64(resolve(global_addrs, *index, "global")?))); + } + GlobalGet128(index) => { + stack.push(TinyWasmValue::Value128(state.globals.get_128(resolve(global_addrs, *index, "global")?))); + } + GlobalGetRef(index) => { + let value = state.globals.get_32(resolve(global_addrs, *index, "global")?); + stack.push(TinyWasmValue::ValueRef(ValueRef::from_raw(value))); + } + Ref(RefValue::Null) => stack.push(TinyWasmValue::ValueRef(ValueRef::NULL)), + Ref(RefValue::Func(func)) => { + let addr = resolve(func_addrs, func.addr(), "function")?; + stack.push(TinyWasmValue::ValueRef(ValueRef::from_category_addr(addr))); + } + Ref(_) => { + return cold!(Err(Error::other("unsupported reference constant"))); + } + RefI31 => { + let value = stack.pop().ok_or_else(|| Error::other("const stack underflow"))?; + let TinyWasmValue::Value32(value) = value else { + return Err(Error::other("type mismatch in const ref.i31")); + }; + stack.push(TinyWasmValue::ValueRef(ValueRef::from_i31(value as i32))); + } + AnyConvertExtern | ExternConvertAny => { + let value = stack.pop().ok_or_else(|| Error::other("const stack underflow"))?; + if !matches!(value, TinyWasmValue::ValueRef(_)) { + return Err(Error::other("type mismatch in const reference conversion")); + } + stack.push(value); + } + StructNew(type_index) | StructNewDefault(type_index) => { + let type_addr = resolve(type_addrs, *type_index, "type")?; + let fields = state + .get_type(type_addr) + .as_struct() + .ok_or_else(|| Error::other("GC constant type is not a struct"))? + .fields + .as_ref(); + let default = matches!(instruction, StructNewDefault(_)); + let mut values = Vec::new(); + cold_err!(values.try_reserve_exact(fields.len())).map_err(|_| Trap::OutOfMemory)?; + if default { + values.extend(fields.iter().map(|field| default_value(field.storage))); + } else { + for field in fields.iter().rev() { + values.push(pop_value(&mut stack, field.storage)?); + } + values.reverse(); + } + alloc_object(state, &mut stack, type_addr, values)?; + } + ArrayNew(type_index) | ArrayNewDefault(type_index) => { + let type_addr = resolve(type_addrs, *type_index, "type")?; + let storage = state + .get_type(type_addr) + .as_array() + .ok_or_else(|| Error::other("GC constant type is not an array"))? + .field + .storage; + let Some(TinyWasmValue::Value32(len)) = stack.pop() else { + return Err(Error::other("type mismatch in const array length")); + }; + let value = if matches!(instruction, ArrayNewDefault(_)) { + default_value(storage) + } else { + pop_value(&mut stack, storage)? + }; + let mut values = Vec::new(); + cold_err!(values.try_reserve_exact(len as usize)).map_err(|_| Trap::OutOfMemory)?; + values.resize(len as usize, value); + alloc_object(state, &mut stack, type_addr, values)?; + } + ArrayNewFixed(type_index, len) => { + let type_addr = resolve(type_addrs, *type_index, "type")?; + let storage = state + .get_type(type_addr) + .as_array() + .ok_or_else(|| Error::other("GC constant type is not an array"))? + .field + .storage; + let mut values = Vec::new(); + cold_err!(values.try_reserve_exact(*len as usize)).map_err(|_| Trap::OutOfMemory)?; + for _ in 0..*len { + values.push(pop_value(&mut stack, storage)?); + } + values.reverse(); + alloc_object(state, &mut stack, type_addr, values)?; + } + I32Add | I32Sub | I32Mul => { + let rhs = stack.pop().ok_or_else(|| Error::other("const stack underflow"))?; + let lhs = stack.pop().ok_or_else(|| Error::other("const stack underflow"))?; + let (TinyWasmValue::Value32(lhs), TinyWasmValue::Value32(rhs)) = (lhs, rhs) else { + return cold!(Err(Error::other("type mismatch in const i32 op"))); + }; + let out = match instruction { + I32Add => (lhs as i32).wrapping_add(rhs as i32), + I32Sub => (lhs as i32).wrapping_sub(rhs as i32), + I32Mul => (lhs as i32).wrapping_mul(rhs as i32), + _ => unreachable!(), + }; + stack.push(TinyWasmValue::Value32(out as u32)); + } + I64Add | I64Sub | I64Mul => { + let rhs = stack.pop(); + let lhs = stack.pop(); + let (Some(TinyWasmValue::Value64(lhs)), Some(TinyWasmValue::Value64(rhs))) = (lhs, rhs) else { + return cold!(Err(Error::other("type mismatch in const i64 op"))); + }; + let out = match instruction { + I64Add => (lhs as i64).wrapping_add(rhs as i64), + I64Sub => (lhs as i64).wrapping_sub(rhs as i64), + I64Mul => (lhs as i64).wrapping_mul(rhs as i64), + _ => unreachable!(), + }; + stack.push(TinyWasmValue::Value64(out as u64)); + } + } + } + + let Some(value) = stack.pop() else { + return cold!(Err(Error::other("empty const expression"))); + }; + if !stack.is_empty() { + return cold!(Err(Error::other("const expression did not reduce to single value"))); + } + Ok(value) +} diff --git a/crates/tinywasm/src/store/element.rs b/crates/tinywasm/src/store/element.rs index f3a86024..8119fe82 100644 --- a/crates/tinywasm/src/store/element.rs +++ b/crates/tinywasm/src/store/element.rs @@ -1,16 +1,27 @@ -use crate::TableElement; +use crate::{Trap, interpreter::ValueRef}; use alloc::vec::Vec; +use tinywasm_types::RefType; /// A WebAssembly Element Instance /// /// See #[cfg_attr(feature = "debug", derive(Debug))] pub(crate) struct ElementInstance { - pub(crate) items: Option>, // none is the element was dropped + pub(crate) items: Option>, // none is the element was dropped + pub(crate) ty: RefType, } impl ElementInstance { pub(crate) fn drop(&mut self) { self.items.take(); } + + pub(crate) fn items_range(&self, offset: usize, len: usize) -> Result<&[ValueRef], Trap> { + let items = self.items.as_deref().unwrap_or(&[]); + let end = offset.checked_add(len).filter(|end| *end <= items.len()).ok_or_else(|| { + core::hint::cold_path(); + Trap::TableOutOfBounds { offset, len, max: items.len() } + })?; + Ok(&items[offset..end]) + } } diff --git a/crates/tinywasm/src/store/exception.rs b/crates/tinywasm/src/store/exception.rs new file mode 100644 index 00000000..0725cda7 --- /dev/null +++ b/crates/tinywasm/src/store/exception.rs @@ -0,0 +1,10 @@ +use alloc::boxed::Box; +use tinywasm_types::TagAddr; + +use crate::interpreter::TinyWasmValue; + +#[cfg_attr(feature = "debug", derive(Debug))] +pub(crate) struct ExceptionInstance { + pub(crate) tag_addr: TagAddr, + pub(crate) payload: Box<[TinyWasmValue]>, +} diff --git a/crates/tinywasm/src/store/function.rs b/crates/tinywasm/src/store/function.rs index f0e3ed9c..3580e9d6 100644 --- a/crates/tinywasm/src/store/function.rs +++ b/crates/tinywasm/src/store/function.rs @@ -1,4 +1,4 @@ -use alloc::{rc::Rc, sync::Arc}; +use alloc::sync::Arc; use tinywasm_types::*; use crate::func::HostFunction; @@ -10,14 +10,22 @@ use crate::func::HostFunction; #[cfg_attr(feature = "debug", derive(Debug))] pub(crate) struct FunctionInstance { pub(crate) type_addr: TypeAddr, + pub(crate) gc: FunctionGcMetadata, pub(crate) kind: FunctionKind, } +#[derive(Clone, Copy)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub(crate) struct FunctionGcMetadata { + pub(crate) params: bool, + pub(crate) results: bool, +} + #[derive(Clone)] #[cfg_attr(feature = "debug", derive(Debug))] pub(crate) enum FunctionKind { /// A host function - Host(Rc), + Host(HostFunction), /// A pointer to a WebAssembly function Wasm(WasmFunctionInstance), diff --git a/crates/tinywasm/src/store/gc/arena.rs b/crates/tinywasm/src/store/gc/arena.rs new file mode 100644 index 00000000..6238466f --- /dev/null +++ b/crates/tinywasm/src/store/gc/arena.rs @@ -0,0 +1,278 @@ +//! Mark-and-sweep storage with stable handles for WebAssembly GC objects. + +use alloc::vec::Vec; +use core::cell::Cell; +use core::mem::size_of; +use core::num::NonZeroU32; + +/// A stable reference to an arena slot. +/// +/// Reclaimed slots increment their generation so stale handles cannot access a +/// new object allocated in the same slot. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct Handle { + index: u32, + generation: NonZeroU32, +} + +/// Adds the handles referenced by an arena object to the mark worklist. +pub(crate) trait Trace { + /// Calls `mark` for every arena object referenced by this object. + fn trace(&self, mark: &mut impl FnMut(Handle)); +} + +/// An arena allocation or capacity error. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct AllocError; + +impl core::fmt::Display for AllocError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_str("GC arena capacity exhausted") + } +} + +impl core::error::Error for AllocError {} + +#[inline] +fn mark(slots: &[Slot], worklist: &mut Vec, handle: Handle) { + let Some(slot) = slots.get(handle.index as usize) else { + return; + }; + if slot.generation != handle.generation || !matches!(slot.state, SlotState::Occupied { .. }) || slot.marked.get() { + return; + } + slot.marked.set(true); + worklist.push(handle.index); +} + +struct Slot { + generation: NonZeroU32, + marked: Cell, + state: SlotState, +} + +enum SlotState { + Occupied { value: T, bytes: usize }, + Free { next: Option }, + Retired, +} + +/// A mark-and-sweep arena with stable generational handles. +pub(crate) struct Arena { + slots: Vec>, + free_head: Option, + worklist: Vec, + len: usize, + allocated_bytes: usize, + collection_threshold: usize, + next_collection: usize, +} + +impl Arena { + /// Creates an empty arena with the given initial collection threshold. + pub(crate) const fn new(collection_threshold: usize) -> Self { + Self { + slots: Vec::new(), + free_head: None, + worklist: Vec::new(), + len: 0, + allocated_bytes: 0, + collection_threshold, + next_collection: collection_threshold, + } + } + + /// Allocates a value with `out_of_line_bytes` of storage owned outside its slot. + /// + /// The byte count must remain valid while the value is in the arena. + pub(crate) fn alloc(&mut self, value: T, out_of_line_bytes: usize) -> Result { + let bytes = size_of::>().checked_add(out_of_line_bytes).ok_or(AllocError)?; + let allocated_bytes = self.allocated_bytes.checked_add(bytes).ok_or(AllocError)?; + + let handle = if let Some(index) = self.free_head { + let slot = &mut self.slots[index as usize]; + let SlotState::Free { next } = slot.state else { unreachable!("free list points to an occupied slot") }; + self.free_head = next; + slot.marked.set(false); + slot.state = SlotState::Occupied { value, bytes }; + Handle { index, generation: slot.generation } + } else { + if self.slots.len() >= u32::MAX as usize { + return Err(AllocError); + } + let index = self.slots.len() as u32; + self.slots.try_reserve(1).map_err(|_| AllocError)?; + self.slots.push(Slot { + generation: NonZeroU32::MIN, + marked: Cell::new(false), + state: SlotState::Occupied { value, bytes }, + }); + Handle { index, generation: NonZeroU32::MIN } + }; + + self.len += 1; + self.allocated_bytes = allocated_bytes; + Ok(handle) + } + + /// Returns a shared reference if the handle is live and current. + pub(crate) fn get(&self, handle: Handle) -> Option<&T> { + let slot = self.slots.get(handle.index as usize)?; + if slot.generation != handle.generation { + return None; + } + match &slot.state { + SlotState::Occupied { value, .. } => Some(value), + SlotState::Free { .. } | SlotState::Retired => None, + } + } + + /// Returns an exclusive reference if the handle is live and current. + pub(crate) fn get_mut(&mut self, handle: Handle) -> Option<&mut T> { + let slot = self.slots.get_mut(handle.index as usize)?; + if slot.generation != handle.generation { + return None; + } + match &mut slot.state { + SlotState::Occupied { value, .. } => Some(value), + SlotState::Free { .. } | SlotState::Retired => None, + } + } + + /// Returns exclusive references for two distinct live handles. + pub(crate) fn get_disjoint_mut(&mut self, first: Handle, second: Handle) -> Option<(&mut T, &mut T)> { + let [first_slot, second_slot] = + self.slots.get_disjoint_mut([first.index as usize, second.index as usize]).ok()?; + if first_slot.generation != first.generation || second_slot.generation != second.generation { + return None; + } + match (&mut first_slot.state, &mut second_slot.state) { + (SlotState::Occupied { value: first, .. }, SlotState::Occupied { value: second, .. }) => { + Some((first, second)) + } + _ => None, + } + } + + /// Returns whether an allocation of this size should trigger collection. + pub(crate) fn should_collect(&self, out_of_line_bytes: usize) -> bool { + size_of::>() + .checked_add(out_of_line_bytes) + .and_then(|bytes| self.allocated_bytes.checked_add(bytes)) + .is_none_or(|bytes| bytes >= self.next_collection) + } +} + +impl Arena { + /// Collects objects that cannot be reached from `roots`. + pub(crate) fn collect(&mut self, roots: impl IntoIterator) -> Result<(), AllocError> { + self.worklist.clear(); + self.worklist.try_reserve(self.len).map_err(|_| AllocError)?; + + for root in roots { + mark(&self.slots, &mut self.worklist, root); + } + + while let Some(index) = self.worklist.pop() { + let slots = &self.slots; + let worklist = &mut self.worklist; + let SlotState::Occupied { value, .. } = &slots[index as usize].state else { + unreachable!("marked slots are occupied") + }; + value.trace(&mut |handle| mark(slots, worklist, handle)); + } + + let mut live_objects = 0; + let mut live_bytes = 0; + for (index, slot) in self.slots.iter_mut().enumerate() { + let SlotState::Occupied { bytes, .. } = &slot.state else { + continue; + }; + if slot.marked.replace(false) { + live_objects += 1; + live_bytes += bytes; + continue; + } + + if let Some(generation) = slot.generation.checked_add(1) { + slot.generation = generation; + slot.state = SlotState::Free { next: self.free_head }; + self.free_head = Some(index as u32); + } else { + slot.state = SlotState::Retired; + } + } + + self.len = live_objects; + self.allocated_bytes = live_bytes; + self.next_collection = live_bytes.saturating_mul(2).max(self.collection_threshold); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::vec; + + struct Object(Vec); + + impl Trace for Object { + fn trace(&self, mark: &mut impl FnMut(Handle)) { + for &handle in &self.0 { + mark(handle); + } + } + } + + #[test] + fn reuses_slots_and_rejects_stale_handles() { + let mut arena = Arena::new(1024); + let stale = arena.alloc(Object(vec![]), 0).unwrap(); + + arena.collect([]).unwrap(); + let current = arena.alloc(Object(vec![]), 0).unwrap(); + + assert_eq!(stale.index, current.index); + assert_ne!(stale.generation, current.generation); + assert!(arena.get(stale).is_none()); + assert!(arena.get(current).is_some()); + } + + #[test] + fn keeps_reachable_cycles() { + let mut arena = Arena::new(1024); + let first = arena.alloc(Object(Vec::with_capacity(1)), size_of::()).unwrap(); + let second = arena.alloc(Object(vec![first]), size_of::()).unwrap(); + arena.get_mut(first).unwrap().0.push(second); + + arena.collect([first]).unwrap(); + + assert_eq!(arena.len, 2); + assert_eq!(arena.allocated_bytes, size_of::>() * 2 + size_of::() * 2); + assert_eq!(arena.get(second).unwrap().0, [first]); + } + + #[test] + fn reclaims_unreachable_cycles() { + let mut arena = Arena::new(1024); + let first = arena.alloc(Object(Vec::with_capacity(1)), size_of::()).unwrap(); + let second = arena.alloc(Object(vec![first]), size_of::()).unwrap(); + arena.get_mut(first).unwrap().0.push(second); + + arena.collect([]).unwrap(); + + assert_eq!(arena.len, 0); + assert_eq!(arena.allocated_bytes, 0); + } + + #[test] + fn accounts_for_object_storage() { + let mut arena = Arena::new(1024); + let root = arena.alloc(Object(vec![]), 24).unwrap(); + + arena.collect([root]).unwrap(); + + assert_eq!(arena.allocated_bytes, size_of::>() + 24); + } +} diff --git a/crates/tinywasm/src/store/gc/mod.rs b/crates/tinywasm/src/store/gc/mod.rs new file mode 100644 index 00000000..6431037e --- /dev/null +++ b/crates/tinywasm/src/store/gc/mod.rs @@ -0,0 +1,92 @@ +mod arena; +mod object; + +use alloc::vec::Vec; +use tinywasm_types::{StorageType, WasmType}; + +use crate::Trap; +use crate::interpreter::stack::ValueStack; +use crate::interpreter::{InternalValue, TinyWasmValue, Value128, ValueRef}; + +pub(crate) use arena::{AllocError, Arena, Handle, Trace}; +pub(crate) use object::{GcHeap, GcObject}; + +/// Returns the zero value for a GC field or array element. +pub(crate) fn default_value(storage: StorageType) -> TinyWasmValue { + match storage { + StorageType::I8 | StorageType::I16 | StorageType::Value(WasmType::I32 | WasmType::F32) => { + TinyWasmValue::Value32(0) + } + StorageType::Value(WasmType::I64 | WasmType::F64) => TinyWasmValue::Value64(0), + StorageType::Value(WasmType::V128) => TinyWasmValue::Value128(Value128::from([0; 16])), + StorageType::Value(WasmType::Ref(_)) => TinyWasmValue::ValueRef(ValueRef::NULL), + } +} + +/// Pops and packs a GC field or array element from the operand stack. +pub(crate) fn pop_value(stack: &mut ValueStack, storage: StorageType) -> TinyWasmValue { + match storage { + StorageType::I8 => TinyWasmValue::Value32(i32::stack_pop(stack) as u8 as u32), + StorageType::I16 => TinyWasmValue::Value32(i32::stack_pop(stack) as u16 as u32), + StorageType::Value(WasmType::I32 | WasmType::F32) => TinyWasmValue::Value32(u32::stack_pop(stack)), + StorageType::Value(WasmType::I64 | WasmType::F64) => TinyWasmValue::Value64(u64::stack_pop(stack)), + StorageType::Value(WasmType::V128) => TinyWasmValue::Value128(Value128::stack_pop(stack)), + StorageType::Value(WasmType::Ref(_)) => TinyWasmValue::ValueRef(ValueRef::stack_pop(stack)), + } +} + +/// Extends a packed value and pushes it onto the operand stack. +pub(crate) fn push_value( + stack: &mut ValueStack, + value: TinyWasmValue, + storage: StorageType, + signed: Option, +) -> Result<(), Trap> { + let value = match (value, storage, signed) { + (TinyWasmValue::Value32(value), StorageType::I8, Some(true)) => { + TinyWasmValue::Value32(value as i8 as i32 as u32) + } + (TinyWasmValue::Value32(value), StorageType::I16, Some(true)) => { + TinyWasmValue::Value32(value as i16 as i32 as u32) + } + (TinyWasmValue::Value32(value), StorageType::I8, Some(false)) => TinyWasmValue::Value32(value & u8::MAX as u32), + (TinyWasmValue::Value32(value), StorageType::I16, Some(false)) => { + TinyWasmValue::Value32(value & u16::MAX as u32) + } + (value, _, None) => value, + _ => unreachable!("validated packed field access"), + }; + stack.push_dyn(value) +} + +/// Decodes numeric array elements from a data segment. +pub(crate) fn decode_data( + storage: StorageType, + data: &[u8], + src: usize, + len: usize, +) -> Result, Trap> { + let width = match storage { + StorageType::I8 => 1, + StorageType::I16 => 2, + StorageType::Value(WasmType::I32 | WasmType::F32) => 4, + StorageType::Value(WasmType::I64 | WasmType::F64) => 8, + StorageType::Value(WasmType::V128) => 16, + StorageType::Value(WasmType::Ref(_)) => unreachable!("array.new_data reference element"), + }; + let Some(end) = len.checked_mul(width).and_then(|bytes| src.checked_add(bytes)).filter(|&end| end <= data.len()) + else { + return Err(Trap::MemoryOutOfBounds { offset: src, len: len.saturating_mul(width), max: data.len() }); + }; + let mut values = Vec::new(); + cold_err!(values.try_reserve_exact(len)).map_err(|_| Trap::OutOfMemory)?; + values.extend(data[src..end].chunks_exact(width).map(|bytes| match width { + 1 => TinyWasmValue::Value32(u32::from(bytes[0])), + 2 => TinyWasmValue::Value32(u32::from(u16::from_le_bytes(bytes.try_into().unwrap()))), + 4 => TinyWasmValue::Value32(u32::from_le_bytes(bytes.try_into().unwrap())), + 8 => TinyWasmValue::Value64(u64::from_le_bytes(bytes.try_into().unwrap())), + 16 => TinyWasmValue::Value128(Value128::from(<[u8; 16]>::try_from(bytes).unwrap())), + _ => unreachable!(), + })); + Ok(values) +} diff --git a/crates/tinywasm/src/store/gc/object.rs b/crates/tinywasm/src/store/gc/object.rs new file mode 100644 index 00000000..4af64c9f --- /dev/null +++ b/crates/tinywasm/src/store/gc/object.rs @@ -0,0 +1,251 @@ +use alloc::{boxed::Box, vec::Vec}; +use core::cell::RefCell; +use core::mem::size_of; +use core::sync::atomic::{AtomicU32, Ordering}; + +use tinywasm_types::TypeAddr; + +use crate::interpreter::{TinyWasmValue, ValueRef}; + +use super::{AllocError, Arena, Handle, Trace}; + +static NEXT_GC_REF: AtomicU32 = AtomicU32::new(0); + +pub(crate) struct GcObject { + pub(crate) type_addr: TypeAddr, + pub(crate) values: Box<[TinyWasmValue]>, + references: Option]>>, +} + +impl Trace for GcObject { + fn trace(&self, mark: &mut impl FnMut(Handle)) { + if let Some(references) = &self.references { + references.iter().flatten().copied().for_each(mark); + } + } +} + +pub(crate) struct GcHeap { + objects: Arena, + directory: Vec<(u32, Handle)>, + pinned: RefCell>, +} + +impl Default for GcHeap { + fn default() -> Self { + Self::new(1024 * 1024) + } +} + +impl GcHeap { + /// Creates a heap with the configured allocation threshold. + pub(crate) const fn new(collection_threshold: usize) -> Self { + Self { objects: Arena::new(collection_threshold), directory: Vec::new(), pinned: RefCell::new(Vec::new()) } + } + + #[inline] + /// Resolves a compact reference to its generation-checked arena handle. + pub(crate) fn handle(&self, value: ValueRef) -> Option { + let key = value.addr()?; + let index = self.directory.binary_search_by_key(&key, |entry| entry.0).ok()?; + Some(self.directory[index].1) + } + + #[inline] + pub(crate) fn get(&self, value: ValueRef) -> Option<&GcObject> { + self.objects.get(self.handle(value)?) + } + + #[inline] + pub(crate) fn get_mut(&mut self, value: ValueRef) -> Option<&mut GcObject> { + self.objects.get_mut(self.handle(value)?) + } + + /// Allocates an object and returns its compact runtime reference. + pub(crate) fn alloc( + &mut self, + type_addr: TypeAddr, + values: Vec, + trace_references: bool, + ) -> Result { + let key = + NEXT_GC_REF.try_update(Ordering::Relaxed, Ordering::Relaxed, |key| (key < (1 << 30)).then_some(key + 1)); + let Ok(key) = key else { + return Err(AllocError); + }; + let references = if trace_references { + let mut references = Vec::new(); + references.try_reserve_exact(values.len()).map_err(|_| AllocError)?; + references.extend(values.iter().map(|value| match value { + TinyWasmValue::ValueRef(value) => self.handle(*value), + _ => None, + })); + Some(references.into_boxed_slice()) + } else { + None + }; + let element_size = size_of::() + if trace_references { size_of::>() } else { 0 }; + let out_of_line_bytes = values.len().checked_mul(element_size).ok_or(AllocError)?; + let object = GcObject { type_addr, values: values.into_boxed_slice(), references }; + self.directory.try_reserve(1).map_err(|_| AllocError)?; + let handle = self.objects.alloc(object, out_of_line_bytes)?; + self.directory.push((key, handle)); + Ok(ValueRef::from_category_addr(key)) + } + + pub(crate) fn set(&mut self, object: ValueRef, index: usize, value: TinyWasmValue) -> Option<()> { + let reference = match value { + TinyWasmValue::ValueRef(value) => self.handle(value), + _ => None, + }; + let object = self.get_mut(object)?; + *object.values.get_mut(index)? = value; + if let Some(references) = &mut object.references { + references[index] = reference; + } + Some(()) + } + + /// Replaces a contiguous range and updates its traced references. + pub(crate) fn set_slice(&mut self, object: ValueRef, index: usize, values: &[TinyWasmValue]) -> Option<()> { + let object_handle = self.handle(object)?; + let end = index.checked_add(values.len())?; + let directory = &self.directory; + let object = self.objects.get_mut(object_handle)?; + object.values.get_mut(index..end)?.copy_from_slice(values); + if let Some(references) = &mut object.references { + for (reference, value) in references[index..end].iter_mut().zip(values) { + *reference = match value { + TinyWasmValue::ValueRef(value) => { + let key = value.addr(); + key.and_then(|key| directory.binary_search_by_key(&key, |entry| entry.0).ok()) + .map(|index| directory[index].1) + } + _ => None, + }; + } + } + Some(()) + } + + /// Fills a contiguous range and updates its traced references. + pub(crate) fn fill( + &mut self, + object: ValueRef, + range: core::ops::Range, + value: TinyWasmValue, + ) -> Option<()> { + let reference = match value { + TinyWasmValue::ValueRef(value) => self.handle(value), + _ => None, + }; + let object = self.get_mut(object)?; + object.values.get_mut(range.clone())?.fill(value); + if let Some(references) = &mut object.references { + references[range].fill(reference); + } + Some(()) + } + + pub(crate) fn should_collect(&self, value_count: usize, trace_references: bool) -> bool { + let element_size = size_of::() + if trace_references { size_of::>() } else { 0 }; + let bytes = value_count.saturating_mul(element_size); + self.objects.should_collect(bytes) + } + + /// Reclaims objects unreachable from runtime and permanent host roots. + pub(crate) fn collect(&mut self, roots: impl IntoIterator) -> Result<(), AllocError> { + let pinned = self.pinned.borrow(); + let directory = &self.directory; + let root_handles = roots.into_iter().filter_map(|value| { + let key = value.addr()?; + Some(directory.get(directory.binary_search_by_key(&key, |entry| entry.0).ok()?)?.1) + }); + self.objects.collect(pinned.iter().copied().chain(root_handles))?; + drop(pinned); + self.directory.retain(|(_, handle)| self.objects.get(*handle).is_some()); + Ok(()) + } + + /// Permanently roots a managed reference exposed through the copyable host API. + pub(crate) fn pin(&self, value: ValueRef) { + if let Some(handle) = self.handle(value) { + let mut pinned = self.pinned.borrow_mut(); + if let Err(index) = pinned.binary_search(&handle) { + pinned.insert(index, handle); + } + } + } + + pub(crate) fn copy_within(&mut self, object: ValueRef, src: core::ops::Range, dst: usize) -> Option<()> { + let object = self.get_mut(object)?; + object.values.copy_within(src.clone(), dst); + if let Some(references) = &mut object.references { + references.copy_within(src, dst); + } + Some(()) + } + + /// Copies values and tracing metadata between two distinct objects. + pub(crate) fn copy_between( + &mut self, + src: ValueRef, + src_range: core::ops::Range, + dst: ValueRef, + dst_index: usize, + ) -> Option<()> { + let src = self.handle(src)?; + let dst = self.handle(dst)?; + let (src, dst) = self.objects.get_disjoint_mut(src, dst)?; + let src_values = src.values.get(src_range.clone())?; + let dst_end = dst_index.checked_add(src_values.len())?; + dst.values.get_mut(dst_index..dst_end)?.copy_from_slice(src_values); + if let Some(dst_refs) = &mut dst.references { + if let Some(src_refs) = &src.references { + dst_refs[dst_index..dst_end].copy_from_slice(&src_refs[src_range]); + } else { + dst_refs[dst_index..dst_end].fill(None); + } + } + Some(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn compact_keys_are_not_reused_after_collection() { + let mut heap = GcHeap::default(); + let stale = heap.alloc(0, Vec::new(), false).unwrap(); + heap.collect([]).unwrap(); + let current = heap.alloc(0, Vec::new(), false).unwrap(); + + assert_ne!(stale, current); + assert!(heap.get(stale).is_none()); + assert!(heap.get(current).is_some()); + } + + #[test] + fn compact_keys_do_not_resolve_in_another_heap() { + let mut first = GcHeap::default(); + let second = GcHeap::default(); + let value = first.alloc(0, Vec::new(), false).unwrap(); + + assert!(second.get(value).is_none()); + } + + #[test] + fn collection_reclaims_object_cycles() { + let mut heap = GcHeap::default(); + let first = heap.alloc(0, alloc::vec![TinyWasmValue::ValueRef(ValueRef::NULL)], true).unwrap(); + let second = heap.alloc(0, alloc::vec![TinyWasmValue::ValueRef(first)], true).unwrap(); + heap.set(first, 0, TinyWasmValue::ValueRef(second)).unwrap(); + + heap.collect([]).unwrap(); + + assert!(heap.get(first).is_none()); + assert!(heap.get(second).is_none()); + } +} diff --git a/crates/tinywasm/src/store/global.rs b/crates/tinywasm/src/store/global.rs index 92804940..69e538f9 100644 --- a/crates/tinywasm/src/store/global.rs +++ b/crates/tinywasm/src/store/global.rs @@ -1,17 +1,159 @@ -use crate::interpreter::TinyWasmValue; +use alloc::vec::Vec; use tinywasm_types::*; -/// A WebAssembly Global Instance +use crate::interpreter::{TinyWasmValue, Value32, Value64, Value128}; + +struct GlobalLane { + values: Vec, + types: Vec, +} + +impl Default for GlobalLane { + fn default() -> Self { + Self { values: Vec::new(), types: Vec::new() } + } +} + +impl GlobalLane { + fn reserve(&mut self, additional: usize) { + self.values.reserve_exact(additional); + self.types.reserve_exact(additional); + } + + fn push(&mut self, ty: GlobalType, value: T) -> usize { + debug_assert_eq!(self.values.len(), self.types.len()); + let index = self.values.len(); + self.values.push(value); + self.types.push(ty); + index + } + + #[inline(always)] + fn get(&self, index: usize, addr: GlobalAddr) -> T { + *self.values.get(index).unwrap_or_else(|| unreachable!("invalid global address: {addr}")) + } + + #[inline(always)] + fn set(&mut self, index: usize, addr: GlobalAddr, value: T) { + *self.values.get_mut(index).unwrap_or_else(|| unreachable!("invalid global address: {addr}")) = value; + } + + fn ty(&self, index: usize, addr: GlobalAddr) -> GlobalType { + *self.types.get(index).unwrap_or_else(|| unreachable!("invalid global address: {addr}")) + } +} + +/// Global instances split into their physical value lanes. /// -/// See -#[cfg_attr(feature = "debug", derive(Debug))] -pub(crate) struct GlobalInstance { - pub(crate) value: TinyWasmValue, - pub(crate) ty: GlobalType, +/// Guest values should normally be accessed through +/// [`InternalValue`](crate::interpreter::InternalValue). Dynamic access is +/// reserved for initialization and host boundaries. +#[derive(Default)] +pub(crate) struct Globals { + globals_32: GlobalLane, + globals_64: GlobalLane, + globals_128: GlobalLane, } -impl GlobalInstance { - pub(crate) fn new(ty: GlobalType, value: TinyWasmValue) -> Self { - Self { ty, value } +impl Globals { + const LANE_SHIFT: u32 = 30; + const INDEX_MASK: u32 = (1 << Self::LANE_SHIFT) - 1; + const LANE_32: u32 = 0; + const LANE_64: u32 = 1 << Self::LANE_SHIFT; + const LANE_128: u32 = 2 << Self::LANE_SHIFT; + + pub(crate) fn reserve(&mut self, globals: &[Global]) { + let (mut count_32, mut count_64, mut count_128) = (0, 0, 0); + for global in globals { + match global.ty.ty { + WasmType::I32 | WasmType::F32 | WasmType::Ref(_) => count_32 += 1, + WasmType::I64 | WasmType::F64 => count_64 += 1, + WasmType::V128 => count_128 += 1, + } + } + self.globals_32.reserve(count_32); + self.globals_64.reserve(count_64); + self.globals_128.reserve(count_128); + } + + fn addr(lane: u32, index: usize) -> GlobalAddr { + assert!(index <= Self::INDEX_MASK as usize, "too many globals in one value lane"); + lane | index as u32 + } + + #[inline(always)] + fn index(addr: GlobalAddr, lane: u32) -> usize { + debug_assert_eq!(addr & !Self::INDEX_MASK, lane, "global address has the wrong value lane"); + (addr & Self::INDEX_MASK) as usize + } + + /// Returns a global's logical type and mutability. + pub(crate) fn ty(&self, addr: GlobalAddr) -> GlobalType { + match addr & !Self::INDEX_MASK { + Self::LANE_32 => self.globals_32.ty(Self::index(addr, Self::LANE_32), addr), + Self::LANE_64 => self.globals_64.ty(Self::index(addr, Self::LANE_64), addr), + Self::LANE_128 => self.globals_128.ty(Self::index(addr, Self::LANE_128), addr), + _ => unreachable!("invalid global address: {addr}"), + } + } + + /// Adds a global and returns its packed store address. + pub(crate) fn push(&mut self, ty: GlobalType, value: TinyWasmValue) -> GlobalAddr { + match (ty.ty, value) { + (WasmType::I32 | WasmType::F32, TinyWasmValue::Value32(value)) => { + Self::addr(Self::LANE_32, self.globals_32.push(ty, value)) + } + (WasmType::Ref(_), TinyWasmValue::ValueRef(value)) => { + Self::addr(Self::LANE_32, self.globals_32.push(ty, value.raw())) + } + (WasmType::I64 | WasmType::F64, TinyWasmValue::Value64(value)) => { + Self::addr(Self::LANE_64, self.globals_64.push(ty, value)) + } + (WasmType::V128, TinyWasmValue::Value128(value)) => { + Self::addr(Self::LANE_128, self.globals_128.push(ty, value)) + } + _ => unreachable!("global value does not match its declared type"), + } + } + + /// Returns a raw value from the 32-bit lane. + #[inline(always)] + pub(crate) fn get_32(&self, addr: GlobalAddr) -> Value32 { + self.globals_32.get(Self::index(addr, Self::LANE_32), addr) + } + + /// Returns a raw value from the 64-bit lane. + #[inline(always)] + pub(crate) fn get_64(&self, addr: GlobalAddr) -> Value64 { + self.globals_64.get(Self::index(addr, Self::LANE_64), addr) + } + + /// Returns a raw value from the 128-bit lane. + #[inline(always)] + pub(crate) fn get_128(&self, addr: GlobalAddr) -> Value128 { + self.globals_128.get(Self::index(addr, Self::LANE_128), addr) + } + + /// Sets a raw value in the 32-bit lane. + #[inline(always)] + pub(crate) fn set_32(&mut self, addr: GlobalAddr, value: Value32) { + self.globals_32.set(Self::index(addr, Self::LANE_32), addr, value); + } + + /// Sets a raw value in the 64-bit lane. + #[inline(always)] + pub(crate) fn set_64(&mut self, addr: GlobalAddr, value: Value64) { + self.globals_64.set(Self::index(addr, Self::LANE_64), addr, value); + } + + /// Sets a raw value in the 128-bit lane. + #[inline(always)] + pub(crate) fn set_128(&mut self, addr: GlobalAddr, value: Value128) { + self.globals_128.set(Self::index(addr, Self::LANE_128), addr, value); + } + + /// Iterates over globals in the 32-bit lane for root tracing. + pub(crate) fn globals_32(&self) -> impl Iterator { + self.globals_32.values.iter().zip(&self.globals_32.types) } } diff --git a/crates/tinywasm/src/store/memory/instance.rs b/crates/tinywasm/src/store/memory/instance.rs index be0f3b4e..68a075b0 100644 --- a/crates/tinywasm/src/store/memory/instance.rs +++ b/crates/tinywasm/src/store/memory/instance.rs @@ -25,42 +25,50 @@ impl core::fmt::Debug for MemoryInstance { impl MemoryInstance { const COPY_CHUNK_SIZE: usize = 4 * 1024; - #[inline(always)] - pub(crate) fn effective_addr_32(&self, base: u32, offset: u64) -> Result { + fn host_size(kind: MemoryType, pages: u64) -> Result { #[cfg(target_pointer_width = "64")] { - debug_assert!(u32::try_from(offset).is_ok(), "validated memory32 offsets fit in u32"); - Ok(base as usize + offset as usize) + pages + .checked_mul(kind.page_size()) + .map(|size| size as usize) + .ok_or(Error::UnsupportedFeature("memory size exceeds the host address space")) } #[cfg(not(target_pointer_width = "64"))] { - match usize::try_from(u64::from(base) + offset) { - Ok(addr) => Ok(addr), - Err(_) => { - cold_path(); - Err(memory_oob(base as usize, N, self.inner.len())) - } - } + let page_size = usize::try_from(kind.page_size()) + .map_err(|_| Error::UnsupportedFeature("memory page size exceeds the host address space"))?; + let pages = usize::try_from(pages) + .map_err(|_| Error::UnsupportedFeature("memory size exceeds the host address space"))?; + pages.checked_mul(page_size).ok_or(Error::UnsupportedFeature("memory size exceeds the host address space")) } } #[inline(always)] - pub(crate) fn effective_addr_64(&self, base: u64, offset: u64) -> Result { - match base.checked_add(offset).and_then(|addr| usize::try_from(addr).ok()) { - Some(addr) => Ok(addr), - None => { - cold_path(); - Err(memory_oob(base as usize, N, self.inner.len())) + pub(crate) fn effective_addr(&self, base: usize, offset: u64) -> Result { + #[cfg(target_pointer_width = "64")] + { + if !self.is_64bit() { + debug_assert!(u32::try_from(offset).is_ok(), "validated memory32 offsets fit in u32"); + return Ok(base + offset as usize); + } + match base.checked_add(offset as usize) { + Some(addr) => Ok(addr), + None => cold!(Err(memory_oob(base, N, self.inner.len()))), + } + } + + #[cfg(not(target_pointer_width = "64"))] + { + match usize::try_from(offset).ok().and_then(|offset| base.checked_add(offset)) { + Some(addr) => Ok(addr), + None => cold!(Err(memory_oob(base, N, self.inner.len()))), } } } pub(crate) fn new(kind: MemoryType, backend: &MemoryBackend) -> Result { - assert!(kind.page_count_initial() <= kind.page_count_max()); - - let initial_len = usize::try_from(kind.initial_size()) - .map_err(|_| Error::UnsupportedFeature("memory size exceeds the host address space"))?; + let initial_len = Self::host_size(kind, kind.page_count_initial())?; crate::log::debug!( "initializing memory with {} pages of {} bytes", @@ -80,10 +88,7 @@ impl MemoryInstance { } pub(crate) fn new_lazy(kind: MemoryType, backend: &MemoryBackend) -> Result { - assert!(kind.page_count_initial() <= kind.page_count_max()); - - let initial_len = usize::try_from(kind.initial_size()) - .map_err(|_| Error::UnsupportedFeature("memory size exceeds the host address space"))?; + let initial_len = Self::host_size(kind, kind.page_count_initial())?; crate::log::debug!( "initializing lazy memory with {} pages of {} bytes", @@ -108,13 +113,11 @@ impl MemoryInstance { ) -> Result<(), Trap> { fn check_range(mem: &MemoryStorage, addr: usize, len: usize) -> Result<(), crate::Trap> { let Some(end) = addr.checked_add(len) else { - cold_path(); - return Err(memory_oob(addr, len, mem.len())); + return cold!(Err(memory_oob(addr, len, mem.len()))); }; if end > mem.len() || end < addr { - cold_path(); - return Err(memory_oob(addr, len, mem.len())); + return cold!(Err(memory_oob(addr, len, mem.len()))); } Ok(()) } @@ -134,7 +137,7 @@ impl MemoryInstance { cold_path(); memory_oob(src + copied, chunk_len, src_memory.inner.len()) })?; - self.inner.write_all(dst + copied, &buf[..chunk_len]).ok_or_else(|| { + self.inner.write_all(dst + copied, &buf[..chunk_len])?.ok_or_else(|| { cold_path(); memory_oob(dst + copied, chunk_len, self.inner.len()) })?; @@ -145,7 +148,7 @@ impl MemoryInstance { } pub(crate) fn copy_within(&mut self, dst: usize, src: usize, len: usize) -> Result<(), Trap> { - self.inner.copy_within(dst, src, len).ok_or_else(|| { + self.inner.copy_within(dst, src, len)?.ok_or_else(|| { cold_path(); memory_oob(dst, len, self.inner.len()) }) @@ -173,16 +176,7 @@ impl MemoryInstance { return Ok(None); } - let Some(new_size) = (new_pages as u64).checked_mul(self.kind.page_size()) else { - return Ok(None); - }; - if new_size > self.kind.max_size() { - cold_path(); - crate::log::debug!("memory.grow failed: new_size={}, max_size={}", new_size, self.kind.max_size()); - return Ok(None); - } - - let Some(new_size) = usize::try_from(new_size).ok() else { + let Ok(new_size) = Self::host_size(self.kind, new_pages as u64) else { return Ok(None); }; if new_size == self.inner.len() { diff --git a/crates/tinywasm/src/store/memory/lazy.rs b/crates/tinywasm/src/store/memory/lazy.rs index ef6dace0..780d5b5b 100644 --- a/crates/tinywasm/src/store/memory/lazy.rs +++ b/crates/tinywasm/src/store/memory/lazy.rs @@ -1,5 +1,4 @@ use alloc::boxed::Box; -use core::hint::cold_path; use tinywasm_types::MemoryType; @@ -21,8 +20,13 @@ pub struct LazyLinearMemory { impl LazyLinearMemory { /// Creates a lazy memory for `ty` using `backend` for eventual storage. pub fn try_new(ty: MemoryType, backend: MemoryBackend) -> Result { - let initial_len = usize::try_from(ty.initial_size()) + let page_size = usize::try_from(ty.page_size()) + .map_err(|_| Error::UnsupportedFeature("memory page size exceeds the host address space"))?; + let pages = usize::try_from(ty.page_count_initial()) .map_err(|_| Error::UnsupportedFeature("memory size exceeds the host address space"))?; + let initial_len = pages + .checked_mul(page_size) + .ok_or(Error::UnsupportedFeature("memory size exceeds the host address space"))?; Ok(Self::new_with_initial_len(ty, initial_len, backend)) } @@ -30,24 +34,9 @@ impl LazyLinearMemory { Self { ty, initial_len, backend, inner: None } } - fn materialize(&mut self) -> &mut dyn LinearMemory { + fn materialize(&mut self) -> core::result::Result<&mut dyn LinearMemory, crate::Trap> { if self.inner.is_none() { - self.inner = - Some(self.backend.create(self.ty, self.initial_len).expect("lazy memory materialization failed")); - } - self.inner.as_deref_mut().expect("lazy memory should be materialized") - } - - fn try_materialize(&mut self) -> core::result::Result<&mut dyn LinearMemory, crate::Trap> { - if self.inner.is_none() { - let storage = match self.backend.create(self.ty, self.initial_len) { - Ok(storage) => storage, - Err(Error::Trap(trap)) => { - cold_path(); - return Err(trap); - } - Err(err) => panic!("lazy memory materialization failed: {err}"), - }; + let storage = cold_err!(self.backend.create(self.ty, self.initial_len))?; self.inner = Some(storage); } Ok(self.inner.as_deref_mut().expect("lazy memory should be materialized")) @@ -60,7 +49,7 @@ impl LinearMemory for LazyLinearMemory { } fn grow_to(&mut self, new_len: usize) -> Result<(), crate::Trap> { - self.try_materialize()?.grow_to(new_len) + self.materialize()?.grow_to(new_len) } fn read(&self, addr: usize, dst: &mut [u8]) -> usize { @@ -75,45 +64,45 @@ impl LinearMemory for LazyLinearMemory { read_len } - fn write(&mut self, addr: usize, src: &[u8]) -> usize { + fn write(&mut self, addr: usize, src: &[u8]) -> core::result::Result { if src.is_empty() || addr >= self.len() { - return 0; + return Ok(0); } - self.materialize().write(addr, src) + self.materialize()?.write(addr, src) } - fn write_all(&mut self, addr: usize, src: &[u8]) -> Option<()> { - let end = addr.checked_add(src.len())?; + fn write_all(&mut self, addr: usize, src: &[u8]) -> core::result::Result, crate::Trap> { + let Some(end) = addr.checked_add(src.len()) else { return Ok(None) }; if end > self.len() { - return None; + return Ok(None); } if src.is_empty() { - return Some(()); + return Ok(Some(())); } - self.materialize().write_all(addr, src) + self.materialize()?.write_all(addr, src) } - fn fill(&mut self, addr: usize, len: usize, val: u8) -> Option<()> { - let end = addr.checked_add(len)?; + fn fill(&mut self, addr: usize, len: usize, val: u8) -> core::result::Result, crate::Trap> { + let Some(end) = addr.checked_add(len) else { return Ok(None) }; if end > self.len() { - return None; + return Ok(None); } if len == 0 || val == 0 && self.inner.is_none() { - return Some(()); + return Ok(Some(())); } - self.materialize().fill(addr, len, val) + self.materialize()?.fill(addr, len, val) } - fn copy_within(&mut self, dst: usize, src: usize, len: usize) -> Option<()> { - let src_end = src.checked_add(len)?; - let dst_end = dst.checked_add(len)?; + fn copy_within(&mut self, dst: usize, src: usize, len: usize) -> core::result::Result, crate::Trap> { + let Some(src_end) = src.checked_add(len) else { return Ok(None) }; + let Some(dst_end) = dst.checked_add(len) else { return Ok(None) }; if src_end > self.len() || dst_end > self.len() { - return None; + return Ok(None); } if self.inner.is_none() || len == 0 || dst == src { - return Some(()); + return Ok(Some(())); } - self.materialize().copy_within(dst, src, len) + self.materialize()?.copy_within(dst, src, len) } } diff --git a/crates/tinywasm/src/store/memory/mod.rs b/crates/tinywasm/src/store/memory/mod.rs index a3f00536..70ffb209 100644 --- a/crates/tinywasm/src/store/memory/mod.rs +++ b/crates/tinywasm/src/store/memory/mod.rs @@ -1,12 +1,12 @@ -use alloc::{boxed::Box, format, sync::Arc}; +use alloc::{boxed::Box, sync::Arc}; use alloc::{vec, vec::Vec}; use core::cmp::min; use core::hint::cold_path; use tinywasm_types::MemoryType; +use crate::Result; use crate::interpreter::Value128; -use crate::{Error, Result}; mod instance; mod lazy; @@ -48,72 +48,76 @@ pub trait LinearMemory { /// Writes up to `src.len()` bytes starting at `addr` and returns the number of bytes written. /// /// Backends may return fewer bytes than requested even when more space is available. This lets - /// non-contiguous backends stop at a natural boundary such as the end of a chunk. - fn write(&mut self, addr: usize, src: &[u8]) -> usize; + /// non-contiguous backends stop at a natural boundary such as the end of a chunk. Backend + /// failures are returned as traps. + fn write(&mut self, addr: usize, src: &[u8]) -> core::result::Result; - /// Writes all bytes in `src` starting at `addr`, or returns `None` if any byte could not be written. - fn write_all(&mut self, addr: usize, src: &[u8]) -> Option<()> { + /// Writes all bytes in `src`, returns `Ok(None)` for an invalid range, or returns a backend trap. + fn write_all(&mut self, addr: usize, src: &[u8]) -> core::result::Result, crate::Trap> { let Some(end) = addr.checked_add(src.len()) else { - cold_path(); - return None; + return cold!(Ok(None)); }; if end > self.len() { - cold_path(); - return None; + return cold!(Ok(None)); } let mut offset = 0; while offset < src.len() { - let written = self.write(addr + offset, &src[offset..]); + let written = self.write(addr + offset, &src[offset..])?; if written == 0 { - cold_path(); - return None; + return cold!(Ok(None)); } offset += written; } - Some(()) + Ok(Some(())) } /// Fills the range `[addr, addr + len)` with `val`. - fn fill(&mut self, addr: usize, len: usize, val: u8) -> Option<()> { - let end = addr.checked_add(len)?; + fn fill(&mut self, addr: usize, len: usize, val: u8) -> core::result::Result, crate::Trap> { + let Some(end) = addr.checked_add(len) else { return Ok(None) }; if end > self.len() { - return None; + return Ok(None); } + let chunk = [val; 1024]; let mut offset = 0; while offset < len { let chunk_len = min(len - offset, 1024); - let chunk = vec![val; chunk_len]; - self.write_all(addr + offset, &chunk)?; + if self.write_all(addr + offset, &chunk[..chunk_len])?.is_none() { + return Ok(None); + } offset += chunk_len; } - Some(()) + Ok(Some(())) } /// Copies `len` bytes from `src` to `dst` within the same memory. - fn copy_within(&mut self, dst: usize, src: usize, len: usize) -> Option<()> { - let src_end = src.checked_add(len)?; - let dst_end = dst.checked_add(len)?; + fn copy_within(&mut self, dst: usize, src: usize, len: usize) -> core::result::Result, crate::Trap> { + let Some(src_end) = src.checked_add(len) else { return Ok(None) }; + let Some(dst_end) = dst.checked_add(len) else { return Ok(None) }; if src_end > self.len() || dst_end > self.len() { - return None; + return Ok(None); } if len == 0 || dst == src { - return Some(()); + return Ok(Some(())); } + let mut chunk = [0; 1024]; + // If the source and destination ranges are disjoint, we can copy forward without a temporary buffer. if dst < src || dst >= src_end { let mut offset = 0; while offset < len { let chunk_len = min(len - offset, 1024); - let mut chunk = vec![0; chunk_len]; - self.read_exact(src + offset, &mut chunk)?; - self.write_all(dst + offset, &chunk)?; + if self.read_exact(src + offset, &mut chunk[..chunk_len]).is_none() + || self.write_all(dst + offset, &chunk[..chunk_len])?.is_none() + { + return Ok(None); + } offset += chunk_len; } } else { @@ -122,33 +126,32 @@ pub trait LinearMemory { while offset > 0 { let chunk_len = min(offset, 1024); offset -= chunk_len; - let mut chunk = vec![0; chunk_len]; - self.read_exact(src + offset, &mut chunk)?; - self.write_all(dst + offset, &chunk)?; + if self.read_exact(src + offset, &mut chunk[..chunk_len]).is_none() + || self.write_all(dst + offset, &chunk[..chunk_len])?.is_none() + { + return Ok(None); + } } } - Some(()) + Ok(Some(())) } /// Reads exactly `dst.len()` bytes starting at `addr`. fn read_exact(&self, addr: usize, dst: &mut [u8]) -> Option<()> { let Some(end) = addr.checked_add(dst.len()) else { - cold_path(); - return None; + return cold!(None); }; if end > self.len() { - cold_path(); - return None; + return cold!(None); } let mut offset = 0; while offset < dst.len() { let read = self.read(addr + offset, &mut dst[offset..]); if read == 0 { - cold_path(); - return None; + return cold!(None); } offset += read; } @@ -220,7 +223,7 @@ pub trait LinearMemory { /// Writes exactly 1 byte at `addr`. fn write_8(&mut self, addr: usize, bytes: &[u8]) -> core::result::Result<(), crate::Trap> { - self.write_all(addr, bytes).ok_or_else(|| { + self.write_all(addr, bytes)?.ok_or_else(|| { cold_path(); memory_oob(addr, 1, self.len()) }) @@ -228,7 +231,7 @@ pub trait LinearMemory { /// Writes exactly 2 bytes at `addr`. fn write_16(&mut self, addr: usize, bytes: &[u8]) -> core::result::Result<(), crate::Trap> { - self.write_all(addr, bytes).ok_or_else(|| { + self.write_all(addr, bytes)?.ok_or_else(|| { cold_path(); memory_oob(addr, 2, self.len()) }) @@ -236,7 +239,7 @@ pub trait LinearMemory { /// Writes exactly 4 bytes at `addr`. fn write_32(&mut self, addr: usize, bytes: &[u8]) -> core::result::Result<(), crate::Trap> { - self.write_all(addr, bytes).ok_or_else(|| { + self.write_all(addr, bytes)?.ok_or_else(|| { cold_path(); memory_oob(addr, 4, self.len()) }) @@ -244,7 +247,7 @@ pub trait LinearMemory { /// Writes exactly 8 bytes at `addr`. fn write_64(&mut self, addr: usize, bytes: &[u8]) -> core::result::Result<(), crate::Trap> { - self.write_all(addr, bytes).ok_or_else(|| { + self.write_all(addr, bytes)?.ok_or_else(|| { cold_path(); memory_oob(addr, 8, self.len()) }) @@ -252,14 +255,14 @@ pub trait LinearMemory { /// Writes exactly 16 bytes at `addr`. fn write_128(&mut self, addr: usize, bytes: &[u8]) -> core::result::Result<(), crate::Trap> { - self.write_all(addr, bytes).ok_or_else(|| { + self.write_all(addr, bytes)?.ok_or_else(|| { cold_path(); memory_oob(addr, 16, self.len()) }) } } -type MemoryFactory = dyn Fn(MemoryType) -> Result> + Send + Sync; +type MemoryFactory = dyn Fn(MemoryType) -> core::result::Result, crate::Trap> + Send + Sync; /// Configures how runtime memory instances are created. #[derive(Clone, Default)] @@ -297,9 +300,11 @@ impl MemoryBackend { } /// Uses a custom factory to create memory instances. + /// + /// Factory traps are returned during eager creation or when a lazy memory first materializes. pub fn custom(factory: F) -> Self where - F: Fn(MemoryType) -> Result + Send + Sync + 'static, + F: Fn(MemoryType) -> core::result::Result + Send + Sync + 'static, M: LinearMemory + 'static, { Self(MemoryBackendInner::Custom(Arc::new(move |ty| { @@ -308,22 +313,21 @@ impl MemoryBackend { }))) } - pub(crate) fn create(&self, ty: MemoryType, initial_len: usize) -> Result { + pub(crate) fn create( + &self, + ty: MemoryType, + initial_len: usize, + ) -> core::result::Result { let storage = match &self.0 { - MemoryBackendInner::Vec => { - Box::new(VecMemory::try_new(initial_len).map_err(Error::Trap)?) as Box - } + MemoryBackendInner::Vec => Box::new(VecMemory::try_new(initial_len)?) as Box, MemoryBackendInner::Paged { chunk_size } => { - Box::new(PagedMemory::try_new(initial_len, *chunk_size).map_err(Error::Trap)?) as Box + Box::new(PagedMemory::try_new(initial_len, *chunk_size)?) as Box } MemoryBackendInner::Custom(factory) => factory(ty)?, }; if storage.len() < initial_len { - return Err(Error::Other(format!( - "memory backend returned {} bytes for a memory that requires at least {initial_len}", - storage.len() - ))); + return Err(crate::Trap::Other("memory backend returned less storage than required")); } Ok(storage) @@ -377,13 +381,7 @@ macro_rules! impl_mem_traits { #[inline(always)] fn load_at(mem: &dyn LinearMemory, addr: usize) -> core::result::Result { - Ok(Self::from_le_bytes(match mem.$read(addr) { - Ok(bytes) => bytes, - Err(trap) => { - cold_path(); - return Err(trap); - } - })) + Ok(Self::from_le_bytes(cold_err!(mem.$read(addr))?)) } #[inline(always)] @@ -402,191 +400,31 @@ macro_rules! impl_mem_traits { impl_mem_traits!( u8, 1, read_8, write_8, i8, 1, read_8, write_8, u16, 2, read_16, write_16, i16, 2, read_16, write_16, u32, 4, read_32, write_32, i32, 4, read_32, write_32, f32, 4, read_32, write_32, u64, 8, read_64, write_64, i64, 8, - read_64, write_64, f64, 8, read_64, write_64, Value128, 16, read_128, write_128 + read_64, write_64, f64, 8, read_64, write_64 ); -fn memory_oob(offset: usize, len: usize, max: usize) -> crate::Trap { - crate::Trap::MemoryOutOfBounds { offset, len, max } -} - -#[cfg(test)] -mod tests { - use super::*; - use tinywasm_types::MemoryArch; - - fn create_test_memory(kind: MemoryType, backend: MemoryBackend) -> MemoryInstance { - MemoryInstance::new(kind, &backend).unwrap() - } - - fn test_backends() -> [MemoryBackend; 2] { - [MemoryBackend::vec(), MemoryBackend::paged(4)] - } - - #[test] - fn effective_memory_addresses_handle_host_and_wasm_overflow() { - let memory = create_test_memory(MemoryType::new(MemoryArch::I32, 1, Some(1), None), MemoryBackend::vec()); - assert_eq!(memory.effective_addr_32::<1>(1, 2), Ok(3)); - assert_eq!(memory.effective_addr_64::<1>(1, 2), Ok(3)); - assert!(memory.effective_addr_64::<1>(u64::MAX, 1).is_err()); - - #[cfg(target_pointer_width = "64")] - assert_eq!( - memory.effective_addr_32::<1>(u32::MAX, u64::from(u32::MAX)), - Ok(usize::try_from(u64::from(u32::MAX) * 2).unwrap()) - ); - - #[cfg(target_pointer_width = "32")] - assert!(memory.effective_addr_32::<1>(u32::MAX, 1).is_err()); - } - - #[test] - fn memory_copy_from_slice_and_read_vec_work() { - for backend in test_backends() { - let kind = MemoryType::new(MemoryArch::I32, 1, Some(2), None); - let mut memory = create_test_memory(kind, backend); - let data = [1, 2, 3, 4]; - assert!(memory.inner.write_all(0, &data).is_some()); - assert_eq!(memory.inner.read_vec(0, data.len()).unwrap(), data); - } - } - - #[test] - fn memory_read_returns_partial_count() { - for backend in test_backends() { - let kind = MemoryType::new(MemoryArch::I32, 1, Some(1), Some(4)); - let memory = create_test_memory(kind, backend); - let mut dst = [9; 8]; - assert_eq!(memory.inner.read(2, &mut dst), 2); - assert_eq!(&dst[..2], &[0, 0]); - assert_eq!(&dst[2..], &[9; 6]); - } +impl MemValue<16> for Value128 { + #[inline(always)] + fn to_mem_bytes(self) -> [u8; 16] { + self.0 } - #[test] - fn memory_copy_from_slice_out_of_bounds_fails() { - for backend in test_backends() { - let kind = MemoryType::new(MemoryArch::I32, 1, Some(2), None); - let mut memory = create_test_memory(kind, backend); - let data = [1, 2, 3, 4]; - let len = memory.inner.len(); - assert!(memory.inner.write_all(len, &data).is_none()); - } + #[inline(always)] + fn from_mem_bytes(bytes: [u8; 16]) -> Self { + Self(bytes) } - #[test] - fn fixed_width_access_out_of_bounds_traps() { - for backend in test_backends() { - let kind = MemoryType::new(MemoryArch::I32, 1, Some(2), None); - let mut memory = create_test_memory(kind, backend); - let len = memory.inner.len(); - - assert!(memory.inner.read_8(len).is_err()); - assert!(memory.inner.read_32(len - 3).is_err()); - assert!(memory.inner.write_8(len, &[0]).is_err()); - assert!(memory.inner.write_32(len - 3, &[0; 4]).is_err()); - } + #[inline(always)] + fn load_at(mem: &dyn LinearMemory, addr: usize) -> core::result::Result { + Ok(Self(cold_err!(mem.read_128(addr))?)) } - #[test] - fn memory_fill_works() { - for backend in test_backends() { - let kind = MemoryType::new(MemoryArch::I32, 1, Some(2), None); - let mut memory = create_test_memory(kind, backend); - assert!(memory.inner.fill(0, 10, 42).is_some()); - assert_eq!(memory.inner.read_vec(0, 10).unwrap(), vec![42; 10]); - } - } - - #[test] - fn memory_fill_out_of_bounds_fails() { - for backend in test_backends() { - let kind = MemoryType::new(MemoryArch::I32, 1, Some(2), None); - let mut memory = create_test_memory(kind, backend); - let len = memory.inner.len(); - assert!(memory.inner.fill(len, 10, 42).is_none()); - } - } - - #[test] - fn memory_copy_within_works() { - for backend in test_backends() { - let kind = MemoryType::new(MemoryArch::I32, 1, Some(2), None); - let mut memory = create_test_memory(kind, backend); - memory.inner.fill(0, 10, 1).unwrap(); - assert!(memory.copy_within(10, 0, 10).is_ok()); - assert_eq!(memory.inner.read_vec(10, 10).unwrap(), vec![1; 10]); - } - } - - #[test] - fn memory_copy_within_out_of_bounds_fails() { - for backend in test_backends() { - let kind = MemoryType::new(MemoryArch::I32, 1, Some(2), None); - let mut memory = create_test_memory(kind, backend); - assert!(memory.copy_within(memory.inner.len(), 0, 10).is_err()); - } - } - - #[test] - fn memory_grow_works() { - for backend in test_backends() { - let kind = MemoryType::new(MemoryArch::I32, 1, Some(2), None); - let mut memory = create_test_memory(kind, backend); - let original_pages = memory.page_count; - assert_eq!(memory.grow(1, false).unwrap(), Some(original_pages as i64)); - assert_eq!(memory.page_count, original_pages + 1); - } - } - - #[test] - fn memory_grow_out_of_bounds_fails() { - for backend in test_backends() { - let kind = MemoryType::new(MemoryArch::I32, 1, Some(2), None); - let mut memory = create_test_memory(kind, backend); - assert_eq!(memory.grow(memory.kind.max_size() as i64 + 1, false).unwrap(), None); - } - } - - #[test] - fn memory_grow_respects_max_pages() { - for backend in test_backends() { - let kind = MemoryType::new(MemoryArch::I32, 1, Some(2), None); - let mut memory = create_test_memory(kind, backend); - assert_eq!(memory.grow(1, false).unwrap(), Some(1)); - assert_eq!(memory.grow(1, false).unwrap(), None); - } - } - - #[test] - fn memory_grow_negative_delta_fails() { - for backend in test_backends() { - let kind = MemoryType::new(MemoryArch::I32, 1, Some(2), None); - let mut memory = create_test_memory(kind, backend); - let original_pages = memory.page_count; - assert_eq!(memory.grow(-1, false).unwrap(), None); - assert_eq!(memory.page_count, original_pages); - } - } - - #[test] - fn memory_custom_page_size_out_of_bounds_fails() { - for backend in test_backends() { - let kind = MemoryType::new(MemoryArch::I32, 1, Some(2), Some(1)); - let mut memory = create_test_memory(kind, backend); - let data = [1, 2]; - assert!(memory.inner.write_all(0, &data).is_none()); - } + #[inline(always)] + fn store_at(self, mem: &mut dyn LinearMemory, addr: usize) -> core::result::Result<(), crate::Trap> { + mem.write_128(addr, &self.0) } +} - #[test] - fn memory_custom_page_size_grow_works() { - for backend in test_backends() { - let kind = MemoryType::new(MemoryArch::I32, 1, Some(2), Some(1)); - let mut memory = create_test_memory(kind, backend); - assert_eq!(memory.grow(1, false).unwrap(), Some(1)); - let data = [1, 2]; - assert!(memory.inner.write_all(0, &data).is_some()); - assert_eq!(memory.inner.read_vec(0, data.len()).unwrap(), data); - } - } +const fn memory_oob(offset: usize, len: usize, max: usize) -> crate::Trap { + crate::Trap::MemoryOutOfBounds { offset, len, max } } diff --git a/crates/tinywasm/src/store/memory/paged.rs b/crates/tinywasm/src/store/memory/paged.rs index 6ae65af2..3e6e5ea3 100644 --- a/crates/tinywasm/src/store/memory/paged.rs +++ b/crates/tinywasm/src/store/memory/paged.rs @@ -45,13 +45,7 @@ impl PagedMemory { #[inline(always)] fn allocate_chunk(&self) -> Result, crate::Trap> { let mut chunk = Vec::new(); - match chunk.try_reserve_exact(self.chunk_size) { - Ok(()) => {} - Err(_) => { - cold_path(); - return Err(crate::Trap::OutOfMemory); - } - } + cold_err!(chunk.try_reserve_exact(self.chunk_size)).map_err(|_| crate::Trap::OutOfMemory)?; chunk.resize(self.chunk_size, 0); Ok(chunk.into_boxed_slice()) } @@ -70,6 +64,41 @@ impl PagedMemory { self.chunks[chunk_idx].as_deref() } + #[inline(always)] + fn read_fixed(&self, addr: usize) -> Result<[u8; N], crate::Trap> { + let Some(end) = addr.checked_add(N).filter(|end| *end <= self.len) else { + return cold!(Err(memory_oob(addr, N, self.len))); + }; + let chunk_idx = addr >> self.chunk_shift; + let chunk_offset = addr & self.chunk_mask; + if end <= ((chunk_idx + 1) << self.chunk_shift) { + let mut bytes = [0; N]; + if let Some(chunk) = self.chunk_slice(chunk_idx) { + bytes.copy_from_slice(&chunk[chunk_offset..chunk_offset + N]); + } + return Ok(bytes); + } + cold_path(); + let mut bytes = [0; N]; + self.read_exact(addr, &mut bytes).ok_or_else(|| memory_oob(addr, N, self.len))?; + Ok(bytes) + } + + #[inline(always)] + fn write_fixed(&mut self, addr: usize, bytes: &[u8]) -> Result<(), crate::Trap> { + let Some(end) = addr.checked_add(N).filter(|end| *end <= self.len) else { + return cold!(Err(memory_oob(addr, N, self.len))); + }; + let chunk_idx = addr >> self.chunk_shift; + let chunk_offset = addr & self.chunk_mask; + if end <= ((chunk_idx + 1) << self.chunk_shift) { + self.chunk_mut(chunk_idx)?[chunk_offset..chunk_offset + N].copy_from_slice(bytes); + return Ok(()); + } + cold_path(); + self.write_all(addr, bytes)?.ok_or_else(|| memory_oob(addr, N, self.len)) + } + #[inline(always)] fn checked_end(&self, addr: usize, len: usize) -> Option { let end = addr.checked_add(len)?; @@ -135,13 +164,8 @@ impl LinearMemory for PagedMemory { let new_chunk_count = if new_len == 0 { 0 } else { new_len.div_ceil(self.chunk_size) }; if new_chunk_count > self.chunks.len() { - match self.chunks.try_reserve_exact(new_chunk_count - self.chunks.len()) { - Ok(()) => {} - Err(_) => { - cold_path(); - return Err(crate::Trap::OutOfMemory); - } - } + cold_err!(self.chunks.try_reserve_exact(new_chunk_count - self.chunks.len())) + .map_err(|_| crate::Trap::OutOfMemory)?; self.chunks.resize_with(new_chunk_count, || None); } else { self.chunks.truncate(new_chunk_count); @@ -171,25 +195,23 @@ impl LinearMemory for PagedMemory { } #[inline(always)] - fn write(&mut self, addr: usize, src: &[u8]) -> usize { + fn write(&mut self, addr: usize, src: &[u8]) -> Result { if addr >= self.len || src.is_empty() { - return 0; + return Ok(0); } let chunk_idx = addr >> self.chunk_shift; let chunk_offset = addr & self.chunk_mask; let write_len = min(min(self.chunk_size - chunk_offset, self.len - addr), src.len()); - let Ok(chunk) = self.chunk_mut(chunk_idx) else { - return 0; - }; + let chunk = self.chunk_mut(chunk_idx)?; chunk[chunk_offset..chunk_offset + write_len].copy_from_slice(&src[..write_len]); - write_len + Ok(write_len) } #[inline(always)] - fn write_all(&mut self, addr: usize, src: &[u8]) -> Option<()> { - let end = self.checked_end(addr, src.len())?; + fn write_all(&mut self, addr: usize, src: &[u8]) -> Result, crate::Trap> { + let Some(end) = self.checked_end(addr, src.len()) else { return Ok(None) }; let mut pos = addr; let mut src_offset = 0; @@ -198,19 +220,19 @@ impl LinearMemory for PagedMemory { let chunk_offset = pos & self.chunk_mask; let copy_len = min(self.chunk_size - chunk_offset, end - pos); - let chunk = self.chunk_mut(chunk_idx).ok()?; + let chunk = self.chunk_mut(chunk_idx)?; chunk[chunk_offset..chunk_offset + copy_len].copy_from_slice(&src[src_offset..src_offset + copy_len]); pos += copy_len; src_offset += copy_len; } - Some(()) + Ok(Some(())) } #[inline(always)] - fn fill(&mut self, addr: usize, len: usize, val: u8) -> Option<()> { - let end = self.checked_end(addr, len)?; + fn fill(&mut self, addr: usize, len: usize, val: u8) -> Result, crate::Trap> { + let Some(end) = self.checked_end(addr, len) else { return Ok(None) }; let mut pos = addr; while pos < end { @@ -228,26 +250,27 @@ impl LinearMemory for PagedMemory { chunk[chunk_offset..chunk_offset + fill_len].fill(0); } } else { - self.chunk_mut(chunk_idx).ok()?[chunk_offset..chunk_offset + fill_len].fill(val); + self.chunk_mut(chunk_idx)?[chunk_offset..chunk_offset + fill_len].fill(val); } pos = chunk_end; } - Some(()) + Ok(Some(())) } #[inline(always)] - fn copy_within(&mut self, dst: usize, src: usize, len: usize) -> Option<()> { - self.checked_end(src, len)?; - self.checked_end(dst, len)?; + fn copy_within(&mut self, dst: usize, src: usize, len: usize) -> Result, crate::Trap> { + if self.checked_end(src, len).is_none() || self.checked_end(dst, len).is_none() { + return Ok(None); + } if len == 0 || dst == src { - return Some(()); + return Ok(Some(())); } if self.copy_within_single_chunk(dst, src, len) { - return Some(()); + return Ok(Some(())); } let mut buf = [0u8; 256]; @@ -256,8 +279,11 @@ impl LinearMemory for PagedMemory { let mut copied = 0; while copied < len { let chunk_len = min(buf.len(), len - copied); - self.read_exact(src + copied, &mut buf[..chunk_len])?; - self.write_all(dst + copied, &buf[..chunk_len])?; + if self.read_exact(src + copied, &mut buf[..chunk_len]).is_none() + || self.write_all(dst + copied, &buf[..chunk_len])?.is_none() + { + return Ok(None); + } copied += chunk_len; } } else { @@ -265,13 +291,16 @@ impl LinearMemory for PagedMemory { while remaining > 0 { let chunk_len = min(buf.len(), remaining); let chunk_start = remaining - chunk_len; - self.read_exact(src + chunk_start, &mut buf[..chunk_len])?; - self.write_all(dst + chunk_start, &buf[..chunk_len])?; + if self.read_exact(src + chunk_start, &mut buf[..chunk_len]).is_none() + || self.write_all(dst + chunk_start, &buf[..chunk_len])?.is_none() + { + return Ok(None); + } remaining = chunk_start; } } - Some(()) + Ok(Some(())) } #[inline(always)] @@ -285,6 +314,26 @@ impl LinearMemory for PagedMemory { Ok([self.chunk_slice(chunk_idx).map_or(0, |chunk| chunk[chunk_offset])]) } + #[inline(always)] + fn read_16(&self, addr: usize) -> core::result::Result<[u8; 2], crate::Trap> { + self.read_fixed::<2>(addr) + } + + #[inline(always)] + fn read_32(&self, addr: usize) -> core::result::Result<[u8; 4], crate::Trap> { + self.read_fixed::<4>(addr) + } + + #[inline(always)] + fn read_64(&self, addr: usize) -> core::result::Result<[u8; 8], crate::Trap> { + self.read_fixed::<8>(addr) + } + + #[inline(always)] + fn read_128(&self, addr: usize) -> core::result::Result<[u8; 16], crate::Trap> { + self.read_fixed::<16>(addr) + } + #[inline(always)] fn write_8(&mut self, addr: usize, bytes: &[u8]) -> core::result::Result<(), crate::Trap> { if addr >= self.len { @@ -296,49 +345,24 @@ impl LinearMemory for PagedMemory { self.chunk_mut(chunk_idx)?[chunk_offset] = bytes[0]; Ok(()) } -} -#[cfg(test)] -mod tests { - use super::{LinearMemory, PagedMemory}; - - #[test] - fn paged_memory_reads_zeroes_from_sparse_chunks() { - let memory = PagedMemory::try_new(16, 4).expect("test memory should be constructible"); - let mut dst = [1; 6]; - assert_eq!(memory.read(5, &mut dst), 3); - assert_eq!(&dst[..3], &[0; 3]); - assert_eq!(&dst[3..], &[1; 3]); + #[inline(always)] + fn write_16(&mut self, addr: usize, bytes: &[u8]) -> core::result::Result<(), crate::Trap> { + self.write_fixed::<2>(addr, bytes) } - #[test] - fn paged_memory_store_and_load_crosses_chunk_boundaries() { - let mut memory = PagedMemory::try_new(16, 4).expect("test memory should be constructible"); - memory.write_all(3, &[1, 2, 3, 4, 5, 6]).unwrap(); - - let mut dst = [0; 6]; - memory.read_exact(3, &mut dst).unwrap(); - assert_eq!(dst, [1, 2, 3, 4, 5, 6]); + #[inline(always)] + fn write_32(&mut self, addr: usize, bytes: &[u8]) -> core::result::Result<(), crate::Trap> { + self.write_fixed::<4>(addr, bytes) } - #[test] - fn paged_memory_copy_within_handles_overlap() { - let mut memory = PagedMemory::try_new(16, 4).expect("test memory should be constructible"); - memory.write_all(0, &[1, 2, 3, 4, 5, 6]).unwrap(); - memory.copy_within(2, 0, 6).unwrap(); - - let mut dst = [0; 8]; - memory.read_exact(0, &mut dst).unwrap(); - assert_eq!(dst, [1, 2, 1, 2, 3, 4, 5, 6]); + #[inline(always)] + fn write_64(&mut self, addr: usize, bytes: &[u8]) -> core::result::Result<(), crate::Trap> { + self.write_fixed::<8>(addr, bytes) } - #[test] - fn paged_memory_write_stops_at_chunk_boundary() { - let mut memory = PagedMemory::try_new(16, 4).expect("test memory should be constructible"); - assert_eq!(memory.write(3, &[1, 2, 3, 4]), 1); - - let mut dst = [0; 4]; - memory.read_exact(3, &mut dst).unwrap(); - assert_eq!(dst, [1, 0, 0, 0]); + #[inline(always)] + fn write_128(&mut self, addr: usize, bytes: &[u8]) -> core::result::Result<(), crate::Trap> { + self.write_fixed::<16>(addr, bytes) } } diff --git a/crates/tinywasm/src/store/memory/vec.rs b/crates/tinywasm/src/store/memory/vec.rs index 2737159d..be85e9b5 100644 --- a/crates/tinywasm/src/store/memory/vec.rs +++ b/crates/tinywasm/src/store/memory/vec.rs @@ -1,5 +1,4 @@ use alloc::vec::Vec; -use core::hint::cold_path; use super::{LinearMemory, memory_oob}; @@ -21,13 +20,7 @@ impl VecMemory { /// Prefer this backend when contiguous access is more important than grow performance. pub fn try_new(len: usize) -> Result { let mut data = Vec::new(); - match data.try_reserve_exact(len) { - Ok(()) => {} - Err(_) => { - cold_path(); - return Err(crate::Trap::OutOfMemory); - } - } + cold_err!(data.try_reserve_exact(len)).map_err(|_| crate::Trap::OutOfMemory)?; data.resize(len, 0); Ok(Self { data }) } @@ -35,14 +28,15 @@ impl VecMemory { #[inline(always)] fn read_fixed(&self, addr: usize) -> Result<[u8; N], crate::Trap> { self.check_fixed_addr::(addr)?; - Ok(self.data[addr..addr + N].try_into().unwrap_or_else(|_| unreachable!("slice length should be {N}"))) + let mut bytes = [0u8; N]; + bytes.copy_from_slice(&self.data[addr..addr + N]); + Ok(bytes) } #[inline(always)] fn check_fixed_addr(&self, addr: usize) -> Result<(), crate::Trap> { if N > self.data.len() || addr > self.data.len() - N { - cold_path(); - return Err(memory_oob(addr, N, self.data.len())); + return cold!(Err(memory_oob(addr, N, self.data.len()))); } Ok(()) } @@ -59,13 +53,8 @@ impl LinearMemory for VecMemory { if new_len < self.data.len() { return Err(crate::Trap::MemoryOutOfBounds { offset: new_len, len: 0, max: self.data.len() }); } - match self.data.try_reserve_exact(new_len.saturating_sub(self.data.len())) { - Ok(()) => {} - Err(_) => { - cold_path(); - return Err(crate::Trap::OutOfMemory); - } - } + cold_err!(self.data.try_reserve_exact(new_len.saturating_sub(self.data.len()))) + .map_err(|_| crate::Trap::OutOfMemory)?; self.data.resize(new_len, 0); Ok(()) } @@ -92,39 +81,42 @@ impl LinearMemory for VecMemory { } #[inline(always)] - fn write(&mut self, addr: usize, src: &[u8]) -> usize { + fn write(&mut self, addr: usize, src: &[u8]) -> Result { if addr >= self.data.len() { - return 0; + return Ok(0); } let write_len = src.len().min(self.data.len() - addr); self.data[addr..addr + write_len].copy_from_slice(&src[..write_len]); - write_len + Ok(write_len) } #[inline(always)] - fn write_all(&mut self, addr: usize, src: &[u8]) -> Option<()> { - let dst = self.data.get_mut(addr..addr.checked_add(src.len())?)?; + fn write_all(&mut self, addr: usize, src: &[u8]) -> Result, crate::Trap> { + let Some(end) = addr.checked_add(src.len()) else { return Ok(None) }; + let Some(dst) = self.data.get_mut(addr..end) else { return Ok(None) }; dst.copy_from_slice(src); - Some(()) + Ok(Some(())) } #[inline(always)] - fn fill(&mut self, addr: usize, len: usize, val: u8) -> Option<()> { - self.data.get_mut(addr..addr.checked_add(len)?)?.fill(val); - Some(()) + fn fill(&mut self, addr: usize, len: usize, val: u8) -> Result, crate::Trap> { + let Some(end) = addr.checked_add(len) else { return Ok(None) }; + let Some(dst) = self.data.get_mut(addr..end) else { return Ok(None) }; + dst.fill(val); + Ok(Some(())) } #[inline(always)] - fn copy_within(&mut self, dst: usize, src: usize, len: usize) -> Option<()> { - let src_end = src.checked_add(len)?; - let dst_end = dst.checked_add(len)?; + fn copy_within(&mut self, dst: usize, src: usize, len: usize) -> Result, crate::Trap> { + let Some(src_end) = src.checked_add(len) else { return Ok(None) }; + let Some(dst_end) = dst.checked_add(len) else { return Ok(None) }; if src_end > self.data.len() || dst_end > self.data.len() { - return None; + return Ok(None); } self.data.copy_within(src..src_end, dst); - Some(()) + Ok(Some(())) } #[inline(always)] diff --git a/crates/tinywasm/src/store/mod.rs b/crates/tinywasm/src/store/mod.rs index 517a5613..33a549ca 100644 --- a/crates/tinywasm/src/store/mod.rs +++ b/crates/tinywasm/src/store/mod.rs @@ -1,46 +1,43 @@ -use alloc::sync::Arc; use alloc::{boxed::Box, format, vec::Vec}; use core::hint::cold_path; -use core::sync::atomic::{AtomicUsize, Ordering}; +use core::sync::atomic::{AtomicU32, Ordering}; use tinywasm_types::*; -use crate::interpreter::stack::{CallStack, ValueStack}; +use crate::func::FromWasmValues; +use crate::interpreter::stack::{CallStack, StackBase, ValueStack}; use crate::interpreter::{TinyWasmValue, ValueRef}; use crate::{Engine, Error, ModuleInstance, Result, Trap}; +mod const_expr; mod data; mod element; +mod exception; mod function; +mod gc; mod global; mod memory; +mod state; mod table; +mod tag; +mod types; +use const_expr::eval_const; +pub(crate) use gc::{decode_data, default_value, pop_value, push_value}; pub use memory::{LazyLinearMemory, LinearMemory, MemoryBackend, PagedMemory, VecMemory}; pub(crate) use memory::{MemValue, MemoryInstance}; -pub(crate) use {data::*, element::*, function::*, global::*, table::*}; +pub(crate) use state::State; +pub(crate) use types::{canonicalize_ref_type, canonicalize_value_type}; +pub(crate) use {data::*, element::*, exception::*, function::*, global::*, table::*, tag::*}; // global store id counter -static STORE_ID: AtomicUsize = AtomicUsize::new(0); - -pub(crate) fn canonicalize_ref_type(ty: RefType, type_addrs: &[TypeAddr]) -> RefType { - let Some(type_addr) = ty.type_index() else { return ty }; - let canonical = - *type_addrs.get(type_addr as usize).unwrap_or_else(|| unreachable!("invalid type address: {type_addr}")); - RefType::new_concrete(ty.is_nullable(), canonical).unwrap() -} - -pub(crate) fn canonicalize_value_type(ty: WasmType, type_addrs: &[TypeAddr]) -> WasmType { - match ty { - WasmType::Ref(ty) => WasmType::Ref(canonicalize_ref_type(ty, type_addrs)), - ty => ty, - } -} +static STORE_ID: AtomicU32 = AtomicU32::new(0); /// Global state that can be manipulated by WebAssembly programs /// -/// Note that the state doesn't do any garbage collection - so it will grow -/// indefinitely if you keep adding modules to it. When calling temporary -/// functions, you should create a new store and then drop it when you're done (e.g. in a request handler). +/// Managed WebAssembly GC objects are collected automatically. Other Store +/// instances, such as modules, functions, memories, and tables, live until the +/// Store is dropped. GC references exposed through the copyable host value API +/// are retained for the Store's lifetime. /// /// ## Example /// ```rust @@ -54,7 +51,7 @@ pub(crate) fn canonicalize_value_type(ty: WasmType, type_addrs: &[TypeAddr]) -> /// /// See pub struct Store { - id: usize, + id: u32, pub(crate) module_instances: Vec, pub(crate) engine: Engine, @@ -63,6 +60,7 @@ pub struct Store { pub(crate) state: State, pub(crate) call_stack: CallStack, pub(crate) value_stack: ValueStack, + pub(crate) host_params: Vec, } #[cfg(feature = "debug")] @@ -76,32 +74,63 @@ impl core::fmt::Debug for Store { } } +impl PartialEq for Store { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Default for Store { + fn default() -> Self { + Self::new(Engine::default()) + } +} + impl Store { /// Create a new store pub fn new(engine: Engine) -> Self { - let id = STORE_ID.fetch_add(1, Ordering::Relaxed); + let id = + STORE_ID.try_update(Ordering::Relaxed, Ordering::Relaxed, |id| id.checked_add(1)).expect("too many stores"); + let state = State::new(engine.config().gc_collection_threshold); Self { id, module_instances: Vec::new(), - state: State::default(), + state, call_stack: CallStack::new(engine.config()), value_stack: ValueStack::new(engine.config()), + host_params: Vec::new(), engine, execution_fuel: 0, execution_active: false, } } + /// Get the store's ID (unique per process) + pub fn id(&self) -> u32 { + self.id + } + /// Get a module instance by the internal id - pub fn get_module_instance(&self, id: ModuleInstanceId) -> Option { - self.module_instances.get(id as usize).cloned() + pub fn get_module_instance(&self, id: ModuleInstanceId) -> Option<&ModuleInstance> { + self.module_instances.get(id as usize) } - #[inline] - pub(crate) fn get_module_instance_internal(&self, id: ModuleInstanceId) -> ModuleInstance { - self.module_instances.get(id as usize).unwrap_or_else(|| unreachable!("invalid module instance: {id}")).clone() + pub(crate) fn next_module_instance_id(&self) -> ModuleInstanceId { + self.module_instances.len() as ModuleInstanceId } + pub(crate) fn add_instance(&mut self, instance: ModuleInstance) { + debug_assert!(instance.id() == self.module_instances.len() as ModuleInstanceId); + self.module_instances.push(instance); + } + + /// Returns whether a public value has the requested runtime type. + #[doc(hidden)] + pub fn value_matches_type(&self, value: WasmValue, ty: WasmType) -> bool { + self.state.value_matches_type(value, ty) + } + + /// Marks the store as executing and rejects nested root calls. pub(crate) fn enter_execution(&mut self) -> Result<()> { if self.execution_active { return Err(Trap::Other( @@ -113,251 +142,95 @@ impl Store { Ok(()) } + /// Marks the current root execution as complete. pub(crate) fn exit_execution(&mut self) { self.execution_active = false; } -} -impl PartialEq for Store { - fn eq(&self, other: &Self) -> bool { - self.id == other.id - } -} - -impl Default for Store { - fn default() -> Self { - Self::new(Engine::default()) - } -} - -#[derive(Default)] -/// Global state that can be manipulated by WebAssembly programs -/// -/// Data should only be addressable by the module that owns it -/// See -pub(crate) struct State { - // Concrete type indexes in store instances address this canonical type space. - canonical_types: Vec, - pub(crate) funcs: Vec, - pub(crate) tables: Vec, - pub(crate) memories: Vec, - pub(crate) globals: Vec, - pub(crate) elements: Vec, - pub(crate) data: Vec, -} - -impl State { - pub(crate) fn value_matches_type(&self, value: WasmValue, expected: WasmType) -> bool { - match (value, expected) { - (WasmValue::Ref(RefValue::Null), WasmType::Ref(expected)) => expected.is_nullable(), - (WasmValue::Ref(RefValue::Func(func)), WasmType::Ref(expected)) => { - self.funcs.get(func.addr() as usize).is_some_and(|func| match expected.type_index() { - Some(expected) => func.type_addr == expected, - None => matches!(expected.abstract_heap_type(), Some(AbstractHeapType::Func)), - }) + /// Validates and pushes typed parameters or results onto the value stack. + pub(crate) fn push_typed_values( + &mut self, + type_addr: TypeAddr, + values: impl Iterator, + stack_base: StackBase, + ) -> Result<()> { + let result: Result<()> = (|| { + let ty = self.state.get_canonical_func_type(type_addr); + let expected = if RESULTS { ty.results() } else { ty.params() }; + let mut values = values; + for &ty in expected { + let value = values.next().ok_or_else(|| Error::other("not enough typed function values"))?; + if !self.state.value_matches_type(value, ty) { + return Err(Error::other("typed function value does not match its signature")); + } + self.value_stack.extend_wasmvalues(core::iter::once(value))?; } - (_, WasmType::Ref(expected)) if expected.is_concrete() => false, - _ => value.matches_type(expected), - } - } - - #[inline] - pub(crate) fn get_func_type(&self, addr: FuncAddr) -> &FuncType { - let type_addr = self.get_func(addr).type_addr; - Self::get(&self.canonical_types, type_addr, "canonical type") - } - - #[inline] - pub(crate) fn get_type(&self, addr: TypeAddr) -> &FuncType { - Self::get(&self.canonical_types, addr, "canonical type") - } - fn get<'a, T>(items: &'a [T], addr: Addr, kind: &str) -> &'a T { - items.get(addr as usize).unwrap_or_else(|| unreachable!("invalid {kind} address: {addr}")) - } - - fn get_mut<'a, T>(items: &'a mut [T], addr: Addr, kind: &str) -> &'a mut T { - items.get_mut(addr as usize).unwrap_or_else(|| unreachable!("invalid {kind} address: {addr}")) - } - - fn get_disjoint_mut<'a, T>(items: &'a mut [T], addr: Addr, addr2: Addr, kind: &str) -> (&'a mut T, &'a mut T) { - let [item_a, item_b] = items - .get_disjoint_mut([addr as usize, addr2 as usize]) - .unwrap_or_else(|_| unreachable!("invalid {kind} addresses: {addr}, {addr2}")); - (item_a, item_b) - } - - /// Get the function at the actual index in the store - pub(crate) fn get_func(&self, addr: FuncAddr) -> &FunctionInstance { - Self::get(&self.funcs, addr, "function") - } - - /// Get a wasm function at the actual index in the store, panicking if it's a host function (which should be guaranteed by the validator) - pub(crate) fn get_wasm_func(&self, addr: FuncAddr) -> &WasmFunctionInstance { - match self.funcs.get(addr as usize).map(|func| &func.kind) { - Some(FunctionKind::Wasm(wasm_func)) => wasm_func, - _ => unreachable!("invalid wasm function address: {addr}"), + if values.next().is_some() { + return Err(Error::other("too many typed function values")); + } + Ok(()) + })(); + if result.is_err() { + self.value_stack.truncate_to_base(stack_base); } + result } - /// Get the memory at the actual index in the store - pub(crate) fn get_mem(&self, addr: MemAddr) -> &MemoryInstance { - Self::get(&self.memories, addr, "memory") - } - - /// Get the memory at the actual index in the store - pub(crate) fn get_mem_mut(&mut self, addr: MemAddr) -> &mut MemoryInstance { - Self::get_mut(&mut self.memories, addr, "memory") - } - - /// Get the memory at the actual index in the store - pub(crate) fn get_mems_mut(&mut self, addr: MemAddr, addr2: MemAddr) -> (&mut MemoryInstance, &mut MemoryInstance) { - Self::get_disjoint_mut(&mut self.memories, addr, addr2, "memory") - } - - /// Get the table at the actual index in the store - pub(crate) fn get_table(&self, addr: TableAddr) -> &TableInstance { - Self::get(&self.tables, addr, "table") - } - - /// Get the table at the actual index in the store - pub(crate) fn get_table_mut(&mut self, addr: TableAddr) -> &mut TableInstance { - Self::get_mut(&mut self.tables, addr, "table") - } - - /// Get two mutable tables at the actual index in the store - pub(crate) fn get_tables_mut( + /// Reads typed results from the value stack and restores its previous base. + pub(crate) fn take_typed_results( &mut self, - addr: TableAddr, - addr2: TableAddr, - ) -> (&mut TableInstance, &mut TableInstance) { - Self::get_disjoint_mut(&mut self.tables, addr, addr2, "table") - } - - /// Get the data at the actual index in the store - pub(crate) fn get_data_mut(&mut self, addr: DataAddr) -> &mut DataInstance { - Self::get_mut(&mut self.data, addr, "data") - } - - /// Get the element at the actual index in the store - pub(crate) fn get_elem_mut(&mut self, addr: ElemAddr) -> &mut ElementInstance { - Self::get_mut(&mut self.elements, addr, "element") - } - - /// Get the global at the actual index in the store - pub(crate) fn get_global(&self, addr: GlobalAddr) -> &GlobalInstance { - Self::get(&self.globals, addr, "global") - } - - /// Get the global at the actual index in the store - pub(crate) fn get_global_mut(&mut self, addr: GlobalAddr) -> &mut GlobalInstance { - Self::get_mut(&mut self.globals, addr, "global") - } - - /// Get the global at the actual index in the store - pub(crate) fn get_global_val(&self, addr: GlobalAddr) -> TinyWasmValue { - self.get_global(addr).value - } - - /// Set the global at the actual index in the store - pub(crate) fn set_global_val(&mut self, addr: GlobalAddr, value: TinyWasmValue) { - self.get_global_mut(addr).value = value; - } -} - -impl Store { - /// Get the store's ID (unique per process) - pub fn id(&self) -> usize { - self.id - } - - pub(crate) fn next_module_instance_id(&self) -> ModuleInstanceId { - self.module_instances.len() as ModuleInstanceId - } - - pub(crate) fn add_instance(&mut self, instance: ModuleInstance) { - debug_assert!(instance.id() == self.module_instances.len() as ModuleInstanceId); - self.module_instances.push(instance); - } - - /// Get the global at the actual index in the store - #[doc(hidden)] - pub fn get_global_val(&self, addr: GlobalAddr) -> TinyWasmValue { - self.state.get_global_val(addr) - } - - /// Set the global at the actual index in the store - #[doc(hidden)] - pub fn set_global_val(&mut self, addr: GlobalAddr, value: TinyWasmValue) { - self.state.set_global_val(addr, value); - } -} - -// Linking related functions -impl Store { - pub(crate) fn register_module_types(&mut self, types: &[FuncType]) -> Box<[TypeAddr]> { - let mut type_addrs = Vec::with_capacity(types.len()); - for (local_addr, ty) in types.iter().enumerate() { - if let Some(addr) = self - .state - .canonical_types - .iter() - .position(|registered| ty.equivalent(types, registered, &self.state.canonical_types)) - { - type_addrs.push(addr as TypeAddr); - continue; + type_addr: TypeAddr, + stack_base: StackBase, + pin_refs: bool, + ) -> Result { + let types = self.state.get_canonical_func_type(type_addr).results(); + let mut values = self.value_stack.wasm_values(&self.state, types, stack_base, pin_refs); + let result = R::from_wasm_values(&mut values).and_then(|result| { + if values.next().is_some() { + Err(Error::other("typed conversion did not consume all WebAssembly values")) + } else { + Ok(result) } - - let addr = self.state.canonical_types.len(); - assert!(addr <= ((1 << 30) - 1), "too many canonical function types"); - type_addrs.push(addr as TypeAddr); - let canonicalize = |ty: WasmType| match ty { - WasmType::Ref(ty) if ty.is_concrete() => { - let module_ref = ty.type_index().unwrap() as usize; - // A singleton recursive group can refer to the type currently being registered. - let canonical = if module_ref == local_addr { - addr as TypeAddr - } else { - *type_addrs - .get(module_ref) - .unwrap_or_else(|| unreachable!("invalid forward type reference: {module_ref}")) - }; - WasmType::Ref(RefType::new_concrete(ty.is_nullable(), canonical).unwrap()) - } - ty => ty, - }; - let params = ty.params().iter().copied().map(canonicalize).collect::>(); - let results = ty.results().iter().copied().map(canonicalize).collect::>(); - self.state.canonical_types.push(FuncType::new(¶ms, &results)); - } - type_addrs.into_boxed_slice() - } - - pub(crate) fn register_host_type(&mut self, ty: &FuncType) -> TypeAddr { - if let Some(addr) = self.state.canonical_types.iter().position(|registered| ty == registered) { - return addr as TypeAddr; - } - let addr = self.state.canonical_types.len(); - assert!(addr <= ((1 << 30) - 1), "too many canonical function types"); - self.state.canonical_types.push(ty.clone()); - addr as TypeAddr + }); + drop(values); + self.value_stack.truncate_to_base(stack_base); + result } /// Add functions to the store, returning their addresses in the store pub(crate) fn init_funcs( &mut self, - funcs: &[Arc], + funcs: &[alloc::sync::Arc], owner: ModuleInstanceId, + module_type_idxs: &[TypeAddr], type_addrs: &[TypeAddr], ) -> impl ExactSizeIterator { let start = self.state.funcs.len() as FuncAddr; - debug_assert_eq!(funcs.len(), type_addrs.len()); - self.state.funcs.extend(funcs.iter().zip(type_addrs).map(|(func, &type_addr)| FunctionInstance { - type_addr, - kind: FunctionKind::Wasm(WasmFunctionInstance { func: func.clone(), owner }), - })); + debug_assert_eq!(funcs.len(), module_type_idxs.len()); + self.state.funcs.reserve_exact(funcs.len()); + for (func, &type_idx) in funcs.iter().cloned().zip(module_type_idxs) { + let type_addr = type_addrs[type_idx as usize]; + self.state.funcs.push(FunctionInstance { + type_addr, + gc: self.state.func_gc_metadata(type_addr), + kind: FunctionKind::Wasm(WasmFunctionInstance { func, owner }), + }); + } start..start + funcs.len() as FuncAddr } + /// Add tags to the store, returning their addresses in the store. + pub(crate) fn init_tags( + &mut self, + tags: &[TagType], + type_addrs: &[TypeAddr], + ) -> impl ExactSizeIterator { + let start = self.state.tags.len() as TagAddr; + self.state.tags.reserve_exact(tags.len()); + self.state.tags.extend(tags.iter().map(|tag| TagInstance { type_addr: type_addrs[tag.type_idx as usize] })); + start..start + tags.len() as TagAddr + } + /// Add tables to the store, returning their addresses in the store pub(crate) fn init_tables( &mut self, @@ -370,18 +243,18 @@ impl Store { self.state.tables.reserve_exact(tables.len()); for table in tables { let init = match &table.init { - Some(expr) => match self.eval_const(expr, global_addrs, func_addrs)? { - TinyWasmValue::ValueRef(value) => TableElement::from(value.addr()), + Some(expr) => match eval_const(&mut self.state, expr, global_addrs, func_addrs, type_addrs)? { + TinyWasmValue::ValueRef(value) => value, _ => return Err(Error::other("table initializer is not a reference value")), }, - None => TableElement::Uninitialized, + None => ValueRef::NULL, }; let element_type = canonicalize_ref_type(table.ty.element_type, type_addrs); let ty = match table.ty.arch() { MemoryArch::I32 => TableType::new(element_type, table.ty.size_initial, table.ty.size_max), MemoryArch::I64 => TableType::new64(element_type, table.ty.size_initial, table.ty.size_max), }; - self.state.tables.push(TableInstance::new_with_init(ty, init)?); + self.state.tables.push(TableInstance::new(ty, init)?); } Ok(start..start + tables.len() as TableAddr) } @@ -393,14 +266,9 @@ impl Store { init: impl Fn(MemoryType, &MemoryBackend) -> Result, ) -> Result> { let start = self.state.memories.len() as MemAddr; + self.state.memories.reserve_exact(memories.len()); for mem in memories { - self.state.memories.push(match init(*mem, &self.engine.config().memory_backend) { - Ok(mem) => mem, - Err(e) => { - cold_path(); - return Err(e); - } - }); + self.state.memories.push(cold_err!(init(*mem, &self.engine.config().memory_backend))?); } Ok(start..start + memories.len() as MemAddr) } @@ -413,35 +281,33 @@ impl Store { func_addrs: &[FuncAddr], type_addrs: &[TypeAddr], ) -> Result<()> { - let start = self.state.globals.len() as Addr; - out.extend(start..start + globals.len() as Addr); - + self.state.globals.reserve(globals); for global in globals { - let value = match self.eval_const(&global.init, out, func_addrs) { - Ok(val) => val, - Err(e) => { - cold_path(); - return Err(e); - } - }; + let value = cold_err!(eval_const(&mut self.state, &global.init, out, func_addrs, type_addrs))?; let ty = global.ty.with_ty(canonicalize_value_type(global.ty.ty, type_addrs)); - self.state.globals.push(GlobalInstance::new(ty, value)); + out.push(self.state.globals.push(ty, value)); } Ok(()) } - fn elem_addr(&self, item: &ElementItem, globals: &[Addr], funcs: &[FuncAddr]) -> Result> { + fn elem_value( + &mut self, + item: &ElementItem, + globals: &[Addr], + funcs: &[FuncAddr], + type_addrs: &[TypeAddr], + ) -> Result { match item { - ElementItem::Expr(expr) => match self.eval_const(expr, globals, funcs)? { - TinyWasmValue::ValueRef(v) => Ok(v.addr()), + ElementItem::Expr(expr) => match eval_const(&mut self.state, expr, globals, funcs, type_addrs)? { + TinyWasmValue::ValueRef(value) => Ok(value), other => { cold_path(); Err(Error::Other(format!("expected ref type, got {other:?}"))) } }, ElementItem::Func(addr) => match funcs.get(*addr as usize) { - Some(func_addr) => Ok(Some(*func_addr)), + Some(func_addr) => Ok(ValueRef::from_category_addr(*func_addr)), None => { cold_path(); Err(Error::Other(format!( @@ -460,26 +326,36 @@ impl Store { func_addrs: &[FuncAddr], global_addrs: &[Addr], elements: &[Element], - ) -> Result<(Box<[Addr]>, Option)> { + type_addrs: &[TypeAddr], + ) -> Result<(Box<[ElemAddr]>, Option)> { let elem_count = self.state.elements.len(); - let mut elem_addrs = Vec::with_capacity(elem_count); + let mut elem_addrs = Vec::with_capacity(elements.len()); + self.state.elements.reserve_exact(elements.len()); for (i, element) in elements.iter().enumerate() { - let init = element - .items - .iter() - .map(|item| Ok(TableElement::from(self.elem_addr(item, global_addrs, func_addrs)?))) - .collect::>>()?; + let elem_addr = self.state.elements.len(); + self.state.elements.push(ElementInstance { + items: Some(Vec::with_capacity(element.items.len())), + ty: canonicalize_ref_type(element.ty, type_addrs), + }); + for item in &element.items { + let value = self.elem_value(item, global_addrs, func_addrs, type_addrs)?; + self.state.elements[elem_addr].items.as_mut().unwrap().push(value); + } - let items = match &element.kind { + match &element.kind { // doesn't need to be initialized, can be initialized lazily using the `table.init` instruction - ElementKind::Passive => Some(init), + ElementKind::Passive => {} // this one is not available to the runtime but needs to be initialized to declare references - ElementKind::Declared => None, // a. Execute the instruction elm.drop i + ElementKind::Declared => self.state.elements[elem_addr].drop(), // this one is active, so we need to initialize it (essentially a `table.init` instruction) ElementKind::Active { offset, table } => { - let offset = self.eval_size_const(offset, global_addrs, func_addrs)?; + let offset = match eval_const(&mut self.state, offset, global_addrs, func_addrs, type_addrs)? { + TinyWasmValue::Value32(value) => u64::from(value), + TinyWasmValue::Value64(value) => value, + other => return Err(Error::Other(format!("expected i32 or i64, got {other:?}"))), + }; let table_addr = table_addrs .get(*table as usize) .copied() @@ -497,21 +373,26 @@ impl Store { let Ok(offset) = usize::try_from(offset) else { return Ok(( elem_addrs.into_boxed_slice(), - Some(Trap::TableOutOfBounds { offset: usize::MAX, len: init.len(), max: table.size() }), + Some(Trap::TableOutOfBounds { + offset: usize::MAX, + len: self.state.elements[elem_addr].items.as_ref().unwrap().len(), + max: table.size(), + }), )); }; - if let Err(trap) = table.init(offset, &init) { + let State { elements, tables, .. } = &mut self.state; + let init = elements[elem_addr].items.as_deref().unwrap(); + let table = &mut tables[table_addr as usize]; + if let Err(trap) = table.init(offset, init) { return Ok((elem_addrs.into_boxed_slice(), Some(trap))); } // f. Execute the instruction elm.drop i - None + elements[elem_addr].drop(); } - }; - - self.state.elements.push(ElementInstance { items }); - elem_addrs.push((i + elem_count) as Addr); + } + elem_addrs.push((i + elem_count) as ElemAddr); } // this should be optimized out by the compiler @@ -525,9 +406,11 @@ impl Store { global_addrs: &[Addr], func_addrs: &[FuncAddr], data: &[Data], - ) -> Result<(Box<[Addr]>, Option)> { + type_addrs: &[TypeAddr], + ) -> Result<(Box<[DataAddr]>, Option)> { let data_count = self.state.data.len(); - let mut data_addrs = Vec::with_capacity(data_count); + let mut data_addrs = Vec::with_capacity(data.len()); + self.state.data.reserve_exact(data.len()); for (i, data) in data.iter().enumerate() { let data_val = match &data.kind { tinywasm_types::DataKind::Active { mem: mem_addr, offset } => { @@ -535,13 +418,17 @@ impl Store { return Err(Error::Other(format!("memory {mem_addr} not found for data segment {i}"))); }; - let offset = self.eval_size_const(offset, global_addrs, func_addrs)?; + let offset = match eval_const(&mut self.state, offset, global_addrs, func_addrs, type_addrs)? { + TinyWasmValue::Value32(value) => u64::from(value), + TinyWasmValue::Value64(value) => value, + other => return Err(Error::Other(format!("expected i32 or i64, got {other:?}"))), + }; let Some(mem) = self.state.memories.get_mut(*mem_addr as usize) else { return Err(Error::Other(format!("memory {mem_addr} not found for data segment {i}"))); }; let offset = usize::try_from(offset).unwrap_or(usize::MAX); - match mem.inner.write_all(offset, &data.data) { + match mem.inner.write_all(offset, &data.data)? { Some(()) => None, None => { return Ok(( @@ -559,164 +446,17 @@ impl Store { }; self.state.data.push(DataInstance { data: data_val }); - data_addrs.push((i + data_count) as Addr); + data_addrs.push((i + data_count) as DataAddr); } // this should be optimized out by the compiler Ok((data_addrs.into_boxed_slice(), None)) } + /// Adds a function and returns its store address. pub(crate) fn add_func(&mut self, func: FunctionInstance) -> FuncAddr { + let addr = self.state.funcs.len() as FuncAddr; self.state.funcs.push(func); - self.state.funcs.len() as FuncAddr - 1 - } - - /// Evaluate a constant expression that's either a i32 or a i64 as a global or a const instruction - fn eval_size_const( - &self, - const_instrs: &[tinywasm_types::ConstInstruction], - module_global_addrs: &[Addr], - module_func_addrs: &[FuncAddr], - ) -> Result { - let value = self.eval_const(const_instrs, module_global_addrs, module_func_addrs)?; - match value { - TinyWasmValue::Value32(i) => Ok(u64::from(i)), - TinyWasmValue::Value64(i) => Ok(i), - other => Err(Error::Other(format!("expected i32 or i64, got {other:?}"))), - } - } - - /// Evaluate a constant expression - #[inline] - fn eval_const( - &self, - const_instrs: &[tinywasm_types::ConstInstruction], - module_global_addrs: &[Addr], - module_func_addrs: &[FuncAddr], - ) -> Result { - use tinywasm_types::ConstInstruction::*; - - let resolve_global = |idx: u32| -> Result { - let Some(addr) = module_global_addrs.get(idx as usize) else { - cold_path(); - return Err(Error::Other(format!( - "global {idx} not found. This should have been caught by the validator" - ))); - }; - - let Some(global) = self.state.globals.get(*addr as usize) else { - cold_path(); - return Err(Error::Other(format!("global {addr} not found"))); - }; - - Ok(global.value) - }; - - let resolve_func = |idx: u32| -> Result { - match module_func_addrs.get(idx as usize) { - Some(func_addr) => Ok(*func_addr), - None => { - cold_path(); - Err(Error::Other(format!( - "function {idx} not found. This should have been caught by the validator" - ))) - } - } - }; - - if const_instrs.len() == 1 { - let val = match &const_instrs[0] { - F32Const(f) => (*f).into(), - F64Const(f) => (*f).into(), - I32Const(i) => (*i).into(), - I64Const(i) => (*i).into(), - V128Const(i) => (*i).into(), - GlobalGet(addr) => resolve_global(*addr)?, - Ref(tinywasm_types::RefValue::Null) => TinyWasmValue::ValueRef(ValueRef::NULL), - Ref(tinywasm_types::RefValue::Func(func)) => { - TinyWasmValue::ValueRef(ValueRef::from_raw(resolve_func(func.addr())?)) - } - Ref(_) => return Err(Error::other("unsupported reference constant")), - _ => { - cold_path(); - return Err(Error::other("unsupported const instruction")); - } - }; - - return Ok(val); - } - - let mut stack = Vec::new(); - for instr in const_instrs { - match instr { - I32Const(i) => stack.push(TinyWasmValue::Value32(*i as u32)), - I64Const(i) => stack.push(TinyWasmValue::Value64(*i as u64)), - F32Const(f) => stack.push(TinyWasmValue::Value32(f.to_bits())), - F64Const(f) => stack.push(TinyWasmValue::Value64(f.to_bits())), - V128Const(i) => stack.push(TinyWasmValue::Value128((*i).into())), - GlobalGet(addr) => stack.push(resolve_global(*addr)?), - Ref(tinywasm_types::RefValue::Null) => stack.push(TinyWasmValue::ValueRef(ValueRef::NULL)), - Ref(tinywasm_types::RefValue::Func(func)) => { - stack.push(TinyWasmValue::ValueRef(ValueRef::from_raw(resolve_func(func.addr())?))) - } - Ref(_) => { - cold_path(); - return Err(Error::other("unsupported reference constant")); - } - I32Add | I32Sub | I32Mul => { - let rhs = stack.pop().ok_or_else(|| Error::other("const stack underflow"))?; - let lhs = stack.pop().ok_or_else(|| Error::other("const stack underflow"))?; - let (TinyWasmValue::Value32(lhs), TinyWasmValue::Value32(rhs)) = (lhs, rhs) else { - cold_path(); - return Err(Error::other("type mismatch in const i32 op")); - }; - let lhs = lhs as i32; - let rhs = rhs as i32; - let out = match instr { - I32Add => lhs.wrapping_add(rhs), - I32Sub => lhs.wrapping_sub(rhs), - I32Mul => lhs.wrapping_mul(rhs), - _ => { - cold_path(); - return Err(Error::other("invalid const instruction in i32 op")); - } - }; - stack.push(TinyWasmValue::Value32(out as u32)); - } - I64Add | I64Sub | I64Mul => { - let rhs = stack.pop(); - let lhs = stack.pop(); - let (Some(TinyWasmValue::Value64(lhs)), Some(TinyWasmValue::Value64(rhs))) = (lhs, rhs) else { - cold_path(); - return Err(Error::other("type mismatch in const i64 op")); - }; - - let lhs = lhs as i64; - let rhs = rhs as i64; - let out = match instr { - I64Add => lhs.wrapping_add(rhs), - I64Sub => lhs.wrapping_sub(rhs), - I64Mul => lhs.wrapping_mul(rhs), - _ => { - cold_path(); - return Err(Error::other("invalid const instruction in i64 op")); - } - }; - stack.push(TinyWasmValue::Value64(out as u64)); - } - } - } - - let Some(value) = stack.pop() else { - cold_path(); - return Err(Error::other("empty const expression")); - }; - - if !stack.is_empty() { - cold_path(); - return Err(Error::other("const expression did not reduce to single value")); - } - - Ok(value) + addr } } diff --git a/crates/tinywasm/src/store/state.rs b/crates/tinywasm/src/store/state.rs new file mode 100644 index 00000000..601bb6a9 --- /dev/null +++ b/crates/tinywasm/src/store/state.rs @@ -0,0 +1,446 @@ +use alloc::vec::Vec; + +use super::*; + +/// Global state that can be manipulated by WebAssembly programs +/// +/// Data should only be addressable by the module that owns it +/// See +pub(crate) struct State { + // Concrete type indexes in store instances address this canonical type space. + pub(crate) canonical_types: Vec, + pub(crate) canonical_rec_group_lengths: Vec, + pub(crate) funcs: Vec, + pub(crate) tables: Vec, + pub(crate) memories: Vec, + pub(crate) globals: Globals, + pub(crate) tags: Vec, + pub(crate) exceptions: Vec, + pub(crate) elements: Vec, + pub(crate) data: Vec, + pub(crate) gc: Box, +} + +impl State { + pub(crate) fn new(gc_collection_threshold: usize) -> Self { + Self { + canonical_types: Vec::new(), + canonical_rec_group_lengths: Vec::new(), + funcs: Vec::new(), + tables: Vec::new(), + memories: Vec::new(), + globals: Globals::default(), + tags: Vec::new(), + exceptions: Vec::new(), + elements: Vec::new(), + data: Vec::new(), + gc: Box::new(gc::GcHeap::new(gc_collection_threshold)), + } + } + + /// Returns whether values of this type can contain a managed GC object. + pub(crate) fn type_may_contain_gc(&self, ty: &WasmType) -> bool { + Self::type_may_contain_gc_in(&self.canonical_types, ty) + } + + fn type_may_contain_gc_in(types: &[SubType], ty: &WasmType) -> bool { + let WasmType::Ref(ty) = ty else { return false }; + if let Some(type_addr) = ty.type_index() { + return matches!(types[type_addr as usize].composite, CompositeType::Struct(_) | CompositeType::Array(_)); + } + matches!( + ty.abstract_heap_type(), + Some( + AbstractHeapType::Any + | AbstractHeapType::Eq + | AbstractHeapType::Struct + | AbstractHeapType::Array + | AbstractHeapType::Extern + ) + ) + } + + /// Precomputes whether a canonical function signature can carry GC objects. + pub(crate) fn func_gc_metadata(&self, type_addr: TypeAddr) -> FunctionGcMetadata { + let ty = self.get_canonical_func_type(type_addr); + FunctionGcMetadata { + params: ty.params().iter().any(|ty| self.type_may_contain_gc(ty)), + results: ty.results().iter().any(|ty| self.type_may_contain_gc(ty)), + } + } + + /// Allocates an object, collecting from all runtime roots when needed. + pub(crate) fn alloc_gc_object( + &mut self, + type_addr: TypeAddr, + values: Vec, + stack_32: impl IntoIterator, + ) -> Result { + let trace_references = match &self.get_type(type_addr).composite { + CompositeType::Struct(ty) => { + ty.fields.iter().any(|field| matches!(field.storage, StorageType::Value(WasmType::Ref(_)))) + } + CompositeType::Array(ty) => matches!(ty.field.storage, StorageType::Value(WasmType::Ref(_))), + CompositeType::Func(_) => unreachable!("GC object type is not a function"), + }; + if self.gc.should_collect(values.len(), trace_references) { + let canonical_types = &self.canonical_types; + let roots = stack_32 + .into_iter() + .map(ValueRef::from_raw) + .chain( + self.globals + .globals_32() + .filter(|(_, ty)| Self::type_may_contain_gc_in(canonical_types, &ty.ty)) + .map(|(value, _)| ValueRef::from_raw(*value)), + ) + .chain( + self.tables + .iter() + .filter(|table| { + Self::type_may_contain_gc_in(canonical_types, &WasmType::Ref(table.kind.element_type)) + }) + .flat_map(|table| table.elements.iter().copied()), + ) + .chain( + self.elements + .iter() + .filter(|element| Self::type_may_contain_gc_in(canonical_types, &WasmType::Ref(element.ty))) + .flat_map(|element| element.items.iter().flatten().copied()), + ) + .chain(self.exceptions.iter().flat_map(|exception| { + exception.payload.iter().filter_map(|value| match value { + TinyWasmValue::ValueRef(value) => Some(*value), + _ => None, + }) + })) + .chain(values.iter().filter_map(|value| match value { + TinyWasmValue::ValueRef(value) => Some(*value), + _ => None, + })); + cold_err!(self.gc.collect(roots)).map_err(|_| Trap::OutOfMemory)?; + } + cold_err!(self.gc.alloc(type_addr, values, trace_references)).map_err(|_| Trap::OutOfMemory) + } + + /// Pins a host-visible reference when it resolves to a managed GC object. + pub(crate) fn pin_host_ref(&self, value: RefValue) { + let raw = match value { + RefValue::Any(value) => value.raw(), + RefValue::Extern(value) => value.raw(), + RefValue::Null | RefValue::Func(_) | RefValue::Exn(_) => return, + }; + self.gc.pin(ValueRef::from_raw(raw)); + } + + /// Pins GC references that have crossed into host-visible values. + pub(crate) fn pin_host_values(&self, values: &[WasmValue]) { + for &value in values { + if let WasmValue::Ref(value) = value { + self.pin_host_ref(value); + } + } + } + + /// Resolves a non-null object of the expected canonical type. + pub(crate) fn gc_object(&self, reference: ValueRef, expected_type: TypeAddr) -> Result<&gc::GcObject, Trap> { + if reference.is_null() { + return Err(if self.get_type(expected_type).as_array().is_some() { + Trap::NullArrayReference + } else { + Trap::NullStructReference + }); + } + let object = self.gc.get(reference).ok_or(Trap::Other("invalid GC reference"))?; + if !self.type_addr_is_subtype(object.type_addr, expected_type) { + return Err(Trap::Other("GC reference type mismatch")); + } + Ok(object) + } + + /// Converts an internal reference using canonical heap type information. + pub(crate) fn to_ref_value(&self, value: ValueRef, ty: RefType) -> RefValue { + if value.is_null() { + return RefValue::Null; + } + + if let Some(type_addr) = ty.type_index() { + return match &self.get_type(type_addr).composite { + CompositeType::Func(_) => { + RefValue::Func(FuncRef::new(value.addr().expect("non-null reference has an address"))) + } + CompositeType::Struct(_) | CompositeType::Array(_) => RefValue::Any(AnyRef::from_raw(value.raw())), + }; + } + if ty.is_func() { + return RefValue::Func(FuncRef::new(value.addr().expect("non-null reference has an address"))); + } + if ty.is_extern() { + return RefValue::Extern(ExternRef::from_raw(value.raw())); + } + if ty.is_exn() { + return RefValue::Exn(ExnRef::new(value.addr().expect("non-null reference has an address"))); + } + RefValue::Any(AnyRef::from_raw(value.raw())) + } + + /// Returns whether one canonical type is a subtype of another. + pub(crate) fn type_addr_is_subtype(&self, mut actual: TypeAddr, expected: TypeAddr) -> bool { + loop { + if actual == expected { + return true; + } + let Some(supertype) = self.get_type(actual).supertype else { return false }; + actual = supertype; + } + } + + /// Returns whether one reference type is a subtype of another. + pub(crate) fn ref_type_is_subtype(&self, actual: RefType, expected: RefType) -> bool { + if actual.is_nullable() && !expected.is_nullable() { + return false; + } + self.heap_type_is_subtype(actual, expected) + } + + /// Returns whether one value type is a subtype of another. + pub(crate) fn value_type_is_subtype(&self, actual: WasmType, expected: WasmType) -> bool { + match (actual, expected) { + (WasmType::Ref(actual), WasmType::Ref(expected)) => self.ref_type_is_subtype(actual, expected), + _ => actual == expected, + } + } + + fn heap_type_is_subtype(&self, actual: RefType, expected: RefType) -> bool { + if let Some(expected_addr) = expected.type_index() { + let Some(actual_addr) = actual.type_index() else { + return matches!( + (actual.abstract_heap_type(), &self.get_type(expected_addr).composite), + (Some(AbstractHeapType::NoFunc), CompositeType::Func(_)) + | (Some(AbstractHeapType::None), CompositeType::Struct(_) | CompositeType::Array(_)) + ); + }; + return self.type_addr_is_subtype(actual_addr, expected_addr); + } + + let expected = expected.abstract_heap_type().expect("abstract reference type"); + if let Some(actual_addr) = actual.type_index() { + return match &self.get_type(actual_addr).composite { + CompositeType::Func(_) => expected == AbstractHeapType::Func, + CompositeType::Struct(_) => { + matches!(expected, AbstractHeapType::Struct | AbstractHeapType::Eq | AbstractHeapType::Any) + } + CompositeType::Array(_) => { + matches!(expected, AbstractHeapType::Array | AbstractHeapType::Eq | AbstractHeapType::Any) + } + }; + } + + let actual = actual.abstract_heap_type().expect("abstract reference type"); + actual == expected + || match actual { + AbstractHeapType::None => matches!( + expected, + AbstractHeapType::I31 + | AbstractHeapType::Struct + | AbstractHeapType::Array + | AbstractHeapType::Eq + | AbstractHeapType::Any + ), + AbstractHeapType::I31 | AbstractHeapType::Struct | AbstractHeapType::Array => { + matches!(expected, AbstractHeapType::Eq | AbstractHeapType::Any) + } + AbstractHeapType::Eq => expected == AbstractHeapType::Any, + AbstractHeapType::NoFunc => expected == AbstractHeapType::Func, + AbstractHeapType::NoExtern => expected == AbstractHeapType::Extern, + AbstractHeapType::NoExn => expected == AbstractHeapType::Exn, + _ => false, + } + } + + /// Returns whether a runtime reference has the expected type. + pub(crate) fn value_ref_matches(&self, value: ValueRef, expected: RefType) -> bool { + if value.is_null() { + return expected.is_nullable(); + } + if expected.abstract_heap_type() == Some(AbstractHeapType::Extern) { + return true; + } + let expected_func = expected.type_index().is_some_and(|addr| self.get_type(addr).as_func().is_some()) + || expected.abstract_heap_type() == Some(AbstractHeapType::Func); + if expected_func { + let Some(func_addr) = value.addr() else { return false }; + let Some(func) = self.funcs.get(func_addr as usize) else { return false }; + return self.ref_type_is_subtype(RefType::new_concrete(false, func.type_addr), expected); + } + if expected.abstract_heap_type() == Some(AbstractHeapType::Exn) { + return value.addr().is_some_and(|addr| self.exceptions.get(addr as usize).is_some()); + } + if value.is_i31() { + return self.ref_type_is_subtype(RefType::new_abstract(false, AbstractHeapType::I31), expected); + } + if value.is_host_any() { + return self.ref_type_is_subtype(RefType::new_abstract(false, AbstractHeapType::Any), expected); + } + + let Some(object) = self.gc.get(value) else { return false }; + let actual = RefType::new_concrete(false, object.type_addr); + self.ref_type_is_subtype(actual, expected) + } + + #[inline] + pub(crate) fn get_func_type(&self, addr: FuncAddr) -> &FuncType { + self.get_canonical_func_type(self.get_func(addr).type_addr) + } + + #[inline] + pub(crate) fn get_type(&self, addr: TypeAddr) -> &SubType { + Self::get(&self.canonical_types, addr, "canonical type") + } + + #[inline] + pub(crate) fn get_canonical_func_type(&self, addr: TypeAddr) -> &FuncType { + self.get_type(addr).as_func().expect("validated function address references a function type") + } + + pub(crate) fn value_matches_type(&self, value: WasmValue, expected: WasmType) -> bool { + match (value, expected) { + (WasmValue::Ref(RefValue::Null), WasmType::Ref(expected)) => expected.is_nullable(), + (WasmValue::Ref(RefValue::Func(func)), WasmType::Ref(expected)) => self + .funcs + .get(func.addr() as usize) + .is_some_and(|func| self.ref_type_is_subtype(RefType::new_concrete(false, func.type_addr), expected)), + (WasmValue::Ref(RefValue::Any(_)), WasmType::Ref(expected)) + if expected.is_func() || expected.is_extern() || expected.is_exn() => + { + false + } + (WasmValue::Ref(RefValue::Exn(value)), WasmType::Ref(expected)) => { + expected.abstract_heap_type() == Some(AbstractHeapType::Exn) + && self.exceptions.get(value.addr() as usize).is_some() + } + (WasmValue::Ref(RefValue::Any(value)), WasmType::Ref(expected)) => { + self.value_ref_matches(ValueRef::from_raw(value.raw()), expected) + } + (_, WasmType::Ref(expected)) if expected.is_concrete() => false, + _ => value.matches_type(expected), + } + } + + pub(super) fn get<'a, T>(items: &'a [T], addr: Addr, kind: &str) -> &'a T { + items.get(addr as usize).unwrap_or_else(|| unreachable!("invalid {kind} address: {addr}")) + } + + fn get_mut<'a, T>(items: &'a mut [T], addr: Addr, kind: &str) -> &'a mut T { + items.get_mut(addr as usize).unwrap_or_else(|| unreachable!("invalid {kind} address: {addr}")) + } + + fn get_disjoint_mut<'a, T>(items: &'a mut [T], addr: Addr, addr2: Addr, kind: &str) -> (&'a mut T, &'a mut T) { + let [item_a, item_b] = items + .get_disjoint_mut([addr as usize, addr2 as usize]) + .unwrap_or_else(|_| unreachable!("invalid {kind} addresses: {addr}, {addr2}")); + (item_a, item_b) + } + + /// Get the function at the actual index in the store + pub(crate) fn get_func(&self, addr: FuncAddr) -> &FunctionInstance { + Self::get(&self.funcs, addr, "function") + } + + pub(crate) fn get_tag(&self, addr: TagAddr) -> &TagInstance { + Self::get(&self.tags, addr, "tag") + } + + /// Get a wasm function at the actual index in the store, panicking if it's a host function (which should be guaranteed by the validator) + pub(crate) fn get_wasm_func(&self, addr: FuncAddr) -> &WasmFunctionInstance { + match self.funcs.get(addr as usize).map(|func| &func.kind) { + Some(FunctionKind::Wasm(wasm_func)) => wasm_func, + _ => unreachable!("invalid wasm function address: {addr}"), + } + } + + /// Get the memory at the actual index in the store + pub(crate) fn get_mem(&self, addr: MemAddr) -> &MemoryInstance { + Self::get(&self.memories, addr, "memory") + } + + /// Get the memory at the actual index in the store + pub(crate) fn get_mem_mut(&mut self, addr: MemAddr) -> &mut MemoryInstance { + Self::get_mut(&mut self.memories, addr, "memory") + } + + /// Get the memory at the actual index in the store + pub(crate) fn get_mems_mut(&mut self, addr: MemAddr, addr2: MemAddr) -> (&mut MemoryInstance, &mut MemoryInstance) { + Self::get_disjoint_mut(&mut self.memories, addr, addr2, "memory") + } + + /// Get the table at the actual index in the store + pub(crate) fn get_table(&self, addr: TableAddr) -> &TableInstance { + Self::get(&self.tables, addr, "table") + } + + /// Get the table at the actual index in the store + pub(crate) fn get_table_mut(&mut self, addr: TableAddr) -> &mut TableInstance { + Self::get_mut(&mut self.tables, addr, "table") + } + + /// Get two mutable tables at the actual index in the store + pub(crate) fn get_tables_mut( + &mut self, + addr: TableAddr, + addr2: TableAddr, + ) -> (&mut TableInstance, &mut TableInstance) { + Self::get_disjoint_mut(&mut self.tables, addr, addr2, "table") + } + + /// Get the data at the actual index in the store + pub(crate) fn get_data_mut(&mut self, addr: DataAddr) -> &mut DataInstance { + Self::get_mut(&mut self.data, addr, "data") + } + + /// Get the element at the actual index in the store + pub(crate) fn get_elem_mut(&mut self, addr: ElemAddr) -> &mut ElementInstance { + Self::get_mut(&mut self.elements, addr, "element") + } + + /// Converts a global directly to its public value representation. + pub(crate) fn get_global_wasmvalue(&self, addr: GlobalAddr) -> WasmValue { + let ty = self.globals.ty(addr).ty; + match ty { + WasmType::I32 => WasmValue::I32(self.globals.get_32(addr) as i32), + WasmType::I64 => WasmValue::I64(self.globals.get_64(addr) as i64), + WasmType::F32 => WasmValue::F32(f32::from_bits(self.globals.get_32(addr))), + WasmType::F64 => WasmValue::F64(f64::from_bits(self.globals.get_64(addr))), + WasmType::Ref(ty) => WasmValue::Ref(self.to_ref_value(ValueRef::from_raw(self.globals.get_32(addr)), ty)), + WasmType::V128 => WasmValue::V128(self.globals.get_128(addr).to_le_bytes()), + } + } + + /// Validates and sets a global from its public value representation. + pub(crate) fn set_global_wasmvalue(&mut self, addr: GlobalAddr, value: WasmValue) -> Result<()> { + let ty = self.globals.ty(addr); + if !ty.mutable { + cold_path(); + return Err(Error::other("global is immutable")); + } + if !self.value_matches_type(value, ty.ty) { + cold_path(); + return Err(Error::other("invalid global value type")); + } + match value { + WasmValue::I32(value) => self.globals.set_32(addr, value as u32), + WasmValue::I64(value) => self.globals.set_64(addr, value as u64), + WasmValue::F32(value) => self.globals.set_32(addr, value.to_bits()), + WasmValue::F64(value) => self.globals.set_64(addr, value.to_bits()), + WasmValue::Ref(value) => self.globals.set_32(addr, ValueRef::from(value).raw()), + WasmValue::V128(value) => self.globals.set_128(addr, value.into()), + } + Ok(()) + } +} + +impl Default for State { + fn default() -> Self { + Self::new(1024 * 1024) + } +} diff --git a/crates/tinywasm/src/store/table.rs b/crates/tinywasm/src/store/table.rs index 68ed99f8..8e6c0551 100644 --- a/crates/tinywasm/src/store/table.rs +++ b/crates/tinywasm/src/store/table.rs @@ -1,4 +1,4 @@ -use crate::{Result, Trap}; +use crate::{Result, Trap, interpreter::ValueRef}; use alloc::vec::Vec; use core::ops::Range; use tinywasm_types::*; @@ -10,23 +10,19 @@ const MAX_TABLE_SIZE: usize = 10_000_000; /// See #[cfg_attr(feature = "debug", derive(Debug))] pub(crate) struct TableInstance { - pub(crate) elements: Vec, + pub(crate) elements: Vec, pub(crate) kind: TableType, } impl TableInstance { - #[cfg(test)] - pub(crate) fn new(kind: TableType) -> Result { - Self::new_with_init(kind, TableElement::Uninitialized) - } - - pub(crate) fn new_with_init(kind: TableType, init: TableElement) -> Result { - let size = usize::try_from(kind.size_initial).map_err(|_| Trap::OutOfMemory)?; + /// Creates a table filled with the given initial reference. + pub(crate) fn new(kind: TableType, init: ValueRef) -> Result { + let size = cold_err!(usize::try_from(kind.size_initial)).map_err(|_| Trap::OutOfMemory)?; if size > MAX_TABLE_SIZE { return Err(Trap::OutOfMemory.into()); } let mut elements = Vec::new(); - elements.try_reserve_exact(size).map_err(|_| Trap::OutOfMemory)?; + cold_err!(elements.try_reserve_exact(size)).map_err(|_| Trap::OutOfMemory)?; elements.resize(size, init); Ok(Self { elements, kind }) } @@ -45,27 +41,23 @@ impl TableInstance { Ok(addr..end) } - pub(crate) fn get_wasm_val(&self, addr: usize) -> Result { - Ok(self.get(addr)?.to_wasm_value(self.kind.element_type)) - } - - pub(crate) fn fill(&mut self, addr: usize, len: usize, val: TableElement) -> Result<(), Trap> { + pub(crate) fn fill(&mut self, addr: usize, len: usize, val: ValueRef) -> Result<(), Trap> { let range = self.checked_range(addr, len)?; self.elements[range].fill(val); Ok(()) } - pub(crate) fn get(&self, addr: usize) -> Result<&TableElement, Trap> { + pub(crate) fn get(&self, addr: usize) -> Result<&ValueRef, Trap> { self.elements.get(addr).ok_or_else(|| self.trap_oob(addr, 1)) } - pub(crate) fn copy_from_slice(&mut self, dst: usize, src: &[TableElement]) -> Result<(), Trap> { + pub(crate) fn copy_from_slice(&mut self, dst: usize, src: &[ValueRef]) -> Result<(), Trap> { let range = self.checked_range(dst, src.len())?; self.elements[range].copy_from_slice(src); Ok(()) } - pub(crate) fn load(&self, addr: usize, len: usize) -> Result<&[TableElement], Trap> { + pub(crate) fn load(&self, addr: usize, len: usize) -> Result<&[ValueRef], Trap> { Ok(&self.elements[self.checked_range(addr, len)?]) } @@ -76,13 +68,13 @@ impl TableInstance { Ok(()) } - pub(crate) fn set(&mut self, table_idx: usize, value: TableElement) -> Result<(), Trap> { + pub(crate) fn set(&mut self, table_idx: usize, value: ValueRef) -> Result<(), Trap> { let range = self.checked_range(table_idx, 1)?; self.elements[range.start] = value; Ok(()) } - pub(crate) fn grow(&mut self, n: usize, init: TableElement) -> Result<(), Trap> { + pub(crate) fn grow(&mut self, n: usize, init: ValueRef) -> Result<(), Trap> { let len = n.checked_add(self.elements.len()).ok_or(Trap::OutOfMemory)?; let declared_max = self.kind.size_max.and_then(|max| usize::try_from(max).ok()).unwrap_or(usize::MAX); let max = declared_max.min(MAX_TABLE_SIZE); @@ -90,7 +82,7 @@ impl TableInstance { return Err(crate::Trap::TableOutOfBounds { offset: len, len: 1, max: self.elements.len() }); } - self.elements.try_reserve_exact(n).map_err(|_| Trap::OutOfMemory)?; + cold_err!(self.elements.try_reserve_exact(n)).map_err(|_| Trap::OutOfMemory)?; self.elements.resize(len, init); Ok(()) } @@ -99,124 +91,9 @@ impl TableInstance { self.elements.len() } - pub(crate) fn init(&mut self, offset: usize, init: &[TableElement]) -> Result<(), Trap> { + pub(crate) fn init(&mut self, offset: usize, init: &[ValueRef]) -> Result<(), Trap> { let range = self.checked_range(offset, init.len())?; self.elements[range].copy_from_slice(init); Ok(()) } } - -#[derive(Clone, Copy)] -#[cfg_attr(feature = "debug", derive(Debug))] -pub(crate) enum TableElement { - Uninitialized, - Initialized(TableAddr), -} - -impl From> for TableElement { - fn from(addr: Option) -> Self { - match addr { - None => Self::Uninitialized, - Some(addr) => Self::Initialized(addr), - } - } -} - -impl TableElement { - pub(crate) fn addr(&self) -> Option { - match self { - Self::Uninitialized => None, - Self::Initialized(addr) => Some(*addr), - } - } - - pub(crate) fn to_wasm_value(self, ty: RefType) -> WasmValue { - let Some(addr) = self.addr() else { return RefValue::Null.into() }; - let value = if ty.is_func() { - RefValue::Func(FuncRef::new(addr)) - } else if ty.is_extern() { - RefValue::Extern(ExternRef::new(addr)) - } else if ty.is_exn() { - RefValue::Exn(ExnRef::new(addr)) - } else { - RefValue::Any(AnyRef::from_raw(addr)) - }; - value.into() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use alloc::vec; - - // Helper to create a dummy TableType - fn dummy_table_type() -> TableType { - TableType::new(RefType::FUNCREF, 10, Some(20)) - } - - #[test] - fn test_table_instance_creation() { - let kind = dummy_table_type(); - let table_instance = TableInstance::new(kind).unwrap(); - assert_eq!(table_instance.size() as u64, kind.size_initial, "Table instance creation failed: size mismatch"); - } - - #[test] - fn test_get_wasm_val() { - let kind = dummy_table_type(); - let mut table_instance = TableInstance::new(kind).unwrap(); - - table_instance.set(0, TableElement::Initialized(0)).expect("Setting table element failed"); - table_instance.set(1, TableElement::Uninitialized).expect("Setting table element failed"); - - match table_instance.get_wasm_val(0) { - Ok(WasmValue::Ref(RefValue::Func(_))) => {} - _ => panic!("get_wasm_val failed to return the correct WasmValue"), - } - - match table_instance.get_wasm_val(1) { - Ok(WasmValue::Ref(RefValue::Null)) => {} - _ => panic!("get_wasm_val failed to return the correct WasmValue"), - } - - match table_instance.get_wasm_val(999) { - Err(Trap::TableOutOfBounds { .. }) => {} - _ => panic!("get_wasm_val failed to handle undefined element correctly"), - } - } - - #[test] - fn test_set_and_get() { - let kind = dummy_table_type(); - let mut table_instance = TableInstance::new(kind).unwrap(); - - let result = table_instance.set(0, TableElement::Initialized(1)); - assert!(result.is_ok(), "Setting table element failed"); - - let elem = table_instance.get(0); - assert!( - elem.is_ok() && matches!(elem.unwrap(), &TableElement::Initialized(1)), - "Getting table element failed or returned incorrect value" - ); - } - - #[test] - fn test_table_init() { - let kind = dummy_table_type(); - let mut table_instance = TableInstance::new(kind).unwrap(); - - let init_elements = vec![TableElement::Initialized(0); 5]; - let result = table_instance.init(0, &init_elements); - - assert!(result.is_ok(), "Initializing table with elements failed"); - - for i in 0..5 { - let elem = table_instance.get(i); - assert!( - elem.is_ok() && matches!(elem.unwrap(), &TableElement::Initialized(_)), - "Element not initialized correctly at index {i}" - ); - } - } -} diff --git a/crates/tinywasm/src/store/tag.rs b/crates/tinywasm/src/store/tag.rs new file mode 100644 index 00000000..49c16d8a --- /dev/null +++ b/crates/tinywasm/src/store/tag.rs @@ -0,0 +1,7 @@ +use tinywasm_types::TypeAddr; + +#[derive(Clone, Copy)] +#[cfg_attr(feature = "debug", derive(Debug))] +pub(crate) struct TagInstance { + pub(crate) type_addr: TypeAddr, +} diff --git a/crates/tinywasm/src/store/types.rs b/crates/tinywasm/src/store/types.rs new file mode 100644 index 00000000..e9fa6d1a --- /dev/null +++ b/crates/tinywasm/src/store/types.rs @@ -0,0 +1,211 @@ +use alloc::{boxed::Box, vec::Vec}; +use tinywasm_types::*; + +use super::Store; + +pub(crate) fn canonicalize_ref_type(ty: RefType, type_addrs: &[TypeAddr]) -> RefType { + let Some(type_addr) = ty.type_index() else { return ty }; + let canonical = *type_addrs.get(type_addr as usize).expect("validated type address should exist"); + RefType::new_concrete(ty.is_nullable(), canonical) +} + +pub(crate) fn canonicalize_value_type(ty: WasmType, type_addrs: &[TypeAddr]) -> WasmType { + match ty { + WasmType::Ref(ty) => WasmType::Ref(canonicalize_ref_type(ty, type_addrs)), + ty => ty, + } +} + +fn map_value_type(ty: WasmType, resolve: &mut impl FnMut(TypeAddr) -> TypeAddr) -> WasmType { + match ty { + WasmType::Ref(ty) if ty.is_concrete() => WasmType::Ref(RefType::new_concrete( + ty.is_nullable(), + resolve(ty.type_index().expect("concrete reference has a type index")), + )), + ty => ty, + } +} + +fn map_field_type(field: FieldType, resolve: &mut impl FnMut(TypeAddr) -> TypeAddr) -> FieldType { + let storage = match field.storage { + StorageType::Value(ty) => StorageType::Value(map_value_type(ty, resolve)), + storage => storage, + }; + FieldType { storage, mutable: field.mutable } +} + +fn map_subtype(ty: &SubType, mut resolve: impl FnMut(TypeAddr) -> TypeAddr) -> SubType { + let supertype = ty.supertype.map(&mut resolve); + let composite = match &ty.composite { + CompositeType::Func(ty) + if !ty + .params() + .iter() + .chain(ty.results()) + .any(|ty| matches!(ty, WasmType::Ref(ty) if ty.is_concrete())) => + { + CompositeType::Func(ty.clone()) + } + CompositeType::Func(ty) => { + let params = ty.params().iter().copied().map(|ty| map_value_type(ty, &mut resolve)).collect::>(); + let results = ty.results().iter().copied().map(|ty| map_value_type(ty, &mut resolve)).collect::>(); + CompositeType::Func(FuncType::new(¶ms, &results)) + } + CompositeType::Struct(ty) => CompositeType::Struct(StructType { + fields: ty.fields.iter().copied().map(|field| map_field_type(field, &mut resolve)).collect(), + }), + CompositeType::Array(ty) => CompositeType::Array(ArrayType { field: map_field_type(ty.field, &mut resolve) }), + }; + SubType { is_final: ty.is_final, supertype, composite } +} + +fn subtypes_equal( + ty: &SubType, + registered: &SubType, + module_group: core::ops::Range, + canonical_group: core::ops::Range, + resolve: impl Fn(TypeAddr) -> TypeAddr + Copy, +) -> bool { + let type_addrs_equal = |ty: TypeAddr, registered: TypeAddr| { + module_group.contains(&(ty as usize)) == canonical_group.contains(&(registered as usize)) + && resolve(ty) == registered + }; + let values_equal = |ty: WasmType, registered: WasmType| match (ty, registered) { + (WasmType::Ref(ty), WasmType::Ref(registered)) if ty.is_concrete() => { + let ty_addr = ty.type_index().expect("concrete reference"); + let Some(registered_addr) = registered.type_index() else { return false }; + ty.is_nullable() == registered.is_nullable() && type_addrs_equal(ty_addr, registered_addr) + } + _ => ty == registered, + }; + let fields_equal = |ty: FieldType, registered: FieldType| { + ty.mutable == registered.mutable + && match (ty.storage, registered.storage) { + (StorageType::Value(ty), StorageType::Value(registered)) => values_equal(ty, registered), + (ty, registered) => ty == registered, + } + }; + + let supertypes_equal = match (ty.supertype, registered.supertype) { + (Some(ty), Some(registered)) => type_addrs_equal(ty, registered), + (None, None) => true, + _ => false, + }; + ty.is_final == registered.is_final + && supertypes_equal + && match (&ty.composite, ®istered.composite) { + (CompositeType::Func(ty), CompositeType::Func(registered)) => { + ty.params().len() == registered.params().len() + && ty.results().len() == registered.results().len() + && ty + .params() + .iter() + .chain(ty.results()) + .copied() + .zip(registered.params().iter().chain(registered.results()).copied()) + .all(|(ty, registered)| values_equal(ty, registered)) + } + (CompositeType::Struct(ty), CompositeType::Struct(registered)) => { + ty.fields.len() == registered.fields.len() + && ty + .fields + .iter() + .copied() + .zip(registered.fields.iter().copied()) + .all(|(ty, registered)| fields_equal(ty, registered)) + } + (CompositeType::Array(ty), CompositeType::Array(registered)) => fields_equal(ty.field, registered.field), + _ => false, + } +} + +impl Store { + pub(crate) fn register_module_types(&mut self, section: &TypeSection) -> Box<[TypeAddr]> { + let mut type_addrs = Vec::with_capacity(section.types.len()); + let mut module_group_start = 0usize; + + for &group_len in §ion.rec_group_lengths { + let group_len = group_len as usize; + let module_group_end = module_group_start.checked_add(group_len).expect("type group is too large"); + let group = section + .types + .get(module_group_start..module_group_end) + .expect("validated recursive group length fits the type section"); + + let resolve = |module_addr: TypeAddr, canonical_group_start: usize| { + let module_addr = module_addr as usize; + if (module_group_start..module_group_end).contains(&module_addr) { + (canonical_group_start + module_addr - module_group_start) as TypeAddr + } else { + *type_addrs + .get(module_addr) + .expect("validated type reference targets the current or a prior recursive group") + } + }; + + let mut canonical_group_start = 0; + let mut matching_group = None; + for &canonical_group_len in &self.state.canonical_rec_group_lengths { + let canonical_group_len = canonical_group_len as usize; + if canonical_group_len == group_len + && group.iter().zip(&self.state.canonical_types[canonical_group_start..]).all(|(ty, registered)| { + subtypes_equal( + ty, + registered, + module_group_start..module_group_end, + canonical_group_start..canonical_group_start + canonical_group_len, + |addr| resolve(addr, canonical_group_start), + ) + }) + { + matching_group = Some(canonical_group_start); + break; + } + canonical_group_start += canonical_group_len; + } + + let canonical_group_start = match matching_group { + Some(start) => start, + None => { + let start = self.state.canonical_types.len(); + assert!( + start.checked_add(group_len).is_some_and(|end| end <= (1 << 30)), + "too many canonical types" + ); + self.state + .canonical_types + .extend(group.iter().map(|ty| map_subtype(ty, |addr| resolve(addr, start)))); + self.state.canonical_rec_group_lengths.push(group_len as u32); + start + } + }; + type_addrs.extend((canonical_group_start..canonical_group_start + group_len).map(|addr| addr as TypeAddr)); + module_group_start = module_group_end; + } + debug_assert_eq!(module_group_start, section.types.len()); + type_addrs.into_boxed_slice() + } + + pub(crate) fn register_host_type(&mut self, ty: &FuncType) -> TypeAddr { + let mut group_start = 0usize; + for &group_len in &self.state.canonical_rec_group_lengths { + if group_len == 1 + && self.state.canonical_types[group_start].is_final + && self.state.canonical_types[group_start].supertype.is_none() + && self.state.canonical_types[group_start].as_func() == Some(ty) + { + return group_start as TypeAddr; + } + group_start += group_len as usize; + } + let addr = self.state.canonical_types.len(); + assert!(addr < (1 << 30), "too many canonical types"); + self.state.canonical_types.push(SubType { + is_final: true, + supertype: None, + composite: CompositeType::Func(ty.clone()), + }); + self.state.canonical_rec_group_lengths.push(1); + addr as TypeAddr + } +} diff --git a/crates/tinywasm/tests/gc_refs.rs b/crates/tinywasm/tests/gc_refs.rs new file mode 100644 index 00000000..d3f29941 --- /dev/null +++ b/crates/tinywasm/tests/gc_refs.rs @@ -0,0 +1,120 @@ +use tinywasm::types::{ExternRef, RefValue, WasmValue}; +use tinywasm::{Engine, ExecProgress, ModuleInstance, Store, engine::Config}; + +const MODULE: &str = r#" + (module + (type $node (struct (field i32))) + (type $bytes (array (mut i8))) + (func (export "new") (result anyref) + (struct.new $node (i32.const 42))) + (func (export "new-extern") (result externref) + (extern.convert_any (struct.new $node (i32.const 42)))) + (func (export "read") (param anyref) (result i32) + (struct.get $node 0 (ref.cast (ref $node) (local.get 0)))) + (func (export "read-extern") (param externref) (result i32) + (struct.get $node 0 + (ref.cast (ref $node) (any.convert_extern (local.get 0))))) + (func (export "churn") + (local $i i32) + (loop $loop + (drop (array.new_default $bytes (i32.const 16))) + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (br_if $loop (i32.lt_u (local.get $i) (i32.const 100))))) + ) +"#; + +fn store() -> Store { + Store::new(Engine::new(Config::new().with_gc_collection_threshold(1))) +} + +#[test] +fn host_result_is_pinned_and_rejected_by_another_store() { + let module = tinywasm::parse_bytes(&wat::parse_str(MODULE).unwrap()).unwrap(); + let mut first_store = store(); + let first = ModuleInstance::instantiate(&mut first_store, &module, None).unwrap(); + let new = first.func_untyped(&first_store, "new").unwrap(); + let read = first.func_untyped(&first_store, "read").unwrap(); + let churn = first.func_untyped(&first_store, "churn").unwrap(); + + let value = new.call(&mut first_store, &[]).unwrap().pop().unwrap(); + assert!(matches!(value, WasmValue::Ref(RefValue::Any(_)))); + churn.call(&mut first_store, &[]).unwrap(); + assert_eq!(read.call(&mut first_store, &[value]).unwrap(), [WasmValue::I32(42)]); + + let mut second_store = store(); + let second = ModuleInstance::instantiate(&mut second_store, &module, None).unwrap(); + let other_read = second.func_untyped(&second_store, "read").unwrap(); + assert!(other_read.call(&mut second_store, &[value]).is_err()); +} + +#[test] +fn externalized_gc_result_is_pinned() { + let module = tinywasm::parse_bytes(&wat::parse_str(MODULE).unwrap()).unwrap(); + let mut store = store(); + let instance = ModuleInstance::instantiate(&mut store, &module, None).unwrap(); + let new = instance.func_untyped(&store, "new-extern").unwrap(); + let read = instance.func_untyped(&store, "read-extern").unwrap(); + let churn = instance.func_untyped(&store, "churn").unwrap(); + + let value = new.call(&mut store, &[]).unwrap().pop().unwrap(); + assert!(matches!(value, WasmValue::Ref(RefValue::Extern(_)))); + churn.call(&mut store, &[]).unwrap(); + assert_eq!(read.call(&mut store, &[value]).unwrap(), [WasmValue::I32(42)]); +} + +#[test] +fn host_externref_does_not_alias_a_gc_object() { + let module = tinywasm::parse_bytes(&wat::parse_str(MODULE).unwrap()).unwrap(); + let mut store = store(); + let instance = ModuleInstance::instantiate(&mut store, &module, None).unwrap(); + let new = instance.func_untyped(&store, "new").unwrap(); + let read = instance.func_untyped(&store, "read-extern").unwrap(); + + new.call(&mut store, &[]).unwrap(); + let host_ref = WasmValue::Ref(RefValue::Extern(ExternRef::new(0))); + assert!(read.call(&mut store, &[host_ref]).is_err()); +} + +#[test] +fn resumable_gc_result_is_pinned() { + let module = tinywasm::parse_bytes(&wat::parse_str(MODULE).unwrap()).unwrap(); + let mut store = store(); + let instance = ModuleInstance::instantiate(&mut store, &module, None).unwrap(); + let new = instance.func_untyped(&store, "new").unwrap(); + let read = instance.func_untyped(&store, "read").unwrap(); + let churn = instance.func_untyped(&store, "churn").unwrap(); + + let mut execution = new.call_resumable(&mut store, &[]).unwrap(); + let value = match execution.resume_with_fuel(1000).unwrap() { + ExecProgress::Completed(mut values) => values.pop().unwrap(), + ExecProgress::Suspended => panic!("constructor unexpectedly suspended"), + }; + drop(execution); + + churn.call(&mut store, &[]).unwrap(); + assert_eq!(read.call(&mut store, &[value]).unwrap(), [WasmValue::I32(42)]); +} + +#[test] +fn element_initializers_root_previous_gc_values() { + let module = tinywasm::parse_bytes( + &wat::parse_str( + r#" + (module + (type $node (struct (field i32))) + (table 2 (ref null $node)) + (elem (i32.const 0) (ref $node) + (struct.new $node (i32.const 1)) + (struct.new $node (i32.const 2))) + (func (export "first") (result i32) + (struct.get $node 0 (table.get 0 (i32.const 0))))) + "#, + ) + .unwrap(), + ) + .unwrap(); + let mut store = store(); + let instance = ModuleInstance::instantiate(&mut store, &module, None).unwrap(); + + assert_eq!(instance.func_untyped(&store, "first").unwrap().call(&mut store, &[]).unwrap(), [WasmValue::I32(1)]); +} diff --git a/crates/tinywasm/tests/generated/wasm-2.csv b/crates/tinywasm/tests/generated/wasm-2.csv index c2472a70..645f0d54 100644 --- a/crates/tinywasm/tests/generated/wasm-2.csv +++ b/crates/tinywasm/tests/generated/wasm-2.csv @@ -13,4 +13,4 @@ 0.9.0,28008,0,[{"name":"address.wast","passed":260,"failed":0},{"name":"align.wast","passed":162,"failed":0},{"name":"binary-leb128.wast","passed":89,"failed":0},{"name":"binary.wast","passed":128,"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":"bulk.wast","passed":117,"failed":0},{"name":"call.wast","passed":91,"failed":0},{"name":"call_indirect.wast","passed":172,"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":98,"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":927,"failed":0},{"name":"float_literals.wast","passed":179,"failed":0},{"name":"float_memory.wast","passed":90,"failed":0},{"name":"float_misc.wast","passed":471,"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":178,"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":88,"failed":0},{"name":"memory_copy.wast","passed":4450,"failed":0},{"name":"memory_fill.wast","passed":100,"failed":0},{"name":"memory_grow.wast","passed":104,"failed":0},{"name":"memory_init.wast","passed":240,"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":"obsolete-keywords.wast","passed":11,"failed":0},{"name":"ref_func.wast","passed":17,"failed":0},{"name":"ref_is_null.wast","passed":16,"failed":0},{"name":"ref_null.wast","passed":3,"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-sub.wast","passed":2,"failed":0},{"name":"table.wast","passed":19,"failed":0},{"name":"table_copy.wast","passed":1728,"failed":0},{"name":"table_fill.wast","passed":45,"failed":0},{"name":"table_get.wast","passed":16,"failed":0},{"name":"table_grow.wast","passed":58,"failed":0},{"name":"table_init.wast","passed":780,"failed":0},{"name":"table_set.wast","passed":26,"failed":0},{"name":"table_size.wast","passed":39,"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.9.1,28008,0,[{"name":"address.wast","passed":260,"failed":0},{"name":"align.wast","passed":162,"failed":0},{"name":"binary-leb128.wast","passed":89,"failed":0},{"name":"binary.wast","passed":128,"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":"bulk.wast","passed":117,"failed":0},{"name":"call.wast","passed":91,"failed":0},{"name":"call_indirect.wast","passed":172,"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":98,"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":927,"failed":0},{"name":"float_literals.wast","passed":179,"failed":0},{"name":"float_memory.wast","passed":90,"failed":0},{"name":"float_misc.wast","passed":471,"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":178,"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":88,"failed":0},{"name":"memory_copy.wast","passed":4450,"failed":0},{"name":"memory_fill.wast","passed":100,"failed":0},{"name":"memory_grow.wast","passed":104,"failed":0},{"name":"memory_init.wast","passed":240,"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":"obsolete-keywords.wast","passed":11,"failed":0},{"name":"ref_func.wast","passed":17,"failed":0},{"name":"ref_is_null.wast","passed":16,"failed":0},{"name":"ref_null.wast","passed":3,"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-sub.wast","passed":2,"failed":0},{"name":"table.wast","passed":19,"failed":0},{"name":"table_copy.wast","passed":1728,"failed":0},{"name":"table_fill.wast","passed":45,"failed":0},{"name":"table_get.wast","passed":16,"failed":0},{"name":"table_grow.wast","passed":58,"failed":0},{"name":"table_init.wast","passed":780,"failed":0},{"name":"table_set.wast","passed":26,"failed":0},{"name":"table_size.wast","passed":39,"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.10.0,28008,0,[{"name":"address.wast","passed":260,"failed":0},{"name":"align.wast","passed":162,"failed":0},{"name":"binary-leb128.wast","passed":89,"failed":0},{"name":"binary.wast","passed":128,"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":"bulk.wast","passed":117,"failed":0},{"name":"call.wast","passed":91,"failed":0},{"name":"call_indirect.wast","passed":172,"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":98,"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":927,"failed":0},{"name":"float_literals.wast","passed":179,"failed":0},{"name":"float_memory.wast","passed":90,"failed":0},{"name":"float_misc.wast","passed":471,"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":178,"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":88,"failed":0},{"name":"memory_copy.wast","passed":4450,"failed":0},{"name":"memory_fill.wast","passed":100,"failed":0},{"name":"memory_grow.wast","passed":104,"failed":0},{"name":"memory_init.wast","passed":240,"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":"obsolete-keywords.wast","passed":11,"failed":0},{"name":"ref_func.wast","passed":17,"failed":0},{"name":"ref_is_null.wast","passed":16,"failed":0},{"name":"ref_null.wast","passed":3,"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-sub.wast","passed":2,"failed":0},{"name":"table.wast","passed":19,"failed":0},{"name":"table_copy.wast","passed":1728,"failed":0},{"name":"table_fill.wast","passed":45,"failed":0},{"name":"table_get.wast","passed":16,"failed":0},{"name":"table_grow.wast","passed":58,"failed":0},{"name":"table_init.wast","passed":780,"failed":0},{"name":"table_set.wast","passed":26,"failed":0},{"name":"table_size.wast","passed":39,"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.11.0-pre.0,28008,0,[{"name":"address.wast","passed":260,"failed":0},{"name":"align.wast","passed":162,"failed":0},{"name":"binary-leb128.wast","passed":89,"failed":0},{"name":"binary.wast","passed":128,"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":"bulk.wast","passed":117,"failed":0},{"name":"call.wast","passed":91,"failed":0},{"name":"call_indirect.wast","passed":172,"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":98,"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":927,"failed":0},{"name":"float_literals.wast","passed":179,"failed":0},{"name":"float_memory.wast","passed":90,"failed":0},{"name":"float_misc.wast","passed":471,"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":178,"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":88,"failed":0},{"name":"memory_copy.wast","passed":4450,"failed":0},{"name":"memory_fill.wast","passed":100,"failed":0},{"name":"memory_grow.wast","passed":104,"failed":0},{"name":"memory_init.wast","passed":240,"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":"obsolete-keywords.wast","passed":11,"failed":0},{"name":"ref_func.wast","passed":17,"failed":0},{"name":"ref_is_null.wast","passed":16,"failed":0},{"name":"ref_null.wast","passed":3,"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-sub.wast","passed":2,"failed":0},{"name":"table.wast","passed":19,"failed":0},{"name":"table_copy.wast","passed":1728,"failed":0},{"name":"table_fill.wast","passed":45,"failed":0},{"name":"table_get.wast","passed":16,"failed":0},{"name":"table_grow.wast","passed":58,"failed":0},{"name":"table_init.wast","passed":780,"failed":0},{"name":"table_set.wast","passed":26,"failed":0},{"name":"table_size.wast","passed":39,"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.11.0-pre.0,28002,0,[{"name":"address.wast","passed":260,"failed":0},{"name":"align.wast","passed":162,"failed":0},{"name":"binary-leb128.wast","passed":89,"failed":0},{"name":"binary.wast","passed":128,"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":"bulk.wast","passed":117,"failed":0},{"name":"call.wast","passed":91,"failed":0},{"name":"call_indirect.wast","passed":172,"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":59,"failed":0},{"name":"elem.wast","passed":96,"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":927,"failed":0},{"name":"float_literals.wast","passed":179,"failed":0},{"name":"float_memory.wast","passed":90,"failed":0},{"name":"float_misc.wast","passed":471,"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":108,"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":178,"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":88,"failed":0},{"name":"memory_copy.wast","passed":4450,"failed":0},{"name":"memory_fill.wast","passed":100,"failed":0},{"name":"memory_grow.wast","passed":104,"failed":0},{"name":"memory_init.wast","passed":240,"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":"obsolete-keywords.wast","passed":11,"failed":0},{"name":"ref_func.wast","passed":17,"failed":0},{"name":"ref_is_null.wast","passed":16,"failed":0},{"name":"ref_null.wast","passed":3,"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-sub.wast","passed":2,"failed":0},{"name":"table.wast","passed":19,"failed":0},{"name":"table_copy.wast","passed":1728,"failed":0},{"name":"table_fill.wast","passed":45,"failed":0},{"name":"table_get.wast","passed":16,"failed":0},{"name":"table_grow.wast","passed":58,"failed":0},{"name":"table_init.wast","passed":780,"failed":0},{"name":"table_set.wast","passed":26,"failed":0},{"name":"table_size.wast","passed":39,"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}] diff --git a/crates/tinywasm/tests/generated/wasm-3.csv b/crates/tinywasm/tests/generated/wasm-3.csv index 9403d274..fdc0e869 100644 --- a/crates/tinywasm/tests/generated/wasm-3.csv +++ b/crates/tinywasm/tests/generated/wasm-3.csv @@ -1,3 +1,3 @@ 0.9.0,20776,452,[{"name":"address.wast","passed":260,"failed":0},{"name":"align.wast","passed":165,"failed":0},{"name":"annotations.wast","passed":74,"failed":0},{"name":"binary-leb128.wast","passed":91,"failed":0},{"name":"binary.wast","passed":127,"failed":0},{"name":"block.wast","passed":223,"failed":0},{"name":"br.wast","passed":97,"failed":0},{"name":"br_if.wast","passed":119,"failed":0},{"name":"br_on_non_null.wast","passed":1,"failed":11},{"name":"br_on_null.wast","passed":1,"failed":9},{"name":"br_table.wast","passed":24,"failed":162},{"name":"call.wast","passed":91,"failed":0},{"name":"call_indirect.wast","passed":172,"failed":0},{"name":"call_ref.wast","passed":4,"failed":31},{"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":63,"failed":2},{"name":"elem.wast","passed":149,"failed":2},{"name":"endianness.wast","passed":69,"failed":0},{"name":"exports.wast","passed":97,"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":927,"failed":0},{"name":"float_literals.wast","passed":179,"failed":0},{"name":"float_memory.wast","passed":90,"failed":0},{"name":"float_misc.wast","passed":471,"failed":0},{"name":"forward.wast","passed":5,"failed":0},{"name":"func.wast","passed":175,"failed":0},{"name":"func_ptrs.wast","passed":36,"failed":0},{"name":"global.wast","passed":116,"failed":8},{"name":"i32.wast","passed":460,"failed":0},{"name":"i64.wast","passed":416,"failed":0},{"name":"id.wast","passed":7,"failed":0},{"name":"if.wast","passed":241,"failed":0},{"name":"imports.wast","passed":198,"failed":20},{"name":"inline-module.wast","passed":1,"failed":0},{"name":"instance.wast","passed":0,"failed":23},{"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":142,"failed":21},{"name":"load.wast","passed":97,"failed":0},{"name":"local_get.wast","passed":36,"failed":0},{"name":"local_init.wast","passed":10,"failed":0},{"name":"local_set.wast","passed":53,"failed":0},{"name":"local_tee.wast","passed":98,"failed":0},{"name":"loop.wast","passed":120,"failed":0},{"name":"memory.wast","passed":89,"failed":1},{"name":"memory_grow.wast","passed":106,"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":"obsolete-keywords.wast","passed":11,"failed":0},{"name":"ref.wast","passed":12,"failed":1},{"name":"ref_as_non_null.wast","passed":1,"failed":6},{"name":"ref_func.wast","passed":17,"failed":0},{"name":"ref_is_null.wast","passed":2,"failed":20},{"name":"ref_null.wast","passed":0,"failed":34},{"name":"return.wast","passed":84,"failed":0},{"name":"return_call.wast","passed":47,"failed":0},{"name":"return_call_indirect.wast","passed":79,"failed":0},{"name":"return_call_ref.wast","passed":11,"failed":40},{"name":"select.wast","passed":155,"failed":2},{"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":37,"failed":9},{"name":"table_get.wast","passed":16,"failed":0},{"name":"table_grow.wast","passed":58,"failed":0},{"name":"table_set.wast","passed":26,"failed":0},{"name":"table_size.wast","passed":39,"failed":0},{"name":"token.wast","passed":61,"failed":0},{"name":"traps.wast","passed":36,"failed":0},{"name":"type-canon.wast","passed":0,"failed":2},{"name":"type-equivalence.wast","passed":12,"failed":20},{"name":"type-rec.wast","passed":10,"failed":17},{"name":"type.wast","passed":3,"failed":0},{"name":"unreachable.wast","passed":64,"failed":0},{"name":"unreached-invalid.wast","passed":121,"failed":0},{"name":"unreached-valid.wast","passed":2,"failed":11},{"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.10.0,20778,450,[{"name":"address.wast","passed":260,"failed":0},{"name":"align.wast","passed":165,"failed":0},{"name":"annotations.wast","passed":74,"failed":0},{"name":"binary-leb128.wast","passed":91,"failed":0},{"name":"binary.wast","passed":127,"failed":0},{"name":"block.wast","passed":223,"failed":0},{"name":"br.wast","passed":97,"failed":0},{"name":"br_if.wast","passed":119,"failed":0},{"name":"br_on_non_null.wast","passed":1,"failed":11},{"name":"br_on_null.wast","passed":1,"failed":9},{"name":"br_table.wast","passed":24,"failed":162},{"name":"call.wast","passed":91,"failed":0},{"name":"call_indirect.wast","passed":172,"failed":0},{"name":"call_ref.wast","passed":4,"failed":31},{"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":63,"failed":2},{"name":"elem.wast","passed":149,"failed":2},{"name":"endianness.wast","passed":69,"failed":0},{"name":"exports.wast","passed":97,"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":927,"failed":0},{"name":"float_literals.wast","passed":179,"failed":0},{"name":"float_memory.wast","passed":90,"failed":0},{"name":"float_misc.wast","passed":471,"failed":0},{"name":"forward.wast","passed":5,"failed":0},{"name":"func.wast","passed":175,"failed":0},{"name":"func_ptrs.wast","passed":36,"failed":0},{"name":"global.wast","passed":116,"failed":8},{"name":"i32.wast","passed":460,"failed":0},{"name":"i64.wast","passed":416,"failed":0},{"name":"id.wast","passed":7,"failed":0},{"name":"if.wast","passed":241,"failed":0},{"name":"imports.wast","passed":198,"failed":20},{"name":"inline-module.wast","passed":1,"failed":0},{"name":"instance.wast","passed":0,"failed":23},{"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":142,"failed":21},{"name":"load.wast","passed":97,"failed":0},{"name":"local_get.wast","passed":36,"failed":0},{"name":"local_init.wast","passed":10,"failed":0},{"name":"local_set.wast","passed":53,"failed":0},{"name":"local_tee.wast","passed":98,"failed":0},{"name":"loop.wast","passed":120,"failed":0},{"name":"memory.wast","passed":90,"failed":0},{"name":"memory_grow.wast","passed":106,"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":"obsolete-keywords.wast","passed":11,"failed":0},{"name":"ref.wast","passed":12,"failed":1},{"name":"ref_as_non_null.wast","passed":1,"failed":6},{"name":"ref_func.wast","passed":17,"failed":0},{"name":"ref_is_null.wast","passed":2,"failed":20},{"name":"ref_null.wast","passed":0,"failed":34},{"name":"return.wast","passed":84,"failed":0},{"name":"return_call.wast","passed":47,"failed":0},{"name":"return_call_indirect.wast","passed":79,"failed":0},{"name":"return_call_ref.wast","passed":11,"failed":40},{"name":"select.wast","passed":155,"failed":2},{"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":38,"failed":8},{"name":"table_get.wast","passed":16,"failed":0},{"name":"table_grow.wast","passed":58,"failed":0},{"name":"table_set.wast","passed":26,"failed":0},{"name":"table_size.wast","passed":39,"failed":0},{"name":"token.wast","passed":61,"failed":0},{"name":"traps.wast","passed":36,"failed":0},{"name":"type-canon.wast","passed":0,"failed":2},{"name":"type-equivalence.wast","passed":12,"failed":20},{"name":"type-rec.wast","passed":10,"failed":17},{"name":"type.wast","passed":3,"failed":0},{"name":"unreachable.wast","passed":64,"failed":0},{"name":"unreached-invalid.wast","passed":121,"failed":0},{"name":"unreached-valid.wast","passed":2,"failed":11},{"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.11.0-pre.0,21107,121,[{"name":"address.wast","passed":260,"failed":0},{"name":"align.wast","passed":165,"failed":0},{"name":"annotations.wast","passed":74,"failed":0},{"name":"binary-leb128.wast","passed":91,"failed":0},{"name":"binary.wast","passed":127,"failed":0},{"name":"block.wast","passed":223,"failed":0},{"name":"br.wast","passed":97,"failed":0},{"name":"br_if.wast","passed":119,"failed":0},{"name":"br_on_non_null.wast","passed":12,"failed":0},{"name":"br_on_null.wast","passed":10,"failed":0},{"name":"br_table.wast","passed":186,"failed":0},{"name":"call.wast","passed":91,"failed":0},{"name":"call_indirect.wast","passed":172,"failed":0},{"name":"call_ref.wast","passed":35,"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":63,"failed":2},{"name":"elem.wast","passed":149,"failed":2},{"name":"endianness.wast","passed":69,"failed":0},{"name":"exports.wast","passed":97,"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":927,"failed":0},{"name":"float_literals.wast","passed":179,"failed":0},{"name":"float_memory.wast","passed":90,"failed":0},{"name":"float_misc.wast","passed":471,"failed":0},{"name":"forward.wast","passed":5,"failed":0},{"name":"func.wast","passed":175,"failed":0},{"name":"func_ptrs.wast","passed":36,"failed":0},{"name":"global.wast","passed":116,"failed":8},{"name":"i32.wast","passed":460,"failed":0},{"name":"i64.wast","passed":416,"failed":0},{"name":"id.wast","passed":7,"failed":0},{"name":"if.wast","passed":241,"failed":0},{"name":"imports.wast","passed":198,"failed":20},{"name":"inline-module.wast","passed":1,"failed":0},{"name":"instance.wast","passed":0,"failed":23},{"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":163,"failed":0},{"name":"load.wast","passed":97,"failed":0},{"name":"local_get.wast","passed":36,"failed":0},{"name":"local_init.wast","passed":10,"failed":0},{"name":"local_set.wast","passed":53,"failed":0},{"name":"local_tee.wast","passed":98,"failed":0},{"name":"loop.wast","passed":120,"failed":0},{"name":"memory.wast","passed":90,"failed":0},{"name":"memory_grow.wast","passed":106,"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":"obsolete-keywords.wast","passed":11,"failed":0},{"name":"ref.wast","passed":13,"failed":0},{"name":"ref_as_non_null.wast","passed":7,"failed":0},{"name":"ref_func.wast","passed":17,"failed":0},{"name":"ref_is_null.wast","passed":22,"failed":0},{"name":"ref_null.wast","passed":0,"failed":34},{"name":"return.wast","passed":84,"failed":0},{"name":"return_call.wast","passed":47,"failed":0},{"name":"return_call_indirect.wast","passed":79,"failed":0},{"name":"return_call_ref.wast","passed":51,"failed":0},{"name":"select.wast","passed":157,"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":46,"failed":0},{"name":"table_get.wast","passed":16,"failed":0},{"name":"table_grow.wast","passed":58,"failed":0},{"name":"table_set.wast","passed":26,"failed":0},{"name":"table_size.wast","passed":39,"failed":0},{"name":"token.wast","passed":61,"failed":0},{"name":"traps.wast","passed":36,"failed":0},{"name":"type-canon.wast","passed":0,"failed":2},{"name":"type-equivalence.wast","passed":19,"failed":13},{"name":"type-rec.wast","passed":10,"failed":17},{"name":"type.wast","passed":3,"failed":0},{"name":"unreachable.wast","passed":64,"failed":0},{"name":"unreached-invalid.wast","passed":121,"failed":0},{"name":"unreached-valid.wast","passed":13,"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.11.0-pre.0,21228,0,[{"name":"address.wast","passed":260,"failed":0},{"name":"align.wast","passed":165,"failed":0},{"name":"annotations.wast","passed":74,"failed":0},{"name":"binary-leb128.wast","passed":91,"failed":0},{"name":"binary.wast","passed":127,"failed":0},{"name":"block.wast","passed":223,"failed":0},{"name":"br.wast","passed":97,"failed":0},{"name":"br_if.wast","passed":119,"failed":0},{"name":"br_on_non_null.wast","passed":12,"failed":0},{"name":"br_on_null.wast","passed":10,"failed":0},{"name":"br_table.wast","passed":186,"failed":0},{"name":"call.wast","passed":91,"failed":0},{"name":"call_indirect.wast","passed":172,"failed":0},{"name":"call_ref.wast","passed":35,"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":65,"failed":0},{"name":"elem.wast","passed":151,"failed":0},{"name":"endianness.wast","passed":69,"failed":0},{"name":"exports.wast","passed":97,"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":927,"failed":0},{"name":"float_literals.wast","passed":179,"failed":0},{"name":"float_memory.wast","passed":90,"failed":0},{"name":"float_misc.wast","passed":471,"failed":0},{"name":"forward.wast","passed":5,"failed":0},{"name":"func.wast","passed":175,"failed":0},{"name":"func_ptrs.wast","passed":36,"failed":0},{"name":"global.wast","passed":124,"failed":0},{"name":"i32.wast","passed":460,"failed":0},{"name":"i64.wast","passed":416,"failed":0},{"name":"id.wast","passed":7,"failed":0},{"name":"if.wast","passed":241,"failed":0},{"name":"imports.wast","passed":218,"failed":0},{"name":"inline-module.wast","passed":1,"failed":0},{"name":"instance.wast","passed":23,"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":163,"failed":0},{"name":"load.wast","passed":97,"failed":0},{"name":"local_get.wast","passed":36,"failed":0},{"name":"local_init.wast","passed":10,"failed":0},{"name":"local_set.wast","passed":53,"failed":0},{"name":"local_tee.wast","passed":98,"failed":0},{"name":"loop.wast","passed":120,"failed":0},{"name":"memory.wast","passed":90,"failed":0},{"name":"memory_grow.wast","passed":106,"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":"obsolete-keywords.wast","passed":11,"failed":0},{"name":"ref.wast","passed":13,"failed":0},{"name":"ref_as_non_null.wast","passed":7,"failed":0},{"name":"ref_func.wast","passed":17,"failed":0},{"name":"ref_is_null.wast","passed":22,"failed":0},{"name":"ref_null.wast","passed":34,"failed":0},{"name":"return.wast","passed":84,"failed":0},{"name":"return_call.wast","passed":47,"failed":0},{"name":"return_call_indirect.wast","passed":79,"failed":0},{"name":"return_call_ref.wast","passed":51,"failed":0},{"name":"select.wast","passed":157,"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":46,"failed":0},{"name":"table_get.wast","passed":16,"failed":0},{"name":"table_grow.wast","passed":58,"failed":0},{"name":"table_set.wast","passed":26,"failed":0},{"name":"table_size.wast","passed":39,"failed":0},{"name":"token.wast","passed":61,"failed":0},{"name":"traps.wast","passed":36,"failed":0},{"name":"type-canon.wast","passed":2,"failed":0},{"name":"type-equivalence.wast","passed":32,"failed":0},{"name":"type-rec.wast","passed":27,"failed":0},{"name":"type.wast","passed":3,"failed":0},{"name":"unreachable.wast","passed":64,"failed":0},{"name":"unreached-invalid.wast","passed":121,"failed":0},{"name":"unreached-valid.wast","passed":13,"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}] diff --git a/crates/tinywasm/tests/generated/wasm-annotations.csv b/crates/tinywasm/tests/generated/wasm-annotations.csv deleted file mode 100644 index f24ba507..00000000 --- a/crates/tinywasm/tests/generated/wasm-annotations.csv +++ /dev/null @@ -1,5 +0,0 @@ -0.8.0,142,0,[{"name":"annotations.wast","passed":74,"failed":0},{"name":"annotations/simd_lane.wast (skipped)","passed":0,"failed":0},{"name":"id.wast","passed":7,"failed":0},{"name":"token.wast","passed":61,"failed":0}] -0.9.0,617,0,[{"name":"annotations.wast","passed":74,"failed":0},{"name":"id.wast","passed":7,"failed":0},{"name":"simd_lane.wast","passed":475,"failed":0},{"name":"token.wast","passed":61,"failed":0}] -0.9.1,617,0,[{"name":"annotations.wast","passed":74,"failed":0},{"name":"id.wast","passed":7,"failed":0},{"name":"simd_lane.wast","passed":475,"failed":0},{"name":"token.wast","passed":61,"failed":0}] -0.10.0,617,0,[{"name":"annotations.wast","passed":74,"failed":0},{"name":"id.wast","passed":7,"failed":0},{"name":"simd_lane.wast","passed":475,"failed":0},{"name":"token.wast","passed":61,"failed":0}] -0.11.0-pre.0,617,0,[{"name":"annotations.wast","passed":74,"failed":0},{"name":"id.wast","passed":7,"failed":0},{"name":"simd_lane.wast","passed":475,"failed":0},{"name":"token.wast","passed":61,"failed":0}] diff --git a/crates/tinywasm/tests/generated/wasm-extended-const.csv b/crates/tinywasm/tests/generated/wasm-extended-const.csv deleted file mode 100644 index 8677772a..00000000 --- a/crates/tinywasm/tests/generated/wasm-extended-const.csv +++ /dev/null @@ -1,5 +0,0 @@ -0.8.0,211,79,[{"name":"data.wast","passed":61,"failed":4},{"name":"elem.wast","passed":99,"failed":12},{"name":"global.wast","passed":51,"failed":63}] -0.9.0,290,0,[{"name":"data.wast","passed":65,"failed":0},{"name":"elem.wast","passed":111,"failed":0},{"name":"global.wast","passed":114,"failed":0}] -0.9.1,290,0,[{"name":"data.wast","passed":65,"failed":0},{"name":"elem.wast","passed":111,"failed":0},{"name":"global.wast","passed":114,"failed":0}] -0.10.0,290,0,[{"name":"data.wast","passed":65,"failed":0},{"name":"elem.wast","passed":111,"failed":0},{"name":"global.wast","passed":114,"failed":0}] -0.11.0-pre.0,290,0,[{"name":"data.wast","passed":65,"failed":0},{"name":"elem.wast","passed":111,"failed":0},{"name":"global.wast","passed":114,"failed":0}] diff --git a/crates/tinywasm/tests/generated/wasm-function-references.csv b/crates/tinywasm/tests/generated/wasm-function-references.csv deleted file mode 100644 index 3e59535d..00000000 --- a/crates/tinywasm/tests/generated/wasm-function-references.csv +++ /dev/null @@ -1,3 +0,0 @@ -0.9.0,1536,331,[{"name":"binary.wast","passed":128,"failed":0},{"name":"br_on_non_null.wast","passed":0,"failed":9},{"name":"br_on_null.wast","passed":0,"failed":9},{"name":"br_table.wast","passed":24,"failed":162},{"name":"call_ref.wast","passed":3,"failed":31},{"name":"data.wast","passed":59,"failed":0},{"name":"elem.wast","passed":138,"failed":0},{"name":"func.wast","passed":175,"failed":0},{"name":"global.wast","passed":108,"failed":0},{"name":"if.wast","passed":241,"failed":0},{"name":"linking.wast","passed":146,"failed":21},{"name":"local_get.wast","passed":36,"failed":0},{"name":"local_init.wast","passed":10,"failed":0},{"name":"ref.wast","passed":12,"failed":1},{"name":"ref_as_non_null.wast","passed":1,"failed":6},{"name":"ref_is_null.wast","passed":2,"failed":20},{"name":"ref_null.wast","passed":0,"failed":4},{"name":"return_call.wast","passed":45,"failed":0},{"name":"return_call_indirect.wast","passed":76,"failed":0},{"name":"return_call_ref.wast","passed":10,"failed":40},{"name":"select.wast","passed":155,"failed":2},{"name":"table-sub.wast","passed":2,"failed":1},{"name":"table.wast","passed":35,"failed":8},{"name":"type-equivalence.wast","passed":7,"failed":7},{"name":"unreached-invalid.wast","passed":121,"failed":0},{"name":"unreached-valid.wast","passed":2,"failed":10}] -0.10.0,1536,331,[{"name":"binary.wast","passed":128,"failed":0},{"name":"br_on_non_null.wast","passed":0,"failed":9},{"name":"br_on_null.wast","passed":0,"failed":9},{"name":"br_table.wast","passed":24,"failed":162},{"name":"call_ref.wast","passed":3,"failed":31},{"name":"data.wast","passed":59,"failed":0},{"name":"elem.wast","passed":138,"failed":0},{"name":"func.wast","passed":175,"failed":0},{"name":"global.wast","passed":108,"failed":0},{"name":"if.wast","passed":241,"failed":0},{"name":"linking.wast","passed":146,"failed":21},{"name":"local_get.wast","passed":36,"failed":0},{"name":"local_init.wast","passed":10,"failed":0},{"name":"ref.wast","passed":12,"failed":1},{"name":"ref_as_non_null.wast","passed":1,"failed":6},{"name":"ref_is_null.wast","passed":2,"failed":20},{"name":"ref_null.wast","passed":0,"failed":4},{"name":"return_call.wast","passed":45,"failed":0},{"name":"return_call_indirect.wast","passed":76,"failed":0},{"name":"return_call_ref.wast","passed":10,"failed":40},{"name":"select.wast","passed":155,"failed":2},{"name":"table-sub.wast","passed":2,"failed":1},{"name":"table.wast","passed":35,"failed":8},{"name":"type-equivalence.wast","passed":7,"failed":7},{"name":"unreached-invalid.wast","passed":121,"failed":0},{"name":"unreached-valid.wast","passed":2,"failed":10}] -0.11.0-pre.0,1867,0,[{"name":"binary.wast","passed":128,"failed":0},{"name":"br_on_non_null.wast","passed":9,"failed":0},{"name":"br_on_null.wast","passed":9,"failed":0},{"name":"br_table.wast","passed":186,"failed":0},{"name":"call_ref.wast","passed":34,"failed":0},{"name":"data.wast","passed":59,"failed":0},{"name":"elem.wast","passed":138,"failed":0},{"name":"func.wast","passed":175,"failed":0},{"name":"global.wast","passed":108,"failed":0},{"name":"if.wast","passed":241,"failed":0},{"name":"linking.wast","passed":167,"failed":0},{"name":"local_get.wast","passed":36,"failed":0},{"name":"local_init.wast","passed":10,"failed":0},{"name":"ref.wast","passed":13,"failed":0},{"name":"ref_as_non_null.wast","passed":7,"failed":0},{"name":"ref_is_null.wast","passed":22,"failed":0},{"name":"ref_null.wast","passed":4,"failed":0},{"name":"return_call.wast","passed":45,"failed":0},{"name":"return_call_indirect.wast","passed":76,"failed":0},{"name":"return_call_ref.wast","passed":50,"failed":0},{"name":"select.wast","passed":157,"failed":0},{"name":"table-sub.wast","passed":3,"failed":0},{"name":"table.wast","passed":43,"failed":0},{"name":"type-equivalence.wast","passed":14,"failed":0},{"name":"unreached-invalid.wast","passed":121,"failed":0},{"name":"unreached-valid.wast","passed":12,"failed":0}] diff --git a/crates/tinywasm/tests/generated/wasm-gc.csv b/crates/tinywasm/tests/generated/wasm-gc.csv index 5b2995fa..ea39c566 100644 --- a/crates/tinywasm/tests/generated/wasm-gc.csv +++ b/crates/tinywasm/tests/generated/wasm-gc.csv @@ -1,2 +1,3 @@ 0.9.0,80,703,[{"name":"array.wast","passed":6,"failed":48},{"name":"array_copy.wast","passed":4,"failed":31},{"name":"array_fill.wast","passed":3,"failed":27},{"name":"array_init_data.wast","passed":2,"failed":44},{"name":"array_init_elem.wast","passed":3,"failed":33},{"name":"array_new_data.wast","passed":0,"failed":28},{"name":"array_new_elem.wast","passed":0,"failed":24},{"name":"binary-gc.wast","passed":1,"failed":0},{"name":"br_on_cast.wast","passed":6,"failed":31},{"name":"br_on_cast_fail.wast","passed":6,"failed":31},{"name":"extern.wast","passed":0,"failed":18},{"name":"i31.wast","passed":2,"failed":71},{"name":"ref_cast.wast","passed":0,"failed":45},{"name":"ref_eq.wast","passed":6,"failed":83},{"name":"ref_test.wast","passed":0,"failed":71},{"name":"struct.wast","passed":5,"failed":25},{"name":"type-subtyping.wast","passed":36,"failed":93}] 0.10.0,80,704,[{"name":"array.wast","passed":6,"failed":48},{"name":"array_copy.wast","passed":4,"failed":31},{"name":"array_fill.wast","passed":3,"failed":27},{"name":"array_init_data.wast","passed":2,"failed":44},{"name":"array_init_elem.wast","passed":3,"failed":33},{"name":"array_new_data.wast","passed":0,"failed":28},{"name":"array_new_elem.wast","passed":0,"failed":24},{"name":"binary-gc.wast","passed":1,"failed":0},{"name":"br_on_cast.wast","passed":6,"failed":31},{"name":"br_on_cast_fail.wast","passed":6,"failed":31},{"name":"extern.wast","passed":0,"failed":18},{"name":"i31.wast","passed":2,"failed":71},{"name":"ref_cast.wast","passed":0,"failed":45},{"name":"ref_eq.wast","passed":6,"failed":83},{"name":"ref_test.wast","passed":0,"failed":71},{"name":"struct.wast","passed":5,"failed":25},{"name":"type-subtyping.wast","passed":36,"failed":94}] +0.11.0-pre.0,784,0,[{"name":"array.wast","passed":54,"failed":0},{"name":"array_copy.wast","passed":35,"failed":0},{"name":"array_fill.wast","passed":30,"failed":0},{"name":"array_init_data.wast","passed":46,"failed":0},{"name":"array_init_elem.wast","passed":36,"failed":0},{"name":"array_new_data.wast","passed":28,"failed":0},{"name":"array_new_elem.wast","passed":24,"failed":0},{"name":"binary-gc.wast","passed":1,"failed":0},{"name":"br_on_cast.wast","passed":37,"failed":0},{"name":"br_on_cast_fail.wast","passed":37,"failed":0},{"name":"extern.wast","passed":18,"failed":0},{"name":"i31.wast","passed":73,"failed":0},{"name":"ref_cast.wast","passed":45,"failed":0},{"name":"ref_eq.wast","passed":89,"failed":0},{"name":"ref_test.wast","passed":71,"failed":0},{"name":"struct.wast","passed":30,"failed":0},{"name":"type-subtyping.wast","passed":130,"failed":0}] diff --git a/crates/tinywasm/tests/generated/wasm-latest.csv b/crates/tinywasm/tests/generated/wasm-latest.csv index ef7e1a25..b25ec134 100644 --- a/crates/tinywasm/tests/generated/wasm-latest.csv +++ b/crates/tinywasm/tests/generated/wasm-latest.csv @@ -1,2 +1,3 @@ 0.9.0,20777,452,[{"name":"address.wast","passed":260,"failed":0},{"name":"align.wast","passed":165,"failed":0},{"name":"annotations.wast","passed":74,"failed":0},{"name":"binary-leb128.wast","passed":91,"failed":0},{"name":"binary.wast","passed":127,"failed":0},{"name":"block.wast","passed":223,"failed":0},{"name":"br.wast","passed":97,"failed":0},{"name":"br_if.wast","passed":119,"failed":0},{"name":"br_on_non_null.wast","passed":1,"failed":11},{"name":"br_on_null.wast","passed":1,"failed":9},{"name":"br_table.wast","passed":24,"failed":162},{"name":"call.wast","passed":91,"failed":0},{"name":"call_indirect.wast","passed":172,"failed":0},{"name":"call_ref.wast","passed":4,"failed":31},{"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":63,"failed":2},{"name":"elem.wast","passed":149,"failed":2},{"name":"endianness.wast","passed":69,"failed":0},{"name":"exports.wast","passed":97,"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":927,"failed":0},{"name":"float_literals.wast","passed":179,"failed":0},{"name":"float_memory.wast","passed":90,"failed":0},{"name":"float_misc.wast","passed":471,"failed":0},{"name":"forward.wast","passed":5,"failed":0},{"name":"func.wast","passed":175,"failed":0},{"name":"func_ptrs.wast","passed":36,"failed":0},{"name":"global.wast","passed":116,"failed":8},{"name":"i32.wast","passed":460,"failed":0},{"name":"i64.wast","passed":416,"failed":0},{"name":"id.wast","passed":7,"failed":0},{"name":"if.wast","passed":241,"failed":0},{"name":"imports.wast","passed":198,"failed":20},{"name":"inline-module.wast","passed":1,"failed":0},{"name":"instance.wast","passed":0,"failed":23},{"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":142,"failed":21},{"name":"load.wast","passed":97,"failed":0},{"name":"local_get.wast","passed":36,"failed":0},{"name":"local_init.wast","passed":10,"failed":0},{"name":"local_set.wast","passed":53,"failed":0},{"name":"local_tee.wast","passed":98,"failed":0},{"name":"loop.wast","passed":121,"failed":0},{"name":"memory.wast","passed":89,"failed":1},{"name":"memory_grow.wast","passed":106,"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":"obsolete-keywords.wast","passed":11,"failed":0},{"name":"ref.wast","passed":12,"failed":1},{"name":"ref_as_non_null.wast","passed":1,"failed":6},{"name":"ref_func.wast","passed":17,"failed":0},{"name":"ref_is_null.wast","passed":2,"failed":20},{"name":"ref_null.wast","passed":0,"failed":34},{"name":"return.wast","passed":84,"failed":0},{"name":"return_call.wast","passed":47,"failed":0},{"name":"return_call_indirect.wast","passed":79,"failed":0},{"name":"return_call_ref.wast","passed":11,"failed":40},{"name":"select.wast","passed":155,"failed":2},{"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":37,"failed":9},{"name":"table_get.wast","passed":16,"failed":0},{"name":"table_grow.wast","passed":58,"failed":0},{"name":"table_set.wast","passed":26,"failed":0},{"name":"table_size.wast","passed":39,"failed":0},{"name":"token.wast","passed":61,"failed":0},{"name":"traps.wast","passed":36,"failed":0},{"name":"type-canon.wast","passed":0,"failed":2},{"name":"type-equivalence.wast","passed":12,"failed":20},{"name":"type-rec.wast","passed":10,"failed":17},{"name":"type.wast","passed":3,"failed":0},{"name":"unreachable.wast","passed":64,"failed":0},{"name":"unreached-invalid.wast","passed":121,"failed":0},{"name":"unreached-valid.wast","passed":2,"failed":11},{"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.10.0,20697,536,[{"name":"address.wast","passed":260,"failed":0},{"name":"align.wast","passed":165,"failed":0},{"name":"annotations.wast","passed":74,"failed":0},{"name":"binary-leb128.wast","passed":91,"failed":0},{"name":"binary.wast","passed":127,"failed":0},{"name":"block.wast","passed":223,"failed":0},{"name":"br.wast","passed":97,"failed":0},{"name":"br_if.wast","passed":119,"failed":0},{"name":"br_on_non_null.wast","passed":1,"failed":11},{"name":"br_on_null.wast","passed":1,"failed":9},{"name":"br_table.wast","passed":24,"failed":162},{"name":"call.wast","passed":91,"failed":0},{"name":"call_indirect.wast","passed":172,"failed":0},{"name":"call_ref.wast","passed":4,"failed":31},{"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":63,"failed":2},{"name":"elem.wast","passed":149,"failed":2},{"name":"endianness.wast","passed":69,"failed":0},{"name":"exports.wast","passed":97,"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":927,"failed":0},{"name":"float_literals.wast","passed":179,"failed":0},{"name":"float_memory.wast","passed":90,"failed":0},{"name":"float_misc.wast","passed":471,"failed":0},{"name":"forward.wast","passed":5,"failed":0},{"name":"func.wast","passed":175,"failed":0},{"name":"func_ptrs.wast","passed":36,"failed":0},{"name":"global.wast","passed":116,"failed":8},{"name":"i32.wast","passed":460,"failed":0},{"name":"i64.wast","passed":416,"failed":0},{"name":"id.wast","passed":7,"failed":0},{"name":"if.wast","passed":241,"failed":0},{"name":"imports.wast","passed":198,"failed":20},{"name":"inline-module.wast","passed":1,"failed":0},{"name":"instance.wast","passed":0,"failed":23},{"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":142,"failed":21},{"name":"load.wast","passed":97,"failed":0},{"name":"local_get.wast","passed":36,"failed":0},{"name":"local_init.wast","passed":10,"failed":0},{"name":"local_set.wast","passed":53,"failed":0},{"name":"local_tee.wast","passed":98,"failed":0},{"name":"loop.wast","passed":121,"failed":0},{"name":"memory.wast","passed":90,"failed":0},{"name":"memory_grow.wast","passed":106,"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":"obsolete-keywords.wast","passed":11,"failed":0},{"name":"ref.wast","passed":12,"failed":1},{"name":"ref_as_non_null.wast","passed":1,"failed":6},{"name":"ref_func.wast","passed":17,"failed":0},{"name":"ref_is_null.wast","passed":2,"failed":20},{"name":"ref_null.wast","passed":0,"failed":34},{"name":"return.wast","passed":84,"failed":0},{"name":"return_call.wast","passed":14,"failed":35},{"name":"return_call_indirect.wast","passed":30,"failed":51},{"name":"return_call_ref.wast","passed":11,"failed":40},{"name":"select.wast","passed":155,"failed":2},{"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":38,"failed":8},{"name":"table_get.wast","passed":16,"failed":0},{"name":"table_grow.wast","passed":58,"failed":0},{"name":"table_set.wast","passed":26,"failed":0},{"name":"table_size.wast","passed":39,"failed":0},{"name":"token.wast","passed":61,"failed":0},{"name":"traps.wast","passed":36,"failed":0},{"name":"type-canon.wast","passed":0,"failed":2},{"name":"type-equivalence.wast","passed":12,"failed":20},{"name":"type-rec.wast","passed":10,"failed":17},{"name":"type.wast","passed":3,"failed":0},{"name":"unreachable.wast","passed":64,"failed":0},{"name":"unreached-invalid.wast","passed":121,"failed":0},{"name":"unreached-valid.wast","passed":2,"failed":11},{"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.11.0-pre.0,21233,0,[{"name":"address.wast","passed":260,"failed":0},{"name":"align.wast","passed":165,"failed":0},{"name":"annotations.wast","passed":74,"failed":0},{"name":"binary-leb128.wast","passed":91,"failed":0},{"name":"binary.wast","passed":127,"failed":0},{"name":"block.wast","passed":223,"failed":0},{"name":"br.wast","passed":97,"failed":0},{"name":"br_if.wast","passed":119,"failed":0},{"name":"br_on_non_null.wast","passed":12,"failed":0},{"name":"br_on_null.wast","passed":10,"failed":0},{"name":"br_table.wast","passed":186,"failed":0},{"name":"call.wast","passed":91,"failed":0},{"name":"call_indirect.wast","passed":172,"failed":0},{"name":"call_ref.wast","passed":35,"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":65,"failed":0},{"name":"elem.wast","passed":151,"failed":0},{"name":"endianness.wast","passed":69,"failed":0},{"name":"exports.wast","passed":97,"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":927,"failed":0},{"name":"float_literals.wast","passed":179,"failed":0},{"name":"float_memory.wast","passed":90,"failed":0},{"name":"float_misc.wast","passed":471,"failed":0},{"name":"forward.wast","passed":5,"failed":0},{"name":"func.wast","passed":175,"failed":0},{"name":"func_ptrs.wast","passed":36,"failed":0},{"name":"global.wast","passed":124,"failed":0},{"name":"i32.wast","passed":460,"failed":0},{"name":"i64.wast","passed":416,"failed":0},{"name":"id.wast","passed":7,"failed":0},{"name":"if.wast","passed":241,"failed":0},{"name":"imports.wast","passed":218,"failed":0},{"name":"inline-module.wast","passed":1,"failed":0},{"name":"instance.wast","passed":23,"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":163,"failed":0},{"name":"load.wast","passed":97,"failed":0},{"name":"local_get.wast","passed":36,"failed":0},{"name":"local_init.wast","passed":10,"failed":0},{"name":"local_set.wast","passed":53,"failed":0},{"name":"local_tee.wast","passed":98,"failed":0},{"name":"loop.wast","passed":121,"failed":0},{"name":"memory.wast","passed":90,"failed":0},{"name":"memory_grow.wast","passed":106,"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":"obsolete-keywords.wast","passed":11,"failed":0},{"name":"ref.wast","passed":13,"failed":0},{"name":"ref_as_non_null.wast","passed":7,"failed":0},{"name":"ref_func.wast","passed":17,"failed":0},{"name":"ref_is_null.wast","passed":22,"failed":0},{"name":"ref_null.wast","passed":34,"failed":0},{"name":"return.wast","passed":84,"failed":0},{"name":"return_call.wast","passed":49,"failed":0},{"name":"return_call_indirect.wast","passed":81,"failed":0},{"name":"return_call_ref.wast","passed":51,"failed":0},{"name":"select.wast","passed":157,"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":46,"failed":0},{"name":"table_get.wast","passed":16,"failed":0},{"name":"table_grow.wast","passed":58,"failed":0},{"name":"table_set.wast","passed":26,"failed":0},{"name":"table_size.wast","passed":39,"failed":0},{"name":"token.wast","passed":61,"failed":0},{"name":"traps.wast","passed":36,"failed":0},{"name":"type-canon.wast","passed":2,"failed":0},{"name":"type-equivalence.wast","passed":32,"failed":0},{"name":"type-rec.wast","passed":27,"failed":0},{"name":"type.wast","passed":3,"failed":0},{"name":"unreachable.wast","passed":64,"failed":0},{"name":"unreached-invalid.wast","passed":121,"failed":0},{"name":"unreached-valid.wast","passed":13,"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}] diff --git a/crates/tinywasm/tests/generated/wasm-nontrapping-float-to-int-conversions.csv b/crates/tinywasm/tests/generated/wasm-nontrapping-float-to-int-conversions.csv deleted file mode 100644 index 66d53292..00000000 --- a/crates/tinywasm/tests/generated/wasm-nontrapping-float-to-int-conversions.csv +++ /dev/null @@ -1,4 +0,0 @@ -0.9.0,615,0,[{"name":"conversions.wast","passed":615,"failed":0}] -0.9.1,615,0,[{"name":"conversions.wast","passed":615,"failed":0}] -0.10.0,615,0,[{"name":"conversions.wast","passed":615,"failed":0}] -0.11.0-pre.0,615,0,[{"name":"conversions.wast","passed":615,"failed":0}] diff --git a/crates/tinywasm/tests/generated/wasm-reference-types.csv b/crates/tinywasm/tests/generated/wasm-reference-types.csv deleted file mode 100644 index 92a4650e..00000000 --- a/crates/tinywasm/tests/generated/wasm-reference-types.csv +++ /dev/null @@ -1,4 +0,0 @@ -0.9.0,9124,0,[{"name":"binary-leb128.wast","passed":77,"failed":0},{"name":"binary.wast","passed":134,"failed":0},{"name":"br_table.wast","passed":172,"failed":0},{"name":"bulk.wast","passed":117,"failed":0},{"name":"call_indirect.wast","passed":169,"failed":0},{"name":"custom.wast","passed":11,"failed":0},{"name":"data.wast","passed":45,"failed":0},{"name":"elem.wast","passed":60,"failed":0},{"name":"exports.wast","passed":84,"failed":0},{"name":"global.wast","passed":86,"failed":0},{"name":"imports.wast","passed":158,"failed":0},{"name":"linking.wast","passed":132,"failed":0},{"name":"memory_copy.wast","passed":4450,"failed":0},{"name":"memory_fill.wast","passed":100,"failed":0},{"name":"memory_grow.wast","passed":96,"failed":0},{"name":"memory_init.wast","passed":240,"failed":0},{"name":"ref_func.wast","passed":17,"failed":0},{"name":"ref_is_null.wast","passed":16,"failed":0},{"name":"ref_null.wast","passed":3,"failed":0},{"name":"select.wast","passed":141,"failed":0},{"name":"table-sub.wast","passed":2,"failed":0},{"name":"table.wast","passed":19,"failed":0},{"name":"table_copy.wast","passed":1728,"failed":0},{"name":"table_fill.wast","passed":45,"failed":0},{"name":"table_get.wast","passed":16,"failed":0},{"name":"table_grow.wast","passed":50,"failed":0},{"name":"table_init.wast","passed":780,"failed":0},{"name":"table_set.wast","passed":26,"failed":0},{"name":"table_size.wast","passed":39,"failed":0},{"name":"unreached-invalid.wast","passed":111,"failed":0}] -0.9.1,9124,0,[{"name":"binary-leb128.wast","passed":77,"failed":0},{"name":"binary.wast","passed":134,"failed":0},{"name":"br_table.wast","passed":172,"failed":0},{"name":"bulk.wast","passed":117,"failed":0},{"name":"call_indirect.wast","passed":169,"failed":0},{"name":"custom.wast","passed":11,"failed":0},{"name":"data.wast","passed":45,"failed":0},{"name":"elem.wast","passed":60,"failed":0},{"name":"exports.wast","passed":84,"failed":0},{"name":"global.wast","passed":86,"failed":0},{"name":"imports.wast","passed":158,"failed":0},{"name":"linking.wast","passed":132,"failed":0},{"name":"memory_copy.wast","passed":4450,"failed":0},{"name":"memory_fill.wast","passed":100,"failed":0},{"name":"memory_grow.wast","passed":96,"failed":0},{"name":"memory_init.wast","passed":240,"failed":0},{"name":"ref_func.wast","passed":17,"failed":0},{"name":"ref_is_null.wast","passed":16,"failed":0},{"name":"ref_null.wast","passed":3,"failed":0},{"name":"select.wast","passed":141,"failed":0},{"name":"table-sub.wast","passed":2,"failed":0},{"name":"table.wast","passed":19,"failed":0},{"name":"table_copy.wast","passed":1728,"failed":0},{"name":"table_fill.wast","passed":45,"failed":0},{"name":"table_get.wast","passed":16,"failed":0},{"name":"table_grow.wast","passed":50,"failed":0},{"name":"table_init.wast","passed":780,"failed":0},{"name":"table_set.wast","passed":26,"failed":0},{"name":"table_size.wast","passed":39,"failed":0},{"name":"unreached-invalid.wast","passed":111,"failed":0}] -0.10.0,9124,0,[{"name":"binary-leb128.wast","passed":77,"failed":0},{"name":"binary.wast","passed":134,"failed":0},{"name":"br_table.wast","passed":172,"failed":0},{"name":"bulk.wast","passed":117,"failed":0},{"name":"call_indirect.wast","passed":169,"failed":0},{"name":"custom.wast","passed":11,"failed":0},{"name":"data.wast","passed":45,"failed":0},{"name":"elem.wast","passed":60,"failed":0},{"name":"exports.wast","passed":84,"failed":0},{"name":"global.wast","passed":86,"failed":0},{"name":"imports.wast","passed":158,"failed":0},{"name":"linking.wast","passed":132,"failed":0},{"name":"memory_copy.wast","passed":4450,"failed":0},{"name":"memory_fill.wast","passed":100,"failed":0},{"name":"memory_grow.wast","passed":96,"failed":0},{"name":"memory_init.wast","passed":240,"failed":0},{"name":"ref_func.wast","passed":17,"failed":0},{"name":"ref_is_null.wast","passed":16,"failed":0},{"name":"ref_null.wast","passed":3,"failed":0},{"name":"select.wast","passed":141,"failed":0},{"name":"table-sub.wast","passed":2,"failed":0},{"name":"table.wast","passed":19,"failed":0},{"name":"table_copy.wast","passed":1728,"failed":0},{"name":"table_fill.wast","passed":45,"failed":0},{"name":"table_get.wast","passed":16,"failed":0},{"name":"table_grow.wast","passed":50,"failed":0},{"name":"table_init.wast","passed":780,"failed":0},{"name":"table_set.wast","passed":26,"failed":0},{"name":"table_size.wast","passed":39,"failed":0},{"name":"unreached-invalid.wast","passed":111,"failed":0}] -0.11.0-pre.0,9124,0,[{"name":"binary-leb128.wast","passed":77,"failed":0},{"name":"binary.wast","passed":134,"failed":0},{"name":"br_table.wast","passed":172,"failed":0},{"name":"bulk.wast","passed":117,"failed":0},{"name":"call_indirect.wast","passed":169,"failed":0},{"name":"custom.wast","passed":11,"failed":0},{"name":"data.wast","passed":45,"failed":0},{"name":"elem.wast","passed":60,"failed":0},{"name":"exports.wast","passed":84,"failed":0},{"name":"global.wast","passed":86,"failed":0},{"name":"imports.wast","passed":158,"failed":0},{"name":"linking.wast","passed":132,"failed":0},{"name":"memory_copy.wast","passed":4450,"failed":0},{"name":"memory_fill.wast","passed":100,"failed":0},{"name":"memory_grow.wast","passed":96,"failed":0},{"name":"memory_init.wast","passed":240,"failed":0},{"name":"ref_func.wast","passed":17,"failed":0},{"name":"ref_is_null.wast","passed":16,"failed":0},{"name":"ref_null.wast","passed":3,"failed":0},{"name":"select.wast","passed":141,"failed":0},{"name":"table-sub.wast","passed":2,"failed":0},{"name":"table.wast","passed":19,"failed":0},{"name":"table_copy.wast","passed":1728,"failed":0},{"name":"table_fill.wast","passed":45,"failed":0},{"name":"table_get.wast","passed":16,"failed":0},{"name":"table_grow.wast","passed":50,"failed":0},{"name":"table_init.wast","passed":780,"failed":0},{"name":"table_set.wast","passed":26,"failed":0},{"name":"table_size.wast","passed":39,"failed":0},{"name":"unreached-invalid.wast","passed":111,"failed":0}] diff --git a/crates/tinywasm/tests/generated/wasm-sign-extension-ops.csv b/crates/tinywasm/tests/generated/wasm-sign-extension-ops.csv deleted file mode 100644 index 4e7e2ee9..00000000 --- a/crates/tinywasm/tests/generated/wasm-sign-extension-ops.csv +++ /dev/null @@ -1,4 +0,0 @@ -0.9.0,872,0,[{"name":"i32.wast","passed":458,"failed":0},{"name":"i64.wast","passed":414,"failed":0}] -0.9.1,872,0,[{"name":"i32.wast","passed":458,"failed":0},{"name":"i64.wast","passed":414,"failed":0}] -0.10.0,872,0,[{"name":"i32.wast","passed":458,"failed":0},{"name":"i64.wast","passed":414,"failed":0}] -0.11.0-pre.0,872,0,[{"name":"i32.wast","passed":458,"failed":0},{"name":"i64.wast","passed":414,"failed":0}] diff --git a/crates/tinywasm/tests/generated/wasm-tail-call.csv b/crates/tinywasm/tests/generated/wasm-tail-call.csv deleted file mode 100644 index 316895d6..00000000 --- a/crates/tinywasm/tests/generated/wasm-tail-call.csv +++ /dev/null @@ -1,4 +0,0 @@ -0.9.0,119,0,[{"name":"return_call.wast","passed":44,"failed":0},{"name":"return_call_indirect.wast","passed":75,"failed":0}] -0.9.1,119,0,[{"name":"return_call.wast","passed":44,"failed":0},{"name":"return_call_indirect.wast","passed":75,"failed":0}] -0.10.0,119,0,[{"name":"return_call.wast","passed":44,"failed":0},{"name":"return_call_indirect.wast","passed":75,"failed":0}] -0.11.0-pre.0,119,0,[{"name":"return_call.wast","passed":44,"failed":0},{"name":"return_call_indirect.wast","passed":75,"failed":0}] diff --git a/crates/tinywasm/tests/generated/wasm-threads.csv b/crates/tinywasm/tests/generated/wasm-threads.csv index f588c660..25d09b77 100644 --- a/crates/tinywasm/tests/generated/wasm-threads.csv +++ b/crates/tinywasm/tests/generated/wasm-threads.csv @@ -1,2 +1,3 @@ 0.9.0,357,262,[{"name":"atomic.wast","passed":48,"failed":249},{"name":"exports.wast","passed":82,"failed":6},{"name":"imports.wast","passed":147,"failed":5},{"name":"memory.wast","passed":80,"failed":2}] 0.10.0,357,262,[{"name":"atomic.wast","passed":48,"failed":249},{"name":"exports.wast","passed":82,"failed":6},{"name":"imports.wast","passed":147,"failed":5},{"name":"memory.wast","passed":80,"failed":2}] +0.11.0-pre.0,357,262,[{"name":"atomic.wast","passed":48,"failed":249},{"name":"exports.wast","passed":82,"failed":6},{"name":"imports.wast","passed":147,"failed":5},{"name":"memory.wast","passed":80,"failed":2}] diff --git a/crates/tinywasm/tests/host_func_signature_check.rs b/crates/tinywasm/tests/host_func_signature_check.rs index b3b5048a..38a85efb 100644 --- a/crates/tinywasm/tests/host_func_signature_check.rs +++ b/crates/tinywasm/tests/host_func_signature_check.rs @@ -1,8 +1,6 @@ -use eyre::Result; use std::fmt::Write; -use tinywasm::types::{FuncType, RefValue, WasmType, WasmValue}; +use tinywasm::types::{ExternRef, FuncType, RefType, RefValue, WasmType, WasmValue}; use tinywasm::{FuncContext, HostFunction, Imports, Module, ModuleInstance, Store}; -use tinywasm_types::ExternRef; const VAL_LISTS: &[&[WasmValue]] = &[ &[], @@ -27,17 +25,17 @@ fn module_cases() -> Vec<(Module, FuncType, Vec)> { } #[test] -fn test_return_invalid_type() -> Result<()> { +fn test_return_invalid_type() -> Result<(), Box> { let cases = module_cases(); for (module, ty, args) in cases { for returned_values in VAL_LISTS { let mut store = Store::default(); let mut imports = Imports::new(); - let hfn = HostFunction::from_untyped(&mut store, &ty, |_: FuncContext<'_>, _| Ok(returned_values.to_vec())); + let hfn = HostFunction::from_untyped(&ty, |_: FuncContext<'_>, _| Ok(returned_values.to_vec())); imports.define("host", "hfn", hfn); - let instance = ModuleInstance::instantiate(&mut store, &module, Some(imports)).unwrap(); + let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports)).unwrap(); let caller = instance.func_untyped(&store, "call_hfn").unwrap(); // Return-type mismatch is only observable at call time. let should_succeed = returned_values.len() == ty.results().len() @@ -51,18 +49,17 @@ fn test_return_invalid_type() -> Result<()> { } #[test] -fn test_linking_invalid_untyped_func() -> Result<()> { +fn test_linking_invalid_untyped_func() -> Result<(), Box> { let cases = module_cases(); for (module, expected_func_ty, _) in &cases { for (_, ty, _) in &cases { let mut store = Store::default(); - let tried_fn = - HostFunction::from_untyped(&mut store, ty, |_: FuncContext<'_>, _| panic!("not intended to be called")); + let tried_fn = HostFunction::from_untyped(ty, |_: FuncContext<'_>, _| panic!("not intended to be called")); let mut imports = Imports::new(); imports.define("host", "hfn", tried_fn); let should_succeed = ty == expected_func_ty; - let link_res = ModuleInstance::instantiate(&mut store, module, Some(imports)); + let link_res = ModuleInstance::instantiate(&mut store, module, Some(&imports)); assert_eq!(link_res.is_ok(), should_succeed); } } @@ -70,7 +67,7 @@ fn test_linking_invalid_untyped_func() -> Result<()> { } #[test] -fn test_linking_invalid_typed_func() -> Result<()> { +fn test_linking_invalid_typed_func() -> Result<(), Box> { type Existing = (i32, i32, f64); type NonMatchingSingle = f64; type NonMatchingTuple = (f64, i32, i32); @@ -80,26 +77,18 @@ fn test_linking_invalid_typed_func() -> Result<()> { for (module, _, _) in cases { let mut store = Store::default(); let matching_none = vec![ - HostFunction::from(&mut store, |_, _: NonMatchingTuple| -> tinywasm::Result { + HostFunction::from(|_, _: NonMatchingTuple| -> tinywasm::Result { panic!("{DONT_CALL}") }), + HostFunction::from(|_, _: NonMatchingTuple| -> tinywasm::Result<()> { panic!("{DONT_CALL}") }), + HostFunction::from(|_, _: NonMatchingSingle| -> tinywasm::Result { panic!("{DONT_CALL}") }), + HostFunction::from(|_, _: NonMatchingSingle| -> tinywasm::Result<()> { panic!("{DONT_CALL}") }), + HostFunction::from(|_, _: Existing| -> tinywasm::Result { panic!("{DONT_CALL}") }), + HostFunction::from(|_, _: Existing| -> tinywasm::Result { panic!("{DONT_CALL}") }), + HostFunction::from(|_, _: ()| -> tinywasm::Result { panic!("{DONT_CALL}") }), + HostFunction::from(|_, _: ()| -> tinywasm::Result { panic!("{DONT_CALL}") }), + HostFunction::from(|_, _: NonMatchingSingle| -> tinywasm::Result { panic!("{DONT_CALL}") }), - HostFunction::from(&mut store, |_, _: NonMatchingTuple| -> tinywasm::Result<()> { panic!("{DONT_CALL}") }), - HostFunction::from(&mut store, |_, _: NonMatchingSingle| -> tinywasm::Result { - panic!("{DONT_CALL}") - }), - HostFunction::from(&mut store, |_, _: NonMatchingSingle| -> tinywasm::Result<()> { panic!("{DONT_CALL}") }), - HostFunction::from(&mut store, |_, _: Existing| -> tinywasm::Result { - panic!("{DONT_CALL}") - }), - HostFunction::from(&mut store, |_, _: Existing| -> tinywasm::Result { - panic!("{DONT_CALL}") - }), - HostFunction::from(&mut store, |_, _: ()| -> tinywasm::Result { panic!("{DONT_CALL}") }), - HostFunction::from(&mut store, |_, _: ()| -> tinywasm::Result { panic!("{DONT_CALL}") }), - HostFunction::from(&mut store, |_, _: NonMatchingSingle| -> tinywasm::Result { - panic!("{DONT_CALL}") - }), - HostFunction::from(&mut store, |_, _: NonMatchingSingle| -> tinywasm::Result { + HostFunction::from(|_, _: NonMatchingSingle| -> tinywasm::Result { panic!("{DONT_CALL}") }), ]; @@ -107,7 +96,7 @@ fn test_linking_invalid_typed_func() -> Result<()> { for typed_fn in matching_none { let mut imports = Imports::new(); imports.define("host", "hfn", typed_fn); - let link_failure = ModuleInstance::instantiate(&mut store, &module, Some(imports)); + let link_failure = ModuleInstance::instantiate(&mut store, &module, Some(&imports)); assert!(link_failure.is_err(), "Expected linking to fail for mismatched typed func, but it succeeded"); } } @@ -116,7 +105,7 @@ fn test_linking_invalid_typed_func() -> Result<()> { } #[test] -fn concrete_host_references_use_canonical_types() -> Result<()> { +fn concrete_host_references_use_canonical_types() -> Result<(), Box> { let wasm = wat::parse_str( r#" (module @@ -142,10 +131,11 @@ fn concrete_host_references_use_canonical_types() -> Result<()> { let param_ty = instance.func_untyped(&store, "takes-a")?.ty(&store)?.clone(); let wrong_result = wrong_ref.clone(); - let wrong_return = HostFunction::from_untyped(&mut store, &return_ty, move |_, _| Ok(wrong_result.clone())); + let wrong_return = + HostFunction::from_untyped(&return_ty, move |_, _| Ok(wrong_result.clone())).instantiate(&mut store)?; assert!(wrong_return.call(&mut store, &[]).is_err()); - let accept_a = HostFunction::from_untyped(&mut store, ¶m_ty, |_, _| Ok(Vec::new())); + let accept_a = HostFunction::from_untyped(¶m_ty, |_, _| Ok(Vec::new())).instantiate(&mut store)?; assert!(accept_a.call(&mut store, &wrong_ref).is_err()); assert!(accept_a.call(&mut store, &right_ref).is_ok()); @@ -153,7 +143,35 @@ fn concrete_host_references_use_canonical_types() -> Result<()> { } #[test] -fn host_tail_calls_return_from_the_current_frame() -> Result<()> { +fn imported_host_functions_resolve_concrete_types() -> Result<(), Box> { + let wasm = wat::parse_str( + r#" + (module + (type $object (struct)) + (import "host" "inspect" (func $inspect (param (ref null $object)))) + (func (export "call") + ref.null $object + call $inspect)) + "#, + )?; + let module = tinywasm::parse_bytes(&wasm)?; + let concrete = RefType::new_concrete(true, 0); + let host_ty = FuncType::new(&[WasmType::Ref(concrete)], &[]); + let host = HostFunction::from_untyped(&host_ty, |_, args| { + assert_eq!(args, &[WasmValue::Ref(RefValue::Null)]); + Ok(Vec::new()) + }); + let mut imports = Imports::new(); + imports.define("host", "inspect", host); + let mut store = Store::default(); + let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports))?; + + instance.func::<(), ()>(&store, "call")?.call(&mut store, ())?; + Ok(()) +} + +#[test] +fn host_tail_calls_return_from_the_current_frame() -> Result<(), Box> { let wasm = wat::parse_str( r#" (module @@ -178,8 +196,8 @@ fn host_tail_calls_return_from_the_current_frame() -> Result<()> { let module = tinywasm::parse_bytes(&wasm)?; let mut store = Store::default(); let mut imports = Imports::new(); - imports.define("host", "answer", HostFunction::from(&mut store, |_, ()| Ok(42_i32))); - let instance = ModuleInstance::instantiate(&mut store, &module, Some(imports))?; + imports.define("host", "answer", HostFunction::from(|_, ()| Ok(42_i32))); + let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports))?; for name in ["direct", "indirect", "reference"] { assert_eq!(instance.func::<(), i32>(&store, name)?.call(&mut store, ())?, 42); @@ -190,13 +208,14 @@ fn host_tail_calls_return_from_the_current_frame() -> Result<()> { } #[test] -fn host_calls_reject_unknown_function_references() { +fn host_calls_reject_unknown_function_references() -> Result<(), Box> { let mut store = Store::default(); let ty = FuncType::new(&[WasmType::Ref(tinywasm::types::RefType::FUNCREF)], &[]); - let host = HostFunction::from_untyped(&mut store, &ty, |_, _| Ok(Vec::new())); + let host = HostFunction::from_untyped(&ty, |_, _| Ok(Vec::new())).instantiate(&mut store)?; let invalid = WasmValue::Ref(RefValue::Func(tinywasm::types::FuncRef::new(u32::MAX))); assert!(host.call(&mut store, &[invalid]).is_err()); + Ok(()) } fn to_name(ty: &WasmType) -> &str { diff --git a/crates/tinywasm/tests/host_import_arg_order.rs b/crates/tinywasm/tests/host_import_arg_order.rs index 2004641b..b04ef77c 100644 --- a/crates/tinywasm/tests/host_import_arg_order.rs +++ b/crates/tinywasm/tests/host_import_arg_order.rs @@ -15,17 +15,16 @@ fn multi_arg_host_imports_preserve_source_order() { .unwrap(); let module = tinywasm::parse_bytes(&wasm).unwrap(); - let mut store = Store::default(); - - let pair = HostFunction::from(&mut store, |_ctx, (left, right): (i32, i32)| -> tinywasm::Result { - Ok(left * 1000 + right) - }); - + let pair = + HostFunction::from(|_ctx, (left, right): (i32, i32)| -> tinywasm::Result { Ok(left * 1000 + right) }); let mut imports = Imports::new(); imports.define("env", "pair", pair); - let instance = ModuleInstance::instantiate(&mut store, &module, Some(imports)).unwrap(); - let func = instance.func::<(i32, i32), i32>(&store, "call_pair").unwrap(); + for _ in 0..2 { + let mut store = Store::default(); + let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports)).unwrap(); + let func = instance.func::<(i32, i32), i32>(&store, "call_pair").unwrap(); - assert_eq!(func.call(&mut store, (12, 34)).unwrap(), 12034); + assert_eq!(func.call(&mut store, (12, 34)).unwrap(), 12034); + } } diff --git a/crates/tinywasm/tests/import_linking.rs b/crates/tinywasm/tests/import_linking.rs index e7948fa1..a65c8a43 100644 --- a/crates/tinywasm/tests/import_linking.rs +++ b/crates/tinywasm/tests/import_linking.rs @@ -1,4 +1,3 @@ -use eyre::Result; use tinywasm::{Error, Imports, Module, ModuleInstance, Store, Trap}; const WASM_ADD: &str = r#" @@ -19,14 +18,14 @@ const WASM_IMPORT: &str = r#" call $add)) "#; -fn parse_modules() -> Result<(Module, Module)> { +fn parse_modules() -> Result<(Module, Module), Box> { let add = tinywasm::parse_bytes(&wat::parse_str(WASM_ADD)?)?; let import = tinywasm::parse_bytes(&wat::parse_str(WASM_IMPORT)?)?; Ok((add, import)) } #[test] -fn link_module_links_same_store_instance() -> Result<()> { +fn link_module_links_same_store_instance() -> Result<(), Box> { let (add_module, import_module) = parse_modules()?; let mut store = Store::default(); @@ -34,14 +33,14 @@ fn link_module_links_same_store_instance() -> Result<()> { let mut imports = Imports::new(); imports.link_module("adder", add_instance)?; - let instance = ModuleInstance::instantiate(&mut store, &import_module, Some(imports))?; + let instance = ModuleInstance::instantiate(&mut store, &import_module, Some(&imports))?; let main = instance.func::<(), i32>(&store, "main")?; assert_eq!(main.call(&mut store, ())?, 3); Ok(()) } #[test] -fn link_module_rejects_cross_store_instance() -> Result<()> { +fn link_module_rejects_cross_store_instance() -> Result<(), Box> { let (add_module, import_module) = parse_modules()?; let mut source_store = Store::default(); @@ -51,7 +50,7 @@ fn link_module_rejects_cross_store_instance() -> Result<()> { let mut imports = Imports::new(); imports.link_module("adder", add_instance)?; - let err = ModuleInstance::instantiate(&mut target_store, &import_module, Some(imports)).unwrap_err(); + let err = ModuleInstance::instantiate(&mut target_store, &import_module, Some(&imports)).unwrap_err(); assert_eq!(err, Error::from(Trap::InvalidStore)); Ok(()) } diff --git a/crates/tinywasm/tests/imported_table_init.rs b/crates/tinywasm/tests/imported_table_init.rs index d7ed636f..aaa781b9 100644 --- a/crates/tinywasm/tests/imported_table_init.rs +++ b/crates/tinywasm/tests/imported_table_init.rs @@ -1,9 +1,8 @@ -use eyre::Result; use tinywasm::types::{FuncRef, RefType, TableType}; use tinywasm::{HostFunction, Imports, ModuleInstance, Store, Table}; #[test] -fn imported_table_uses_provided_init_value() -> Result<()> { +fn imported_table_uses_provided_init_value() -> Result<(), Box> { let wasm = wat::parse_str( r#" (module @@ -19,11 +18,11 @@ fn imported_table_uses_provided_init_value() -> Result<()> { let module = tinywasm::parse_bytes(&wasm)?; let mut store = Store::default(); let mut imports = Imports::new(); - let _function_at_zero = HostFunction::from(&mut store, |_, ()| Ok(())); + let _function_at_zero = HostFunction::from(|_, ()| Ok(())).instantiate(&mut store)?; let table = Table::new(&mut store, TableType::new(RefType::FUNCREF, 3, None), FuncRef::new(0).into())?; imports.define("host", "table", table); - let instance = ModuleInstance::instantiate(&mut store, &module, Some(imports))?; + let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports))?; let slot_is_null = instance.func::(&store, "slot_is_null")?; assert_eq!(slot_is_null.call(&mut store, 0)?, 0); diff --git a/crates/tinywasm/tests/internal_refs.rs b/crates/tinywasm/tests/internal_refs.rs index 7a68f011..07cbde2f 100644 --- a/crates/tinywasm/tests/internal_refs.rs +++ b/crates/tinywasm/tests/internal_refs.rs @@ -1,4 +1,3 @@ -use eyre::Result; use tinywasm::types::WasmValue; #[cfg(feature = "guest-debug")] use tinywasm::types::{FuncRef, RefValue}; @@ -6,7 +5,7 @@ use tinywasm::{ExternItem, ModuleInstance, Store}; #[test] #[cfg(feature = "guest-debug")] -fn private_items_are_accessible_by_index() -> Result<()> { +fn private_items_are_accessible_by_index() -> Result<(), Box> { let wasm = wat::parse_str( r#" (module @@ -42,7 +41,7 @@ fn private_items_are_accessible_by_index() -> Result<()> { } #[test] -fn exported_tables_and_globals_have_handle_and_helper_apis() -> Result<()> { +fn exported_tables_and_globals_have_handle_and_helper_apis() -> Result<(), Box> { let wasm = wat::parse_str( r#" (module @@ -73,7 +72,7 @@ fn exported_tables_and_globals_have_handle_and_helper_apis() -> Result<()> { } #[test] -fn extern_item_lookup_returns_expected_kinds() -> Result<()> { +fn extern_item_lookup_returns_expected_kinds() -> Result<(), Box> { let wasm = wat::parse_str( r#" (module @@ -98,7 +97,7 @@ fn extern_item_lookup_returns_expected_kinds() -> Result<()> { } #[test] -fn extern_item_and_exports_use_actual_function_type() -> Result<()> { +fn extern_item_and_exports_use_actual_function_type() -> Result<(), Box> { let wasm = wat::parse_str( r#" (module @@ -117,9 +116,9 @@ fn extern_item_and_exports_use_actual_function_type() -> Result<()> { imports.define( "host", "imported", - tinywasm::HostFunction::from(&mut store, |_ctx: tinywasm::FuncContext<'_>, _arg: i64| Ok(())), + tinywasm::HostFunction::from(|_ctx: tinywasm::FuncContext<'_>, _arg: i64| Ok(())), ); - let instance = ModuleInstance::instantiate(&mut store, &module, Some(imports))?; + let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports))?; let ExternItem::Func(func) = instance.extern_item("f")? else { panic!("expected function export") }; assert_eq!(func.call(&mut store, &[])?, vec![]); @@ -134,7 +133,7 @@ fn extern_item_and_exports_use_actual_function_type() -> Result<()> { } #[test] -fn export_func_type_index_mismatch_fixture_would_break_old_lookup() -> Result<()> { +fn export_func_type_index_mismatch_fixture_would_break_old_lookup() -> Result<(), Box> { let wasm = wat::parse_str( r#" (module @@ -149,8 +148,13 @@ fn export_func_type_index_mismatch_fixture_would_break_old_lookup() -> Result<() let module = tinywasm::parse_bytes(&wasm)?; let export = module.exports.iter().find(|export| export.name.as_ref() == "f").expect("export f not found"); - let old_lookup_ty = module.func_types.get(export.index as usize).expect("old lookup type missing"); - let func_ty = &module.func_types[module.func_type_idxs[export.index as usize] as usize]; + let old_lookup_ty = module.types.get(export.index).and_then(|ty| ty.as_func()).expect("old lookup type missing"); + let func_ty = module + .types + .get(module.func_type_idxs[export.index as usize]) + .expect("export function type index should exist") + .as_func() + .expect("export function should reference a function type"); assert_eq!(old_lookup_ty.params(), &[tinywasm::types::WasmType::I64]); assert_eq!(func_ty.params(), &[]); @@ -161,9 +165,9 @@ fn export_func_type_index_mismatch_fixture_would_break_old_lookup() -> Result<() imports.define( "spectest", "print_i64", - tinywasm::HostFunction::from(&mut store, |_ctx: tinywasm::FuncContext<'_>, _arg: i64| Ok(())), + tinywasm::HostFunction::from(|_ctx: tinywasm::FuncContext<'_>, _arg: i64| Ok(())), ); - let instance = ModuleInstance::instantiate(&mut store, &module, Some(imports))?; + let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports))?; let ExternItem::Func(func) = instance.extern_item("f")? else { panic!("expected function export") }; assert_eq!(func.call(&mut store, &[])?, vec![]); @@ -172,7 +176,7 @@ fn export_func_type_index_mismatch_fixture_would_break_old_lookup() -> Result<() } #[test] -fn start_resolves_module_func_index_to_store_addr() -> Result<()> { +fn start_resolves_module_func_index_to_store_addr() -> Result<(), Box> { let wasm = wat::parse_str( r#" (module @@ -187,7 +191,8 @@ fn start_resolves_module_func_index_to_store_addr() -> Result<()> { let module = tinywasm::parse_bytes(&wasm)?; let mut store = Store::default(); - let _unused = tinywasm::HostFunction::from(&mut store, |_ctx: tinywasm::FuncContext<'_>, (): ()| Ok(())); + let _unused = + tinywasm::HostFunction::from(|_ctx: tinywasm::FuncContext<'_>, (): ()| Ok(())).instantiate(&mut store)?; let instance = ModuleInstance::instantiate_no_start(&mut store, &module, None)?; instance.start(&mut store)?; diff --git a/crates/tinywasm/tests/memory_backends.rs b/crates/tinywasm/tests/memory_backends.rs index c231bc11..73657df5 100644 --- a/crates/tinywasm/tests/memory_backends.rs +++ b/crates/tinywasm/tests/memory_backends.rs @@ -6,18 +6,26 @@ use core::sync::atomic::{AtomicUsize, Ordering}; #[cfg(feature = "std")] use std::io::{Read, Seek, SeekFrom, Write}; -use eyre::Result; use tinywasm::engine::Config; use tinywasm::types::{MemoryArch, MemoryType}; use tinywasm::{Engine, Memory, MemoryBackend, Module, ModuleInstance, PagedMemory, Store}; use tinywasm_parser::{Parser, ParserOptions}; -fn instantiate_module_with_counting_backend(module: Module) -> Result { +type TestResult = Result>; + +fn initial_memory_size(ty: MemoryType) -> usize { + usize::try_from(ty.page_count_initial()) + .ok() + .and_then(|pages| pages.checked_mul(usize::try_from(ty.page_size()).ok()?)) + .expect("test memory size should fit usize") +} + +fn instantiate_module_with_counting_backend(module: Module) -> TestResult { let created = Arc::new(AtomicUsize::new(0)); let factory_calls = created.clone(); let backend = MemoryBackend::custom(move |ty| { factory_calls.fetch_add(1, Ordering::Relaxed); - Ok(PagedMemory::try_new(ty.initial_size() as usize, 16)?) + PagedMemory::try_new(initial_memory_size(ty), 16) }); let engine = Engine::new(Config::new().with_memory_backend(backend)); let mut store = Store::new(engine); @@ -27,7 +35,7 @@ fn instantiate_module_with_counting_backend(module: Module) -> Result { Ok(created.load(Ordering::Relaxed)) } -fn instantiate_with_counting_backend(wat: &str) -> Result { +fn instantiate_with_counting_backend(wat: &str) -> TestResult { let wasm = wat::parse_str(wat)?; let module = tinywasm::parse_bytes(&wasm)?; instantiate_module_with_counting_backend(module) @@ -35,14 +43,14 @@ fn instantiate_with_counting_backend(wat: &str) -> Result { fn instantiate_exported_memory_with_counting_backend( wat: &str, -) -> Result<(Store, tinywasm::ModuleInstance, Arc)> { +) -> TestResult<(Store, tinywasm::ModuleInstance, Arc)> { let wasm = wat::parse_str(wat)?; let module = tinywasm::parse_bytes(&wasm)?; let created = Arc::new(AtomicUsize::new(0)); let factory_calls = created.clone(); let backend = MemoryBackend::custom(move |ty| { factory_calls.fetch_add(1, Ordering::Relaxed); - Ok(PagedMemory::try_new(ty.initial_size() as usize, 16)?) + PagedMemory::try_new(initial_memory_size(ty), 16) }); let engine = Engine::new(Config::new().with_memory_backend(backend)); let mut store = Store::new(engine); @@ -51,7 +59,7 @@ fn instantiate_exported_memory_with_counting_backend( } #[test] -fn paged_backend_works_for_module_memories() -> Result<()> { +fn paged_backend_works_for_module_memories() -> TestResult { let wasm = wat::parse_str( r#" (module @@ -73,7 +81,7 @@ fn paged_backend_works_for_module_memories() -> Result<()> { } #[test] -fn custom_backend_factory_is_used_for_host_memories() -> Result<()> { +fn custom_backend_factory_is_used_for_host_memories() -> TestResult { let created = Arc::new(AtomicUsize::new(0)); let seen_page_size = Arc::new(AtomicUsize::new(0)); let factory_calls = created.clone(); @@ -82,7 +90,7 @@ fn custom_backend_factory_is_used_for_host_memories() -> Result<()> { let backend = MemoryBackend::custom(move |ty| { factory_calls.fetch_add(1, Ordering::Relaxed); page_size_seen.store(ty.page_size() as usize, Ordering::Relaxed); - Ok(PagedMemory::try_new(ty.initial_size() as usize, 16)?) + PagedMemory::try_new(initial_memory_size(ty), 16) }); let engine = Engine::new(Config::new().with_memory_backend(backend)); @@ -100,7 +108,7 @@ fn custom_backend_factory_is_used_for_host_memories() -> Result<()> { } #[test] -fn local_memory_without_observable_use_is_not_allocated() -> Result<()> { +fn local_memory_without_observable_use_is_not_allocated() -> TestResult { let created = instantiate_with_counting_backend( r#" (module @@ -115,7 +123,7 @@ fn local_memory_without_observable_use_is_not_allocated() -> Result<()> { } #[test] -fn exported_local_memory_is_not_eagerly_allocated() -> Result<()> { +fn exported_local_memory_is_not_eagerly_allocated() -> TestResult { let created = instantiate_with_counting_backend( r#" (module @@ -129,7 +137,7 @@ fn exported_local_memory_is_not_eagerly_allocated() -> Result<()> { } #[test] -fn exported_local_memory_reads_zeroes_without_materializing() -> Result<()> { +fn exported_local_memory_reads_zeroes_without_materializing() -> TestResult { let (mut store, instance, created) = instantiate_exported_memory_with_counting_backend( r#" (module @@ -151,7 +159,7 @@ fn exported_local_memory_reads_zeroes_without_materializing() -> Result<()> { } #[test] -fn active_data_segment_on_local_memory_is_allocated() -> Result<()> { +fn active_data_segment_on_local_memory_is_allocated() -> TestResult { let created = instantiate_with_counting_backend( r#" (module @@ -166,7 +174,7 @@ fn active_data_segment_on_local_memory_is_allocated() -> Result<()> { } #[test] -fn local_memory_instruction_is_allocated() -> Result<()> { +fn local_memory_instruction_is_allocated() -> TestResult { let created = instantiate_with_counting_backend( r#" (module @@ -181,7 +189,7 @@ fn local_memory_instruction_is_allocated() -> Result<()> { } #[test] -fn disabled_local_memory_allocation_optimization_keeps_old_behavior() -> Result<()> { +fn disabled_local_memory_allocation_optimization_keeps_old_behavior() -> TestResult { let wasm = wat::parse_str( r#" (module @@ -190,7 +198,7 @@ fn disabled_local_memory_allocation_optimization_keeps_old_behavior() -> Result< ) "#, )?; - let parser = Parser::with_options(ParserOptions::default().with_local_memory_allocation_optimization(false)); + let parser = Parser::new(ParserOptions::default().with_local_memory_allocation_optimization(false)); let module = parser.parse_module_bytes(&wasm)?; let created = instantiate_module_with_counting_backend(module)?; @@ -200,7 +208,7 @@ fn disabled_local_memory_allocation_optimization_keeps_old_behavior() -> Result< } #[test] -fn read_returns_short_count_at_end_of_memory() -> Result<()> { +fn read_returns_short_count_at_end_of_memory() -> TestResult { let mut store = Store::default(); let memory = Memory::new(&mut store, MemoryType::new(MemoryArch::I32, 1, Some(1), Some(4)))?; memory.copy_from_slice(&mut store, 0, &[1, 2, 3, 4])?; @@ -214,7 +222,7 @@ fn read_returns_short_count_at_end_of_memory() -> Result<()> { } #[test] -fn paged_read_and_write_stop_at_chunk_boundaries() -> Result<()> { +fn paged_read_and_write_stop_at_chunk_boundaries() -> TestResult { let engine = Engine::new(Config::new().with_memory_backend(MemoryBackend::paged(4))); let mut store = Store::new(engine); let memory = Memory::new(&mut store, MemoryType::new(MemoryArch::I32, 1, Some(1), Some(16)))?; @@ -239,9 +247,46 @@ fn paged_read_and_write_stop_at_chunk_boundaries() -> Result<()> { Ok(()) } +#[test] +fn paged_fixed_width_accesses_cross_chunk_boundaries() -> TestResult { + use tinywasm::LinearMemory; + + let mut memory = PagedMemory::try_new(32, 4).map_err(tinywasm::Error::from)?; + memory.write_32(2, &0x1234_5678u32.to_le_bytes()).map_err(tinywasm::Error::from)?; + memory.write_64(6, &0x0123_4567_89ab_cdefu64.to_le_bytes()).map_err(tinywasm::Error::from)?; + memory.write_128(14, &u128::MAX.to_le_bytes()).map_err(tinywasm::Error::from)?; + + assert_eq!(u32::from_le_bytes(memory.read_32(2).map_err(tinywasm::Error::from)?), 0x1234_5678); + assert_eq!(u64::from_le_bytes(memory.read_64(6).map_err(tinywasm::Error::from)?), 0x0123_4567_89ab_cdef); + assert_eq!(u128::from_le_bytes(memory.read_128(14).map_err(tinywasm::Error::from)?), u128::MAX); + Ok(()) +} + +#[test] +fn lazy_custom_backend_creation_trap_is_propagated() -> TestResult { + let wasm = wat::parse_str(r#"(module (memory (export "memory") 1))"#)?; + let module = tinywasm::parse_bytes(&wasm)?; + let backend = MemoryBackend::custom(|_| Err::(tinywasm::Trap::Other("backend unavailable"))); + let mut store = Store::new(Engine::new(Config::new().with_memory_backend(backend))); + let instance = ModuleInstance::instantiate(&mut store, &module, None)?; + let memory = instance.memory("memory")?; + + assert_eq!( + memory.copy_from_slice(&mut store, 0, &[1]).unwrap_err(), + tinywasm::Error::from(tinywasm::Trap::Other("backend unavailable")) + ); + Ok(()) +} + +#[test] +fn memory64_default_limit_is_not_memory32_limit() { + let ty = MemoryType::new(MemoryArch::I64, 65_537, None, None); + assert!(ty.page_count_max() > 65_536); +} + #[cfg(feature = "std")] #[test] -fn memory_cursor_supports_read_write_and_seek() -> Result<()> { +fn memory_cursor_supports_read_write_and_seek() -> TestResult { let mut store = Store::default(); let memory = Memory::new(&mut store, MemoryType::new(MemoryArch::I32, 1, Some(1), Some(8)))?; diff --git a/crates/tinywasm/tests/memory_ref_api.rs b/crates/tinywasm/tests/memory_ref_api.rs index 6f724775..768c8730 100644 --- a/crates/tinywasm/tests/memory_ref_api.rs +++ b/crates/tinywasm/tests/memory_ref_api.rs @@ -1,8 +1,7 @@ -use eyre::Result; use tinywasm::{ModuleInstance, Store}; #[test] -fn memory_ref_mut_copy_within_uses_src_then_dst_order() -> Result<()> { +fn memory_ref_mut_copy_within_uses_src_then_dst_order() -> Result<(), Box> { let wasm = wat::parse_str( r#" (module diff --git a/crates/tinywasm/tests/module_descriptors.rs b/crates/tinywasm/tests/module_descriptors.rs index 81ab5abc..8bd73b6c 100644 --- a/crates/tinywasm/tests/module_descriptors.rs +++ b/crates/tinywasm/tests/module_descriptors.rs @@ -1,9 +1,7 @@ -use eyre::Result; -use tinywasm::types::WasmType; -use tinywasm_types::{ExportType, ImportType}; +use tinywasm::types::{ExportType, ImportType, WasmType}; #[test] -fn module_descriptors_resolve_imported_and_local_export_types() -> Result<()> { +fn module_descriptors_resolve_imported_and_local_export_types() -> Result<(), Box> { let wasm = wat::parse_str( r#" (module @@ -91,7 +89,7 @@ fn module_descriptors_resolve_imported_and_local_export_types() -> Result<()> { } #[test] -fn module_descriptors_resolve_imported_and_local_table_and_memory_exports() -> Result<()> { +fn module_descriptors_resolve_imported_and_local_table_and_memory_exports() -> Result<(), Box> { let wasm = wat::parse_str( r#" (module diff --git a/crates/tinywasm/tests/resume_execution.rs b/crates/tinywasm/tests/resume_execution.rs index 060cefb4..2798e7bf 100644 --- a/crates/tinywasm/tests/resume_execution.rs +++ b/crates/tinywasm/tests/resume_execution.rs @@ -1,6 +1,5 @@ -use eyre::Result; use tinywasm::engine::{Config, FuelPolicy}; -use tinywasm::{ExecProgress, ModuleInstance, types::WasmValue}; +use tinywasm::{ExecProgress, ModuleInstance, Result, types::WasmValue}; #[cfg(feature = "std")] use std::time::Duration; diff --git a/crates/tinywasm/tests/start_function.rs b/crates/tinywasm/tests/start_function.rs index 077c0fc1..912b200d 100644 --- a/crates/tinywasm/tests/start_function.rs +++ b/crates/tinywasm/tests/start_function.rs @@ -1,8 +1,7 @@ -use eyre::Result; use tinywasm::{ModuleInstance, Store}; #[test] -fn exported_wasi_start_is_not_run_during_instantiation() -> Result<()> { +fn exported_wasi_start_is_not_run_during_instantiation() -> Result<(), Box> { let wasm = wat::parse_str( r#" (module diff --git a/crates/tinywasm/tests/store_ownership.rs b/crates/tinywasm/tests/store_ownership.rs index 8577d73f..a5088176 100644 --- a/crates/tinywasm/tests/store_ownership.rs +++ b/crates/tinywasm/tests/store_ownership.rs @@ -1,5 +1,5 @@ -use eyre::Result; -use tinywasm::{ModuleInstance, Store}; +use tinywasm::types::{GlobalType, WasmType}; +use tinywasm::{Global, Imports, ModuleInstance, Store}; const MODULE_WAT: &str = r#" (module @@ -12,7 +12,7 @@ const MODULE_WAT: &str = r#" "#; #[test] -fn func_handle_rejects_wrong_store() -> Result<()> { +fn func_handle_rejects_wrong_store() -> Result<(), Box> { let wasm = wat::parse_str(MODULE_WAT)?; let module = tinywasm::parse_bytes(&wasm)?; @@ -28,7 +28,7 @@ fn func_handle_rejects_wrong_store() -> Result<()> { } #[test] -fn memory_access_rejects_wrong_store() -> Result<()> { +fn memory_access_rejects_wrong_store() -> Result<(), Box> { let wasm = wat::parse_str(MODULE_WAT)?; let module = tinywasm::parse_bytes(&wasm)?; @@ -44,7 +44,7 @@ fn memory_access_rejects_wrong_store() -> Result<()> { } #[test] -fn global_access_rejects_wrong_store() -> Result<()> { +fn global_access_rejects_wrong_store() -> Result<(), Box> { let wasm = wat::parse_str( r#" (module @@ -66,7 +66,7 @@ fn global_access_rejects_wrong_store() -> Result<()> { } #[test] -fn table_grow_rejects_wrong_store_with_invalid_store_error() -> Result<()> { +fn table_grow_rejects_wrong_store_with_invalid_store_error() -> Result<(), Box> { let wasm = wat::parse_str( r#" (module @@ -86,3 +86,18 @@ fn table_grow_rejects_wrong_store_with_invalid_store_error() -> Result<()> { Ok(()) } + +#[test] +fn global_import_rejects_wrong_store() -> Result<(), Box> { + let wasm = wat::parse_str(r#"(module (import "env" "g" (global i32)))"#)?; + let module = tinywasm::parse_bytes(&wasm)?; + let mut owner_store = Store::default(); + let global = Global::new(&mut owner_store, GlobalType::new(WasmType::I32, false), 1.into())?; + let mut imports = Imports::default(); + imports.define("env", "g", global); + + let mut other_store = Store::default(); + let err = ModuleInstance::instantiate(&mut other_store, &module, Some(&imports)).unwrap_err(); + assert_eq!(err, tinywasm::Error::Trap(tinywasm::Trap::InvalidStore)); + Ok(()) +} diff --git a/crates/tinywasm/tests/test-wasm-1.rs b/crates/tinywasm/tests/test-wasm-1.rs index be235ae5..723f0078 100644 --- a/crates/tinywasm/tests/test-wasm-1.rs +++ b/crates/tinywasm/tests/test-wasm-1.rs @@ -1,9 +1,7 @@ -mod testsuite; -use eyre::Result; -use testsuite::TestSuite; +use tinywasm_cli::testsuite::TestSuite; use wasm_testsuite::data::{SpecVersion, spec}; -fn main() -> Result<()> { +fn main() -> Result<(), Box> { TestSuite::set_log_level(log::LevelFilter::Off); let mut test_suite = TestSuite::new(); diff --git a/crates/tinywasm/tests/test-wasm-2.rs b/crates/tinywasm/tests/test-wasm-2.rs index 0379ca6e..c762eec2 100644 --- a/crates/tinywasm/tests/test-wasm-2.rs +++ b/crates/tinywasm/tests/test-wasm-2.rs @@ -1,9 +1,7 @@ -mod testsuite; -use eyre::Result; -use testsuite::TestSuite; +use tinywasm_cli::testsuite::TestSuite; use wasm_testsuite::data::{SpecVersion, spec}; -fn main() -> Result<()> { +fn main() -> Result<(), Box> { TestSuite::set_log_level(log::LevelFilter::Off); let mut test_suite = TestSuite::new(); diff --git a/crates/tinywasm/tests/test-wasm-3.rs b/crates/tinywasm/tests/test-wasm-3.rs index 11ebbc84..a843ae01 100644 --- a/crates/tinywasm/tests/test-wasm-3.rs +++ b/crates/tinywasm/tests/test-wasm-3.rs @@ -1,9 +1,7 @@ -mod testsuite; -use eyre::Result; -use testsuite::TestSuite; +use tinywasm_cli::testsuite::TestSuite; use wasm_testsuite::data::{SpecVersion, spec}; -fn main() -> Result<()> { +fn main() -> Result<(), Box> { TestSuite::set_log_level(log::LevelFilter::Off); let mut test_suite = TestSuite::new(); diff --git a/crates/tinywasm/tests/test-wasm-annotations.rs b/crates/tinywasm/tests/test-wasm-annotations.rs deleted file mode 100644 index 8655d152..00000000 --- a/crates/tinywasm/tests/test-wasm-annotations.rs +++ /dev/null @@ -1,13 +0,0 @@ -mod testsuite; -use eyre::Result; -use testsuite::TestSuite; -use wasm_testsuite::data::{Proposal, proposal}; - -fn main() -> Result<()> { - TestSuite::set_log_level(log::LevelFilter::Off); - - let mut test_suite = TestSuite::new(); - test_suite.run_files(proposal(&Proposal::Annotations))?; - test_suite.save_csv("./tests/generated/wasm-annotations.csv", env!("CARGO_PKG_VERSION"))?; - test_suite.report_status() -} diff --git a/crates/tinywasm/tests/test-wasm-custom-page-sizes.rs b/crates/tinywasm/tests/test-wasm-custom-page-sizes.rs index e4aacfc8..a71266a0 100644 --- a/crates/tinywasm/tests/test-wasm-custom-page-sizes.rs +++ b/crates/tinywasm/tests/test-wasm-custom-page-sizes.rs @@ -1,9 +1,7 @@ -mod testsuite; -use eyre::Result; -use testsuite::TestSuite; +use tinywasm_cli::testsuite::TestSuite; use wasm_testsuite::data::{Proposal, proposal}; -fn main() -> Result<()> { +fn main() -> Result<(), Box> { TestSuite::set_log_level(log::LevelFilter::Off); let mut test_suite = TestSuite::new(); diff --git a/crates/tinywasm/tests/test-wasm-custom.rs b/crates/tinywasm/tests/test-wasm-custom.rs index ae081420..b516428f 100644 --- a/crates/tinywasm/tests/test-wasm-custom.rs +++ b/crates/tinywasm/tests/test-wasm-custom.rs @@ -1,8 +1,6 @@ -mod testsuite; -use eyre::Result; -use testsuite::TestSuite; +use tinywasm_cli::testsuite::TestSuite; -fn main() -> Result<()> { +fn main() -> Result<(), Box> { TestSuite::set_log_level(log::LevelFilter::Off); let custom_dir = std::path::Path::new("./tests/wasm-custom"); diff --git a/crates/tinywasm/tests/test-wasm-extended-const.rs b/crates/tinywasm/tests/test-wasm-extended-const.rs deleted file mode 100644 index 888a49fd..00000000 --- a/crates/tinywasm/tests/test-wasm-extended-const.rs +++ /dev/null @@ -1,13 +0,0 @@ -mod testsuite; -use eyre::Result; -use testsuite::TestSuite; -use wasm_testsuite::data::{Proposal, proposal}; - -fn main() -> Result<()> { - TestSuite::set_log_level(log::LevelFilter::Off); - - let mut test_suite = TestSuite::new(); - test_suite.run_files(proposal(&Proposal::ExtendedConst))?; - test_suite.save_csv("./tests/generated/wasm-extended-const.csv", env!("CARGO_PKG_VERSION"))?; - test_suite.report_status() -} diff --git a/crates/tinywasm/tests/test-wasm-function-references.rs b/crates/tinywasm/tests/test-wasm-function-references.rs deleted file mode 100644 index d91ff82b..00000000 --- a/crates/tinywasm/tests/test-wasm-function-references.rs +++ /dev/null @@ -1,13 +0,0 @@ -mod testsuite; -use eyre::Result; -use testsuite::TestSuite; -use wasm_testsuite::data::{Proposal, proposal}; - -fn main() -> Result<()> { - TestSuite::set_log_level(log::LevelFilter::Off); - - let mut test_suite = TestSuite::new(); - test_suite.run_files(proposal(&Proposal::FunctionReferences))?; - test_suite.save_csv("./tests/generated/wasm-function-references.csv", env!("CARGO_PKG_VERSION"))?; - test_suite.report_status() -} diff --git a/crates/tinywasm/tests/test-wasm-gc.rs b/crates/tinywasm/tests/test-wasm-gc.rs index a80d347e..86496c47 100644 --- a/crates/tinywasm/tests/test-wasm-gc.rs +++ b/crates/tinywasm/tests/test-wasm-gc.rs @@ -1,9 +1,7 @@ -mod testsuite; -use eyre::Result; -use testsuite::TestSuite; +use tinywasm_cli::testsuite::TestSuite; use wasm_testsuite::data::{Proposal, proposal}; -fn main() -> Result<()> { +fn main() -> Result<(), Box> { TestSuite::set_log_level(log::LevelFilter::Off); let mut test_suite = TestSuite::new(); diff --git a/crates/tinywasm/tests/test-wasm-latest.rs b/crates/tinywasm/tests/test-wasm-latest.rs index af83c2f3..649c44ca 100644 --- a/crates/tinywasm/tests/test-wasm-latest.rs +++ b/crates/tinywasm/tests/test-wasm-latest.rs @@ -1,9 +1,7 @@ -mod testsuite; -use eyre::Result; -use testsuite::TestSuite; +use tinywasm_cli::testsuite::TestSuite; use wasm_testsuite::data::{SpecVersion, spec}; -fn main() -> Result<()> { +fn main() -> Result<(), Box> { TestSuite::set_log_level(log::LevelFilter::Off); let mut test_suite = TestSuite::new(); diff --git a/crates/tinywasm/tests/test-wasm-memory64.rs b/crates/tinywasm/tests/test-wasm-memory64.rs index 447bbfc3..717ae030 100644 --- a/crates/tinywasm/tests/test-wasm-memory64.rs +++ b/crates/tinywasm/tests/test-wasm-memory64.rs @@ -1,9 +1,7 @@ -mod testsuite; -use eyre::Result; -use testsuite::TestSuite; +use tinywasm_cli::testsuite::TestSuite; use wasm_testsuite::data::{Proposal, proposal}; -fn main() -> Result<()> { +fn main() -> Result<(), Box> { TestSuite::set_log_level(log::LevelFilter::Off); let mut test_suite = TestSuite::new(); diff --git a/crates/tinywasm/tests/test-wasm-multi-memory.rs b/crates/tinywasm/tests/test-wasm-multi-memory.rs index e4487f11..b0963b26 100644 --- a/crates/tinywasm/tests/test-wasm-multi-memory.rs +++ b/crates/tinywasm/tests/test-wasm-multi-memory.rs @@ -1,9 +1,7 @@ -mod testsuite; -use eyre::Result; -use testsuite::TestSuite; +use tinywasm_cli::testsuite::TestSuite; use wasm_testsuite::data::{Proposal, proposal}; -fn main() -> Result<()> { +fn main() -> Result<(), Box> { TestSuite::set_log_level(log::LevelFilter::Off); let mut test_suite = TestSuite::new(); diff --git a/crates/tinywasm/tests/test-wasm-nontrapping-float-to-int-conversions.rs b/crates/tinywasm/tests/test-wasm-nontrapping-float-to-int-conversions.rs deleted file mode 100644 index 0475fac7..00000000 --- a/crates/tinywasm/tests/test-wasm-nontrapping-float-to-int-conversions.rs +++ /dev/null @@ -1,14 +0,0 @@ -mod testsuite; -use eyre::Result; -use testsuite::TestSuite; -use wasm_testsuite::data::{Proposal, proposal}; - -fn main() -> Result<()> { - TestSuite::set_log_level(log::LevelFilter::Off); - let mut test_suite = TestSuite::new(); - - test_suite.run_files(proposal(&Proposal::NontrappingFloatToIntConversions))?; - test_suite - .save_csv("./tests/generated/wasm-nontrapping-float-to-int-conversions.csv", env!("CARGO_PKG_VERSION"))?; - test_suite.report_status() -} diff --git a/crates/tinywasm/tests/test-wasm-reference-types.rs b/crates/tinywasm/tests/test-wasm-reference-types.rs deleted file mode 100644 index b6f11553..00000000 --- a/crates/tinywasm/tests/test-wasm-reference-types.rs +++ /dev/null @@ -1,13 +0,0 @@ -mod testsuite; -use eyre::Result; -use testsuite::TestSuite; -use wasm_testsuite::data::{Proposal, proposal}; - -fn main() -> Result<()> { - TestSuite::set_log_level(log::LevelFilter::Off); - let mut test_suite = TestSuite::new(); - - test_suite.run_files(proposal(&Proposal::ReferenceTypes))?; - test_suite.save_csv("./tests/generated/wasm-reference-types.csv", env!("CARGO_PKG_VERSION"))?; - test_suite.report_status() -} diff --git a/crates/tinywasm/tests/test-wasm-relaxed-simd.rs b/crates/tinywasm/tests/test-wasm-relaxed-simd.rs index 0851d187..92a5991e 100644 --- a/crates/tinywasm/tests/test-wasm-relaxed-simd.rs +++ b/crates/tinywasm/tests/test-wasm-relaxed-simd.rs @@ -1,9 +1,7 @@ -mod testsuite; -use eyre::Result; -use testsuite::TestSuite; +use tinywasm_cli::testsuite::TestSuite; use wasm_testsuite::data::{Proposal, proposal}; -fn main() -> Result<()> { +fn main() -> Result<(), Box> { TestSuite::set_log_level(log::LevelFilter::Off); let mut test_suite = TestSuite::new(); diff --git a/crates/tinywasm/tests/test-wasm-sign-extension-op.rs b/crates/tinywasm/tests/test-wasm-sign-extension-op.rs deleted file mode 100644 index c0ef1436..00000000 --- a/crates/tinywasm/tests/test-wasm-sign-extension-op.rs +++ /dev/null @@ -1,13 +0,0 @@ -mod testsuite; -use eyre::Result; -use testsuite::TestSuite; -use wasm_testsuite::data::{Proposal, proposal}; - -fn main() -> Result<()> { - TestSuite::set_log_level(log::LevelFilter::Off); - let mut test_suite = TestSuite::new(); - - test_suite.run_files(proposal(&Proposal::SignExtensionOps))?; - test_suite.save_csv("./tests/generated/wasm-sign-extension-ops.csv", env!("CARGO_PKG_VERSION"))?; - test_suite.report_status() -} diff --git a/crates/tinywasm/tests/test-wasm-simd.rs b/crates/tinywasm/tests/test-wasm-simd.rs index d8f15ea2..3d1932f6 100644 --- a/crates/tinywasm/tests/test-wasm-simd.rs +++ b/crates/tinywasm/tests/test-wasm-simd.rs @@ -1,9 +1,7 @@ -mod testsuite; -use eyre::Result; -use testsuite::TestSuite; +use tinywasm_cli::testsuite::TestSuite; use wasm_testsuite::data::{Proposal, proposal}; -fn main() -> Result<()> { +fn main() -> Result<(), Box> { TestSuite::set_log_level(log::LevelFilter::Off); let mut test_suite = TestSuite::new(); diff --git a/crates/tinywasm/tests/test-wasm-tail-call.rs b/crates/tinywasm/tests/test-wasm-tail-call.rs deleted file mode 100644 index b6d8b2bf..00000000 --- a/crates/tinywasm/tests/test-wasm-tail-call.rs +++ /dev/null @@ -1,14 +0,0 @@ -mod testsuite; -use eyre::Result; -use testsuite::TestSuite; -use wasm_testsuite::data::{Proposal, proposal}; - -fn main() -> Result<()> { - TestSuite::set_log_level(log::LevelFilter::Off); - - let mut test_suite = TestSuite::new(); - test_suite.run_files(proposal(&Proposal::TailCall))?; - test_suite.print_errors(); - test_suite.save_csv("./tests/generated/wasm-tail-call.csv", env!("CARGO_PKG_VERSION"))?; - test_suite.report_status() -} diff --git a/crates/tinywasm/tests/test-wasm-threads.rs b/crates/tinywasm/tests/test-wasm-threads.rs index 2fd5a9b6..5cff2350 100644 --- a/crates/tinywasm/tests/test-wasm-threads.rs +++ b/crates/tinywasm/tests/test-wasm-threads.rs @@ -1,9 +1,7 @@ -mod testsuite; -use eyre::Result; -use testsuite::TestSuite; +use tinywasm_cli::testsuite::TestSuite; use wasm_testsuite::data::{Proposal, proposal}; -fn main() -> Result<()> { +fn main() -> Result<(), Box> { TestSuite::set_log_level(log::LevelFilter::Off); let mut test_suite = TestSuite::new(); diff --git a/crates/tinywasm/tests/test-wasm-wide-arithmetic.rs b/crates/tinywasm/tests/test-wasm-wide-arithmetic.rs index 2aa6b435..e4b56479 100644 --- a/crates/tinywasm/tests/test-wasm-wide-arithmetic.rs +++ b/crates/tinywasm/tests/test-wasm-wide-arithmetic.rs @@ -1,9 +1,7 @@ -mod testsuite; -use eyre::Result; -use testsuite::TestSuite; +use tinywasm_cli::testsuite::TestSuite; use wasm_testsuite::data::{Proposal, proposal}; -fn main() -> Result<()> { +fn main() -> Result<(), Box> { TestSuite::set_log_level(log::LevelFilter::Off); let mut test_suite = TestSuite::new(); diff --git a/crates/tinywasm/tests/test-wast.rs b/crates/tinywasm/tests/test-wast.rs index 92fcbd24..004bafa0 100644 --- a/crates/tinywasm/tests/test-wast.rs +++ b/crates/tinywasm/tests/test-wast.rs @@ -1,12 +1,9 @@ -use std::path::PathBuf; - -use eyre::{Result, bail}; use tinywasm_cli::wast_runner::WastRunner; -fn main() -> Result<()> { - let args = std::env::args().collect::>(); - if args.len() < 2 { - bail!("usage: cargo test-wast "); +fn main() -> Result<(), Box> { + let Some(input) = std::env::args().nth(1) else { + eprintln!("usage: cargo test-wast "); + std::process::exit(2); }; let mut cwd = std::env::current_dir()?; @@ -15,8 +12,7 @@ fn main() -> Result<()> { cwd.pop(); } - // if its a folder, run all the wast files in the folder - let arg = PathBuf::from(cwd.clone()).join(&args[1]); + let arg = cwd.join(input); println!("running tests in {:?}", arg); let files = if arg.is_dir() { @@ -26,5 +22,6 @@ fn main() -> Result<()> { }; let mut test_suite = WastRunner::new(); - test_suite.run_paths(&files) + test_suite.run_paths(&files)?; + Ok(()) } diff --git a/crates/tinywasm/tests/typed_globals.rs b/crates/tinywasm/tests/typed_globals.rs new file mode 100644 index 00000000..e57309f5 --- /dev/null +++ b/crates/tinywasm/tests/typed_globals.rs @@ -0,0 +1,92 @@ +use tinywasm::types::{GlobalType, Instruction, RefValue, WasmType, WasmValue}; +use tinywasm::{Global, Imports, ModuleInstance, Store}; + +#[test] +fn globals_use_typed_instructions_and_roundtrip_values() -> Result<(), Box> { + let wasm = wat::parse_str( + r#" + (module + (global (mut i32) (i32.const 1)) + (global (mut f32) (f32.const 2)) + (global (mut i64) (i64.const 3)) + (global (mut f64) (f64.const 4)) + (global (mut v128) (v128.const i32x4 1 2 3 4)) + (global (mut funcref) (ref.null func)) + + (func (export "i32") (param i32) (result i32) + local.get 0 global.set 0 global.get 0) + (func (export "f32") (param f32) (result f32) + local.get 0 global.set 1 global.get 1) + (func (export "i64") (param i64) (result i64) + local.get 0 global.set 2 global.get 2) + (func (export "f64") (param f64) (result f64) + local.get 0 global.set 3 global.get 3) + (func (export "v128") (param v128) (result v128) + local.get 0 global.set 4 global.get 4) + (func (export "ref") (param funcref) (result funcref) + local.get 0 global.set 5 global.get 5) + (func (export "add-i32") (param i32) (result i32) + local.get 0 global.get 0 i32.add) + (func (export "add-i64") (param i64) (result i64) + local.get 0 global.get 2 i64.add) + ) + "#, + )?; + + let module = tinywasm::parse_bytes(&wasm)?; + let instructions = module.funcs.iter().flat_map(|func| func.instructions.iter()); + let (mut tee32, mut tee64, mut tee128, mut fused32, mut fused64) = (0, 0, 0, 0, 0); + for instruction in instructions { + match instruction { + Instruction::GlobalTee32(_) => tee32 += 1, + Instruction::GlobalTee64(_) => tee64 += 1, + Instruction::GlobalTee128(_) => tee128 += 1, + Instruction::BinOpStackGlobal32(..) => fused32 += 1, + Instruction::BinOpStackGlobal64(..) => fused64 += 1, + _ => {} + } + } + assert_eq!((tee32, tee64, tee128), (3, 2, 1)); + assert_eq!((fused32, fused64), (1, 1)); + + let mut store = Store::default(); + let instance = ModuleInstance::instantiate(&mut store, &module, None)?; + let cases = [ + ("i32", WasmValue::I32(11)), + ("f32", WasmValue::F32(12.5)), + ("i64", WasmValue::I64(13)), + ("f64", WasmValue::F64(14.5)), + ("v128", WasmValue::V128([15; 16])), + ("ref", WasmValue::Ref(RefValue::Null)), + ]; + for (name, value) in cases { + assert_eq!(instance.func_untyped(&store, name)?.call(&mut store, &[value])?, vec![value]); + } + assert_eq!(instance.func::(&store, "add-i32")?.call(&mut store, 2)?, 13); + assert_eq!(instance.func::(&store, "add-i64")?.call(&mut store, 2)?, 15); + + Ok(()) +} + +#[test] +fn imported_global_keeps_its_typed_store_address() -> Result<(), Box> { + let wasm = wat::parse_str( + r#" + (module + (import "env" "g" (global (mut i64))) + (func (export "roundtrip") (param i64) (result i64) + local.get 0 global.set 0 global.get 0) + ) + "#, + )?; + let module = tinywasm::parse_bytes(&wasm)?; + let mut store = Store::default(); + let global = Global::new(&mut store, GlobalType::new(WasmType::I64, true), WasmValue::I64(1))?; + let mut imports = Imports::default(); + imports.define("env", "g", global); + let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports))?; + + assert_eq!(instance.func::(&store, "roundtrip")?.call(&mut store, 42)?, 42); + assert_eq!(global.get(&store)?, WasmValue::I64(42)); + Ok(()) +} diff --git a/crates/tinywasm/tests/typed_lookup.rs b/crates/tinywasm/tests/typed_lookup.rs index 15b31b11..c5e052ee 100644 --- a/crates/tinywasm/tests/typed_lookup.rs +++ b/crates/tinywasm/tests/typed_lookup.rs @@ -1,8 +1,7 @@ -use eyre::Result; use tinywasm::ModuleInstance; #[test] -fn func_typed_rejects_wrong_param_or_result_types() -> Result<()> { +fn func_typed_rejects_wrong_param_or_result_types() -> Result<(), Box> { let wasm = wat::parse_str( r#" (module @@ -26,7 +25,7 @@ fn func_typed_rejects_wrong_param_or_result_types() -> Result<()> { } #[test] -fn func_typed_rejects_partial_multi_value_results() -> Result<()> { +fn func_typed_rejects_partial_multi_value_results() -> Result<(), Box> { let wasm = wat::parse_str( r#" (module diff --git a/crates/tinywasm/tests/wasm-custom/memory64-bulk.wast b/crates/tinywasm/tests/wasm-custom/memory64-bulk.wast new file mode 100644 index 00000000..3302a727 --- /dev/null +++ b/crates/tinywasm/tests/wasm-custom/memory64-bulk.wast @@ -0,0 +1,74 @@ +;; Memory64 bulk operations use the target memory's address width. +(module + (memory (export "memory") i64 1) + (data "abc") + (func (export "run") + (memory.init 0 (i64.const 4) (i32.const 0) (i32.const 3)) + (memory.copy (i64.const 8) (i64.const 4) (i64.const 3)) + (memory.fill (i64.const 12) (i32.const 120) (i64.const 2))) + (func (export "load8") (param i64) (result i32) + (i32.load8_u (local.get 0)))) + +(invoke "run") +(assert_return (invoke "load8" (i64.const 4)) (i32.const 97)) +(assert_return (invoke "load8" (i64.const 6)) (i32.const 99)) +(assert_return (invoke "load8" (i64.const 8)) (i32.const 97)) +(assert_return (invoke "load8" (i64.const 10)) (i32.const 99)) +(assert_return (invoke "load8" (i64.const 12)) (i32.const 120)) +(assert_return (invoke "load8" (i64.const 13)) (i32.const 120)) + +;; Mixed-width memory.copy pops each operand from the correct value lane. +(module + (memory $src 1) + (memory $dst i64 1) + (data (memory $src) (i32.const 0) "abc") + (func (export "run") + (memory.copy $dst $src (i64.const 5) (i32.const 0) (i32.const 3))) + (func (export "load8") (param i64) (result i32) + (i32.load8_u $dst (local.get 0)))) + +(invoke "run") +(assert_return (invoke "load8" (i64.const 5)) (i32.const 97)) +(assert_return (invoke "load8" (i64.const 7)) (i32.const 99)) + +;; Unsigned memory.init source bounds produce a Wasm trap rather than a host panic. +(module + (memory 1) + (data "x") + (func (export "run") + (memory.init 0 (i32.const 0) (i32.const -1) (i32.const 1)))) + +(assert_trap (invoke "run") "out of bounds memory access") + +;; memory.init must mark its target local memory as used by allocation analysis. +(module $host + (memory (export "memory") 1)) +(register "host" $host) + +(module + (import "host" "memory" (memory 1)) + (memory $local 1) + (data $data "x") + (func (export "run") + (memory.init $local $data (i32.const 0) (i32.const 0) (i32.const 1)))) + +(assert_return (invoke "run")) + +;; Store superinstructions select the address lane from the target memory. +(module + (memory i64 1) + (func (export "store") (param i64 i32) + (i32.store (local.get 0) (local.get 1))) + (func (export "fma") (param i64 f32 f32 f32) + (f32.store + (local.get 0) + (f32.add (local.get 1) (f32.mul (local.get 2) (local.get 3))))) + (func (export "load_i32") (param i64) (result i32) + (i32.load (local.get 0))) + (func (export "load_f32") (param i64) (result f32) + (f32.load (local.get 0)))) + +(invoke "store" (i64.const 4) (i32.const 42)) +(invoke "fma" (i64.const 8) (f32.const 1) (f32.const 2) (f32.const 3)) +(assert_return (invoke "load_i32" (i64.const 4)) (i32.const 42)) +(assert_return (invoke "load_f32" (i64.const 8)) (f32.const 7)) diff --git a/crates/tinywasm/tests/wasm-custom/table-basics.wast b/crates/tinywasm/tests/wasm-custom/table-basics.wast new file mode 100644 index 00000000..64bda6c1 --- /dev/null +++ b/crates/tinywasm/tests/wasm-custom/table-basics.wast @@ -0,0 +1,32 @@ +(module + (type $result (func (result i32))) + (func $seven (type $result) (i32.const 7)) + (func $nine (type $result) (i32.const 9)) + + (table $table 3 5 funcref) + (elem (table $table) (i32.const 1) func $seven) + (elem $passive func $nine) + + (func (export "size") (result i32) + (table.size $table)) + (func (export "is-null") (param $index i32) (result i32) + (ref.is_null (table.get $table (local.get $index)))) + (func (export "call") (param $index i32) (result i32) + (call_indirect $table (type $result) (local.get $index))) + (func (export "set-nine") (param $index i32) + (table.set $table (local.get $index) (ref.func $nine))) + (func (export "init-nine") (param $index i32) + (table.init $table $passive (local.get $index) (i32.const 0) (i32.const 1))) +) + +(assert_return (invoke "size") (i32.const 3)) +(assert_return (invoke "is-null" (i32.const 0)) (i32.const 1)) +(assert_return (invoke "is-null" (i32.const 1)) (i32.const 0)) +(assert_return (invoke "call" (i32.const 1)) (i32.const 7)) +(assert_return (invoke "set-nine" (i32.const 0))) +(assert_return (invoke "is-null" (i32.const 0)) (i32.const 0)) +(assert_return (invoke "call" (i32.const 0)) (i32.const 9)) +(assert_return (invoke "is-null" (i32.const 2)) (i32.const 1)) +(assert_return (invoke "init-nine" (i32.const 2))) +(assert_return (invoke "is-null" (i32.const 2)) (i32.const 0)) +(assert_return (invoke "call" (i32.const 2)) (i32.const 9)) diff --git a/crates/types/src/archive.rs b/crates/types/src/archive.rs index 99238abb..9904a7da 100644 --- a/crates/types/src/archive.rs +++ b/crates/types/src/archive.rs @@ -7,7 +7,7 @@ use crate::Module; #[rustfmt::skip] const TWASM_MAGIC: [u8; 16] = [ TWASM_MAGIC_PREFIX[0], TWASM_MAGIC_PREFIX[1], TWASM_MAGIC_PREFIX[2], TWASM_MAGIC_PREFIX[3], TWASM_VERSION[0], TWASM_VERSION[1], 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; const TWASM_MAGIC_PREFIX: &[u8; 4] = b"TWAS"; -const TWASM_VERSION: &[u8; 2] = b"04"; +const TWASM_VERSION: &[u8; 2] = b"05"; fn validate_magic(wasm: &[u8]) -> Result { if wasm.len() < TWASM_MAGIC.len() || &wasm[..TWASM_MAGIC_PREFIX.len()] != TWASM_MAGIC_PREFIX { diff --git a/crates/types/src/instructions.rs b/crates/types/src/instructions.rs index a53576ee..10ab028d 100644 --- a/crates/types/src/instructions.rs +++ b/crates/types/src/instructions.rs @@ -1,4 +1,6 @@ -use super::{FuncAddr, GlobalAddr, LocalAddr, TableAddr, TypeAddr, ValueCounts}; +use alloc::boxed::Box; + +use super::{FuncAddr, GlobalAddr, LocalAddr, TableAddr, TagAddr, TypeAddr, ValueCounts}; use crate::{ConstIdx, DataAddr, ElemAddr, MemAddr, RefType, RefValue}; /// Represents a memory immediate in a WebAssembly memory instruction. @@ -20,6 +22,46 @@ pub struct DropKeep { pub keep: ValueCounts, } +/// A catch clause attached to a lowered `try_table` instruction. +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "debug", derive(Debug))] +#[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] +pub enum ExceptionCatch { + /// Catch exceptions carrying the specified module-local tag. + Tag { tag: TagAddr, landing_pad: u32, base: ValueCounts, with_ref: bool }, + /// Catch any exception. + All { landing_pad: u32, base: ValueCounts, with_ref: bool }, +} + +impl ExceptionCatch { + /// Returns the catch landing pad instruction pointer. + pub const fn landing_pad(self) -> u32 { + match self { + Self::Tag { landing_pad, .. } | Self::All { landing_pad, .. } => landing_pad, + } + } + + /// Returns whether this clause exposes the caught exception reference. + pub const fn with_ref(self) -> bool { + match self { + Self::Tag { with_ref, .. } | Self::All { with_ref, .. } => with_ref, + } + } +} + +/// A statically lowered exception handler range. +#[derive(Clone, PartialEq, Eq)] +#[cfg_attr(feature = "debug", derive(Debug))] +#[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] +pub struct ExceptionHandler { + /// First protected instruction, inclusive. + pub start_ip: u32, + /// End of the protected instruction range, exclusive. + pub end_ip: u32, + /// Catch clauses in source order. + pub catches: Box<[ExceptionCatch]>, +} + impl From<(ValueCounts, ValueCounts)> for DropKeep { fn from((base, keep): (ValueCounts, ValueCounts)) -> Self { Self { base, keep } @@ -52,8 +94,19 @@ pub enum ConstInstruction { F32Const(f32), F64Const(f64), V128Const([u8; 16]), - GlobalGet(GlobalAddr), + GlobalGet32(GlobalAddr), + GlobalGet64(GlobalAddr), + GlobalGet128(GlobalAddr), + GlobalGetRef(GlobalAddr), Ref(RefValue), + RefI31, + AnyConvertExtern, + ExternConvertAny, + StructNew(TypeAddr), + StructNewDefault(TypeAddr), + ArrayNew(TypeAddr), + ArrayNewDefault(TypeAddr), + ArrayNewFixed(TypeAddr, u32), I32Add, I32Sub, I32Mul, @@ -133,6 +186,7 @@ pub enum Instruction { // The 32/64 suffix describes the operand width. Future compare-style ops may still yield i32 results. BinOpLocalLocal32(BinOp, LocalAddr, LocalAddr), BinOpLocalLocal64(BinOp, LocalAddr, LocalAddr), BinOpLocalLocal128(BinOp128, LocalAddr, LocalAddr), + CmpLocalLocal32(CmpOp, LocalAddr, LocalAddr), CmpLocalLocal64(CmpOp, LocalAddr, LocalAddr), BinOpLocalLocalSet32(BinOp, LocalAddr, LocalAddr, LocalAddr), BinOpLocalLocalSet64(BinOp, LocalAddr, LocalAddr, LocalAddr), BinOpLocalLocalSet128(BinOp128, LocalAddr, LocalAddr, LocalAddr), @@ -140,6 +194,8 @@ pub enum Instruction { BinOpLocalLocalTee64(BinOp, LocalAddr, LocalAddr, LocalAddr), BinOpLocalLocalTee128(BinOp128, LocalAddr, LocalAddr, LocalAddr), BinOpLocalConst32(BinOp, LocalAddr, i32), BinOpLocalConst64(BinOp, LocalAddr, i64), + BinOpGlobalConst32(BinOp, GlobalAddr, i32), BinOpGlobalConst64(BinOp, GlobalAddr, i64), + BinOpGlobalConst128(BinOp128, GlobalAddr, ConstIdx), BinOpLocalConst128(BinOp128, LocalAddr, ConstIdx), BinOpLocalConstSet32(BinOp, LocalAddr, i32, LocalAddr), BinOpLocalConstSet64(BinOp, LocalAddr, i64, LocalAddr), @@ -147,6 +203,9 @@ pub enum Instruction { BinOpLocalConstTee32(BinOp, LocalAddr, i32, LocalAddr), BinOpLocalConstTee64(BinOp, LocalAddr, i64, LocalAddr), BinOpLocalConstTee128(BinOp128, LocalAddr, ConstIdx, LocalAddr), + BinOpStackLocal32(BinOp, LocalAddr), + BinOpStackLocalSet32(BinOp, LocalAddr, LocalAddr), + BinOpStackLocalTee32(BinOp, LocalAddr, LocalAddr), BinOpStackGlobal32(BinOp, u32), BinOpStackGlobal64(BinOp, u32), SetLocalConst32(LocalAddr, i32), SetLocalConst64(LocalAddr, i64), SetLocalConst128(LocalAddr, ConstIdx), @@ -202,6 +261,16 @@ pub enum Instruction { JumpIfLocalNonZero64 { target_ip: u32, local: LocalAddr }, JumpCmpStackConst32 { target_ip: u32, imm: i32, op: CmpOp }, JumpCmpStackConst64 { target_ip: u32, imm: i64, op: CmpOp }, + JumpCmpStackLocal32 { target_ip: u32, local: LocalAddr, op: CmpOp }, + JumpCmpStackLocal64 { target_ip: u32, local: LocalAddr, op: CmpOp }, + BinOpLocalConstJump32 { target_ip: u32, local: LocalAddr, imm: i32, op: BinOp, on_zero: bool }, + BinOpLocalConstJumpCmpLocal32 { target_ip: u32, local: LocalAddr, imm: i32, binop: BinOp, right: LocalAddr, cmp: CmpOp }, + BinOpStackConstTeeLocalJump32 { target_ip: u32, local: LocalAddr, imm: i32, op: BinOp, on_zero: bool }, + BinOpGlobalConstJump32 { target_ip: u32, global: GlobalAddr, imm: i32, op: BinOp, on_zero: bool }, + IncLocalJump32 { target_ip: u32, local: LocalAddr, delta: i32, on_zero: bool }, + IncStackTeeLocalJump32 { target_ip: u32, local: LocalAddr, delta: i32, on_zero: bool }, + IncGlobalJump32 { target_ip: u32, global: GlobalAddr, delta: i32, on_zero: bool }, + IncLocalJumpCmpLocal32 { target_ip: u32, local: LocalAddr, delta: i32, right: LocalAddr, op: CmpOp }, JumpCmpLocalConst32 { target_ip: u32, local: LocalAddr, imm: i32, op: CmpOp }, JumpCmpLocalConst64 { target_ip: u32, local: LocalAddr, imm: i32, op: CmpOp }, JumpCmpLocalLocal32 { target_ip: u32, left: LocalAddr, right: LocalAddr, op: CmpOp }, @@ -221,6 +290,8 @@ pub enum Instruction { ReturnCallSelf, ReturnCallIndirect(TypeAddr, TableAddr), ReturnCallRef(TypeAddr), + Throw(TagAddr), + ThrowRef, // > Parametric Instructions // See @@ -231,10 +302,9 @@ pub enum Instruction { // > Variable Instructions // See - GlobalGet(GlobalAddr), - LocalGet32(LocalAddr), LocalSet32(LocalAddr), LocalTee32(LocalAddr), GlobalSet32(GlobalAddr), - LocalGet64(LocalAddr), LocalSet64(LocalAddr), LocalTee64(LocalAddr), GlobalSet64(GlobalAddr), - LocalGet128(LocalAddr), LocalSet128(LocalAddr), LocalTee128(LocalAddr), GlobalSet128(GlobalAddr), + GlobalGet32(GlobalAddr), GlobalSet32(GlobalAddr), GlobalTee32(GlobalAddr), LocalGet32(LocalAddr), LocalSet32(LocalAddr), LocalTee32(LocalAddr), + GlobalGet64(GlobalAddr), GlobalSet64(GlobalAddr), GlobalTee64(GlobalAddr), LocalGet64(LocalAddr), LocalSet64(LocalAddr), LocalTee64(LocalAddr), + GlobalGet128(GlobalAddr), GlobalSet128(GlobalAddr), GlobalTee128(GlobalAddr), LocalGet128(LocalAddr), LocalSet128(LocalAddr), LocalTee128(LocalAddr), // > Memory Instructions I32Load(MemoryArg), @@ -272,6 +342,35 @@ pub enum Instruction { RefFunc(FuncAddr), RefIsNull, RefAsNonNull, + RefI31, + I31GetS, + I31GetU, + RefEq, + RefTest(RefType), + RefCast(RefType), + BrOnCast(u32, RefType, bool), + + // > GC Objects + StructNew(TypeAddr), + StructNewDefault(TypeAddr), + StructGet(TypeAddr, u32), + StructGetS(TypeAddr, u32), + StructGetU(TypeAddr, u32), + StructSet(TypeAddr, u32), + ArrayNew(TypeAddr), + ArrayNewDefault(TypeAddr), + ArrayNewFixed(TypeAddr, u32), + ArrayNewData(TypeAddr, DataAddr), + ArrayNewElem(TypeAddr, ElemAddr), + ArrayGet(TypeAddr), + ArrayGetS(TypeAddr), + ArrayGetU(TypeAddr), + ArraySet(TypeAddr), + ArrayLen, + ArrayFill(TypeAddr), + ArrayCopy(TypeAddr, TypeAddr), + ArrayInitData(TypeAddr, DataAddr), + ArrayInitElem(TypeAddr, ElemAddr), // > Numeric Instructions // See @@ -310,7 +409,7 @@ pub enum Instruction { TableFill(TableAddr), // > Bulk Memory Instructions - MemoryInit(MemAddr, DataAddr), + MemoryInit(DataAddr, MemAddr), MemoryCopy { dst_mem: MemAddr, src_mem: MemAddr }, MemoryFill(MemAddr), MemoryFillImm(MemAddr, u8, i32), @@ -394,9 +493,11 @@ pub enum Instruction { F64x2RelaxedMin, F64x2RelaxedMax, I16x8RelaxedQ15mulrS, I16x8RelaxedDotI8x16I7x16S, - I32x4RelaxedDotI8x16I7x16AddS + I32x4RelaxedDotI8x16I7x16AddS, } +const _: () = assert!(core::mem::size_of::() <= 16); + impl Instruction { #[inline] pub const fn memory_addr(&self) -> Option { @@ -471,23 +572,12 @@ impl Instruction { | Self::V128Store16Lane(arg, ..) | Self::V128Store32Lane(arg, ..) | Self::V128Store64Lane(arg, ..) => Some(arg.mem_addr()), - Self::MemorySize(mem) - | Self::MemoryGrow(mem) - | Self::MemoryInit(mem, ..) - | Self::MemoryFill(mem) - | Self::MemoryFillImm(mem, ..) => Some(*mem), + Self::MemorySize(mem) | Self::MemoryGrow(mem) | Self::MemoryFill(mem) | Self::MemoryFillImm(mem, ..) => { + Some(*mem) + } + Self::MemoryInit(_, mem) => Some(*mem), Self::MemoryCopy { dst_mem, src_mem } => Some(if *dst_mem >= *src_mem { *dst_mem } else { *src_mem }), _ => None, } } } - -#[cfg(test)] -mod tests { - use super::Instruction; - - #[test] - fn instruction_layout_size_is_stable() { - assert_eq!(core::mem::size_of::(), 16); - } -} diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index 0b159270..9a7071cb 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -14,17 +14,20 @@ use core::ops::{Deref, Range}; // Memory defaults const MEM_PAGE_SIZE: u64 = 65536; -const MAX_MEMORY_SIZE: u64 = 4294967296; - -const fn max_page_count(page_size: u64) -> u64 { - MAX_MEMORY_SIZE / page_size +const fn max_page_count(arch: MemoryArch, page_size: u64) -> u64 { + match arch { + MemoryArch::I32 => (1u64 << 32) / page_size, + MemoryArch::I64 => u64::MAX / page_size, + } } mod instructions; mod reference; +mod types; mod value; pub use instructions::*; pub use reference::*; +pub use types::*; pub use value::*; #[cfg(feature = "archive")] @@ -81,10 +84,10 @@ pub struct ModuleInner { /// Contains data from to the `code`, `func`, and `type` sections of the original WebAssembly module. pub funcs: Box<[Arc]>, - /// A vector of type definitions, indexed by `TypeAddr` + /// The dense type definitions, indexed by `TypeAddr`. /// /// Corresponds to the `type` section of the original WebAssembly module. - pub func_types: Box<[FuncType]>, + pub types: TypeSection, /// Function index to type index mapping in module index space, including imports. pub func_type_idxs: Box<[TypeAddr]>, @@ -109,6 +112,11 @@ pub struct ModuleInner { /// Corresponds to the `memory` section of the original WebAssembly module. pub memory_types: Box<[MemoryType]>, + /// Tag components of the WebAssembly module. + /// + /// Corresponds to the `tag` section of the original WebAssembly module. + pub tags: Box<[TagType]>, + /// Imports of the WebAssembly module. /// /// Corresponds to the `import` section of the original WebAssembly module. @@ -135,10 +143,11 @@ impl Module { pub fn imports(&self) -> impl Iterator> { self.0.imports.iter().filter_map(|import| { let ty = match &import.kind { - ImportKind::Function(type_idx) => Some(ImportType::Func(self.0.func_types.get(*type_idx as usize)?)), + ImportKind::Function(type_idx) => Some(ImportType::Func(self.0.types.get(*type_idx)?.as_func()?)), ImportKind::Table(table_ty) => Some(ImportType::Table(table_ty)), ImportKind::Memory(memory_ty) => Some(ImportType::Memory(memory_ty)), ImportKind::Global(global_ty) => Some(ImportType::Global(global_ty)), + ImportKind::Tag(tag_ty) => Some(ImportType::Tag(self.0.types.get(tag_ty.type_idx)?.as_func()?)), }?; Some(ModuleImport { module: import.module.as_ref(), name: import.name.as_ref(), ty }) @@ -160,6 +169,7 @@ impl Module { | (ExternalKind::Table, ImportKind::Table(_)) | (ExternalKind::Memory, ImportKind::Memory(_)) | (ExternalKind::Global, ImportKind::Global(_)) + | (ExternalKind::Tag, ImportKind::Tag(_)) ) }) .count() @@ -173,15 +183,17 @@ impl Module { | (ExternalKind::Table, ImportKind::Table(_)) | (ExternalKind::Memory, ImportKind::Memory(_)) | (ExternalKind::Global, ImportKind::Global(_)) + | (ExternalKind::Tag, ImportKind::Tag(_)) ) }); let import = imports.nth(index)?; match &import.kind { - ImportKind::Function(type_idx) => Some(ExportType::Func(module.func_types.get(*type_idx as usize)?)), + ImportKind::Function(type_idx) => Some(ExportType::Func(module.types.get(*type_idx)?.as_func()?)), ImportKind::Table(table_ty) => Some(ExportType::Table(table_ty)), ImportKind::Memory(memory_ty) => Some(ExportType::Memory(memory_ty)), ImportKind::Global(global_ty) => Some(ExportType::Global(global_ty)), + ImportKind::Tag(tag_ty) => Some(ExportType::Tag(module.types.get(tag_ty.type_idx)?.as_func()?)), } } @@ -194,7 +206,7 @@ impl Module { imported_type(&self.0, ExternalKind::Func, idx)? } else { let type_idx = *self.0.func_type_idxs.get(idx)?; - ExportType::Func(self.0.func_types.get(type_idx as usize)?) + ExportType::Func(self.0.types.get(type_idx)?.as_func()?) } } ExternalKind::Table => { @@ -221,6 +233,15 @@ impl Module { ExportType::Global(&self.0.globals.get(idx - imported_globals)?.ty) } } + ExternalKind::Tag => { + let imported_tags = imported_count(&self.0, ExternalKind::Tag); + if idx < imported_tags { + imported_type(&self.0, ExternalKind::Tag, idx)? + } else { + let tag_ty = self.0.tags.get(idx - imported_tags)?; + ExportType::Tag(self.0.types.get(tag_ty.type_idx)?.as_func()?) + } + } }; Some(ModuleExport { name: export.name.as_ref(), ty }) @@ -256,6 +277,8 @@ pub enum ImportType<'a> { Memory(&'a MemoryType), /// Imported global type. Global(&'a GlobalType), + /// Imported tag type. + Tag(&'a FuncType), } /// Exported entity type. @@ -268,6 +291,8 @@ pub enum ExportType<'a> { Memory(&'a MemoryType), /// Exported global type. Global(&'a GlobalType), + /// Exported tag type. + Tag(&'a FuncType), } /// How instantiation should prepare local memories declared by the module. @@ -299,6 +324,8 @@ pub enum ExternalKind { Memory, /// A WebAssembly Global. Global, + /// A WebAssembly Tag. + Tag, } /// A WebAssembly Address. @@ -313,6 +340,8 @@ pub type FuncAddr = Addr; pub type TableAddr = Addr; pub type MemAddr = Addr; pub type GlobalAddr = Addr; +pub type TagAddr = Addr; +pub type ExnAddr = Addr; pub type ElemAddr = Addr; pub type DataAddr = Addr; pub type ExternAddr = Addr; @@ -336,6 +365,7 @@ pub enum ExternVal { Table(TableAddr), Memory(MemAddr), Global(GlobalAddr), + Tag(TagAddr), } impl ExternVal { @@ -346,6 +376,7 @@ impl ExternVal { Self::Table(_) => ExternalKind::Table, Self::Memory(_) => ExternalKind::Memory, Self::Global(_) => ExternalKind::Global, + Self::Tag(_) => ExternalKind::Tag, } } @@ -356,91 +387,41 @@ impl ExternVal { ExternalKind::Table => Self::Table(addr), ExternalKind::Memory => Self::Memory(addr), ExternalKind::Global => Self::Global(addr), + ExternalKind::Tag => Self::Tag(addr), } } } -/// The type of a WebAssembly Function. -/// -/// See -#[derive(Clone, PartialEq, Eq, Default)] +/// The physical storage lane used by a [`WasmType`]. +#[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "debug", derive(Debug))] -#[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] -pub struct FuncType { - data: Box<[WasmType]>, - param_count: u16, -} - -impl FuncType { - /// Create a new function type. - pub fn new(params: &[WasmType], results: &[WasmType]) -> Self { - let data: Box<[WasmType]> = params.iter().cloned().chain(results.iter().cloned()).collect(); - Self { data, param_count: params.len() as u16 } - } - - /// Get the parameter types of this function type. - pub fn params(&self) -> &[WasmType] { - &self.data[..self.param_count as usize] - } - - /// Get the result types of this function type. - pub fn results(&self) -> &[WasmType] { - &self.data[self.param_count as usize..] - } - - /// Compare function types while resolving concrete references in their respective type spaces. - pub fn equivalent(&self, types: &[FuncType], other: &Self, other_types: &[FuncType]) -> bool { - fn refs_equal( - left_types: &[FuncType], - left: RefType, - right_types: &[FuncType], - right: RefType, - visited: &mut alloc::vec::Vec<(u32, u32)>, - ) -> bool { - if left.is_nullable() != right.is_nullable() { - return false; - } - match (left.type_index(), right.type_index()) { - (Some(left_idx), Some(right_idx)) => { - if visited.contains(&(left_idx, right_idx)) { - return true; - } - let (Some(left), Some(right)) = - (left_types.get(left_idx as usize), right_types.get(right_idx as usize)) - else { - return false; - }; - visited.push((left_idx, right_idx)); - funcs_equal(left, left_types, right, right_types, visited) - } - (None, None) => left.abstract_heap_type() == right.abstract_heap_type(), - _ => false, - } - } - - fn funcs_equal( - left: &FuncType, - left_types: &[FuncType], - right: &FuncType, - right_types: &[FuncType], - visited: &mut alloc::vec::Vec<(u32, u32)>, - ) -> bool { - left.params().len() == right.params().len() - && left.results().len() == right.results().len() - && left.params().iter().chain(left.results()).zip(right.params().iter().chain(right.results())).all( - |(&left, &right)| match (left, right) { - (WasmType::Ref(left), WasmType::Ref(right)) => { - refs_equal(left_types, left, right_types, right, visited) - } - _ => left == right, - }, - ) +pub enum ValueLane { + /// A 32-bit value or reference. + S32, + /// A 64-bit value. + S64, + /// A 128-bit SIMD value. + S128, +} + +impl ValueLane { + /// Selects one of three values based on this lane. + pub fn select(self, s32: T, s64: T, s128: T) -> T { + match self { + Self::S32 => s32, + Self::S64 => s64, + Self::S128 => s128, } + } +} - if core::ptr::eq(types, other_types) && self == other { - return true; +impl From<&WasmType> for ValueLane { + fn from(ty: &WasmType) -> Self { + match ty { + WasmType::I32 | WasmType::F32 | WasmType::Ref(_) => Self::S32, + WasmType::I64 | WasmType::F64 => Self::S64, + WasmType::V128 => Self::S128, } - funcs_equal(self, types, other, other_types, &mut alloc::vec::Vec::new()) } } @@ -466,10 +447,10 @@ impl<'a> FromIterator<&'a WasmType> for ValueCounts { let mut counts = Self::default(); for ty in iter { - match ty { - WasmType::I32 | WasmType::F32 | WasmType::Ref(_) => counts.c32 += 1, - WasmType::I64 | WasmType::F64 => counts.c64 += 1, - WasmType::V128 => counts.c128 += 1, + match ValueLane::from(ty) { + ValueLane::S32 => counts.c32 += 1, + ValueLane::S64 => counts.c64 += 1, + ValueLane::S128 => counts.c128 += 1, } } counts @@ -493,6 +474,7 @@ pub struct WasmFunction { pub struct WasmFunctionData { pub v128_constants: Box<[[u8; 16]]>, pub branch_table_targets: Box<[u32]>, + pub exception_handlers: Box<[ExceptionHandler]>, } impl WasmFunctionData { @@ -630,7 +612,11 @@ impl MemoryType { #[inline] pub const fn page_count_max(&self) -> u64 { - if let Some(page_count_max) = self.page_count_max { page_count_max } else { max_page_count(self.page_size()) } + if let Some(page_count_max) = self.page_count_max { + page_count_max + } else { + max_page_count(self.arch, self.page_size()) + } } #[inline] @@ -638,16 +624,6 @@ impl MemoryType { if let Some(page_size) = self.page_size { page_size } else { MEM_PAGE_SIZE } } - #[inline] - pub const fn initial_size(&self) -> u64 { - self.page_count_initial * self.page_size() - } - - #[inline] - pub const fn max_size(&self) -> u64 { - self.page_count_max() * self.page_size() - } - /// Set a different memory architecture. pub const fn with_arch(mut self, arch: MemoryArch) -> Self { self.arch = arch; @@ -687,6 +663,22 @@ pub enum MemoryArch { I64, } +/// A WebAssembly tag type. +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "debug", derive(Debug))] +#[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] +pub struct TagType { + /// The module-local function type index used by this tag. + pub type_idx: TypeAddr, +} + +impl TagType { + /// Creates a tag type from a module-local function type index. + pub const fn new(type_idx: TypeAddr) -> Self { + Self { type_idx } + } +} + #[derive(Clone, PartialEq, Eq)] #[cfg_attr(feature = "debug", derive(Debug))] #[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] @@ -704,6 +696,7 @@ pub enum ImportKind { Table(TableType), Memory(MemoryType), Global(GlobalType), + Tag(TagType), } impl From<&ImportKind> for ExternalKind { @@ -713,6 +706,7 @@ impl From<&ImportKind> for ExternalKind { ImportKind::Table(_) => Self::Table, ImportKind::Memory(_) => Self::Memory, ImportKind::Global(_) => Self::Global, + ImportKind::Tag(_) => Self::Tag, } } } diff --git a/crates/types/src/reference.rs b/crates/types/src/reference.rs index ee669656..9ed5df14 100644 --- a/crates/types/src/reference.rs +++ b/crates/types/src/reference.rs @@ -1,3 +1,12 @@ +const HOST_REF_TAG: u32 = 1 << 30; + +const fn encode_host_ref(addr: u32) -> Option { + if addr >= HOST_REF_TAG - 1 { + return None; + } + Some((addr | HOST_REF_TAG).wrapping_add(1).wrapping_mul(2)) +} + /// An abstract WebAssembly heap type. /// /// This contains exactly the abstract heap types in core Wasm 3.0. @@ -31,7 +40,8 @@ pub enum AbstractHeapType { /// [nullable:1 concrete:1 payload:30] /// ``` /// -/// For concrete types, `payload` is a module type index. +/// For concrete types, `payload` is a module type index before instantiation +/// and a canonical store type address at runtime. /// Otherwise, it is an [`AbstractHeapType`]. #[derive(Clone, Copy, PartialEq, Eq, Hash)] #[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] @@ -63,12 +73,9 @@ impl RefType { } #[inline] - pub const fn new_concrete(nullable: bool, type_index: u32) -> Option { - if type_index <= Self::PAYLOAD_MASK { - Some(Self(((nullable as u32) << 31) | Self::CONCRETE | type_index)) - } else { - None - } + pub const fn new_concrete(nullable: bool, type_index: u32) -> Self { + assert!(type_index <= Self::PAYLOAD_MASK, "type index is too large for a reference type"); + Self(((nullable as u32) << 31) | Self::CONCRETE | type_index) } #[inline] @@ -116,9 +123,7 @@ impl RefType { #[inline] pub const fn is_func(self) -> bool { - // TODO(wasm3): Classify concrete refs from the module type definition once GC types are represented. - self.is_concrete() - || matches!(self.abstract_heap_type(), Some(AbstractHeapType::Func | AbstractHeapType::NoFunc)) + matches!(self.abstract_heap_type(), Some(AbstractHeapType::Func | AbstractHeapType::NoFunc)) } #[inline] @@ -132,6 +137,7 @@ impl RefType { } } +/// A host-facing WebAssembly reference value. #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "debug", derive(Debug))] #[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] @@ -143,19 +149,9 @@ pub enum RefValue { Exn(ExnRef), } -impl RefValue { - /// Return the reference's raw representation, or `None` for null. - pub const fn raw(self) -> Option { - match self { - Self::Null => None, - Self::Func(value) => Some(value.addr()), - Self::Extern(value) => Some(value.addr()), - Self::Any(value) => Some(value.raw()), - Self::Exn(value) => Some(value.addr()), - } - } -} - +/// A reference to a function in a store. +/// +/// The payload is the function's store-local address. #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "debug", derive(Debug))] #[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] @@ -173,6 +169,11 @@ impl FuncRef { } } +/// An opaque external reference. +/// +/// Packed as `[payload:31 i31:1]`. Host addresses use the upper payload +/// category, Store-managed objects use the lower category, and odd values +/// contain an externalized i31. #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "debug", derive(Debug))] #[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] @@ -181,15 +182,35 @@ pub struct ExternRef(u32); impl ExternRef { #[inline] pub const fn new(addr: u32) -> Self { - Self(addr) + let Some(value) = Self::try_new(addr) else { panic!("external reference address is too large") }; + value } + /// Creates an external reference when `addr` fits the runtime encoding. #[inline] - pub const fn addr(self) -> u32 { + pub const fn try_new(addr: u32) -> Option { + match encode_host_ref(addr) { + Some(encoded) => Some(Self(encoded)), + None => None, + } + } + + #[doc(hidden)] + #[inline] + pub const fn from_raw(raw: u32) -> Self { + Self(raw) + } + + #[doc(hidden)] + #[inline] + pub const fn raw(self) -> u32 { self.0 } } +/// A reference to an exception in a store. +/// +/// The payload is the exception's store-local address. #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "debug", derive(Debug))] #[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] @@ -207,14 +228,30 @@ impl ExnRef { } } +/// A WebAssembly `anyref` value. +/// +/// Packed as: +/// +/// ```text +/// [payload:31 i31:1] +/// ``` +/// +/// Odd values contain an inline signed i31. Non-zero even values are reserved +/// for store-managed references, and zero is reserved for null by the runtime. #[derive(Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "debug", derive(Debug))] #[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] pub struct AnyRef(u32); impl AnyRef { - // Odd values are inline i31s. - // Even values are GC object handles. + /// Creates a host reference when `addr` fits the runtime encoding. + #[inline] + pub const fn from_host(addr: u32) -> Option { + match encode_host_ref(addr) { + Some(encoded) => Some(Self(encoded)), + None => None, + } + } #[doc(hidden)] #[inline] @@ -234,22 +271,21 @@ impl AnyRef { if self.0 & 1 == 1 { Some((self.0 as i32) >> 1) } else { None } } - pub const fn from_gc_addr(addr: u32) -> Option { - match addr.checked_add(1) { - Some(raw) => match raw.checked_mul(2) { - Some(raw) => Some(Self(raw)), - None => None, - }, - None => None, - } - } - - pub const fn gc_addr(self) -> Option { - if self.0 != 0 && self.0 & 1 == 0 { Some(self.0 / 2 - 1) } else { None } - } - #[inline] pub const fn raw(self) -> u32 { self.0 } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn host_reference_encoding_is_checked_and_unique() { + assert_ne!(AnyRef::from_host(0), AnyRef::from_host(1)); + assert!(AnyRef::from_host(HOST_REF_TAG - 2).is_some()); + assert!(AnyRef::from_host(HOST_REF_TAG - 1).is_none()); + assert!(ExternRef::try_new(u32::MAX).is_none()); + } +} diff --git a/crates/types/src/types.rs b/crates/types/src/types.rs new file mode 100644 index 00000000..08018497 --- /dev/null +++ b/crates/types/src/types.rs @@ -0,0 +1,158 @@ +use alloc::{boxed::Box, sync::Arc}; + +use crate::{TypeAddr, WasmType}; + +/// The dense type index space of a WebAssembly module. +#[derive(Clone, PartialEq, Eq, Default)] +#[cfg_attr(feature = "debug", derive(Debug))] +#[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] +pub struct TypeSection { + /// Types in module index order. + pub types: Box<[SubType]>, + /// Number of types in each recursive group, in section order. + pub rec_group_lengths: Box<[u32]>, +} + +impl TypeSection { + #[inline] + pub fn get(&self, index: TypeAddr) -> Option<&SubType> { + self.types.get(index as usize) + } + + #[inline] + pub fn len(&self) -> usize { + self.types.len() + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.types.is_empty() + } +} + +/// A type with optional declared subtyping. +#[derive(Clone, PartialEq, Eq)] +#[cfg_attr(feature = "debug", derive(Debug))] +#[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] +pub struct SubType { + pub is_final: bool, + pub supertype: Option, + pub composite: CompositeType, +} + +impl SubType { + #[inline] + pub const fn as_func(&self) -> Option<&FuncType> { + self.composite.as_func() + } + + #[inline] + pub const fn as_struct(&self) -> Option<&StructType> { + self.composite.as_struct() + } + + #[inline] + pub const fn as_array(&self) -> Option<&ArrayType> { + self.composite.as_array() + } +} + +/// A function, struct, or array type. +#[derive(Clone, PartialEq, Eq)] +#[cfg_attr(feature = "debug", derive(Debug))] +#[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] +pub enum CompositeType { + Func(FuncType), + Struct(StructType), + Array(ArrayType), +} + +impl CompositeType { + #[inline] + pub const fn as_func(&self) -> Option<&FuncType> { + match self { + Self::Func(ty) => Some(ty), + _ => None, + } + } + + #[inline] + pub const fn as_struct(&self) -> Option<&StructType> { + match self { + Self::Struct(ty) => Some(ty), + _ => None, + } + } + + #[inline] + pub const fn as_array(&self) -> Option<&ArrayType> { + match self { + Self::Array(ty) => Some(ty), + _ => None, + } + } +} + +/// The type of a WebAssembly function. +/// +/// See +#[derive(Clone, PartialEq, Eq, Default)] +#[cfg_attr(feature = "debug", derive(Debug))] +#[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] +pub struct FuncType { + data: Arc<[WasmType]>, + param_count: u16, +} + +impl FuncType { + /// Create a new function type. + pub fn new(params: &[WasmType], results: &[WasmType]) -> Self { + let data: Box<[WasmType]> = params.iter().cloned().chain(results.iter().cloned()).collect(); + Self { data: data.into(), param_count: params.len() as u16 } + } + + /// Get the parameter types of this function type. + pub fn params(&self) -> &[WasmType] { + &self.data[..self.param_count as usize] + } + + /// Get the result types of this function type. + pub fn results(&self) -> &[WasmType] { + &self.data[self.param_count as usize..] + } +} + +/// A WebAssembly struct type. +#[derive(Clone, PartialEq, Eq, Default)] +#[cfg_attr(feature = "debug", derive(Debug))] +#[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] +pub struct StructType { + pub fields: Box<[FieldType]>, +} + +/// A WebAssembly array type. +#[derive(Clone, PartialEq, Eq)] +#[cfg_attr(feature = "debug", derive(Debug))] +#[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] +pub struct ArrayType { + pub field: FieldType, +} + +/// A struct field or array element type. +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "debug", derive(Debug))] +#[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] +pub struct FieldType { + pub storage: StorageType, + pub mutable: bool, +} + +/// A field's packed or unpacked storage type. +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "debug", derive(Debug))] +#[cfg_attr(feature = "archive", derive(serde::Serialize, serde::Deserialize))] +pub enum StorageType { + I8, + I16, + Value(WasmType), +} diff --git a/crates/types/src/value.rs b/crates/types/src/value.rs index 92c660b8..5049ba22 100644 --- a/crates/types/src/value.rs +++ b/crates/types/src/value.rs @@ -54,7 +54,7 @@ impl WasmValue { | (Self::V128(_), WasmType::V128) => true, (Self::Ref(RefValue::Null), WasmType::Ref(ty)) => ty.is_nullable(), (Self::Ref(RefValue::Func(_)), WasmType::Ref(ty)) => { - ty.is_concrete() || matches!(ty.abstract_heap_type(), Some(crate::AbstractHeapType::Func)) + matches!(ty.abstract_heap_type(), Some(crate::AbstractHeapType::Func)) } (Self::Ref(RefValue::Extern(_)), WasmType::Ref(ty)) => { matches!(ty.abstract_heap_type(), Some(crate::AbstractHeapType::Extern)) @@ -62,12 +62,42 @@ impl WasmValue { (Self::Ref(RefValue::Exn(_)), WasmType::Ref(ty)) => { matches!(ty.abstract_heap_type(), Some(crate::AbstractHeapType::Exn)) } - (Self::Ref(RefValue::Any(_)), WasmType::Ref(ty)) => !ty.is_func() && !ty.is_extern() && !ty.is_exn(), + (Self::Ref(RefValue::Any(value)), WasmType::Ref(ty)) => matches!( + (value.as_i31(), ty.abstract_heap_type()), + ( + Some(_), + Some(crate::AbstractHeapType::I31 | crate::AbstractHeapType::Eq | crate::AbstractHeapType::Any) + ) | (None, Some(crate::AbstractHeapType::Any)) + ), _ => false, } } } +#[cfg(test)] +mod tests { + use super::*; + use crate::{AbstractHeapType, AnyRef, FuncRef, RefType}; + + #[test] + fn concrete_references_require_store_type_information() { + let concrete = WasmType::Ref(RefType::new_concrete(false, 0)); + + assert!(!WasmValue::from(FuncRef::new(0)).matches_type(concrete)); + assert!(!WasmValue::from(AnyRef::from_host(0).unwrap()).matches_type(concrete)); + } + + #[test] + fn i31_only_matches_its_store_independent_supertypes() { + let value = WasmValue::from(AnyRef::from_i31(0).unwrap()); + + for ty in [AbstractHeapType::I31, AbstractHeapType::Eq, AbstractHeapType::Any] { + assert!(value.matches_type(WasmType::Ref(RefType::new_abstract(false, ty)))); + } + assert!(!value.matches_type(WasmType::Ref(RefType::new_abstract(false, AbstractHeapType::Struct)))); + } +} + impl Debug for WasmValue { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { diff --git a/examples/archive.rs b/examples/archive.rs index 7a2ebabe..3f647046 100644 --- a/examples/archive.rs +++ b/examples/archive.rs @@ -1,4 +1,4 @@ -use eyre::Result; +use anyhow::Result; use tinywasm::{Module, ModuleInstance, Store, parser::Parser}; const WASM: &str = r#" diff --git a/examples/doom/Cargo.toml b/examples/doom/Cargo.toml index 38e16c4e..abec73d0 100644 --- a/examples/doom/Cargo.toml +++ b/examples/doom/Cargo.toml @@ -7,7 +7,7 @@ publish = false [workspace] [dependencies] -eyre = "0.6" +anyhow = "1.0" log = "0.4" pretty_env_logger = "0.5" softbuffer = "0.4" diff --git a/examples/doom/src/main.rs b/examples/doom/src/main.rs index 095183dc..c5e49b5f 100644 --- a/examples/doom/src/main.rs +++ b/examples/doom/src/main.rs @@ -4,7 +4,7 @@ use std::num::NonZeroU32; use std::path::{Path, PathBuf}; use std::rc::Rc; -use eyre::{ContextCompat, Result, bail, eyre}; +use anyhow::{Context, Result, anyhow, bail}; use runtime::{Runtime, SCREEN_HEIGHT, SCREEN_WIDTH}; use softbuffer::{Context as SoftbufferContext, Surface}; use winit::application::ApplicationHandler; @@ -60,10 +60,10 @@ impl DoomApp { surface .resize(NonZeroU32::new(SCREEN_WIDTH as u32).unwrap(), NonZeroU32::new(SCREEN_HEIGHT as u32).unwrap()) - .map_err(|err| eyre!(err.to_string()))?; - let mut buffer = surface.buffer_mut().map_err(|err| eyre!(err.to_string()))?; + .map_err(|err| anyhow!(err.to_string()))?; + let mut buffer = surface.buffer_mut().map_err(|err| anyhow!(err.to_string()))?; self.runtime.write_framebuffer(&mut buffer)?; - buffer.present().map_err(|err| eyre!(err.to_string()))?; + buffer.present().map_err(|err| anyhow!(err.to_string()))?; Ok(()) } } @@ -129,7 +129,7 @@ impl ApplicationHandler for DoomApp { window.request_redraw(); } - if self.runtime.host_state.borrow().exit_code.is_some() { + if self.runtime.host_state.lock().unwrap().exit_code.is_some() { event_loop.exit(); } } diff --git a/examples/doom/src/runtime.rs b/examples/doom/src/runtime.rs index 980dc21c..2ad02ae0 100644 --- a/examples/doom/src/runtime.rs +++ b/examples/doom/src/runtime.rs @@ -1,12 +1,11 @@ -use std::cell::RefCell; use std::collections::BTreeMap; use std::fs::{File, OpenOptions, create_dir_all}; use std::io::{Read, Seek, SeekFrom, Write}; use std::path::{Path, PathBuf}; -use std::rc::Rc; -use std::time::Instant; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; -use eyre::Result; +use anyhow::Result; use tinywasm::{FuncContext, HostFunction, Imports, ModuleInstance, Store}; const IMPORT_MODULE: &str = "env"; @@ -21,16 +20,16 @@ pub struct Runtime { key_up: tinywasm::FunctionTyped, memory: tinywasm::Memory, framebuffer_bytes: Vec, - pub host_state: Rc>, + pub host_state: Arc>, } impl Runtime { pub fn new(wad_path: PathBuf, guest_path: PathBuf) -> Result { let module = tinywasm::parse_file(&guest_path)?; let mut store = Store::default(); - let host_state = Rc::new(RefCell::new(HostState::new(wad_path))); - let imports = build_imports(&mut store, host_state.clone()); - let instance = ModuleInstance::instantiate(&mut store, &module, Some(imports))?; + let host_state = Arc::new(Mutex::new(HostState::new(wad_path))); + let imports = build_imports(host_state.clone()); + let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports))?; let wad_path_buf = instance.func::<(), i32>(&store, "tinywasm_doom_wad_path_buf")?; let init = instance.func::<(), ()>(&store, "tinywasm_doom_init")?; @@ -41,7 +40,7 @@ impl Runtime { let memory = instance.memory("memory")?; let buf_ptr = wad_path_buf.call(&mut store, ())? as usize; - let wad_path_string = host_state.borrow().wad_path.to_string_lossy().into_owned(); + let wad_path_string = host_state.lock().unwrap().wad_path.to_string_lossy().into_owned(); memory.write_cstring_bytes(&mut store, buf_ptr, &wad_path_string)?; init.call(&mut store, ())?; @@ -161,7 +160,7 @@ impl HostState { } } -fn build_imports(store: &mut Store, state: Rc>) -> Imports { +fn build_imports(state: Arc>) -> Imports { let mut imports = Imports::new(); { @@ -169,13 +168,13 @@ fn build_imports(store: &mut Store, state: Rc>) -> Imports { imports.define( IMPORT_MODULE, "host_open", - HostFunction::from(store, move |ctx: FuncContext<'_>, (filename_ptr, mode_ptr): (i32, i32)| { + HostFunction::from(move |ctx: FuncContext<'_>, (filename_ptr, mode_ptr): (i32, i32)| { let memory = ctx.memory("memory")?; let filename = memory.read_cstring_until_null(ctx.store(), filename_ptr as usize, 1024)?; let mode = memory.read_cstring_until_null(ctx.store(), mode_ptr as usize, 16)?; let filename = filename.to_string_lossy(); let mode = mode.to_string_lossy(); - let mut state = state.borrow_mut(); + let mut state = state.lock().unwrap(); let path = if filename == state.wad_path.to_string_lossy() || state.should_redirect_to_wad(&filename) { state.wad_path.clone() } else { @@ -208,8 +207,8 @@ fn build_imports(store: &mut Store, state: Rc>) -> Imports { imports.define( IMPORT_MODULE, "host_close", - HostFunction::from(store, move |_ctx: FuncContext<'_>, handle: i32| { - state.borrow_mut().files.remove(&handle); + HostFunction::from(move |_ctx: FuncContext<'_>, handle: i32| { + state.lock().unwrap().files.remove(&handle); Ok(()) }), ); @@ -220,8 +219,8 @@ fn build_imports(store: &mut Store, state: Rc>) -> Imports { imports.define( IMPORT_MODULE, "host_read", - HostFunction::from(store, move |mut ctx: FuncContext<'_>, (handle, buf_ptr, count): (i32, i32, i32)| { - let mut state = state.borrow_mut(); + HostFunction::from(move |mut ctx: FuncContext<'_>, (handle, buf_ptr, count): (i32, i32, i32)| { + let mut state = state.lock().unwrap(); let Some(file) = state.files.get_mut(&handle) else { return Ok(0); }; @@ -238,9 +237,9 @@ fn build_imports(store: &mut Store, state: Rc>) -> Imports { imports.define( IMPORT_MODULE, "host_write", - HostFunction::from(store, move |ctx: FuncContext<'_>, (handle, buf_ptr, count): (i32, i32, i32)| { + HostFunction::from(move |ctx: FuncContext<'_>, (handle, buf_ptr, count): (i32, i32, i32)| { let data = ctx.memory("memory")?.read_vec(ctx.store(), buf_ptr as usize, count.max(0) as usize)?; - let mut state = state.borrow_mut(); + let mut state = state.lock().unwrap(); let Some(file) = state.files.get_mut(&handle) else { return Ok(-1); }; @@ -255,14 +254,14 @@ fn build_imports(store: &mut Store, state: Rc>) -> Imports { imports.define( IMPORT_MODULE, "host_seek", - HostFunction::from(store, move |_ctx: FuncContext<'_>, (handle, offset, origin): (i32, i32, i32)| { + HostFunction::from(move |_ctx: FuncContext<'_>, (handle, offset, origin): (i32, i32, i32)| { let seek_from = match origin { 0 => SeekFrom::Start(offset.max(0) as u64), 1 => SeekFrom::Current(offset as i64), 2 => SeekFrom::End(offset as i64), _ => return Err(tinywasm::Error::Other(format!("invalid seek origin: {origin}"))), }; - let mut state = state.borrow_mut(); + let mut state = state.lock().unwrap(); let Some(file) = state.files.get_mut(&handle) else { return Ok(-1); }; @@ -277,8 +276,8 @@ fn build_imports(store: &mut Store, state: Rc>) -> Imports { imports.define( IMPORT_MODULE, "host_tell", - HostFunction::from(store, move |_ctx: FuncContext<'_>, handle: i32| { - let mut state = state.borrow_mut(); + HostFunction::from(move |_ctx: FuncContext<'_>, handle: i32| { + let mut state = state.lock().unwrap(); let Some(file) = state.files.get_mut(&handle) else { return Ok(-1); }; @@ -293,8 +292,8 @@ fn build_imports(store: &mut Store, state: Rc>) -> Imports { imports.define( IMPORT_MODULE, "host_eof", - HostFunction::from(store, move |_ctx: FuncContext<'_>, handle: i32| { - let mut state = state.borrow_mut(); + HostFunction::from(move |_ctx: FuncContext<'_>, handle: i32| { + let mut state = state.lock().unwrap(); let Some(file) = state.files.get_mut(&handle) else { return Ok(1); }; @@ -310,8 +309,8 @@ fn build_imports(store: &mut Store, state: Rc>) -> Imports { imports.define( IMPORT_MODULE, "host_gettime", - HostFunction::from(store, move |mut ctx: FuncContext<'_>, (sec_ptr, usec_ptr): (i32, i32)| { - let elapsed = state.borrow().start.elapsed(); + HostFunction::from(move |mut ctx: FuncContext<'_>, (sec_ptr, usec_ptr): (i32, i32)| { + let elapsed = state.lock().unwrap().start.elapsed() + Duration::from_secs(1); let sec = elapsed.as_secs().min(i32::MAX as u64) as i32; let usec = elapsed.subsec_micros() as i32; let memory = ctx.memory("memory")?; @@ -327,8 +326,8 @@ fn build_imports(store: &mut Store, state: Rc>) -> Imports { imports.define( IMPORT_MODULE, "host_exit", - HostFunction::from(store, move |_ctx: FuncContext<'_>, code: i32| { - state.borrow_mut().exit_code = Some(code); + HostFunction::from(move |_ctx: FuncContext<'_>, code: i32| { + state.lock().unwrap().exit_code = Some(code); Ok(()) }), ); @@ -337,7 +336,7 @@ fn build_imports(store: &mut Store, state: Rc>) -> Imports { imports.define( IMPORT_MODULE, "host_print", - HostFunction::from(store, move |ctx: FuncContext<'_>, ptr: i32| { + HostFunction::from(move |ctx: FuncContext<'_>, ptr: i32| { let text = ctx.memory("memory")?.read_cstring_until_null(ctx.store(), ptr as usize, 4096)?; log::info!("guest: {}", text.to_string_lossy()); Ok(()) diff --git a/examples/funcref_callbacks.rs b/examples/funcref_callbacks.rs index 8ff9b491..959d798d 100644 --- a/examples/funcref_callbacks.rs +++ b/examples/funcref_callbacks.rs @@ -1,4 +1,4 @@ -use eyre::Result; +use anyhow::Result; use tinywasm::{FuncContext, HostFunction, Imports, ModuleInstance, Store, types::FuncRef}; const LHS: i32 = 5; @@ -50,19 +50,18 @@ fn run_passed_funcref_example() -> Result<()> { let module = tinywasm::parse_bytes(&wasm)?; let mut store = Store::default(); - let mul = HostFunction::from(&mut store, |_, (lhs, rhs): (i32, i32)| -> tinywasm::Result { Ok(lhs * rhs) }); - let call_this = - HostFunction::from(&mut store, |mut ctx: FuncContext<'_>, func_ref: FuncRef| -> tinywasm::Result<()> { - // Host cannot call a funcref directly, so it routes through Wasm. - let call_by_ref = ctx.module().func::<(FuncRef, i32, i32), i32>(ctx.store(), "call_binop_by_ref")?; - let _result = call_by_ref.call(ctx.store_mut(), (func_ref, LHS, RHS))?; - Ok(()) - }); + let mul = HostFunction::from(|_, (lhs, rhs): (i32, i32)| -> tinywasm::Result { Ok(lhs * rhs) }); + let call_this = HostFunction::from(|mut ctx: FuncContext<'_>, func_ref: FuncRef| -> tinywasm::Result<()> { + // Host cannot call a funcref directly, so it routes through Wasm. + let call_by_ref = ctx.module().func::<(FuncRef, i32, i32), i32>(ctx.store(), "call_binop_by_ref")?; + let _result = call_by_ref.call(ctx.store_mut(), (func_ref, LHS, RHS))?; + Ok(()) + }); let mut imports = Imports::new(); imports.define("host", "call_this", call_this).define("host", "mul", mul); - let instance = ModuleInstance::instantiate(&mut store, &module, Some(imports))?; + let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports))?; let caller = instance.func::<(), ()>(&store, "tell_host_to_call")?; caller.call(&mut store, ())?; @@ -107,10 +106,10 @@ fn run_returned_funcref_example() -> Result<()> { let mut store = Store::default(); let mut imports = Imports::new(); - let mul = HostFunction::from(&mut store, |_, (lhs, rhs): (i32, i32)| -> tinywasm::Result { Ok(lhs * rhs) }); + let mul = HostFunction::from(|_, (lhs, rhs): (i32, i32)| -> tinywasm::Result { Ok(lhs * rhs) }); imports.define("host", "mul", mul); - let instance = ModuleInstance::instantiate(&mut store, &module, Some(imports))?; + let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports))?; let (add_ref, sub_ref, mul_ref) = { let get_funcrefs = instance.func::<(), (FuncRef, FuncRef, FuncRef)>(&store, "what_should_host_call")?; get_funcrefs.call(&mut store, ())? diff --git a/examples/linking.rs b/examples/linking.rs index b3164ae3..2df7133e 100644 --- a/examples/linking.rs +++ b/examples/linking.rs @@ -1,4 +1,4 @@ -use eyre::Result; +use anyhow::Result; use tinywasm::{ModuleInstance, Store}; // WebAssembly module defining and exporting an `add` function. @@ -40,7 +40,7 @@ fn main() -> Result<()> { imports.link_module("adder", add_instance)?; // Instantiate the `import` module with the linked imports. - let import_instance = ModuleInstance::instantiate(&mut store, &import_module, Some(imports))?; + let import_instance = ModuleInstance::instantiate(&mut store, &import_module, Some(&imports))?; // Call the `main` function, which uses the imported `add` function. let main = import_instance.func::<(), i32>(&store, "main")?; diff --git a/examples/reentrance.rs b/examples/reentrance.rs index 432fac76..0084a614 100644 --- a/examples/reentrance.rs +++ b/examples/reentrance.rs @@ -1,4 +1,4 @@ -use eyre::Result; +use anyhow::Result; use tinywasm::{FuncContext, HostFunction, Imports, ModuleInstance, Store}; const WASM: &str = r#" @@ -22,7 +22,7 @@ fn main() -> Result<()> { let module = tinywasm::parse_bytes(&wasm)?; let mut store = Store::default(); - let call_add_twice = HostFunction::from(&mut store, |mut ctx: FuncContext<'_>, value: i32| { + let call_add_twice = HostFunction::from(|mut ctx: FuncContext<'_>, value: i32| { let add_one = ctx.module().func::(ctx.store(), "add_one")?; // Use ctx.call for reentrant calls from host functions. Function::call @@ -34,7 +34,7 @@ fn main() -> Result<()> { let mut imports = Imports::new(); imports.define("host", "call_add_twice", call_add_twice); - let instance = ModuleInstance::instantiate(&mut store, &module, Some(imports))?; + let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports))?; let run = instance.func::(&store, "run")?; assert_eq!(run.call(&mut store, 40)?, 52); diff --git a/examples/resumable.rs b/examples/resumable.rs index 01634149..26b5f47b 100644 --- a/examples/resumable.rs +++ b/examples/resumable.rs @@ -1,4 +1,4 @@ -use eyre::Result; +use anyhow::Result; use tinywasm::{ExecProgress, ModuleInstance, Store}; const WASM: &str = r#" diff --git a/examples/rust/Cargo.toml b/examples/rust/Cargo.toml index efce27ab..b338bd2c 100644 --- a/examples/rust/Cargo.toml +++ b/examples/rust/Cargo.toml @@ -22,6 +22,10 @@ path = "src/print.rs" name = "tinywasm" path = "src/tinywasm.rs" +[[bin]] +name = "tinywasm_precompiled" +path = "src/tinywasm_precompiled.rs" + [[bin]] name = "tinywasm_no_std" path = "src/tinywasm_no_std.rs" diff --git a/examples/rust/build.sh b/examples/rust/build.sh index db4cb439..b2a61497 100755 --- a/examples/rust/build.sh +++ b/examples/rust/build.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash cd "$(dirname "$0")" || exit -bins=("host_fn" "hello" "fibonacci" "print" "tinywasm" "argon2id") -exclude_wat=("tinywasm") +bins=("host_fn" "hello" "fibonacci" "print" "tinywasm" "tinywasm_precompiled" "argon2id") +exclude_wat=("tinywasm" "tinywasm_precompiled") out_dir="./target/wasm32-unknown-unknown/wasm" dest_dir="out" @@ -13,8 +13,9 @@ wasmopt_features="--enable-simd --enable-relaxed-simd --enable-tail-call --enabl mkdir -p "$dest_dir" # build no_std -cargo build --target wasm32-unknown-unknown --package rust-wasm-examples --profile=wasm --bin tinywasm_no_std --no-default-features +RUSTFLAGS="-Zlocation-detail=none -Zfmt-debug=none -C target-feature=$rust_features -C panic=abort" cargo build -Z build-std=core,alloc,panic_abort -Z build-std-features="optimize_for_size" --target wasm32-unknown-unknown --package rust-wasm-examples --profile=wasm --bin tinywasm_no_std --no-default-features cp "$out_dir/tinywasm_no_std.wasm" "$dest_dir/" +wasm-opt "$dest_dir/tinywasm_no_std.wasm" -o "$dest_dir/tinywasm_no_std.opt.wasm" -O3 $wasmopt_features for bin in "${bins[@]}"; do RUSTFLAGS="-Zlocation-detail=none -Zfmt-debug=none -C target-feature=$rust_features -C panic=abort" cargo build -Z build-std=std,panic_abort -Z build-std-features="optimize_for_size" --target wasm32-unknown-unknown --package rust-wasm-examples --profile=wasm --bin "$bin" diff --git a/examples/rust/src/print.twasm b/examples/rust/src/print.twasm new file mode 100644 index 00000000..f0fa9a87 Binary files /dev/null and b/examples/rust/src/print.twasm differ diff --git a/examples/rust/src/tinywasm.rs b/examples/rust/src/tinywasm.rs index 70de73e0..b0a2e47d 100644 --- a/examples/rust/src/tinywasm.rs +++ b/examples/rust/src/tinywasm.rs @@ -15,7 +15,7 @@ fn run() -> tinywasm::Result<()> { let module = tinywasm::parse_stream(&include_bytes!("./print.wasm")[..])?; let mut store = tinywasm::Store::default(); - let printi32 = HostFunction::from(&mut store, |_: FuncContext<'_>, v: i32| { + let printi32 = HostFunction::from(|_: FuncContext<'_>, v: i32| { unsafe { printi32(v) } Ok(()) }); @@ -23,7 +23,7 @@ fn run() -> tinywasm::Result<()> { let mut imports = tinywasm::Imports::new(); imports.define("env", "printi32", printi32); - let instance = ModuleInstance::instantiate(&mut store, &module, Some(imports))?; + let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports))?; let add_and_print = instance.func::<(i32, i32), ()>(&store, "add_and_print")?; add_and_print.call(&mut store, (1, 2))?; Ok(()) diff --git a/examples/rust/src/tinywasm_no_std.rs b/examples/rust/src/tinywasm_no_std.rs index c1dd9108..a70abb93 100644 --- a/examples/rust/src/tinywasm_no_std.rs +++ b/examples/rust/src/tinywasm_no_std.rs @@ -1,7 +1,7 @@ #![no_main] #![no_std] use dlmalloc::GlobalDlmalloc; -use tinywasm::{FuncContext, HostFunction, ModuleInstance}; +use tinywasm::{FuncContext, HostFunction, Module, ModuleInstance}; extern crate alloc; @@ -28,17 +28,15 @@ fn run() -> tinywasm::Result<()> { let mut store = tinywasm::Store::default(); let mut imports = tinywasm::Imports::new(); - let res = tinywasm::parser::Parser::new().parse_module_bytes(include_bytes!("./print.wasm"))?; - let twasm = res.serialize_twasm()?; - let module = tinywasm::parse_bytes(&twasm)?; + let module = Module::try_from_twasm(include_bytes!("./print.twasm"))?; - let printi32 = HostFunction::from(&mut store, |_: FuncContext<'_>, v: i32| { + let printi32 = HostFunction::from(|_: FuncContext<'_>, v: i32| { unsafe { printi32(v) } Ok(()) }); imports.define("env", "printi32", printi32); - let instance = ModuleInstance::instantiate(&mut store, &module, Some(imports))?; + let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports))?; let add_and_print = instance.func::<(i32, i32), ()>(&store, "add_and_print")?; add_and_print.call(&mut store, (1, 2))?; Ok(()) diff --git a/examples/rust/src/tinywasm_precompiled.rs b/examples/rust/src/tinywasm_precompiled.rs new file mode 100644 index 00000000..bc0e0c1a --- /dev/null +++ b/examples/rust/src/tinywasm_precompiled.rs @@ -0,0 +1,30 @@ +#![no_main] +use tinywasm::{FuncContext, HostFunction, Module, ModuleInstance}; + +#[link(wasm_import_module = "env")] +unsafe extern "C" { + fn printi32(x: i32); +} + +#[unsafe(no_mangle)] +pub extern "C" fn hello() { + let _ = run(); +} + +fn run() -> tinywasm::Result<()> { + let module = Module::try_from_twasm(include_bytes!("./print.twasm"))?; + let mut store = tinywasm::Store::default(); + + let printi32 = HostFunction::from(|_: FuncContext<'_>, v: i32| { + unsafe { printi32(v) } + Ok(()) + }); + + let mut imports = tinywasm::Imports::new(); + imports.define("env", "printi32", printi32); + + let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports))?; + let add_and_print = instance.func::<(i32, i32), ()>(&store, "add_and_print")?; + add_and_print.call(&mut store, (1, 2))?; + Ok(()) +} diff --git a/examples/simple.rs b/examples/simple.rs index a902b4c6..0d57265e 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -1,4 +1,4 @@ -use eyre::Result; +use anyhow::Result; use tinywasm::{ModuleInstance, Store}; const WASM: &str = r#" diff --git a/examples/simple2.rs b/examples/simple2.rs index da7def79..f673dfe5 100644 --- a/examples/simple2.rs +++ b/examples/simple2.rs @@ -1,4 +1,4 @@ -use eyre::Result; +use anyhow::Result; use tinywasm::{ModuleInstance, Store}; const WASM: &str = r#" diff --git a/examples/wasm-rust.rs b/examples/wasm-rust.rs index e0b9968f..b3398f8b 100644 --- a/examples/wasm-rust.rs +++ b/examples/wasm-rust.rs @@ -1,6 +1,6 @@ use std::hint::black_box; -use eyre::{Result, eyre}; +use anyhow::{Result, anyhow}; use tinywasm::{FuncContext, HostFunction, Imports, ModuleInstance, Store}; /// Examples of using WebAssembly compiled from Rust with tinywasm. @@ -23,7 +23,7 @@ fn main() -> Result<()> { pretty_env_logger::init(); if !std::path::Path::new("./examples/rust/out/").exists() { - return Err(eyre!("No WebAssembly files found. See examples/wasm-rust.rs for instructions.")); + return Err(anyhow!("No WebAssembly files found. See examples/wasm-rust.rs for instructions.")); } let args = std::env::args().collect::>(); @@ -35,6 +35,8 @@ fn main() -> Result<()> { println!(" host_fn"); println!(" fibonacci - calculate fibonacci(30)"); println!(" tinywasm - run printi32 inside of tinywasm inside of itself"); + println!(" tinywasm_precompiled - run a precompiled module inside of tinywasm"); + println!(" tinywasm_no_std - run a precompiled module inside of no_std tinywasm"); println!(" argon2id - run argon2id(1000, 2, 1)"); return Ok(()); } @@ -44,6 +46,7 @@ fn main() -> Result<()> { "printi32" => printi32()?, "fibonacci" => fibonacci()?, "tinywasm" => tinywasm()?, + "tinywasm_precompiled" => tinywasm_precompiled()?, "tinywasm_no_std" => tinywasm_no_std()?, "argon2id" => argon2id()?, "host_fn" => host_fn()?, @@ -57,6 +60,8 @@ fn main() -> Result<()> { fibonacci()?; println!("\ntinywasm.wasm:"); tinywasm()?; + println!("\ntinywasm_precompiled.wasm:"); + tinywasm_precompiled()?; println!("\ntinywasm_no_std.wasm:"); tinywasm_no_std()?; println!("argon2id.wasm:"); @@ -75,8 +80,8 @@ fn tinywasm() -> Result<()> { let mut store = Store::default(); let mut imports = Imports::new(); - imports.define("env", "printi32", HostFunction::from(&mut store, |_: FuncContext<'_>, _x: i32| Ok(()))); - let instance = ModuleInstance::instantiate(&mut store, &module, Some(black_box(imports)))?; + imports.define("env", "printi32", HostFunction::from(|_: FuncContext<'_>, _x: i32| Ok(()))); + let instance = ModuleInstance::instantiate(&mut store, &module, Some(black_box(&imports)))?; let hello = instance.func::<(), ()>(&store, "hello")?; hello.call(&mut store, black_box(()))?; @@ -90,8 +95,23 @@ fn tinywasm_no_std() -> Result<()> { let mut store = Store::default(); let mut imports = Imports::new(); - imports.define("env", "printi32", HostFunction::from(&mut store, |_: FuncContext<'_>, _x: i32| Ok(()))); - let instance = ModuleInstance::instantiate(&mut store, &module, Some(black_box(imports)))?; + imports.define("env", "printi32", HostFunction::from(|_: FuncContext<'_>, _x: i32| Ok(()))); + let instance = ModuleInstance::instantiate(&mut store, &module, Some(black_box(&imports)))?; + + let hello = instance.func::<(), ()>(&store, "hello")?; + hello.call(&mut store, black_box(()))?; + hello.call(&mut store, black_box(()))?; + hello.call(&mut store, black_box(()))?; + Ok(()) +} + +fn tinywasm_precompiled() -> Result<()> { + let module = tinywasm::parse_file("./examples/rust/out/tinywasm_precompiled.opt.wasm")?; + let mut store = Store::default(); + + let mut imports = Imports::new(); + imports.define("env", "printi32", HostFunction::from(|_: FuncContext<'_>, _x: i32| Ok(()))); + let instance = ModuleInstance::instantiate(&mut store, &module, Some(black_box(&imports)))?; let hello = instance.func::<(), ()>(&store, "hello")?; hello.call(&mut store, black_box(()))?; @@ -104,7 +124,7 @@ fn hello() -> Result<()> { let module = tinywasm::parse_file("./examples/rust/out/hello.opt.wasm")?; let mut store = Store::default(); - let print_utf8 = HostFunction::from(&mut store, |ctx: FuncContext<'_>, (ptr, len): (i64, i32)| { + let print_utf8 = HostFunction::from(|ctx: FuncContext<'_>, (ptr, len): (i64, i32)| { let mem = ctx.memory("memory")?; let string = mem.read_string(ctx.store(), ptr as usize, len as usize)?; println!("{string}"); @@ -114,7 +134,7 @@ fn hello() -> Result<()> { let mut imports = Imports::new(); imports.define("env", "print_utf8", print_utf8); - let instance = ModuleInstance::instantiate(&mut store, &module, Some(imports))?; + let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports))?; let arg_ptr = instance.func::<(), i32>(&store, "arg_ptr")?.call(&mut store, ())?; let arg = b"world"; @@ -129,7 +149,7 @@ fn host_fn() -> Result<()> { let module = tinywasm::parse_file("./examples/rust/out/host_fn.opt.wasm")?; let mut store = Store::default(); - let bar = HostFunction::from(&mut store, |_: FuncContext<'_>, (left, right): (i64, i32)| { + let bar = HostFunction::from(|_: FuncContext<'_>, (left, right): (i64, i32)| { assert_eq!(left, 1); assert_eq!(right, 2); Ok(left as i32 + right) @@ -138,7 +158,7 @@ fn host_fn() -> Result<()> { let mut imports = Imports::new(); imports.define("env", "bar", bar); - let instance = ModuleInstance::instantiate(&mut store, &module, Some(imports))?; + let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports))?; let host_fn = instance.func::<(), i32>(&store, "foo")?; assert_eq!(host_fn.call(&mut store, ())?, 3); Ok(()) @@ -148,7 +168,7 @@ fn printi32() -> Result<()> { let module = tinywasm::parse_file("./examples/rust/out/print.opt.wasm")?; let mut store = Store::default(); - let printi32 = HostFunction::from(&mut store, |_: FuncContext<'_>, x: i32| { + let printi32 = HostFunction::from(|_: FuncContext<'_>, x: i32| { println!("{x}"); Ok(()) }); @@ -156,7 +176,7 @@ fn printi32() -> Result<()> { let mut imports = Imports::new(); imports.define("env", "printi32", printi32); - let instance = ModuleInstance::instantiate(&mut store, &module, Some(imports))?; + let instance = ModuleInstance::instantiate(&mut store, &module, Some(&imports))?; let add_and_print = instance.func::<(i32, i32), ()>(&store, "add_and_print")?; add_and_print.call(&mut store, (1, 2))?; @@ -211,6 +231,11 @@ mod tests { tinywasm().unwrap(); } + #[test] + fn test_tinywasm_precompiled() { + tinywasm_precompiled().unwrap(); + } + #[test] fn test_tinywasm_no_std() { tinywasm_no_std().unwrap();