submilli-wasm 0.2.0

A WebAssembly interpreter with a wasmtime-compatible API, built for fast compilation and startup
Documentation
//! Compiled-code storage: one set of module-wide arenas ([`CodeArenas`]) holding every
//! function's op stream and side tables contiguously, a `Copy` per-function record of
//! [`Span`]s into them ([`CompiledFunc`]), and the refcounted runtime handle ([`Code`])
//! the interpreter walks. One allocation per stream for the whole module — no
//! per-function `Box`es, no `Vec` over-reservation slack, and instances share it all
//! through the module's `Arc`.

use std::sync::Arc;

use crate::canon::{IrHeap, IrVal};
use crate::module::handler::HandlerSpan;
use crate::module::inner::ModuleInner;
use crate::module::op::{BigMemArg, BranchTarget, Op, TypeIdx};

/// A range into one of the module's code arenas.
#[derive(Copy, Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
pub(crate) struct Span {
    pub start: u32,
    pub len: u32,
}

impl Span {
    /// The arena range. `start + len` can't overflow for a span the compiler built; saturating
    /// keeps a corrupt artifact's span from wrapping into a valid-looking range (it then fails the
    /// arena bounds check instead).
    #[inline]
    pub(crate) fn range(self) -> std::ops::Range<usize> {
        self.start as usize..self.start.saturating_add(self.len) as usize
    }

    /// This span of `arena` — an error, not a panic, if it doesn't fit: spans are built by the
    /// compiler against the arenas they index, so only a corrupt deserialized artifact misses.
    #[inline]
    fn of<T>(self, arena: &[T]) -> crate::Result<&[T]> {
        arena.get(self.range()).ok_or_else(span_error)
    }
}

#[cold]
fn span_error() -> crate::Error {
    crate::error::InternalError::Compiler("code span out of range").into()
}

/// Module-wide arenas for compiled code: every defined function's streams, back to back,
/// addressed by the [`Span`]s in its [`CompiledFunc`]. Indices *within* a function
/// (branch `ip`s, `br_table`/`br_on_cast` pool indices, `BigMemArg` pool indices) stay
/// function-relative — the executor always works on the function's sub-slice. The exception is
/// the `v128s` pool, indexed module-wide (still bounds-checked; no per-function span needed).
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
pub(crate) struct CodeArenas {
    pub ops: Vec<Op>,
    pub local_types: Vec<IrVal>,
    pub handlers: Vec<HandlerSpan>,
    pub br_tables: Vec<BranchTarget>,
    pub big_memargs: Vec<BigMemArg>,
    /// Per-`Op` source wasm byte offsets (#29a); empty unless debug retention is on.
    pub offsets: Vec<u32>,
    /// Pooled 16-byte SIMD immediates (`v128.const` values, `i8x16.shuffle` lanes), indexed
    /// module-wide: inline they would widen every `Op` from 16 to 48 bytes.
    pub v128s: Vec<[u8; 16]>,
    /// Pooled `br_on_cast` target heap types, indexed module-wide like `v128s`.
    pub cast_heaps: Vec<IrHeap>,
}

/// A function compiled to internal bytecode: scalar facts plus [`Span`]s into the
/// module's [`CodeArenas`]. Plain `Copy` data — the whole `functions` table is one
/// allocation.
#[derive(Copy, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub(crate) struct CompiledFunc {
    pub ops: Span,
    /// Index into the module's type section (gives param/result types).
    pub type_idx: TypeIdx,
    pub n_params: u32,
    pub n_results: u32,
    /// Declared local types (params excluded); default-zero the frame's locals at call time.
    pub local_types: Span,
    /// Peak operand-stack depth above the locals (for stack pre-reservation).
    pub max_operands: u32,
    /// Exception-handler table (one entry per `try_table`; #28d). Consulted only on throw.
    pub handlers: Span,
    /// Flattened `br_table` target lists + pooled `br_on_cast` edges (function-relative).
    pub br_tables: Span,
    /// Pooled wide memory immediates (see `BIG_MEMARG`); empty for virtually every module.
    pub big_memargs: Span,
    /// Per-`Op` source offsets (#29a); an empty span when debug retention is off.
    pub offsets: Span,
}

/// Runtime handle to one compiled function: the owning module + defined-function index.
/// `Clone` is an `Arc` bump (what each call frame holds); every accessor resolves through
/// the module's arenas.
#[derive(Debug, Clone)]
pub(crate) struct Code {
    pub module: Arc<ModuleInner>,
    /// *Defined*-function index (module func index minus imports).
    pub index: u32,
}

// `index` is a defined-function index by construction (instantiation, `enter_defined`).
#[allow(clippy::indexing_slicing)]
impl Code {
    /// The module's identity, for comparison only: frames record it instead of holding a handle
    /// (their instance keeps the module alive), so a same-module call or return costs no refcount
    /// traffic. Never turned back into a pointer.
    #[inline]
    pub(crate) fn key(&self) -> usize {
        Arc::as_ptr(&self.module).addr()
    }

    #[inline]
    fn f(&self) -> &CompiledFunc {
        &self.module.functions[self.index as usize]
    }

    /// A copy of the function record (`CompiledFunc` is `Copy`): frames cache it so hot
    /// per-frame facts don't re-resolve through the `functions` table on every call/return.
    #[inline]
    pub(crate) fn func(&self) -> CompiledFunc {
        *self.f()
    }

    /// Re-points this handle at its module's defined function `index` (an `Op::CallInternal`
    /// target) and returns that function's module-space index; `None` (handle unchanged) if the
    /// module has no such function — a corrupt artifact, so the caller fails closed.
    #[inline]
    pub(crate) fn enter_defined(&mut self, index: u32) -> Option<u32> {
        if index as usize >= self.module.functions.len() {
            return None;
        }
        let func_index = index.checked_add(self.module.num_imported_funcs)?;
        self.index = index;
        Some(func_index)
    }

    /// The op stream at a frame-cached span (the run loop's fast path).
    #[inline]
    pub(crate) fn ops_in(&self, span: Span) -> crate::Result<&[Op]> {
        span.of(&self.module.code.ops)
    }

    /// The declared locals of an already-copied record (the `push_call` fast path).
    #[inline]
    pub(crate) fn local_types_of(&self, f: &CompiledFunc) -> crate::Result<&[IrVal]> {
        f.local_types.of(&self.module.code.local_types)
    }

    #[inline]
    pub(crate) fn handlers(&self) -> crate::Result<&[HandlerSpan]> {
        self.f().handlers.of(&self.module.code.handlers)
    }

    #[inline]
    pub(crate) fn br_tables(&self) -> crate::Result<&[BranchTarget]> {
        self.f().br_tables.of(&self.module.code.br_tables)
    }

    #[inline]
    pub(crate) fn big_memargs(&self) -> crate::Result<&[BigMemArg]> {
        self.f().big_memargs.of(&self.module.code.big_memargs)
    }

    /// Per-`Op` source offsets, or `None` when debug retention was off at compile (or the span is
    /// corrupt — offsets are advisory, so a backtrace just goes without).
    #[inline]
    pub(crate) fn offsets(&self) -> Option<&[u32]> {
        let span = self.f().offsets;
        if span.len == 0 {
            return None;
        }
        span.of(&self.module.code.offsets).ok()
    }

    /// A pooled `br_on_cast` heap type (see [`CodeArenas::cast_heaps`]).
    pub(crate) fn cast_heap(&self, index: u32) -> crate::Result<&IrHeap> {
        self.module
            .code
            .cast_heaps
            .get(index as usize)
            .ok_or_else(|| {
                crate::error::InternalError::Compiler("cast heap index out of range").into()
            })
    }

    /// A pooled SIMD immediate (see [`CodeArenas::v128s`]).
    pub(crate) fn v128(&self, index: u32) -> crate::Result<[u8; 16]> {
        self.module
            .code
            .v128s
            .get(index as usize)
            .copied()
            .ok_or_else(|| {
                crate::error::InternalError::Compiler("SIMD immediate index out of range").into()
            })
    }

    #[inline]
    pub(crate) fn type_idx(&self) -> TypeIdx {
        self.f().type_idx
    }

    #[inline]
    pub(crate) fn n_params(&self) -> u32 {
        self.f().n_params
    }

    #[inline]
    pub(crate) fn n_results(&self) -> u32 {
        self.f().n_results
    }

    #[inline]
    pub(crate) fn max_operands(&self) -> u32 {
        self.f().max_operands
    }
}