|
1 | 1 | # TinyWasm Architecture |
2 | 2 |
|
3 | | -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. |
| 3 | +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. |
4 | 4 |
|
5 | | -## Runtime Layout |
| 5 | +## Execution Pipeline |
6 | 6 |
|
7 | | -- Values are stored in untyped stacks: |
8 | | - - `stack_32` for `i32`, `f32`, `funcref`, and `externref` |
9 | | - - `stack_64` for `i64` and `f64` |
10 | | - - `stack_128` for `v128` |
11 | | -- Locals are stored directly in the value stacks. Each `CallFrame` stores a `locals_base`, and local instructions index from that base. |
12 | | -- Structured control flow (`block`, `loop`, `if`, `br*`) is lowered during parsing to jump-oriented internal instructions such as `Jump`, `JumpIfZero`, `BranchTable*`, `DropKeep*`, and `Return`. |
13 | | -- Execution is a single iterative interpreter loop over the lowered instruction stream. |
| 7 | +TinyWasm does not execute WebAssembly instructions directly. Parsing lowers them into an internal bytecode designed to make execution simpler and cheaper: |
14 | 8 |
|
15 | | -## Internal Bytecode |
| 9 | +- structured control flow (`block`, `loop`, `if`, and `br*`) becomes jump-oriented instructions such as `Jump`, `JumpIfZero`, `BranchTable*`, `DropKeep*`, and `Return` |
| 10 | +- operand widths are encoded in instruction variants, and branch stack reshaping is explicit |
| 11 | +- instructions retain compact module-local indexes, which each instance maps to Store-wide runtime addresses |
| 12 | +- when enabled, the optimizer applies local rewrites, including superinstruction fusion, specialized calls and returns, and redundant-instruction removal |
| 13 | +- modules can be serialized as `.twasm` archives containing this lowered representation |
| 14 | +- execution uses a single iterative dispatch loop over the resulting instruction stream |
16 | 15 |
|
17 | | -TinyWasm does not interpret WebAssembly instructions directly. During parsing and validation, WebAssembly is translated into TinyWasm's internal bytecode format. |
| 16 | +## Value Stacks |
18 | 17 |
|
19 | | -This internal representation is designed to make execution simpler and cheaper: |
| 18 | +WebAssembly combines an operand stack with function-scoped locals. TinyWasm stores both in the same width-specific physical stacks: |
20 | 19 |
|
21 | | -- structured control flow is resolved ahead of time |
22 | | -- stack effects are made explicit |
23 | | -- common instruction sequences can be fused into superinstructions |
24 | | -- modules can optionally be serialized as `.twasm` for reuse |
| 20 | +- `stack_32` for `i32`, `f32`, and reference values, including GC and exception references |
| 21 | +- `stack_64` for `i64` and `f64` |
| 22 | +- `stack_128` for `v128` |
25 | 23 |
|
26 | | -## Optimizer |
| 24 | +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. |
27 | 25 |
|
28 | | -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. |
| 26 | +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. |
29 | 27 |
|
30 | | -Examples include: |
| 28 | +## Interpreter Optimization |
31 | 29 |
|
32 | | -- **Fused binops**: `BinOpLocalLocal*`, `BinOpLocalConst*`, `BinOpStackGlobal*` |
33 | | - Combine local/global access, a binary operation, and sometimes a store/tee. |
34 | | -- **Fused jumps**: `JumpCmpLocalConst*`, `JumpCmpLocalLocal*`, `JumpCmpStackConst*` |
35 | | - Combine comparison and conditional branch logic. |
| 30 | +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()`. |
| 31 | + |
| 32 | +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: |
| 33 | + |
| 34 | +- fused binary operations such as `BinOpLocalLocal*`, `BinOpLocalConst*`, and `BinOpStackGlobal*` |
| 35 | +- fused conditional branches such as `JumpCmpLocalConst*`, `JumpCmpLocalLocal*`, and `JumpCmpStackConst*` |
| 36 | + |
| 37 | +The default runtime remains safe Rust throughout rather than relying on unchecked operations. |
| 38 | + |
| 39 | +## SIMD |
| 40 | + |
| 41 | +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. |
36 | 42 |
|
37 | 43 | ## Memory Backends |
38 | 44 |
|
39 | 45 | Linear memory is implemented through the `LinearMemory` trait. The backend is selected with `engine::Config::with_memory_backend()`. |
40 | 46 |
|
| 47 | +`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. |
| 48 | + |
| 49 | +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. |
| 50 | + |
41 | 51 | Available backends: |
42 | 52 |
|
43 | | -- `VecMemory` - contiguous `Vec<u8>` backing; the default backend. |
44 | | -- `PagedMemory` - chunk-based allocation, useful when growing memory without reallocating one large buffer. |
45 | | -- `LazyLinearMemory` - wraps another backend and allocates memory on first access. |
| 53 | +- `VecMemory` - contiguous `Vec<u8>` backing and the default backend. |
| 54 | +- `PagedMemory` - sparse chunk-based allocation, with untouched chunks left unallocated and growth avoiding relocation of one contiguous buffer. |
| 55 | +- `LazyLinearMemory` - serves zero-filled reads without allocation and creates the configured backend on the first mutation or growth. |
46 | 56 | - Custom backends through `MemoryBackend::custom()`. |
47 | 57 |
|
| 58 | +`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. |
| 59 | + |
48 | 60 | ## Future Experiments |
49 | 61 |
|
50 | | -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. |
| 62 | +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. |
51 | 63 |
|
52 | | -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. |
| 64 | +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. |
53 | 65 |
|
54 | | -## Code Map |
| 66 | +## Important Modules |
55 | 67 |
|
56 | | -- [visit.rs](./crates/parser/src/visit.rs) - WebAssembly binary visitor |
| 68 | +- [visit.rs](./crates/parser/src/visit.rs) - function-body operator lowering |
57 | 69 | - [optimize.rs](./crates/parser/src/optimize.rs) - peephole optimizer and superinstruction fusion |
58 | | -- [parallel.rs](./crates/parser/src/parallel.rs) - multithreaded function parsing |
| 70 | +- [parallel.rs](./crates/parser/src/parallel.rs) - parallel function parsing |
59 | 71 | - [instructions.rs](./crates/types/src/instructions.rs) - internal instruction set |
60 | | -- [value_stack.rs](./crates/tinywasm/src/interpreter/stack/value_stack.rs) - typed value stacks |
| 72 | +- [value_stack.rs](./crates/tinywasm/src/interpreter/stack/value_stack.rs) - width-specific stacks |
61 | 73 | - [call_stack.rs](./crates/tinywasm/src/interpreter/stack/call_stack.rs) - call frame stack |
62 | 74 | - [memory/mod.rs](./crates/tinywasm/src/store/memory/mod.rs) - memory backend trait and implementations |
0 commit comments