rwasm 0.4.8

ZK-friendly WebAssembly runtime optimized for blockchain and zero-knowledge applications
Documentation
use crate::{ElementSegmentIdx, RwasmExecutor, TableEntity, TableIdx, TrapCode};

impl<'a, T> RwasmExecutor<'a, T> {
    /// Resolves a table by its index, materializing an empty one if it doesn't exist yet.
    ///
    /// Tables enter the store only when the module executes `TableGrow` (the translator emits one
    /// per declared table), but a module is free to reference a table index it never grows, and
    /// verification can't reject that because table indices carry no declaration.
    /// Such a table is treated as a zero-length table, which matches Wasm semantics: `table.size`
    /// returns 0 and every element access is bound-checked into `TrapCode::TableOutOfBounds`.
    #[inline(always)]
    pub(crate) fn resolve_table(&mut self, table_idx: TableIdx) -> &mut TableEntity {
        self.store.tables.entry(table_idx).or_default()
    }

    #[inline(always)]
    pub(crate) fn visit_table_size(&mut self, table_idx: TableIdx) {
        let table_size = self.resolve_table(table_idx).size();
        self.sp.push_as(table_size);
        self.ip.add(1);
    }

    #[inline(always)]
    pub(crate) fn visit_table_grow(&mut self, table_idx: TableIdx) -> Result<(), TrapCode> {
        let (init, delta) = self.sp.pop2();
        let delta: u32 = delta.into();
        let table = self.resolve_table(table_idx);
        let result = table.grow_untyped(delta, init);
        self.sp.push_as(result);
        #[cfg(feature = "tracing")]
        self.store
            .tracer
            .table_size_change(table_idx as u32, init.into(), delta);
        self.ip.add(1);
        Ok(())
    }

    #[inline(always)]
    pub(crate) fn visit_table_fill(&mut self, table_idx: TableIdx) -> Result<(), TrapCode> {
        let (i, val, n) = self.sp.pop3();
        self.resolve_table(table_idx)
            .fill_untyped(i.into(), val, n.into())?;
        self.ip.add(1);
        Ok(())
    }

    #[inline(always)]
    pub(crate) fn visit_table_get(&mut self, table_idx: TableIdx) -> Result<(), TrapCode> {
        let index = self.sp.pop();
        let value = self
            .resolve_table(table_idx)
            .get_untyped(index.into())
            .ok_or(TrapCode::TableOutOfBounds)?;
        self.sp.push(value);
        self.ip.add(1);
        Ok(())
    }

    #[inline(always)]
    pub(crate) fn visit_table_set(&mut self, table_idx: TableIdx) -> Result<(), TrapCode> {
        let (index, value) = self.sp.pop2();
        self.resolve_table(table_idx)
            .set_untyped(index.into(), value)
            .map_err(|_| TrapCode::TableOutOfBounds)?;
        #[cfg(feature = "tracing")]
        self.store
            .tracer
            .table_change(table_idx as u32, index.into(), value);
        self.ip.add(1);
        Ok(())
    }

    #[inline(always)]
    pub(crate) fn visit_table_copy(
        &mut self,
        dst_table_idx: TableIdx,
        src_table_idx: TableIdx,
    ) -> Result<(), TrapCode> {
        let (d, s, n) = self.sp.pop3();
        let len = u32::from(n);
        let src_index = u32::from(s);
        let dst_index = u32::from(d);
        // Query both tables and check if they are the same:
        if src_table_idx != dst_table_idx {
            self.resolve_table(src_table_idx);
            self.resolve_table(dst_table_idx);
            // Both tables are materialized above, so the disjoint lookup always succeeds
            let [Some(src), Some(dst)] = self
                .store
                .tables
                .get_disjoint_mut([&src_table_idx, &dst_table_idx])
            else {
                return Err(TrapCode::TableOutOfBounds);
            };
            TableEntity::copy(dst, dst_index, src, src_index, len)?;
        } else {
            let src = self.resolve_table(src_table_idx);
            src.copy_within(dst_index, src_index, len)?;
        }
        self.ip.add(1);
        Ok(())
    }

    #[inline(always)]
    pub(crate) fn visit_table_init(
        &mut self,
        element_segment_idx: ElementSegmentIdx,
    ) -> Result<(), TrapCode> {
        let table_idx = self.fetch_table_index(1);

        let (d, s, n) = self.sp.pop3();
        let len = u32::from(n);
        let src_index = u32::from(s);
        let dst_index = u32::from(d);

        // There is a trick with `element_segment_idx`:
        // it refers to the segment number.
        // However, in rwasm, all elements are stored in segment 0,
        // so there is no need to store information about the remaining segments.
        // According to the WebAssembly standards, though,
        // we must retain information about all dropped element segments
        // to perform an emptiness check.
        // Therefore, in `element_segment_idx`, we store the original index,
        // which is always > 0.
        let is_empty_segment = self
            .store
            .empty_elem_segments
            .get(element_segment_idx as usize)
            .as_deref()
            .copied()
            .unwrap_or(false);

        let mut module_elements_section = &self.module.elem_section[..];
        if is_empty_segment {
            module_elements_section = &[];
        }
        let table = self.resolve_table(table_idx);
        table.init_untyped(dst_index, module_elements_section, src_index, len)?;

        self.ip.add(2);
        Ok(())
    }

    #[inline(always)]
    pub(crate) fn visit_element_drop(&mut self, element_segment_idx: ElementSegmentIdx) {
        let empty_elem_segments = &mut self.store.empty_elem_segments;
        // grow only, `resize` would truncate the bitset and forget previously dropped segments
        // with a higher index
        let idx = element_segment_idx as usize;
        if idx >= empty_elem_segments.len() {
            empty_elem_segments.resize(idx + 1, false);
        }
        empty_elem_segments.set(idx, true);
        self.ip.add(1);
    }
}