//! Stack-machine interpreter for WebAssembly's straight-line instructions. //! //! Implements the numeric, parametric, variable, table, and memory instruction //! groups from §4.4 of the WebAssembly Core Specification 2.0. Control flow //! (`block`/`loop`/`if`/`br*`/`return`) and function calls (`call`/ //! `call_indirect`) live in [`super::engine`], which drives this dispatch with //! a label and call stack on top. //! //! The interpreter trusts the validator: types on the operand stack always //! match the instruction's declared signature. Mismatches are an internal //! contract violation and surface as panics, not traps. The only failures it //! reports are the dynamic ones the spec calls out: out-of-bounds memory and //! table access, integer divide-by-zero and overflow, NaN/out-of-range float //! conversions, and the explicit `unreachable` instruction. //! //! IEEE-754 details that diverge from Rust's `std` semantics are handled //! explicitly: `min`/`max` propagate NaN and respect the `-0 < +0` ordering, //! `nearest` rounds half-to-even, and the integer ↔ float conversions reject //! their out-of-range inputs in the trapping variants and saturate in the //! `*_trunc_sat_*` variants. use crate::instruction::Instruction; use crate::runtime::store::{DataAddr, ElementAddr, MemoryAddr, ModuleAddr, Store, TableAddr}; use crate::runtime::trap::Trap; use crate::runtime::value::{Ref, WasmValue}; use crate::types::MemArg; /// Result of executing a single straight-line instruction. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum StepOutcome { /// Continue executing the next instruction in the function body. Continue, /// Hit the function's top-level `End`. The current operand stack is the /// function's result vector (arity already validated). FunctionEnd, } /// Stack-machine interpreter over a [`Store`] and a single active module. /// /// Construct one per activation. The operand stack and locals frame live /// inside the interpreter; callers run a function by calling [`Interpreter::step`] /// repeatedly until they get [`StepOutcome::FunctionEnd`] or a trap. pub struct Interpreter<'a> { store: &'a mut Store, module: ModuleAddr, locals: Vec, stack: Vec, } impl<'a> Interpreter<'a> { /// Build a fresh interpreter. `locals` already contains the function's /// parameters followed by zero-initialized declared locals — the /// instantiation pipeline is responsible for that layout per spec §4.5.4. pub fn new(store: &'a mut Store, module: ModuleAddr, locals: Vec) -> Self { Self { store, module, locals, stack: Vec::new(), } } /// Pre-allocate the operand stack. Useful for tight per-function reuse. pub fn with_capacity( store: &'a mut Store, module: ModuleAddr, locals: Vec, stack_capacity: usize, ) -> Self { Self { store, module, locals, stack: Vec::with_capacity(stack_capacity), } } /// Currently-active module address. pub fn module(&self) -> ModuleAddr { self.module } /// View the current locals frame. pub fn locals(&self) -> &[WasmValue] { &self.locals } /// Mutable access to the locals frame. pub fn locals_mut(&mut self) -> &mut Vec { &mut self.locals } /// View the current operand stack. pub fn stack(&self) -> &[WasmValue] { &self.stack } /// Push a value onto the operand stack. pub fn push(&mut self, v: WasmValue) { self.stack.push(v); } /// Pop a value off the operand stack. Panics if empty: the validator /// guarantees stack underflow cannot happen at runtime. pub fn pop(&mut self) -> WasmValue { self.stack.pop().expect("operand stack underflow") } /// Consume the interpreter and return the final operand stack. pub fn into_stack(self) -> Vec { self.stack } /// The shared store, for tests that need to inspect post-execution state. pub fn store(&self) -> &Store { self.store } /// Mutable access to the shared store. pub fn store_mut(&mut self) -> &mut Store { self.store } /// Execute one straight-line instruction. /// /// Returns [`StepOutcome::Continue`] for normal flow, [`StepOutcome::FunctionEnd`] /// when the function's terminating `End` is reached, or a [`Trap`] for any /// dynamic failure. Passing a control-flow or call instruction here is a /// programming error — that dispatch lives in [`super::engine`]. pub fn step(&mut self, instr: &Instruction) -> Result { step_straight( self.store, self.module, &mut self.locals, &mut self.stack, instr, ) } } // ============================================================================ // Free-function dispatch — shared by [`Interpreter`] and [`super::engine`]. // ============================================================================ /// Execute one straight-line instruction against the supplied stack and locals. /// /// Control-flow (`block`/`loop`/`if`/`else`/`br*`/`return`) and call (`call`/ /// `call_indirect`) instructions are NOT handled here; passing one is a /// programming error and panics. The engine driver layers above us must filter /// them out before dispatch. pub(crate) fn step_straight( store: &mut Store, module: ModuleAddr, locals: &mut [WasmValue], stack: &mut Vec, instr: &Instruction, ) -> Result { match instr { // ---- Trivial control -------------------------------------- Instruction::Unreachable => Err(Trap::Unreachable), Instruction::Nop => Ok(StepOutcome::Continue), Instruction::End => Ok(StepOutcome::FunctionEnd), // ---- Out-of-scope control flow / calls -------------------- Instruction::Block(_) | Instruction::Loop(_) | Instruction::If(_) | Instruction::Else | Instruction::Br(_) | Instruction::BrIf(_) | Instruction::BrTable { .. } | Instruction::Return | Instruction::Call(_) | Instruction::CallIndirect { .. } => { unreachable!( "control-flow / call instruction {:?} is handled by the \ engine driver, not step_straight", instr ) } // ---- Reference instructions ------------------------------ Instruction::RefNull(rt) => { stack.push(WasmValue::Ref(Ref::Null(*rt))); Ok(StepOutcome::Continue) } Instruction::RefIsNull => { let r = pop_ref(stack); stack.push(WasmValue::I32(bool_to_i32(r.is_null()))); Ok(StepOutcome::Continue) } Instruction::RefFunc(idx) => { let addr = store .module(module) .resolve_func(*idx) .expect("validator: function index out of range"); stack.push(WasmValue::Ref(Ref::Func(addr))); Ok(StepOutcome::Continue) } // ---- Bulk table ------------------------------------------ Instruction::TableInit { elem_index, table_index, } => { let n = pop_i32(stack) as u32; let src = pop_i32(stack) as u32; let dst = pop_i32(stack) as u32; let element_addr = store .module(module) .resolve_element(*elem_index) .expect("validator: element index out of range"); let table_addr = store .module(module) .resolve_table(*table_index) .expect("validator: table index out of range"); let refs = element_slice(store, element_addr, src, n)?.to_vec(); store.table_mut(table_addr).init(dst, &refs)?; Ok(StepOutcome::Continue) } Instruction::ElemDrop(idx) => { let addr = store .module(module) .resolve_element(*idx) .expect("validator: element index out of range"); store.element_mut(addr).drop_segment(); Ok(StepOutcome::Continue) } Instruction::TableCopy { dst_table, src_table, } => { let n = pop_i32(stack) as u32; let src = pop_i32(stack) as u32; let dst = pop_i32(stack) as u32; table_copy(store, module, *dst_table, *src_table, dst, src, n)?; Ok(StepOutcome::Continue) } // ---- Bulk memory ----------------------------------------- Instruction::MemoryInit { data_index, memory } => { let n = pop_i32(stack) as u32; let src = pop_i32(stack) as u32; let dst = pop_i32(stack) as u32; let data_addr = store .module(module) .resolve_data(*data_index) .expect("validator: data index out of range"); let memory_addr = store .module(module) .resolve_memory(*memory) .expect("validator: memory index out of range"); let bytes = data_slice(store, data_addr, src, n)?.to_vec(); store.memory_mut(memory_addr).init(dst as u64, &bytes)?; Ok(StepOutcome::Continue) } Instruction::DataDrop(idx) => { let addr = store .module(module) .resolve_data(*idx) .expect("validator: data index out of range"); store.data_mut(addr).drop_segment(); Ok(StepOutcome::Continue) } Instruction::MemoryCopy { dst_memory, src_memory, } => { let n = pop_i32(stack) as u32; let src = pop_i32(stack) as u32; let dst = pop_i32(stack) as u32; memory_copy(store, module, *dst_memory, *src_memory, dst, src, n)?; Ok(StepOutcome::Continue) } Instruction::MemoryFill(mem) => { let n = pop_i32(stack) as u32; let value = pop_i32(stack) as u8; let dst = pop_i32(stack) as u32; with_memory_mut(store, module, *mem, |memory| { memory.fill(dst as u64, n as u64, value) })?; Ok(StepOutcome::Continue) } // ---- Parametric ------------------------------------------- Instruction::Drop => { pop_any(stack); Ok(StepOutcome::Continue) } Instruction::Select | Instruction::SelectTyped(_) => { let c = pop_i32(stack); let v2 = pop_any(stack); let v1 = pop_any(stack); stack.push(if c != 0 { v1 } else { v2 }); Ok(StepOutcome::Continue) } // ---- Variable: locals ------------------------------------- Instruction::LocalGet(idx) => { stack.push(locals[*idx as usize]); Ok(StepOutcome::Continue) } Instruction::LocalSet(idx) => { let v = pop_any(stack); locals[*idx as usize] = v; Ok(StepOutcome::Continue) } Instruction::LocalTee(idx) => { let v = *stack.last().expect("operand stack underflow"); locals[*idx as usize] = v; Ok(StepOutcome::Continue) } // ---- Variable: globals ------------------------------------ Instruction::GlobalGet(idx) => { let addr = store .module(module) .resolve_global(*idx) .expect("validator: global index out of range"); stack.push(store.global(addr).get()); Ok(StepOutcome::Continue) } Instruction::GlobalSet(idx) => { let addr = store .module(module) .resolve_global(*idx) .expect("validator: global index out of range"); let v = pop_any(stack); store .global_mut(addr) .set(v) .expect("validator should have rejected global.set"); Ok(StepOutcome::Continue) } // ---- Table instructions ----------------------------------- Instruction::TableGet(idx) => { let i = pop_i32(stack) as u32; let addr = store .module(module) .resolve_table(*idx) .expect("validator: table index out of range"); let r = store.table(addr).get(i)?; stack.push(WasmValue::Ref(r)); Ok(StepOutcome::Continue) } Instruction::TableSet(idx) => { let val = pop_ref(stack); let i = pop_i32(stack) as u32; let addr = store .module(module) .resolve_table(*idx) .expect("validator: table index out of range"); store.table_mut(addr).set(i, val)?; Ok(StepOutcome::Continue) } Instruction::TableSize(idx) => { let addr = store .module(module) .resolve_table(*idx) .expect("validator: table index out of range"); let size = store.table(addr).size(); stack.push(WasmValue::I32(size as i32)); Ok(StepOutcome::Continue) } Instruction::TableGrow(idx) => { let n = pop_i32(stack) as u32; let init = pop_ref(stack); let addr = store .module(module) .resolve_table(*idx) .expect("validator: table index out of range"); let result = match store.table_mut(addr).grow(n, init) { Some(prev) => prev as i32, None => -1, }; stack.push(WasmValue::I32(result)); Ok(StepOutcome::Continue) } Instruction::TableFill(idx) => { let n = pop_i32(stack) as u32; let val = pop_ref(stack); let i = pop_i32(stack) as u32; let addr = store .module(module) .resolve_table(*idx) .expect("validator: table index out of range"); store.table_mut(addr).fill(i, n, val)?; Ok(StepOutcome::Continue) } // ---- Memory: size / grow ---------------------------------- Instruction::MemorySize(mem_idx) => { let addr = store .module(module) .resolve_memory(*mem_idx) .expect("validator: memory index out of range"); let pages = store.memory(addr).size(); stack.push(WasmValue::I32(pages as i32)); Ok(StepOutcome::Continue) } Instruction::MemoryGrow(mem_idx) => { let delta = pop_i32(stack) as u32; let addr = store .module(module) .resolve_memory(*mem_idx) .expect("validator: memory index out of range"); let result = match store.memory_mut(addr).grow(delta) { Some(prev) => prev as i32, None => -1, }; stack.push(WasmValue::I32(result)); Ok(StepOutcome::Continue) } // ---- Memory loads ---------------------------------------- Instruction::I32Load(m) => exec_load( store, module, stack, m, |mem, a| mem.load_i32(a), |s, v| s.push(WasmValue::I32(v)), ), Instruction::I64Load(m) => exec_load( store, module, stack, m, |mem, a| mem.load_i64(a), |s, v| s.push(WasmValue::I64(v)), ), Instruction::F32Load(m) => exec_load( store, module, stack, m, |mem, a| mem.load_f32_bits(a), |s, bits| s.push(WasmValue::F32(bits)), ), Instruction::F64Load(m) => exec_load( store, module, stack, m, |mem, a| mem.load_f64_bits(a), |s, bits| s.push(WasmValue::F64(bits)), ), Instruction::I32Load8S(m) => exec_load( store, module, stack, m, |mem, a| mem.load_i8(a), |s, v| s.push(WasmValue::I32(v as i32)), ), Instruction::I32Load8U(m) => exec_load( store, module, stack, m, |mem, a| mem.load_u8(a), |s, v| s.push(WasmValue::I32(v as i32)), ), Instruction::I32Load16S(m) => exec_load( store, module, stack, m, |mem, a| mem.load_i16(a), |s, v| s.push(WasmValue::I32(v as i32)), ), Instruction::I32Load16U(m) => exec_load( store, module, stack, m, |mem, a| mem.load_u16(a), |s, v| s.push(WasmValue::I32(v as i32)), ), Instruction::I64Load8S(m) => exec_load( store, module, stack, m, |mem, a| mem.load_i8(a), |s, v| s.push(WasmValue::I64(v as i64)), ), Instruction::I64Load8U(m) => exec_load( store, module, stack, m, |mem, a| mem.load_u8(a), |s, v| s.push(WasmValue::I64(v as i64)), ), Instruction::I64Load16S(m) => exec_load( store, module, stack, m, |mem, a| mem.load_i16(a), |s, v| s.push(WasmValue::I64(v as i64)), ), Instruction::I64Load16U(m) => exec_load( store, module, stack, m, |mem, a| mem.load_u16(a), |s, v| s.push(WasmValue::I64(v as i64)), ), Instruction::I64Load32S(m) => exec_load( store, module, stack, m, |mem, a| mem.load_i32(a), |s, v| s.push(WasmValue::I64(v as i64)), ), Instruction::I64Load32U(m) => exec_load( store, module, stack, m, |mem, a| mem.load_u32(a), |s, v| s.push(WasmValue::I64(v as i64)), ), // ---- Memory stores --------------------------------------- Instruction::I32Store(m) => { let v = pop_i32(stack); let addr = effective_addr(stack, m); with_memory_mut(store, module, m.memory, |mem| mem.store_i32(addr, v))?; Ok(StepOutcome::Continue) } Instruction::I64Store(m) => { let v = pop_i64(stack); let addr = effective_addr(stack, m); with_memory_mut(store, module, m.memory, |mem| mem.store_i64(addr, v))?; Ok(StepOutcome::Continue) } Instruction::F32Store(m) => { let bits = match pop_any(stack) { WasmValue::F32(b) => b, other => unreachable!("expected f32, got {:?}", other), }; let addr = effective_addr(stack, m); with_memory_mut(store, module, m.memory, |mem| { mem.store_f32_bits(addr, bits) })?; Ok(StepOutcome::Continue) } Instruction::F64Store(m) => { let bits = match pop_any(stack) { WasmValue::F64(b) => b, other => unreachable!("expected f64, got {:?}", other), }; let addr = effective_addr(stack, m); with_memory_mut(store, module, m.memory, |mem| { mem.store_f64_bits(addr, bits) })?; Ok(StepOutcome::Continue) } Instruction::I32Store8(m) => { let v = pop_i32(stack) as u8; let addr = effective_addr(stack, m); with_memory_mut(store, module, m.memory, |mem| mem.store_u8(addr, v))?; Ok(StepOutcome::Continue) } Instruction::I32Store16(m) => { let v = pop_i32(stack) as u16; let addr = effective_addr(stack, m); with_memory_mut(store, module, m.memory, |mem| mem.store_u16(addr, v))?; Ok(StepOutcome::Continue) } Instruction::I64Store8(m) => { let v = pop_i64(stack) as u8; let addr = effective_addr(stack, m); with_memory_mut(store, module, m.memory, |mem| mem.store_u8(addr, v))?; Ok(StepOutcome::Continue) } Instruction::I64Store16(m) => { let v = pop_i64(stack) as u16; let addr = effective_addr(stack, m); with_memory_mut(store, module, m.memory, |mem| mem.store_u16(addr, v))?; Ok(StepOutcome::Continue) } Instruction::I64Store32(m) => { let v = pop_i64(stack) as u32; let addr = effective_addr(stack, m); with_memory_mut(store, module, m.memory, |mem| mem.store_u32(addr, v))?; Ok(StepOutcome::Continue) } // ---- Numeric constants ----------------------------------- Instruction::I32Const(v) => { stack.push(WasmValue::I32(*v)); Ok(StepOutcome::Continue) } Instruction::I64Const(v) => { stack.push(WasmValue::I64(*v)); Ok(StepOutcome::Continue) } Instruction::F32Const(v) => { stack.push(WasmValue::F32(v.to_bits())); Ok(StepOutcome::Continue) } Instruction::F64Const(v) => { stack.push(WasmValue::F64(v.to_bits())); Ok(StepOutcome::Continue) } // ---- i32 comparisons ------------------------------------- Instruction::I32Eqz => { let a = pop_i32(stack); stack.push(WasmValue::I32(bool_to_i32(a == 0))); Ok(StepOutcome::Continue) } Instruction::I32Eq => bin_i32_cmp(stack, |a, b| a == b), Instruction::I32Ne => bin_i32_cmp(stack, |a, b| a != b), Instruction::I32LtS => bin_i32_cmp(stack, |a, b| a < b), Instruction::I32LtU => bin_i32_cmp(stack, |a, b| (a as u32) < (b as u32)), Instruction::I32GtS => bin_i32_cmp(stack, |a, b| a > b), Instruction::I32GtU => bin_i32_cmp(stack, |a, b| (a as u32) > (b as u32)), Instruction::I32LeS => bin_i32_cmp(stack, |a, b| a <= b), Instruction::I32LeU => bin_i32_cmp(stack, |a, b| (a as u32) <= (b as u32)), Instruction::I32GeS => bin_i32_cmp(stack, |a, b| a >= b), Instruction::I32GeU => bin_i32_cmp(stack, |a, b| (a as u32) >= (b as u32)), // ---- i64 comparisons ------------------------------------- Instruction::I64Eqz => { let a = pop_i64(stack); stack.push(WasmValue::I32(bool_to_i32(a == 0))); Ok(StepOutcome::Continue) } Instruction::I64Eq => bin_i64_cmp(stack, |a, b| a == b), Instruction::I64Ne => bin_i64_cmp(stack, |a, b| a != b), Instruction::I64LtS => bin_i64_cmp(stack, |a, b| a < b), Instruction::I64LtU => bin_i64_cmp(stack, |a, b| (a as u64) < (b as u64)), Instruction::I64GtS => bin_i64_cmp(stack, |a, b| a > b), Instruction::I64GtU => bin_i64_cmp(stack, |a, b| (a as u64) > (b as u64)), Instruction::I64LeS => bin_i64_cmp(stack, |a, b| a <= b), Instruction::I64LeU => bin_i64_cmp(stack, |a, b| (a as u64) <= (b as u64)), Instruction::I64GeS => bin_i64_cmp(stack, |a, b| a >= b), Instruction::I64GeU => bin_i64_cmp(stack, |a, b| (a as u64) >= (b as u64)), // ---- f32 comparisons ------------------------------------- Instruction::F32Eq => bin_f32_cmp(stack, |a, b| a == b), Instruction::F32Ne => bin_f32_cmp(stack, |a, b| a != b), Instruction::F32Lt => bin_f32_cmp(stack, |a, b| a < b), Instruction::F32Gt => bin_f32_cmp(stack, |a, b| a > b), Instruction::F32Le => bin_f32_cmp(stack, |a, b| a <= b), Instruction::F32Ge => bin_f32_cmp(stack, |a, b| a >= b), // ---- f64 comparisons ------------------------------------- Instruction::F64Eq => bin_f64_cmp(stack, |a, b| a == b), Instruction::F64Ne => bin_f64_cmp(stack, |a, b| a != b), Instruction::F64Lt => bin_f64_cmp(stack, |a, b| a < b), Instruction::F64Gt => bin_f64_cmp(stack, |a, b| a > b), Instruction::F64Le => bin_f64_cmp(stack, |a, b| a <= b), Instruction::F64Ge => bin_f64_cmp(stack, |a, b| a >= b), // ---- i32 numeric ----------------------------------------- Instruction::I32Clz => un_i32(stack, |a| a.leading_zeros() as i32), Instruction::I32Ctz => un_i32(stack, |a| a.trailing_zeros() as i32), Instruction::I32Popcnt => un_i32(stack, |a| a.count_ones() as i32), Instruction::I32Add => bin_i32(stack, |a, b| a.wrapping_add(b)), Instruction::I32Sub => bin_i32(stack, |a, b| a.wrapping_sub(b)), Instruction::I32Mul => bin_i32(stack, |a, b| a.wrapping_mul(b)), Instruction::I32DivS => bin_i32_try(stack, |a, b| { if b == 0 { Err(Trap::IntegerDivideByZero) } else if a == i32::MIN && b == -1 { Err(Trap::IntegerOverflow) } else { Ok(a.wrapping_div(b)) } }), Instruction::I32DivU => bin_i32_try(stack, |a, b| { if b == 0 { Err(Trap::IntegerDivideByZero) } else { Ok(((a as u32) / (b as u32)) as i32) } }), Instruction::I32RemS => bin_i32_try(stack, |a, b| { if b == 0 { Err(Trap::IntegerDivideByZero) } else if a == i32::MIN && b == -1 { Ok(0) } else { Ok(a.wrapping_rem(b)) } }), Instruction::I32RemU => bin_i32_try(stack, |a, b| { if b == 0 { Err(Trap::IntegerDivideByZero) } else { Ok(((a as u32) % (b as u32)) as i32) } }), Instruction::I32And => bin_i32(stack, |a, b| a & b), Instruction::I32Or => bin_i32(stack, |a, b| a | b), Instruction::I32Xor => bin_i32(stack, |a, b| a ^ b), Instruction::I32Shl => bin_i32(stack, |a, b| a.wrapping_shl((b as u32) % 32)), Instruction::I32ShrS => bin_i32(stack, |a, b| a.wrapping_shr((b as u32) % 32)), Instruction::I32ShrU => bin_i32(stack, |a, b| { ((a as u32).wrapping_shr((b as u32) % 32)) as i32 }), Instruction::I32Rotl => { bin_i32(stack, |a, b| (a as u32).rotate_left((b as u32) % 32) as i32) } Instruction::I32Rotr => bin_i32(stack, |a, b| { (a as u32).rotate_right((b as u32) % 32) as i32 }), // ---- i64 numeric ----------------------------------------- Instruction::I64Clz => un_i64(stack, |a| a.leading_zeros() as i64), Instruction::I64Ctz => un_i64(stack, |a| a.trailing_zeros() as i64), Instruction::I64Popcnt => un_i64(stack, |a| a.count_ones() as i64), Instruction::I64Add => bin_i64(stack, |a, b| a.wrapping_add(b)), Instruction::I64Sub => bin_i64(stack, |a, b| a.wrapping_sub(b)), Instruction::I64Mul => bin_i64(stack, |a, b| a.wrapping_mul(b)), Instruction::I64DivS => bin_i64_try(stack, |a, b| { if b == 0 { Err(Trap::IntegerDivideByZero) } else if a == i64::MIN && b == -1 { Err(Trap::IntegerOverflow) } else { Ok(a.wrapping_div(b)) } }), Instruction::I64DivU => bin_i64_try(stack, |a, b| { if b == 0 { Err(Trap::IntegerDivideByZero) } else { Ok(((a as u64) / (b as u64)) as i64) } }), Instruction::I64RemS => bin_i64_try(stack, |a, b| { if b == 0 { Err(Trap::IntegerDivideByZero) } else if a == i64::MIN && b == -1 { Ok(0) } else { Ok(a.wrapping_rem(b)) } }), Instruction::I64RemU => bin_i64_try(stack, |a, b| { if b == 0 { Err(Trap::IntegerDivideByZero) } else { Ok(((a as u64) % (b as u64)) as i64) } }), Instruction::I64And => bin_i64(stack, |a, b| a & b), Instruction::I64Or => bin_i64(stack, |a, b| a | b), Instruction::I64Xor => bin_i64(stack, |a, b| a ^ b), Instruction::I64Shl => bin_i64(stack, |a, b| a.wrapping_shl((b as u64 % 64) as u32)), Instruction::I64ShrS => bin_i64(stack, |a, b| a.wrapping_shr((b as u64 % 64) as u32)), Instruction::I64ShrU => bin_i64(stack, |a, b| { ((a as u64).wrapping_shr((b as u64 % 64) as u32)) as i64 }), Instruction::I64Rotl => bin_i64(stack, |a, b| { (a as u64).rotate_left((b as u64 % 64) as u32) as i64 }), Instruction::I64Rotr => bin_i64(stack, |a, b| { (a as u64).rotate_right((b as u64 % 64) as u32) as i64 }), // ---- f32 numeric ----------------------------------------- Instruction::F32Abs => un_f32(stack, f32::abs), Instruction::F32Neg => un_f32(stack, |a| -a), Instruction::F32Ceil => un_f32(stack, f32::ceil), Instruction::F32Floor => un_f32(stack, f32::floor), Instruction::F32Trunc => un_f32(stack, f32::trunc), Instruction::F32Nearest => un_f32(stack, nearest_f32), Instruction::F32Sqrt => un_f32(stack, f32::sqrt), Instruction::F32Add => bin_f32(stack, |a, b| a + b), Instruction::F32Sub => bin_f32(stack, |a, b| a - b), Instruction::F32Mul => bin_f32(stack, |a, b| a * b), Instruction::F32Div => bin_f32(stack, |a, b| a / b), Instruction::F32Min => bin_f32(stack, wasm_f32_min), Instruction::F32Max => bin_f32(stack, wasm_f32_max), Instruction::F32Copysign => bin_f32(stack, f32::copysign), // ---- f64 numeric ----------------------------------------- Instruction::F64Abs => un_f64(stack, f64::abs), Instruction::F64Neg => un_f64(stack, |a| -a), Instruction::F64Ceil => un_f64(stack, f64::ceil), Instruction::F64Floor => un_f64(stack, f64::floor), Instruction::F64Trunc => un_f64(stack, f64::trunc), Instruction::F64Nearest => un_f64(stack, nearest_f64), Instruction::F64Sqrt => un_f64(stack, f64::sqrt), Instruction::F64Add => bin_f64(stack, |a, b| a + b), Instruction::F64Sub => bin_f64(stack, |a, b| a - b), Instruction::F64Mul => bin_f64(stack, |a, b| a * b), Instruction::F64Div => bin_f64(stack, |a, b| a / b), Instruction::F64Min => bin_f64(stack, wasm_f64_min), Instruction::F64Max => bin_f64(stack, wasm_f64_max), Instruction::F64Copysign => bin_f64(stack, f64::copysign), // ---- Conversions (trapping) ------------------------------ Instruction::I32WrapI64 => { let v = pop_i64(stack); stack.push(WasmValue::I32(v as i32)); Ok(StepOutcome::Continue) } Instruction::I32TruncF32S => { let v = pop_f32(stack); stack.push(WasmValue::I32(trunc_f32_to_i32(v)?)); Ok(StepOutcome::Continue) } Instruction::I32TruncF32U => { let v = pop_f32(stack); stack.push(WasmValue::I32(trunc_f32_to_u32(v)? as i32)); Ok(StepOutcome::Continue) } Instruction::I32TruncF64S => { let v = pop_f64(stack); stack.push(WasmValue::I32(trunc_f64_to_i32(v)?)); Ok(StepOutcome::Continue) } Instruction::I32TruncF64U => { let v = pop_f64(stack); stack.push(WasmValue::I32(trunc_f64_to_u32(v)? as i32)); Ok(StepOutcome::Continue) } Instruction::I64ExtendI32S => { let v = pop_i32(stack); stack.push(WasmValue::I64(v as i64)); Ok(StepOutcome::Continue) } Instruction::I64ExtendI32U => { let v = pop_i32(stack); stack.push(WasmValue::I64(v as u32 as i64)); Ok(StepOutcome::Continue) } Instruction::I64TruncF32S => { let v = pop_f32(stack); stack.push(WasmValue::I64(trunc_f32_to_i64(v)?)); Ok(StepOutcome::Continue) } Instruction::I64TruncF32U => { let v = pop_f32(stack); stack.push(WasmValue::I64(trunc_f32_to_u64(v)? as i64)); Ok(StepOutcome::Continue) } Instruction::I64TruncF64S => { let v = pop_f64(stack); stack.push(WasmValue::I64(trunc_f64_to_i64(v)?)); Ok(StepOutcome::Continue) } Instruction::I64TruncF64U => { let v = pop_f64(stack); stack.push(WasmValue::I64(trunc_f64_to_u64(v)? as i64)); Ok(StepOutcome::Continue) } Instruction::F32ConvertI32S => { let v = pop_i32(stack); stack.push(WasmValue::F32((v as f32).to_bits())); Ok(StepOutcome::Continue) } Instruction::F32ConvertI32U => { let v = pop_i32(stack); stack.push(WasmValue::F32((v as u32 as f32).to_bits())); Ok(StepOutcome::Continue) } Instruction::F32ConvertI64S => { let v = pop_i64(stack); stack.push(WasmValue::F32((v as f32).to_bits())); Ok(StepOutcome::Continue) } Instruction::F32ConvertI64U => { let v = pop_i64(stack); stack.push(WasmValue::F32((v as u64 as f32).to_bits())); Ok(StepOutcome::Continue) } Instruction::F32DemoteF64 => { let v = pop_f64(stack); stack.push(WasmValue::F32((v as f32).to_bits())); Ok(StepOutcome::Continue) } Instruction::F64ConvertI32S => { let v = pop_i32(stack); stack.push(WasmValue::F64((v as f64).to_bits())); Ok(StepOutcome::Continue) } Instruction::F64ConvertI32U => { let v = pop_i32(stack); stack.push(WasmValue::F64((v as u32 as f64).to_bits())); Ok(StepOutcome::Continue) } Instruction::F64ConvertI64S => { let v = pop_i64(stack); stack.push(WasmValue::F64((v as f64).to_bits())); Ok(StepOutcome::Continue) } Instruction::F64ConvertI64U => { let v = pop_i64(stack); stack.push(WasmValue::F64((v as u64 as f64).to_bits())); Ok(StepOutcome::Continue) } Instruction::F64PromoteF32 => { let v = pop_f32(stack); stack.push(WasmValue::F64((v as f64).to_bits())); Ok(StepOutcome::Continue) } Instruction::I32ReinterpretF32 => { let bits = match pop_any(stack) { WasmValue::F32(b) => b, other => unreachable!("expected f32, got {:?}", other), }; stack.push(WasmValue::I32(bits as i32)); Ok(StepOutcome::Continue) } Instruction::I64ReinterpretF64 => { let bits = match pop_any(stack) { WasmValue::F64(b) => b, other => unreachable!("expected f64, got {:?}", other), }; stack.push(WasmValue::I64(bits as i64)); Ok(StepOutcome::Continue) } Instruction::F32ReinterpretI32 => { let v = pop_i32(stack); stack.push(WasmValue::F32(v as u32)); Ok(StepOutcome::Continue) } Instruction::F64ReinterpretI64 => { let v = pop_i64(stack); stack.push(WasmValue::F64(v as u64)); Ok(StepOutcome::Continue) } // ---- Sign-extension (post-MVP) --------------------------- Instruction::I32Extend8S => un_i32(stack, |a| (a as i8) as i32), Instruction::I32Extend16S => un_i32(stack, |a| (a as i16) as i32), Instruction::I64Extend8S => un_i64(stack, |a| (a as i8) as i64), Instruction::I64Extend16S => un_i64(stack, |a| (a as i16) as i64), Instruction::I64Extend32S => un_i64(stack, |a| (a as i32) as i64), // ---- Saturating float-to-int ----------------------------- Instruction::I32TruncSatF32S => { let v = pop_f32(stack); stack.push(WasmValue::I32(trunc_sat_f32_to_i32(v))); Ok(StepOutcome::Continue) } Instruction::I32TruncSatF32U => { let v = pop_f32(stack); stack.push(WasmValue::I32(trunc_sat_f32_to_u32(v) as i32)); Ok(StepOutcome::Continue) } Instruction::I32TruncSatF64S => { let v = pop_f64(stack); stack.push(WasmValue::I32(trunc_sat_f64_to_i32(v))); Ok(StepOutcome::Continue) } Instruction::I32TruncSatF64U => { let v = pop_f64(stack); stack.push(WasmValue::I32(trunc_sat_f64_to_u32(v) as i32)); Ok(StepOutcome::Continue) } Instruction::I64TruncSatF32S => { let v = pop_f32(stack); stack.push(WasmValue::I64(trunc_sat_f32_to_i64(v))); Ok(StepOutcome::Continue) } Instruction::I64TruncSatF32U => { let v = pop_f32(stack); stack.push(WasmValue::I64(trunc_sat_f32_to_u64(v) as i64)); Ok(StepOutcome::Continue) } Instruction::I64TruncSatF64S => { let v = pop_f64(stack); stack.push(WasmValue::I64(trunc_sat_f64_to_i64(v))); Ok(StepOutcome::Continue) } Instruction::I64TruncSatF64U => { let v = pop_f64(stack); stack.push(WasmValue::I64(trunc_sat_f64_to_u64(v) as i64)); Ok(StepOutcome::Continue) } } } // ---- Typed pop/push helpers ------------------------------------------------ fn pop_any(stack: &mut Vec) -> WasmValue { stack.pop().expect("operand stack underflow") } fn pop_i32(stack: &mut Vec) -> i32 { match pop_any(stack) { WasmValue::I32(v) => v, other => unreachable!("validator should have rejected non-i32: {:?}", other), } } fn pop_i64(stack: &mut Vec) -> i64 { match pop_any(stack) { WasmValue::I64(v) => v, other => unreachable!("validator should have rejected non-i64: {:?}", other), } } fn pop_f32(stack: &mut Vec) -> f32 { match pop_any(stack) { WasmValue::F32(bits) => f32::from_bits(bits), other => unreachable!("validator should have rejected non-f32: {:?}", other), } } fn pop_f64(stack: &mut Vec) -> f64 { match pop_any(stack) { WasmValue::F64(bits) => f64::from_bits(bits), other => unreachable!("validator should have rejected non-f64: {:?}", other), } } fn pop_ref(stack: &mut Vec) -> Ref { match pop_any(stack) { WasmValue::Ref(r) => r, other => unreachable!("validator should have rejected non-ref: {:?}", other), } } // ---- Memory address helpers ------------------------------------------------ fn effective_addr(stack: &mut Vec, memarg: &MemArg) -> u64 { let addr = pop_i32(stack) as u32; addr as u64 + memarg.offset as u64 } fn exec_load( store: &mut Store, module: ModuleAddr, stack: &mut Vec, memarg: &MemArg, load: F, push: P, ) -> Result where F: FnOnce(&crate::runtime::memory::Memory, u64) -> Result, P: FnOnce(&mut Vec, T), { let addr = effective_addr(stack, memarg); let mem_addr = store .module(module) .resolve_memory(memarg.memory) .expect("validator: memory index out of range"); let value = load(store.memory(mem_addr), addr)?; push(stack, value); Ok(StepOutcome::Continue) } fn with_memory_mut(store: &mut Store, module: ModuleAddr, mem_idx: u32, f: F) -> Result<(), Trap> where F: FnOnce(&mut crate::runtime::memory::Memory) -> Result<(), Trap>, { let mem_addr = store .module(module) .resolve_memory(mem_idx) .expect("validator: memory index out of range"); f(store.memory_mut(mem_addr)) } fn element_slice(store: &Store, addr: ElementAddr, src: u32, len: u32) -> Result<&[Ref], Trap> { let segment = store.element(addr); let start = src as u64; let end = start .checked_add(len as u64) .ok_or(Trap::OutOfBoundsElementSegment)?; if end > segment.len() as u64 { return Err(Trap::OutOfBoundsElementSegment); } Ok(&segment.refs()[src as usize..end as usize]) } fn data_slice(store: &Store, addr: DataAddr, src: u32, len: u32) -> Result<&[u8], Trap> { let data = store.data(addr); let start = src as u64; let end = start .checked_add(len as u64) .ok_or(Trap::OutOfBoundsDataSegment)?; if end > data.len() as u64 { return Err(Trap::OutOfBoundsDataSegment); } Ok(&data.bytes()[src as usize..end as usize]) } fn table_copy( store: &mut Store, module: ModuleAddr, dst_table: u32, src_table: u32, dst: u32, src: u32, len: u32, ) -> Result<(), Trap> { let dst_addr = resolve_table(store, module, dst_table); let src_addr = resolve_table(store, module, src_table); if dst_addr == src_addr { return store.table_mut(dst_addr).copy_within(dst, src, len); } let refs = table_slice(store, src_addr, src, len)?.to_vec(); store.table_mut(dst_addr).init(dst, &refs) } fn table_slice(store: &Store, addr: TableAddr, src: u32, len: u32) -> Result<&[Ref], Trap> { let table = store.table(addr); let start = src as u64; let end = start .checked_add(len as u64) .ok_or(Trap::OutOfBoundsTable)?; if end > table.size() as u64 { return Err(Trap::OutOfBoundsTable); } Ok(&table.elements()[src as usize..end as usize]) } fn memory_copy( store: &mut Store, module: ModuleAddr, dst_memory: u32, src_memory: u32, dst: u32, src: u32, len: u32, ) -> Result<(), Trap> { let dst_addr = resolve_memory(store, module, dst_memory); let src_addr = resolve_memory(store, module, src_memory); if dst_addr == src_addr { return store .memory_mut(dst_addr) .copy_within(dst as u64, src as u64, len as u64); } let bytes = memory_slice(store, src_addr, src, len)?.to_vec(); store.memory_mut(dst_addr).init(dst as u64, &bytes) } fn memory_slice(store: &Store, addr: MemoryAddr, src: u32, len: u32) -> Result<&[u8], Trap> { let memory = store.memory(addr); let start = src as u64; let end = start .checked_add(len as u64) .ok_or(Trap::OutOfBoundsMemory)?; if end > memory.bytes().len() as u64 { return Err(Trap::OutOfBoundsMemory); } Ok(&memory.bytes()[src as usize..end as usize]) } fn resolve_table(store: &Store, module: ModuleAddr, table_idx: u32) -> TableAddr { store .module(module) .resolve_table(table_idx) .expect("validator: table index out of range") } fn resolve_memory(store: &Store, module: ModuleAddr, memory_idx: u32) -> MemoryAddr { store .module(module) .resolve_memory(memory_idx) .expect("validator: memory index out of range") } fn bin_i32 i32>( stack: &mut Vec, f: F, ) -> Result { let b = pop_i32(stack); let a = pop_i32(stack); stack.push(WasmValue::I32(f(a, b))); Ok(StepOutcome::Continue) } fn bin_i32_try Result>( stack: &mut Vec, f: F, ) -> Result { let b = pop_i32(stack); let a = pop_i32(stack); stack.push(WasmValue::I32(f(a, b)?)); Ok(StepOutcome::Continue) } fn bin_i32_cmp bool>( stack: &mut Vec, f: F, ) -> Result { let b = pop_i32(stack); let a = pop_i32(stack); stack.push(WasmValue::I32(bool_to_i32(f(a, b)))); Ok(StepOutcome::Continue) } fn un_i32 i32>(stack: &mut Vec, f: F) -> Result { let a = pop_i32(stack); stack.push(WasmValue::I32(f(a))); Ok(StepOutcome::Continue) } fn bin_i64 i64>( stack: &mut Vec, f: F, ) -> Result { let b = pop_i64(stack); let a = pop_i64(stack); stack.push(WasmValue::I64(f(a, b))); Ok(StepOutcome::Continue) } fn bin_i64_try Result>( stack: &mut Vec, f: F, ) -> Result { let b = pop_i64(stack); let a = pop_i64(stack); stack.push(WasmValue::I64(f(a, b)?)); Ok(StepOutcome::Continue) } fn bin_i64_cmp bool>( stack: &mut Vec, f: F, ) -> Result { let b = pop_i64(stack); let a = pop_i64(stack); stack.push(WasmValue::I32(bool_to_i32(f(a, b)))); Ok(StepOutcome::Continue) } fn un_i64 i64>(stack: &mut Vec, f: F) -> Result { let a = pop_i64(stack); stack.push(WasmValue::I64(f(a))); Ok(StepOutcome::Continue) } fn un_f32 f32>(stack: &mut Vec, f: F) -> Result { let a = pop_f32(stack); stack.push(WasmValue::F32(f(a).to_bits())); Ok(StepOutcome::Continue) } fn bin_f32 f32>( stack: &mut Vec, f: F, ) -> Result { let b = pop_f32(stack); let a = pop_f32(stack); stack.push(WasmValue::F32(f(a, b).to_bits())); Ok(StepOutcome::Continue) } fn bin_f32_cmp bool>( stack: &mut Vec, f: F, ) -> Result { let b = pop_f32(stack); let a = pop_f32(stack); stack.push(WasmValue::I32(bool_to_i32(f(a, b)))); Ok(StepOutcome::Continue) } fn un_f64 f64>(stack: &mut Vec, f: F) -> Result { let a = pop_f64(stack); stack.push(WasmValue::F64(f(a).to_bits())); Ok(StepOutcome::Continue) } fn bin_f64 f64>( stack: &mut Vec, f: F, ) -> Result { let b = pop_f64(stack); let a = pop_f64(stack); stack.push(WasmValue::F64(f(a, b).to_bits())); Ok(StepOutcome::Continue) } fn bin_f64_cmp bool>( stack: &mut Vec, f: F, ) -> Result { let b = pop_f64(stack); let a = pop_f64(stack); stack.push(WasmValue::I32(bool_to_i32(f(a, b)))); Ok(StepOutcome::Continue) } // ============================================================================ // Numeric helpers — IEEE-754 details the wasm spec pins down explicitly // ============================================================================ fn bool_to_i32(b: bool) -> i32 { if b { 1 } else { 0 } } /// Wasm `f32.min`: NaN propagates, `-0 < +0`. fn wasm_f32_min(a: f32, b: f32) -> f32 { if a.is_nan() { return canonicalize_f32_nan(a); } if b.is_nan() { return canonicalize_f32_nan(b); } if a == 0.0 && b == 0.0 { if a.is_sign_negative() { a } else { b } } else if a < b { a } else { b } } /// Wasm `f32.max`: NaN propagates, `+0 > -0`. fn wasm_f32_max(a: f32, b: f32) -> f32 { if a.is_nan() { return canonicalize_f32_nan(a); } if b.is_nan() { return canonicalize_f32_nan(b); } if a == 0.0 && b == 0.0 { if a.is_sign_positive() { a } else { b } } else if a > b { a } else { b } } fn wasm_f64_min(a: f64, b: f64) -> f64 { if a.is_nan() { return canonicalize_f64_nan(a); } if b.is_nan() { return canonicalize_f64_nan(b); } if a == 0.0 && b == 0.0 { if a.is_sign_negative() { a } else { b } } else if a < b { a } else { b } } fn wasm_f64_max(a: f64, b: f64) -> f64 { if a.is_nan() { return canonicalize_f64_nan(a); } if b.is_nan() { return canonicalize_f64_nan(b); } if a == 0.0 && b == 0.0 { if a.is_sign_positive() { a } else { b } } else if a > b { a } else { b } } /// Force a NaN payload to be quiet. Wasm requires arithmetic NaNs (quiet, top /// fraction bit set); signaling NaN inputs to non-bitpattern-preserving ops /// must be quieted in the result. fn canonicalize_f32_nan(x: f32) -> f32 { let bits = x.to_bits(); // Force quiet bit (bit 22, the top fraction bit) and zero out the rest of // the payload to land on the canonical NaN per spec §4.3.3. f32::from_bits((bits & 0x8000_0000) | 0x7FC0_0000) } fn canonicalize_f64_nan(x: f64) -> f64 { let bits = x.to_bits(); f64::from_bits((bits & 0x8000_0000_0000_0000) | 0x7FF8_0000_0000_0000) } /// Wasm `f32.nearest` — round-to-nearest, ties to even. Preserves the sign of /// zero results. fn nearest_f32(x: f32) -> f32 { if x.is_nan() || x.is_infinite() { return x; } if x == 0.0 { return x; } let trunc = x.trunc(); let diff = (x - trunc).abs(); let step: f32 = if x.is_sign_negative() { -1.0 } else { 1.0 }; let result = if diff < 0.5 { trunc } else if diff > 0.5 { trunc + step } else { // Exact tie — round to the nearest even integer. if (trunc as i64).rem_euclid(2) == 0 { trunc } else { trunc + step } }; if result == 0.0 && x.is_sign_negative() { -0.0 } else { result } } fn nearest_f64(x: f64) -> f64 { if x.is_nan() || x.is_infinite() { return x; } if x == 0.0 { return x; } let trunc = x.trunc(); let diff = (x - trunc).abs(); let step: f64 = if x.is_sign_negative() { -1.0 } else { 1.0 }; let result = if diff < 0.5 { trunc } else if diff > 0.5 { trunc + step } else if (trunc as i128).rem_euclid(2) == 0 { trunc } else { trunc + step }; if result == 0.0 && x.is_sign_negative() { -0.0 } else { result } } // ---- Trapping float-to-int conversions ------------------------------------ // // Bounds rationale: an f32/f64 input is acceptable iff its truncation lies in // the destination integer's range. The exact-representable boundary for each // (target, source) pair is what we compare against — for f32 -> i32 the lower // bound is `-2^31` (exactly representable) and the upper bound is `2^31` (the // first value above the i32 range that f32 can name). fn trunc_f32_to_i32(x: f32) -> Result { if x.is_nan() { return Err(Trap::InvalidConversion); } if !x.is_finite() { return Err(Trap::IntegerOverflow); } let t = x.trunc(); if !(-2_147_483_648.0_f32..2_147_483_648.0_f32).contains(&t) { return Err(Trap::IntegerOverflow); } Ok(t as i32) } fn trunc_f32_to_u32(x: f32) -> Result { if x.is_nan() { return Err(Trap::InvalidConversion); } if !x.is_finite() { return Err(Trap::IntegerOverflow); } let t = x.trunc(); if t <= -1.0_f32 || t >= 4_294_967_296.0_f32 { return Err(Trap::IntegerOverflow); } Ok(t as u32) } fn trunc_f64_to_i32(x: f64) -> Result { if x.is_nan() { return Err(Trap::InvalidConversion); } if !x.is_finite() { return Err(Trap::IntegerOverflow); } let t = x.trunc(); if !(-2_147_483_648.0_f64..=2_147_483_647.0_f64).contains(&t) { return Err(Trap::IntegerOverflow); } Ok(t as i32) } fn trunc_f64_to_u32(x: f64) -> Result { if x.is_nan() { return Err(Trap::InvalidConversion); } if !x.is_finite() { return Err(Trap::IntegerOverflow); } let t = x.trunc(); if t <= -1.0_f64 || t > 4_294_967_295.0_f64 { return Err(Trap::IntegerOverflow); } Ok(t as u32) } fn trunc_f32_to_i64(x: f32) -> Result { if x.is_nan() { return Err(Trap::InvalidConversion); } if !x.is_finite() { return Err(Trap::IntegerOverflow); } let t = x.trunc(); if !(-9_223_372_036_854_775_808.0_f32..9_223_372_036_854_775_808.0_f32).contains(&t) { return Err(Trap::IntegerOverflow); } Ok(t as i64) } fn trunc_f32_to_u64(x: f32) -> Result { if x.is_nan() { return Err(Trap::InvalidConversion); } if !x.is_finite() { return Err(Trap::IntegerOverflow); } let t = x.trunc(); if t <= -1.0_f32 || t >= 18_446_744_073_709_551_616.0_f32 { return Err(Trap::IntegerOverflow); } Ok(t as u64) } fn trunc_f64_to_i64(x: f64) -> Result { if x.is_nan() { return Err(Trap::InvalidConversion); } if !x.is_finite() { return Err(Trap::IntegerOverflow); } let t = x.trunc(); if !(-9_223_372_036_854_775_808.0_f64..9_223_372_036_854_775_808.0_f64).contains(&t) { return Err(Trap::IntegerOverflow); } Ok(t as i64) } fn trunc_f64_to_u64(x: f64) -> Result { if x.is_nan() { return Err(Trap::InvalidConversion); } if !x.is_finite() { return Err(Trap::IntegerOverflow); } let t = x.trunc(); if t <= -1.0_f64 || t >= 18_446_744_073_709_551_616.0_f64 { return Err(Trap::IntegerOverflow); } Ok(t as u64) } // ---- Saturating float-to-int conversions ----------------------------------- // // Rust's `as` for float → integer already implements saturating semantics // matching the spec (NaN → 0, ±∞ → bounds, out-of-range → clamp). The // wrappers exist so the dispatch table reads symmetrically with the trapping // variants. fn trunc_sat_f32_to_i32(x: f32) -> i32 { x as i32 } fn trunc_sat_f32_to_u32(x: f32) -> u32 { x as u32 } fn trunc_sat_f64_to_i32(x: f64) -> i32 { x as i32 } fn trunc_sat_f64_to_u32(x: f64) -> u32 { x as u32 } fn trunc_sat_f32_to_i64(x: f32) -> i64 { x as i64 } fn trunc_sat_f32_to_u64(x: f32) -> u64 { x as u64 } fn trunc_sat_f64_to_i64(x: f64) -> i64 { x as i64 } fn trunc_sat_f64_to_u64(x: f64) -> u64 { x as u64 }