pub mod engine;
use crate::runtime::primitives::Word;
use crate::runtime::vm::heap::{self, HeapStorage};
use std::collections::HashMap;
use wasmtime::{
AsContextMut, Caller, Config, Engine, FuncType, Linker, Module, OptLevel, Store, Val, ValType,
WasmBacktraceDetails,
};
const MAX_SPECIALIZED_BUILTIN_ARITY: usize = 16;
const MAX_WASM_DELAY_SAMPLES: usize = 16 * 1024 * 1024;
const UNINITIALIZED_ARRAY_HANDLE_SENTINEL: i64 = 0;
pub trait WasmSystemPluginAudioWorker: Send {
fn on_sample(
&mut self,
time: crate::runtime::Time,
engine: &mut engine::WasmEngine,
) -> crate::runtime::vm::ReturnCode;
}
pub type WasmPluginFn = std::sync::Arc<dyn Fn(&[f64]) -> Option<f64> + Send + Sync>;
pub type WasmPluginFnMap = HashMap<String, WasmPluginFn>;
pub struct WasmRuntime {
engine: Engine,
linker: Linker<RuntimeState>,
}
#[derive(Debug, Clone, Default)]
struct StateStorage {
pos: usize,
data: Vec<u64>,
}
impl StateStorage {
fn with_size(size: usize) -> Self {
Self {
pos: 0,
data: vec![0u64; size],
}
}
}
pub struct RuntimeState {
memory: Option<wasmtime::Memory>,
heap: HeapStorage,
arrays: HashMap<Word, Vec<Word>>,
global_state: StateStorage,
closure_states: HashMap<i64, StateStorage>,
state_stack: Vec<i64>,
pub(crate) current_time: u64,
pub(crate) sample_rate: f64,
}
impl RuntimeState {
fn get_current_state(&mut self) -> &mut StateStorage {
if let Some(&closure_addr) = self.state_stack.last() {
self.closure_states
.get_mut(&closure_addr)
.expect("closure_state_push must be called before accessing closure state")
} else {
&mut self.global_state
}
}
}
impl Default for RuntimeState {
fn default() -> Self {
Self {
memory: None,
heap: HeapStorage::default(),
arrays: HashMap::new(),
global_state: StateStorage::default(),
closure_states: HashMap::new(),
state_stack: Vec::new(),
current_time: 0,
sample_rate: 44100.0,
}
}
}
impl WasmRuntime {
#[cfg(not(target_arch = "wasm32"))]
pub fn new(
ext_fns: &[crate::plugin::ExtFunTypeInfo],
plugin_fns: Option<WasmPluginFnMap>,
) -> Result<Self, String> {
let mut config = Config::new();
config.cranelift_opt_level(OptLevel::Speed);
config.parallel_compilation(true);
config.wasm_simd(true); config.wasm_bulk_memory(true); config.wasm_backtrace_details(WasmBacktraceDetails::Enable);
config.debug_info(true);
let engine =
Engine::new(&config).map_err(|e| format!("Failed to create WASM engine: {e}"))?;
let mut linker = Linker::new(&engine);
Self::register_runtime_primitives(&mut linker)?;
Self::register_plugin_functions(&mut linker, ext_fns, plugin_fns)?;
Ok(Self { engine, linker })
}
#[cfg(target_arch = "wasm32")]
pub fn new(ext_fns: &[crate::plugin::ExtFunTypeInfo]) -> Result<Self, String> {
let mut config = Config::new();
config.cranelift_opt_level(OptLevel::Speed);
config.parallel_compilation(true);
config.wasm_simd(true); config.wasm_bulk_memory(true); config.wasm_backtrace_details(WasmBacktraceDetails::Enable);
config.debug_info(true);
let engine =
Engine::new(&config).map_err(|e| format!("Failed to create WASM engine: {e}"))?;
let mut linker = Linker::new(&engine);
Self::register_runtime_primitives(&mut linker)?;
Self::register_plugin_functions(&mut linker, ext_fns, None)?;
Ok(Self { engine, linker })
}
pub fn load_module(&mut self, wasm_bytes: &[u8]) -> Result<WasmModule, String> {
let module = Module::from_binary(&self.engine, wasm_bytes)
.map_err(|e| format!("Failed to load WASM module: {e:#}"))?;
self.instantiate_module(module)
}
pub fn load_precompiled_module(
&mut self,
serialized_module: &[u8],
) -> Result<WasmModule, String> {
let module = unsafe { Module::deserialize(&self.engine, serialized_module) }
.map_err(|e| format!("Failed to deserialize precompiled WASM module: {e:#}"))?;
self.instantiate_module(module)
}
fn instantiate_module(&mut self, module: Module) -> Result<WasmModule, String> {
let mut runtime_state = RuntimeState::default();
let mut store = Store::new(&self.engine, runtime_state);
let instance = self
.linker
.instantiate(&mut store, &module)
.map_err(|e| format!("Failed to instantiate WASM module: {e:#}"))?;
let memory = instance
.get_memory(&mut store, "memory")
.ok_or("WASM module does not export memory")?;
store.data_mut().memory = Some(memory);
Ok(WasmModule {
module,
store,
instance,
function_cache: std::collections::HashMap::new(),
})
}
fn register_runtime_primitives(linker: &mut Linker<RuntimeState>) -> Result<(), String> {
macro_rules! register {
($($name:expr => $func:expr),* $(,)?) => {
$(
linker
.func_wrap("runtime", $name, $func)
.map_err(|e| format!("Failed to register {}: {}", $name, e))?;
)*
};
}
register! {
"heap_alloc" => heap_alloc_host,
"heap_retain" => heap_retain_host,
"heap_release" => heap_release_host,
"heap_load" => heap_load_host,
"heap_store" => heap_store_host,
"box_alloc" => box_alloc_host,
"box_load" => box_load_host,
"box_clone" => box_clone_host,
"box_release" => box_release_host,
"box_store" => box_store_host,
"usersum_clone" => usersum_clone_host,
"usersum_release" => usersum_release_host,
"closure_make" => closure_make_host,
"closure_close" => closure_close_host,
"closure_call" => closure_call_host,
"closure_state_push" => closure_state_push_host,
"closure_state_pop" => closure_state_pop_host,
"state_push" => state_push_host,
"state_pop" => state_pop_host,
"state_get" => state_get_host,
"state_set" => state_set_host,
"state_delay" => state_delay_host,
"state_mem" => state_mem_host,
"array_alloc" => array_alloc_host,
"array_get_elem" => array_get_elem_host,
"array_set_elem" => array_set_elem_host,
"runtime_get_now" => runtime_get_now_host,
"runtime_get_samplerate" => runtime_get_samplerate_host,
}
macro_rules! register_math_f1 {
($($name:expr => $func:expr),* $(,)?) => {
$(
linker
.func_wrap("math", $name, $func)
.map_err(|e| format!("Failed to register math::{}: {}", $name, e))?;
)*
};
}
macro_rules! register_math_f2 {
($($name:expr => $func:expr),* $(,)?) => {
$(
linker
.func_wrap("math", $name, $func)
.map_err(|e| format!("Failed to register math::{}: {}", $name, e))?;
)*
};
}
register_math_f1! {
"sin" => |_caller: Caller<'_, RuntimeState>, x: f64| -> f64 { x.sin() },
"cos" => |_caller: Caller<'_, RuntimeState>, x: f64| -> f64 { x.cos() },
"tan" => |_caller: Caller<'_, RuntimeState>, x: f64| -> f64 { x.tan() },
"sinh" => |_caller: Caller<'_, RuntimeState>, x: f64| -> f64 { x.sinh() },
"cosh" => |_caller: Caller<'_, RuntimeState>, x: f64| -> f64 { x.cosh() },
"tanh" => |_caller: Caller<'_, RuntimeState>, x: f64| -> f64 { x.tanh() },
"asin" => |_caller: Caller<'_, RuntimeState>, x: f64| -> f64 { x.asin() },
"acos" => |_caller: Caller<'_, RuntimeState>, x: f64| -> f64 { x.acos() },
"atan" => |_caller: Caller<'_, RuntimeState>, x: f64| -> f64 { x.atan() },
"round" => |_caller: Caller<'_, RuntimeState>, x: f64| -> f64 { x.round() },
"floor" => |_caller: Caller<'_, RuntimeState>, x: f64| -> f64 { x.floor() },
"ceil" => |_caller: Caller<'_, RuntimeState>, x: f64| -> f64 { x.ceil() },
"log" => |_caller: Caller<'_, RuntimeState>, x: f64| -> f64 { x.ln() },
}
register_math_f2! {
"atan2" => |_caller: Caller<'_, RuntimeState>, y: f64, x: f64| -> f64 { y.atan2(x) },
"pow" => |_caller: Caller<'_, RuntimeState>, base: f64, exp: f64| -> f64 { base.powf(exp) },
"min" => |_caller: Caller<'_, RuntimeState>, a: f64, b: f64| -> f64 { a.min(b) },
"max" => |_caller: Caller<'_, RuntimeState>, a: f64, b: f64| -> f64 { a.max(b) },
}
macro_rules! register_builtin {
($($name:expr => $func:expr),* $(,)?) => {
$(
linker
.func_wrap("builtin", $name, $func)
.map_err(|e| format!("Failed to register builtin::{}: {}", $name, e))?;
)*
};
}
register_builtin! {
"probeln" => builtin_probeln_host,
"probe" => builtin_probe_host,
"len" => builtin_length_array_host,
"split_head" => builtin_split_head_host,
"split_tail" => builtin_split_tail_host,
}
linker
.func_new(
"builtin",
"prepend",
FuncType::new(
linker.engine(),
vec![ValType::I64, ValType::I64],
vec![ValType::I64],
),
move |caller, params, results| {
builtin_prepend_arity_host(caller, params, results, 1);
Ok(())
},
)
.map_err(|e| format!("Failed to register builtin::prepend: {e}"))?;
linker
.func_new(
"builtin",
"append",
FuncType::new(
linker.engine(),
vec![ValType::I64, ValType::I64],
vec![ValType::I64],
),
move |caller, params, results| {
builtin_append_arity_host(caller, params, results, 1);
Ok(())
},
)
.map_err(|e| format!("Failed to register builtin::append: {e}"))?;
for arity in 1..=MAX_SPECIALIZED_BUILTIN_ARITY {
let split_head_name = format!("split_head$arity{arity}");
linker
.func_wrap(
"builtin",
split_head_name.as_str(),
move |caller: Caller<'_, RuntimeState>, array: i64, dst_ptr: i32| {
builtin_split_head_arity_host(caller, array, dst_ptr, arity)
},
)
.map_err(|e| format!("Failed to register builtin::split_head$arity{arity}: {e}"))?;
let split_tail_name = format!("split_tail$arity{arity}");
linker
.func_wrap(
"builtin",
split_tail_name.as_str(),
move |caller: Caller<'_, RuntimeState>, array: i64, dst_ptr: i32| {
builtin_split_tail_arity_host(caller, array, dst_ptr, arity)
},
)
.map_err(|e| format!("Failed to register builtin::split_tail$arity{arity}: {e}"))?;
let prepend_name = format!("prepend$arity{arity}");
linker
.func_new(
"builtin",
prepend_name.as_str(),
FuncType::new(
linker.engine(),
vec![ValType::I64, ValType::I64],
vec![ValType::I64],
),
move |caller, params, results| {
builtin_prepend_arity_host(caller, params, results, arity);
Ok(())
},
)
.map_err(|e| format!("Failed to register builtin::prepend$arity{arity}: {e}"))?;
let append_name = format!("append$arity{arity}");
linker
.func_new(
"builtin",
append_name.as_str(),
FuncType::new(
linker.engine(),
vec![ValType::I64, ValType::I64],
vec![ValType::I64],
),
move |caller, params, results| {
builtin_append_arity_host(caller, params, results, arity);
Ok(())
},
)
.map_err(|e| format!("Failed to register builtin::append$arity{arity}: {e}"))?;
}
Ok(())
}
#[cfg(not(target_arch = "wasm32"))]
fn register_plugin_functions(
linker: &mut Linker<RuntimeState>,
ext_fns: &[crate::plugin::ExtFunTypeInfo],
plugin_fns: Option<WasmPluginFnMap>,
) -> Result<(), String> {
use crate::types::Type;
for type_info in ext_fns {
let name = type_info.name;
let fn_ty = type_info.ty.to_type();
let Type::Function { arg, ret } = fn_ty else {
log::warn!("Plugin '{}' has non-function type, skipping", name.as_str());
continue;
};
if name.as_str() == "__probe_value_intercept$arity1" {
let func_type = FuncType::new(
linker.engine(),
vec![ValType::F64, ValType::F64],
vec![ValType::F64],
);
let plugin_name = name.as_str().to_string();
let handler: Option<WasmPluginFn> = plugin_fns
.as_ref()
.and_then(|map| map.get(name.as_str()).cloned());
linker
.func_new(
"plugin",
name.as_str(),
func_type,
move |_caller, params, results| {
log::trace!("Plugin trampoline called: {plugin_name}({params:?})");
let args = decode_trampoline_args(params);
let passthrough = if let Some(ref func) = handler {
func(&args).unwrap_or_else(|| args.first().copied().unwrap_or(0.0))
} else {
args.first().copied().unwrap_or(0.0)
};
results[0] = Val::F64(passthrough.to_bits());
Ok(())
},
)
.map_err(|e| format!("Failed to register plugin '{}': {e}", name.as_str()))?;
continue;
}
let mut param_valtypes: Vec<ValType> = match arg.to_type() {
Type::Tuple(elems) => {
elems
.iter()
.map(|t| mimium_type_to_wasmtime_valtype(&t.to_type()))
.collect()
}
arg_ty => {
vec![mimium_type_to_wasmtime_valtype(&arg_ty)]
}
};
let ret_type = ret.to_type();
let is_void = matches!(ret_type, Type::Primitive(crate::types::PType::Unit));
let flat_ret_count = flat_return_word_count(&ret_type);
let uses_dest_ptr =
!is_void && flat_ret_count > 1 && plugin_uses_dest_ptr(name.as_str());
let return_valtypes: Vec<ValType> = if uses_dest_ptr {
param_valtypes.push(ValType::I32); vec![] } else if is_void {
vec![]
} else {
vec![mimium_type_to_wasmtime_valtype(&ret_type)]
};
let plugin_name = name.as_str().to_string();
log::debug!(
"Registering plugin host function: {} ({} params -> {:?}{})",
plugin_name,
param_valtypes.len(),
return_valtypes,
if uses_dest_ptr { " [dest-ptr]" } else { "" },
);
let func_type = FuncType::new(
linker.engine(),
param_valtypes.clone(),
return_valtypes.clone(),
);
let return_vts = return_valtypes.clone();
let handler: Option<WasmPluginFn> = plugin_fns
.as_ref()
.and_then(|map| map.get(name.as_str()).cloned());
if uses_dest_ptr {
let ret_words = flat_ret_count;
linker
.func_new(
"plugin",
name.as_str(),
func_type,
move |mut caller, params, _results| {
log::trace!(
"Plugin dest-ptr trampoline called: {plugin_name}({params:?})"
);
let dst_ptr = match params.last() {
Some(wasmtime::Val::I32(p)) => *p as usize,
_ => {
log::error!(
"{plugin_name}: missing dest pointer in last param"
);
return Ok(());
}
};
let value_params = ¶ms[..params.len() - 1];
let args: Vec<f64> = value_params
.iter()
.filter_map(|v| match v {
wasmtime::Val::F64(f) => Some(f64::from_bits(*f)),
wasmtime::Val::I64(i) => Some(*i as f64),
wasmtime::Val::I32(i) => Some(*i as f64),
_ => None,
})
.collect();
if let Some(ref func) = handler {
let _ = func(&args);
}
let memory = caller
.get_export("memory")
.and_then(|e| e.into_memory())
.expect("WASM module must export 'memory'");
let mem_data = memory.data_mut(&mut caller);
for i in 0..ret_words.min(args.len()) {
let offset = dst_ptr + i * 8;
let end = offset + 8;
if end <= mem_data.len() {
mem_data[offset..end].copy_from_slice(&args[i].to_le_bytes());
}
}
Ok(())
},
)
.map_err(|e| format!("Failed to register plugin '{}': {e}", name.as_str()))?;
} else {
let is_passthrough = !return_valtypes.is_empty()
&& param_valtypes.len() >= 2
&& param_valtypes
.first()
.is_some_and(|first| valtype_eq(first, &return_valtypes[0]))
&& plugin_name.contains("intercept");
linker
.func_new(
"plugin",
name.as_str(),
func_type,
move |_caller, params, results| {
log::trace!("Plugin trampoline called: {plugin_name}({params:?})");
if results.is_empty() {
if let Some(ref func) = handler {
let args = decode_trampoline_args(params);
let _ = func(&args);
}
return Ok(());
}
if let Some(ref func) = handler {
let args = decode_trampoline_args(params);
if let Some(result) = func(&args) {
results[0] = match &return_vts[0] {
ValType::I64 => wasmtime::Val::I64(result as i64),
ValType::I32 => wasmtime::Val::I32(result as i32),
_ => wasmtime::Val::F64(result.to_bits()),
};
if results.len() > 1 {
for i in 1..results.len() {
if let Some(param) = params.get(i) {
results[i] = *param;
} else {
results[i] =
default_val_for_valtype(return_vts[i].clone());
}
}
}
return Ok(());
}
}
if is_passthrough && !params.is_empty() {
for (i, result) in results.iter_mut().enumerate() {
*result = params.get(i).copied().unwrap_or_else(|| {
default_val_for_valtype(return_vts[i].clone())
});
}
} else {
for (i, result) in results.iter_mut().enumerate() {
*result = default_val_for_valtype(return_vts[i].clone());
}
}
Ok(())
},
)
.map_err(|e| format!("Failed to register plugin '{}': {e}", name.as_str()))?;
}
}
Ok(())
}
}
impl Default for WasmRuntime {
fn default() -> Self {
Self::new(&[], None).expect("Failed to create WASM runtime")
}
}
pub struct WasmModule {
#[allow(dead_code)]
module: Module,
store: Store<RuntimeState>,
instance: wasmtime::Instance,
function_cache: std::collections::HashMap<String, wasmtime::Func>,
}
impl WasmModule {
pub fn serialize_compiled_module(&self) -> Result<Vec<u8>, String> {
self.module
.serialize()
.map_err(|e| format!("Failed to serialize compiled WASM module: {e:#}"))
}
pub fn get_or_cache_function(&mut self, name: &str) -> Result<wasmtime::Func, String> {
if let Some(func) = self.function_cache.get(name) {
return Ok(*func);
}
let func = self
.instance
.get_func(&mut self.store, name)
.ok_or_else(|| format!("Function '{name}' not found in WASM module"))?;
self.function_cache.insert(name.to_string(), func);
Ok(func)
}
pub fn call_func_direct(
&mut self,
func: &wasmtime::Func,
args: &[Word],
) -> Result<Vec<Word>, String> {
let func_ty = func.ty(&self.store);
let param_types = func_ty.params().collect::<Vec<_>>();
if param_types.len() != args.len() {
return Err(format!(
"Failed to call function: argument count mismatch: expected {}, found {}",
param_types.len(),
args.len()
));
}
let wasm_args: Vec<wasmtime::Val> = args
.iter()
.zip(param_types.iter())
.map(|(&word, ty)| match ty {
ValType::F64 => wasmtime::Val::F64(word),
ValType::F32 => wasmtime::Val::F32(word as u32),
ValType::I64 => wasmtime::Val::I64(word as i64),
ValType::I32 => wasmtime::Val::I32(word as i32),
_ => wasmtime::Val::I64(word as i64),
})
.collect();
let mut results = func_ty
.results()
.map(default_val_for_valtype)
.collect::<Vec<_>>();
func.call(&mut self.store, &wasm_args, &mut results)
.map_err(|e| {
format!("Failed to call function: {e:#}")
})?;
Ok(results
.into_iter()
.map(|v| match v {
wasmtime::Val::I64(i) => i as u64,
wasmtime::Val::F64(f) => f,
wasmtime::Val::I32(i) => i as u64,
wasmtime::Val::F32(f) => f as u64,
_ => 0,
})
.collect())
}
pub fn call_function(&mut self, name: &str, args: &[Word]) -> Result<Vec<Word>, String> {
let func = self.get_or_cache_function(name)?;
self.call_func_direct(&func, args)
}
pub fn read_memory_f64(&mut self, offset: usize) -> Result<f64, String> {
let memory = self
.instance
.get_memory(&mut self.store, "memory")
.ok_or("No memory export")?;
let data = memory.data(&self.store);
if offset + 8 > data.len() {
return Err(format!(
"Memory read out of bounds: offset={offset}, memory size={}",
data.len()
));
}
let bytes: [u8; 8] = data[offset..offset + 8]
.try_into()
.map_err(|e| format!("Failed to read memory: {e}"))?;
Ok(f64::from_le_bytes(bytes))
}
pub fn get_runtime_state_mut(&mut self) -> Option<&mut RuntimeState> {
Some(self.store.data_mut())
}
pub fn set_current_time(&mut self, time: u64) {
self.store.data_mut().current_time = time;
}
pub fn get_alloc_ptr(&mut self) -> Result<i32, String> {
let global = self
.instance
.get_global(&mut self.store, "__alloc_ptr")
.ok_or("No __alloc_ptr global export")?;
match global.get(&mut self.store) {
wasmtime::Val::I32(v) => Ok(v),
_ => Err("__alloc_ptr global type mismatch".to_string()),
}
}
pub fn set_alloc_ptr(&mut self, value: i32) -> Result<(), String> {
let global = self
.instance
.get_global(&mut self.store, "__alloc_ptr")
.ok_or("No __alloc_ptr global export")?;
global
.set(&mut self.store, wasmtime::Val::I32(value))
.map_err(|e| format!("Failed to set __alloc_ptr: {e:#}"))
}
}
#[cfg(not(target_arch = "wasm32"))]
fn plugin_uses_dest_ptr(name: &str) -> bool {
name.starts_with("split_head")
|| name.starts_with("split_tail")
|| name.starts_with("__probe_intercept$arity")
|| name.starts_with("__probe_value_intercept$arity")
}
#[cfg(not(target_arch = "wasm32"))]
fn flat_return_word_count(ty: &crate::types::Type) -> usize {
use crate::types::{PType, Type};
match ty {
Type::Primitive(PType::Unit) => 0,
Type::Tuple(elems) => elems
.iter()
.map(|e| flat_return_word_count(&e.to_type()))
.sum(),
Type::Record(fields) => fields
.iter()
.map(|f| flat_return_word_count(&f.ty.to_type()))
.sum(),
_ => 1,
}
}
#[cfg(not(target_arch = "wasm32"))]
fn mimium_type_to_wasmtime_valtype(ty: &crate::types::Type) -> ValType {
use crate::types::{PType, Type};
match ty {
Type::Primitive(PType::Numeric) => ValType::F64,
Type::Primitive(PType::Int) => ValType::I64,
Type::Primitive(PType::String) => ValType::I64,
Type::Primitive(PType::Unit) => ValType::I64,
Type::Function { .. } => ValType::I64,
Type::Record(fields) if fields.len() == 1 => {
mimium_type_to_wasmtime_valtype(&fields[0].ty.to_type())
}
Type::Tuple(elems) if elems.len() == 1 => {
mimium_type_to_wasmtime_valtype(&elems[0].to_type())
}
Type::Tuple(_) | Type::Record(_) => ValType::I64,
Type::Array(_) => ValType::I64,
Type::Union(_) | Type::UserSum { .. } => ValType::I64,
Type::Ref(_) => ValType::I64,
Type::Boxed(_) => ValType::I64,
Type::Code(_) => ValType::I64,
Type::Intermediate(cell) => {
let tv = cell.read().unwrap();
tv.parent.as_ref().map_or(ValType::I64, |parent| {
mimium_type_to_wasmtime_valtype(&parent.to_type())
})
}
_ => ValType::I64,
}
}
#[cfg(not(target_arch = "wasm32"))]
fn valtype_eq(a: &ValType, b: &ValType) -> bool {
matches!(
(a, b),
(ValType::I32, ValType::I32)
| (ValType::I64, ValType::I64)
| (ValType::F32, ValType::F32)
| (ValType::F64, ValType::F64)
)
}
#[cfg(not(target_arch = "wasm32"))]
fn decode_trampoline_args(params: &[Val]) -> Vec<f64> {
params
.iter()
.filter_map(|v| match v {
Val::F64(f) => Some(f64::from_bits(*f)),
Val::I64(i) => Some(*i as f64),
Val::I32(i) => Some(*i as f64),
_ => None,
})
.collect()
}
#[cfg(not(target_arch = "wasm32"))]
fn default_val_for_valtype(vt: ValType) -> Val {
match vt {
ValType::F64 => Val::F64(0.0f64.to_bits()),
ValType::F32 => Val::F32(0.0f32.to_bits()),
ValType::I64 => Val::I64(0),
ValType::I32 => Val::I32(0),
_ => Val::I64(0),
}
}
fn heap_alloc_host(mut caller: Caller<'_, RuntimeState>, size_words: i32) -> i64 {
log::trace!("heap_alloc_host: size_words={size_words}");
let state = caller.data_mut();
let heap_obj = heap::HeapObject::new(size_words as usize);
let heap_idx = state.heap.insert(heap_obj);
unsafe { std::mem::transmute::<heap::HeapIdx, u64>(heap_idx) as i64 }
}
fn heap_retain_host(mut caller: Caller<'_, RuntimeState>, obj: i64) {
log::trace!("heap_retain_host: obj={obj}");
let state = caller.data_mut();
let heap_idx: heap::HeapIdx = unsafe { std::mem::transmute::<u64, heap::HeapIdx>(obj as u64) };
heap::heap_retain(&mut state.heap, heap_idx);
}
fn heap_release_host(mut caller: Caller<'_, RuntimeState>, obj: i64) {
log::trace!("heap_release_host: obj={obj}");
let state = caller.data_mut();
let heap_idx: heap::HeapIdx = unsafe { std::mem::transmute::<u64, heap::HeapIdx>(obj as u64) };
heap::heap_release(&mut state.heap, heap_idx);
}
fn heap_load_host(mut caller: Caller<'_, RuntimeState>, dst_ptr: i32, obj: i64, size_words: i32) {
log::trace!("heap_load_host: dst_ptr={dst_ptr}, obj={obj}, size_words={size_words}");
let heap_idx: heap::HeapIdx = unsafe { std::mem::transmute::<u64, heap::HeapIdx>(obj as u64) };
let size = size_words as usize;
let data = {
let state = caller.data();
state
.heap
.get(heap_idx)
.map(|heap_obj| heap_obj.data[..size].to_vec())
.expect("heap_load: invalid heap index")
};
let memory = caller.data().memory.expect("Memory not initialized");
let offset = dst_ptr as usize;
let bytes = unsafe {
std::slice::from_raw_parts(
data.as_ptr() as *const u8,
size * std::mem::size_of::<Word>(),
)
};
memory
.write(&mut caller, offset, bytes)
.expect("Failed to write to WASM memory");
}
fn heap_store_host(mut caller: Caller<'_, RuntimeState>, obj: i64, src_ptr: i32, size_words: i32) {
log::trace!("heap_store_host: obj={obj}, src_ptr={src_ptr}, size_words={size_words}");
let heap_idx: heap::HeapIdx = unsafe { std::mem::transmute::<u64, heap::HeapIdx>(obj as u64) };
let size = size_words as usize;
let mut buffer = vec![0u64; size];
let memory = caller.data().memory.expect("Memory not initialized");
let offset = src_ptr as usize;
let bytes = unsafe {
std::slice::from_raw_parts_mut(
buffer.as_mut_ptr() as *mut u8,
size * std::mem::size_of::<Word>(),
)
};
memory
.read(&caller, offset, bytes)
.expect("Failed to read from WASM memory");
let state = caller.data_mut();
let heap_obj = state
.heap
.get_mut(heap_idx)
.expect("heap_store: invalid heap index");
heap_obj.data[..size].copy_from_slice(&buffer[..size]);
}
fn box_alloc_host(mut caller: Caller<'_, RuntimeState>, src_ptr: i32, size_words: i32) -> i64 {
log::trace!("box_alloc_host: src_ptr={src_ptr}, size_words={size_words}");
let size = size_words as usize;
let mut buffer = vec![0u64; size];
let memory = caller.data().memory.expect("Memory not initialized");
let offset = src_ptr as usize;
let bytes = unsafe {
std::slice::from_raw_parts_mut(
buffer.as_mut_ptr() as *mut u8,
size * std::mem::size_of::<Word>(),
)
};
memory
.read(&caller, offset, bytes)
.expect("Failed to read from WASM memory");
let state = caller.data_mut();
let heap_obj = heap::HeapObject::with_data(buffer);
let heap_idx = state.heap.insert(heap_obj);
unsafe { std::mem::transmute::<heap::HeapIdx, u64>(heap_idx) as i64 }
}
fn box_load_host(caller: Caller<'_, RuntimeState>, dst_ptr: i32, obj: i64, size_words: i32) {
log::trace!("box_load_host: dst_ptr={dst_ptr}, obj={obj}, size_words={size_words}");
heap_load_host(caller, dst_ptr, obj, size_words);
}
fn box_clone_host(mut caller: Caller<'_, RuntimeState>, obj: i64) {
log::trace!("box_clone_host: obj={obj}");
let state = caller.data_mut();
let heap_idx: heap::HeapIdx = unsafe { std::mem::transmute::<u64, heap::HeapIdx>(obj as u64) };
heap::heap_retain(&mut state.heap, heap_idx);
}
fn box_release_host(mut caller: Caller<'_, RuntimeState>, obj: i64) {
log::trace!("box_release_host: obj={obj}");
let state = caller.data_mut();
let heap_idx: heap::HeapIdx = unsafe { std::mem::transmute::<u64, heap::HeapIdx>(obj as u64) };
heap::heap_release(&mut state.heap, heap_idx);
}
fn box_store_host(caller: Caller<'_, RuntimeState>, obj: i64, src_ptr: i32, size_words: i32) {
log::trace!("box_store_host: obj={obj}, src_ptr={src_ptr}, size_words={size_words}");
heap_store_host(caller, obj, src_ptr, size_words);
}
fn usersum_clone_host(mut caller: Caller<'_, RuntimeState>, dst_ptr: i32, src_ptr: i32, size: i32) {
log::trace!("usersum_clone_host: dst_ptr={dst_ptr}, src_ptr={src_ptr}, size={size}");
if size <= 0 || src_ptr == dst_ptr {
return;
}
let byte_len = size as usize * std::mem::size_of::<Word>();
let memory = caller.data().memory.expect("Memory not initialized");
let mut buf = vec![0u8; byte_len];
memory
.read(&caller, src_ptr as usize, &mut buf)
.expect("usersum_clone: failed to read source");
memory
.write(&mut caller, dst_ptr as usize, &buf)
.expect("usersum_clone: failed to write destination");
}
fn usersum_release_host(_caller: Caller<'_, RuntimeState>, ptr: i32, tag: i32, size: i32) {
log::trace!("usersum_release_host: ptr={ptr}, tag={tag}, size={size}");
}
fn closure_make_host(
_caller: Caller<'_, RuntimeState>,
fn_idx: i64,
captured_ptr: i32,
captured_size: i32,
) -> i64 {
log::trace!(
"closure_make_host: fn_idx={fn_idx}, captured_ptr={captured_ptr}, captured_size={captured_size}"
);
0
}
fn closure_close_host(_caller: Caller<'_, RuntimeState>, closure: i64) {
log::trace!("closure_close_host: closure={closure}");
}
fn closure_call_host(
_caller: Caller<'_, RuntimeState>,
closure: i64,
args_ptr: i32,
args_size: i32,
dst_ptr: i32,
dst_size: i32,
) {
log::trace!(
"closure_call_host: closure={closure}, args_ptr={args_ptr}, args_size={args_size}, dst_ptr={dst_ptr}, dst_size={dst_size}"
);
}
fn closure_state_push_host(
mut caller: Caller<'_, RuntimeState>,
closure_addr: i64,
state_size: i64,
) {
log::trace!("closure_state_push_host: closure_addr={closure_addr}, state_size={state_size}");
let state = caller.data_mut();
state.state_stack.push(closure_addr);
state
.closure_states
.entry(closure_addr)
.or_insert_with(|| StateStorage::with_size(state_size as usize));
}
fn closure_state_pop_host(mut caller: Caller<'_, RuntimeState>) {
log::trace!("closure_state_pop_host");
let state = caller.data_mut();
if let Some(&closure_addr) = state.state_stack.last()
&& let Some(cls_state) = state.closure_states.get_mut(&closure_addr)
{
cls_state.pos = 0;
}
state.state_stack.pop();
}
fn state_push_host(mut caller: Caller<'_, RuntimeState>, offset: i64) {
log::trace!("state_push_host: offset={offset}");
let state = caller.data_mut();
let current = state.get_current_state();
if offset >= 0 {
let delta = usize::try_from(offset).unwrap_or(usize::MAX);
current.pos = current.pos.saturating_add(delta);
} else {
let delta_u64 = offset.unsigned_abs();
let delta = usize::try_from(delta_u64).unwrap_or(usize::MAX);
current.pos = current.pos.saturating_sub(delta);
}
}
fn state_pop_host(mut caller: Caller<'_, RuntimeState>, offset: i64) {
log::trace!("state_pop_host: offset={offset}");
let state = caller.data_mut();
let current = state.get_current_state();
if offset >= 0 {
let delta = usize::try_from(offset).unwrap_or(usize::MAX);
current.pos = current.pos.saturating_sub(delta);
} else {
let delta_u64 = offset.unsigned_abs();
let delta = usize::try_from(delta_u64).unwrap_or(usize::MAX);
current.pos = current.pos.saturating_add(delta);
}
}
fn state_get_host(mut caller: Caller<'_, RuntimeState>, dst_ptr: i32, size_words: i32) {
log::trace!("state_get_host: dst_ptr={dst_ptr}, size_words={size_words}");
let size = size_words as usize;
let state_values: Vec<u64> = {
let state = caller.data_mut();
let current = state.get_current_state();
let pos = current.pos;
let needed = pos + size;
if needed > current.data.len() {
current.data.resize(needed, 0);
}
current.data[pos..pos + size].to_vec()
};
let memory = caller.data().memory.expect("Memory not initialized");
let bytes: Vec<u8> = state_values.iter().flat_map(|w| w.to_le_bytes()).collect();
memory
.write(&mut caller.as_context_mut(), dst_ptr as usize, &bytes)
.expect("Failed to write state to WASM memory");
}
fn state_set_host(mut caller: Caller<'_, RuntimeState>, src_ptr: i32, size_words: i32) {
log::trace!("state_set_host: src_ptr={src_ptr}, size_words={size_words}");
if size_words < 0 {
panic!("state_set_host: negative size_words={size_words}, src_ptr={src_ptr}");
}
let size = size_words as usize;
let state_values: Vec<u64> = {
let memory = caller.data().memory.expect("Memory not initialized");
let byte_len = size
.checked_mul(std::mem::size_of::<Word>())
.expect("state_set_host: byte length overflow");
let offset = src_ptr as u32 as usize;
let memory_size = memory.data_size(&caller);
let end = offset
.checked_add(byte_len)
.expect("state_set_host: address overflow");
if end > memory_size {
let state = caller.data();
let active_pos = if let Some(&closure_addr) = state.state_stack.last() {
state
.closure_states
.get(&closure_addr)
.map(|s| s.pos)
.unwrap_or(0)
} else {
state.global_state.pos
};
panic!(
"state_set_host OOB read: src_ptr(i32)={src_ptr}, src_ptr(u32)={}, size_words={size}, byte_len={byte_len}, offset={offset}, end={end}, memory_size={memory_size}, state_stack_depth={}, active_state_pos={}, current_time={}",
src_ptr as u32,
state.state_stack.len(),
active_pos,
state.current_time,
);
}
let mut bytes = vec![0u8; byte_len];
if let Err(err) = memory.read(&caller, offset, &mut bytes) {
panic!(
"state_set_host failed to read WASM memory: src_ptr(i32)={src_ptr}, src_ptr(u32)={}, size_words={size}, byte_len={byte_len}, offset={offset}, memory_size={memory_size}, err={err:?}",
src_ptr as u32,
);
}
bytes
.chunks(std::mem::size_of::<Word>())
.map(|chunk| {
let mut word_bytes = [0u8; 8];
word_bytes.copy_from_slice(chunk);
u64::from_le_bytes(word_bytes)
})
.collect()
};
let state = caller.data_mut();
let current = state.get_current_state();
let pos = current.pos;
let needed = pos + size;
if needed > current.data.len() {
current.data.resize(needed, 0);
}
current.data[pos..pos + size].copy_from_slice(&state_values);
}
fn state_delay_host(
mut caller: Caller<'_, RuntimeState>,
input: f64,
time: f64,
max_len: i64,
) -> f64 {
let state = caller.data_mut();
let current = state.get_current_state();
let pos = current.pos;
let Some(buf_size) = usize::try_from(max_len).ok() else {
return 0.0;
};
if buf_size == 0 || buf_size > MAX_WASM_DELAY_SAMPLES {
return 0.0;
}
let Some(total_needed) = pos.checked_add(2).and_then(|v| v.checked_add(buf_size)) else {
return 0.0;
};
if total_needed > current.data.len() {
current.data.resize(total_needed, 0);
}
let len = buf_size as u64;
let max_delay = (len - 1) as f64;
let delay_samples = time.clamp(0.0, max_delay) as u64;
let write_idx = current.data[pos + 1] % len;
let read_idx = (write_idx + len - delay_samples) % len;
let res_bits = current.data[pos + 2 + read_idx as usize];
let res = f64::from_bits(res_bits);
current.data[pos + 2 + write_idx as usize] = input.to_bits();
current.data[pos] = read_idx;
current.data[pos + 1] = (write_idx + 1) % len;
res
}
fn state_mem_host(mut caller: Caller<'_, RuntimeState>, input: f64) -> f64 {
let state = caller.data_mut();
let current = state.get_current_state();
let pos = current.pos;
let needed = pos + 1;
if needed > current.data.len() {
current.data.resize(needed, 0);
}
let old_bits = current.data[pos];
let old_value = f64::from_bits(old_bits);
current.data[pos] = input.to_bits();
old_value
}
fn array_alloc_host(mut caller: Caller<'_, RuntimeState>, src_ptr: i64, size: i32) -> i64 {
log::trace!("array_alloc_host: src_ptr={src_ptr}, size={size}");
let size_usize = size as usize;
let mut buffer = vec![0u64; size_usize];
let memory = caller.data().memory.expect("Memory not initialized");
let offset = src_ptr as usize;
let bytes = unsafe {
std::slice::from_raw_parts_mut(
buffer.as_mut_ptr() as *mut u8,
size_usize * std::mem::size_of::<Word>(),
)
};
memory
.read(&caller, offset, bytes)
.expect("Failed to read from WASM memory");
let state = caller.data_mut();
let array_id = (state.arrays.len() + 1) as Word;
state.arrays.insert(array_id, buffer);
array_id as i64
}
fn array_get_elem_host(
mut caller: Caller<'_, RuntimeState>,
dst_ptr: i32,
array: i64,
index: i64,
elem_size: i32,
) {
log::trace!(
"array_get_elem_host: dst_ptr={dst_ptr}, array={array}, index={index}, elem_size={elem_size}"
);
if elem_size <= 0 {
panic!("array_get_elem: invalid elem_size {elem_size}");
}
let elem_words = elem_size as usize;
if elem_words == 0 {
panic!("array_get_elem: invalid zero elem_words");
}
if array == UNINITIALIZED_ARRAY_HANDLE_SENTINEL {
let data = vec![0; elem_words];
let memory = caller.data().memory.expect("Memory not initialized");
let bytes = unsafe {
std::slice::from_raw_parts(
data.as_ptr() as *const u8,
elem_words * std::mem::size_of::<Word>(),
)
};
memory
.write(&mut caller, dst_ptr as usize, bytes)
.expect("Failed to write to WASM memory");
return;
}
let data = {
let state = caller.data();
let array_data = state
.arrays
.get(&(array as Word))
.expect("array_get_elem: invalid array ID");
let len_elems = array_data.len() / elem_words;
if len_elems == 0 {
vec![0; elem_words]
} else {
let max_idx = len_elems - 1;
let idx = index.clamp(0, max_idx as i64) as usize;
if idx as i64 != index {
log::trace!(
"array_get_elem: clamped index {} -> {} (len={})",
index,
idx,
len_elems
);
}
let base = idx
.checked_mul(elem_words)
.expect("array_get_elem: index multiplication overflow");
let end = base
.checked_add(elem_words)
.expect("array_get_elem: index addition overflow");
array_data[base..end].to_vec()
}
};
let memory = caller.data().memory.expect("Memory not initialized");
let bytes = unsafe {
std::slice::from_raw_parts(
data.as_ptr() as *const u8,
elem_words * std::mem::size_of::<Word>(),
)
};
memory
.write(&mut caller, dst_ptr as usize, bytes)
.expect("Failed to write to WASM memory");
}
fn array_set_elem_host(
mut caller: Caller<'_, RuntimeState>,
array: i64,
index: i64,
src_ptr: i32,
elem_size: i32,
) {
log::trace!(
"array_set_elem_host: array={array}, index={index}, src_ptr={src_ptr}, elem_size={elem_size}"
);
if elem_size <= 0 {
panic!("array_set_elem: invalid elem_size {elem_size}");
}
let elem_words = elem_size as usize;
if elem_words == 0 {
panic!("array_set_elem: invalid zero elem_words");
}
let mut buffer = vec![0u64; elem_words];
let memory = caller.data().memory.expect("Memory not initialized");
let bytes = unsafe {
std::slice::from_raw_parts_mut(
buffer.as_mut_ptr() as *mut u8,
elem_words * std::mem::size_of::<Word>(),
)
};
memory
.read(&caller, src_ptr as usize, bytes)
.expect("Failed to read from WASM memory");
let state = caller.data_mut();
let array_data = state
.arrays
.get_mut(&(array as Word))
.expect("array_set_elem: invalid array ID");
let len_elems = array_data.len() / elem_words;
if len_elems == 0 {
return;
}
let max_idx = len_elems - 1;
let idx = index.clamp(0, max_idx as i64) as usize;
if idx as i64 != index {
log::trace!(
"array_set_elem: clamped index {} -> {} (len={})",
index,
idx,
len_elems
);
}
let base = idx
.checked_mul(elem_words)
.expect("array_set_elem: index multiplication overflow");
let end = base
.checked_add(elem_words)
.expect("array_set_elem: index addition overflow");
array_data[base..end].copy_from_slice(&buffer);
}
fn runtime_get_now_host(caller: Caller<'_, RuntimeState>) -> f64 {
log::trace!("runtime_get_now_host");
let state = caller.data();
state.current_time as f64
}
fn runtime_get_samplerate_host(caller: Caller<'_, RuntimeState>) -> f64 {
log::trace!("runtime_get_samplerate_host");
let state = caller.data();
state.sample_rate
}
fn builtin_probeln_host(_caller: Caller<'_, RuntimeState>, x: f64) -> f64 {
println!("{x}");
x
}
fn builtin_probe_host(_caller: Caller<'_, RuntimeState>, x: f64) -> f64 {
print!("{x}");
x
}
fn builtin_length_array_host(caller: Caller<'_, RuntimeState>, array: i64) -> f64 {
log::trace!("builtin_length_array_host: array={array}");
if array == UNINITIALIZED_ARRAY_HANDLE_SENTINEL {
return 0.0;
}
let state = caller.data();
let array_data = state
.arrays
.get(&(array as Word))
.expect("len: invalid array ID");
array_data.len() as f64
}
fn builtin_split_head_arity_host(
mut caller: Caller<'_, RuntimeState>,
array: i64,
dst_ptr: i32,
elem_words: usize,
) {
log::trace!("builtin_split_head$arity{elem_words}: array={array}, dst_ptr={dst_ptr}");
if array == UNINITIALIZED_ARRAY_HANDLE_SENTINEL {
let memory = caller.data().memory.expect("Memory not initialized");
(0..elem_words).for_each(|idx| {
memory
.write(
&mut caller,
(dst_ptr as usize) + idx * std::mem::size_of::<Word>(),
&0i64.to_le_bytes(),
)
.expect("split_head: failed to write zero head word");
});
memory
.write(
&mut caller,
(dst_ptr as usize) + elem_words * std::mem::size_of::<Word>(),
&0i64.to_le_bytes(),
)
.expect("split_head: failed to write zero rest array handle");
return;
}
if array == UNINITIALIZED_ARRAY_HANDLE_SENTINEL {
let memory = caller.data().memory.expect("Memory not initialized");
memory
.write(&mut caller, dst_ptr as usize, &0i64.to_le_bytes())
.expect("split_tail: failed to write zero rest array handle");
(0..elem_words).for_each(|idx| {
memory
.write(
&mut caller,
(dst_ptr as usize) + (idx + 1) * std::mem::size_of::<Word>(),
&0i64.to_le_bytes(),
)
.expect("split_tail: failed to write zero tail word");
});
return;
}
let (head_words, rest_data) = {
let state = caller.data();
let array_data = state
.arrays
.get(&(array as Word))
.expect("split_head: invalid array ID");
debug_assert!(
array_data.len() >= elem_words,
"split_head$arity{elem_words}: array shorter than one element"
);
assert!(
array_data.len() % elem_words == 0,
"split_head$arity{}: array length {} is not divisible by elem_words",
elem_words,
array_data.len()
);
let head_words = array_data[..elem_words].to_vec();
let rest_data = array_data[elem_words..].to_vec();
(head_words, rest_data)
};
let rest_id = {
let state = caller.data_mut();
let rest_id = (state.arrays.len() + 1) as Word;
state.arrays.insert(rest_id, rest_data);
rest_id
};
let memory = caller.data().memory.expect("Memory not initialized");
head_words.iter().enumerate().for_each(|(idx, word)| {
memory
.write(
&mut caller,
(dst_ptr as usize) + idx * std::mem::size_of::<Word>(),
&word.to_le_bytes(),
)
.expect("split_head: failed to write head word");
});
memory
.write(
&mut caller,
(dst_ptr as usize) + elem_words * std::mem::size_of::<Word>(),
&(rest_id as i64).to_le_bytes(),
)
.expect("split_head: failed to write rest array handle");
}
fn builtin_split_tail_arity_host(
mut caller: Caller<'_, RuntimeState>,
array: i64,
dst_ptr: i32,
elem_words: usize,
) {
log::trace!("builtin_split_tail$arity{elem_words}: array={array}, dst_ptr={dst_ptr}");
let (tail_words, rest_data) = {
let state = caller.data();
let array_data = state
.arrays
.get(&(array as Word))
.expect("split_tail: invalid array ID");
debug_assert!(
array_data.len() >= elem_words,
"split_tail$arity{elem_words}: array shorter than one element"
);
debug_assert!(
array_data.len() % elem_words == 0,
"split_tail$arity{}: array length {} is not divisible by elem_words",
elem_words,
array_data.len()
);
let tail_start = array_data.len() - elem_words;
let tail_words = array_data[tail_start..].to_vec();
let rest_data = array_data[..tail_start].to_vec();
(tail_words, rest_data)
};
let rest_id = {
let state = caller.data_mut();
let rest_id = (state.arrays.len() + 1) as Word;
state.arrays.insert(rest_id, rest_data);
rest_id
};
let memory = caller.data().memory.expect("Memory not initialized");
memory
.write(
&mut caller,
dst_ptr as usize,
&(rest_id as i64).to_le_bytes(),
)
.expect("split_tail: failed to write rest array handle");
tail_words.iter().enumerate().for_each(|(idx, word)| {
memory
.write(
&mut caller,
(dst_ptr as usize) + (idx + 1) * std::mem::size_of::<Word>(),
&word.to_le_bytes(),
)
.expect("split_tail: failed to write tail word");
});
}
fn builtin_prepend_arity_host(
mut caller: Caller<'_, RuntimeState>,
params: &[Val],
results: &mut [Val],
elem_words: usize,
) {
let expected_params = 2;
if params.len() != expected_params {
panic!(
"prepend$arity{} expected {} params, got {}",
elem_words,
expected_params,
params.len()
);
}
let elem_or_ptr = match params[0] {
Val::I64(i) => i as u64,
Val::F64(bits) => bits,
ref other => {
panic!("prepend$arity{elem_words}: expected first arg as I64/F64, got {other:?}")
}
};
let elem_bits = if elem_words == 1 {
vec![elem_or_ptr]
} else {
let mut words = vec![0u64; elem_words];
let memory = caller.data().memory.expect("Memory not initialized");
let src_ptr = elem_or_ptr as usize;
let bytes = unsafe {
std::slice::from_raw_parts_mut(
words.as_mut_ptr() as *mut u8,
elem_words * std::mem::size_of::<Word>(),
)
};
memory
.read(&caller, src_ptr, bytes)
.expect("prepend$arityN: failed to read element words from memory");
words
};
let array_handle = match params[1] {
Val::I64(i) => i as Word,
Val::F64(bits) => i64::from_le_bytes(bits.to_le_bytes()) as Word,
ref other => {
panic!("prepend$arity{elem_words}: expected array handle as I64/F64, got {other:?}")
}
};
let mut new_array = elem_bits;
{
let state = caller.data();
let old = state
.arrays
.get(&array_handle)
.unwrap_or_else(|| panic!("prepend: invalid array ID {array_handle}"));
if old.len() % elem_words != 0 {
panic!(
"prepend$arity{}: array length {} is not divisible by elem_words",
elem_words,
old.len()
);
}
new_array.extend_from_slice(old);
}
let new_id = {
let state = caller.data_mut();
let new_id = (state.arrays.len() + 1) as Word;
state.arrays.insert(new_id, new_array);
new_id
};
if let Some(slot) = results.get_mut(0) {
*slot = Val::I64(new_id as i64);
}
}
fn builtin_append_arity_host(
mut caller: Caller<'_, RuntimeState>,
params: &[Val],
results: &mut [Val],
elem_words: usize,
) {
let expected_params = 2;
if params.len() != expected_params {
panic!(
"append$arity{} expected {} params, got {}",
elem_words,
expected_params,
params.len()
);
}
let array_handle = match params[0] {
Val::I64(i) => i as Word,
Val::F64(bits) => i64::from_le_bytes(bits.to_le_bytes()) as Word,
ref other => {
panic!("append$arity{elem_words}: expected array handle as I64/F64, got {other:?}")
}
};
let elem_or_ptr = match params[1] {
Val::I64(i) => i as u64,
Val::F64(bits) => bits,
ref other => {
panic!("append$arity{elem_words}: expected second arg as I64/F64, got {other:?}")
}
};
let elem_bits = if elem_words == 1 {
vec![elem_or_ptr]
} else {
let mut words = vec![0u64; elem_words];
let memory = caller.data().memory.expect("Memory not initialized");
let src_ptr = elem_or_ptr as usize;
let bytes = unsafe {
std::slice::from_raw_parts_mut(
words.as_mut_ptr() as *mut u8,
elem_words * std::mem::size_of::<Word>(),
)
};
memory
.read(&caller, src_ptr, bytes)
.expect("append$arityN: failed to read element words from memory");
words
};
let mut new_array = {
let state = caller.data();
let old = state
.arrays
.get(&array_handle)
.unwrap_or_else(|| panic!("append: invalid array ID {array_handle}"));
if old.len() % elem_words != 0 {
panic!(
"append$arity{}: array length {} is not divisible by elem_words",
elem_words,
old.len()
);
}
old.to_vec()
};
new_array.extend_from_slice(&elem_bits);
let new_id = {
let state = caller.data_mut();
let new_id = (state.arrays.len() + 1) as Word;
state.arrays.insert(new_id, new_array);
new_id
};
if let Some(slot) = results.get_mut(0) {
*slot = Val::I64(new_id as i64);
}
}
fn builtin_split_head_host(caller: Caller<'_, RuntimeState>, array: i64, dst_ptr: i32) {
builtin_split_head_arity_host(caller, array, dst_ptr, 1);
}
fn builtin_split_tail_host(caller: Caller<'_, RuntimeState>, array: i64, dst_ptr: i32) {
builtin_split_tail_arity_host(caller, array, dst_ptr, 1);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::repo_test_artifact_path;
#[test]
fn test_wasm_runtime_create() {
let runtime = WasmRuntime::new(&[], None);
assert!(runtime.is_ok(), "Should create WASM runtime");
}
#[test]
fn test_wasm_runtime_load_empty_module() {
let mut runtime = WasmRuntime::new(&[], None).unwrap();
let wasm_bytes = wat::parse_str(
r#"
(module
(memory (export "memory") 1)
)
"#,
)
.expect("Failed to parse WAT");
let result = runtime.load_module(&wasm_bytes);
assert!(result.is_ok(), "Should load minimal WASM module");
}
#[test]
fn test_heap_operations() {
let mut runtime = WasmRuntime::new(&[], None).unwrap();
let wasm_bytes = wat::parse_str(
r#"
(module
(import "runtime" "heap_alloc" (func $heap_alloc (param i32) (result i64)))
(import "runtime" "heap_retain" (func $heap_retain (param i64)))
(import "runtime" "heap_release" (func $heap_release (param i64)))
(memory (export "memory") 1)
(func (export "test_heap") (result i64)
(local $obj i64)
;; Allocate heap object with 2 words
i32.const 2
call $heap_alloc
local.set $obj
;; Retain it
local.get $obj
call $heap_retain
;; Return the heap index
local.get $obj
)
)
"#,
)
.expect("Failed to parse WAT");
let mut module = runtime
.load_module(&wasm_bytes)
.expect("Failed to load module");
let result = module
.call_function("test_heap", &[])
.expect("Failed to call function");
assert_eq!(result.len(), 1, "Should return one value");
assert_ne!(result[0], 0, "Heap allocation should return non-zero index");
}
#[test]
fn test_array_operations() {
let mut runtime = WasmRuntime::new(&[], None).unwrap();
let wasm_bytes = wat::parse_str(
r#"
(module
(import "runtime" "array_alloc" (func $array_alloc (param i64 i32) (result i64)))
(import "runtime" "array_get_elem" (func $array_get_elem (param i32 i64 i64 i32)))
(import "runtime" "array_set_elem" (func $array_set_elem (param i64 i64 i32 i32)))
(memory (export "memory") 1)
(func (export "test_array") (result i64)
(local $arr i64)
;; Store some values in memory
i32.const 0
i64.const 42
i64.store
i32.const 8
i64.const 99
i64.store
;; Allocate array from memory (ptr=0 as i64, size=2)
i64.const 0
i32.const 2
call $array_alloc
local.set $arr
;; Get first element into memory at offset 16
;; (dst_ptr=16, array, index=0 as i64, elem_size=1)
i32.const 16
local.get $arr
i64.const 0
i32.const 1
call $array_get_elem
;; Load the result from memory offset 16
i32.const 16
i64.load
)
)
"#,
)
.expect("Failed to parse WAT");
let mut module = runtime
.load_module(&wasm_bytes)
.expect("Failed to load module");
let result = module
.call_function("test_array", &[])
.expect("Failed to call function");
assert_eq!(result.len(), 1, "Should return one value");
assert_eq!(result[0], 42, "Array should contain stored value");
}
#[test]
fn test_math_hyperbolic_imports() {
let mut runtime = WasmRuntime::new(&[], None).unwrap();
let wasm_bytes = wat::parse_str(
r#"
(module
(import "math" "sinh" (func $sinh (param f64) (result f64)))
(import "math" "cosh" (func $cosh (param f64) (result f64)))
(import "math" "tanh" (func $tanh (param f64) (result f64)))
(memory (export "memory") 1)
(func (export "test_sinh") (result f64)
f64.const 0.5
call $sinh)
(func (export "test_cosh") (result f64)
f64.const 0.5
call $cosh)
(func (export "test_tanh") (result f64)
f64.const 0.5
call $tanh)
)
"#,
)
.expect("Failed to parse WAT");
let mut module = runtime
.load_module(&wasm_bytes)
.expect("Failed to load module");
let sinh_res = module
.call_function("test_sinh", &[])
.expect("Failed to call test_sinh");
let cosh_res = module
.call_function("test_cosh", &[])
.expect("Failed to call test_cosh");
let tanh_res = module
.call_function("test_tanh", &[])
.expect("Failed to call test_tanh");
let sinh_val = f64::from_bits(sinh_res[0]);
let cosh_val = f64::from_bits(cosh_res[0]);
let tanh_val = f64::from_bits(tanh_res[0]);
assert!((sinh_val - 0.5f64.sinh()).abs() < 1e-12);
assert!((cosh_val - 0.5f64.cosh()).abs() < 1e-12);
assert!((tanh_val - 0.5f64.tanh()).abs() < 1e-12);
}
#[test]
fn test_load_generated_wasm() {
let wasm_path = repo_test_artifact_path("test_integration.wasm");
if !wasm_path.exists() {
eprintln!("Skipping test_load_generated_wasm: file not found at {wasm_path:?}");
return;
}
eprintln!("Loading WASM from: {wasm_path:?}");
let wasm_bytes = std::fs::read(&wasm_path).expect("Failed to read generated WASM file");
let mut runtime = WasmRuntime::new(&[], None).unwrap();
let result = runtime.load_module(&wasm_bytes);
assert!(
result.is_ok(),
"Should load generated WASM module: {:?}",
result.err()
);
let mut module = result.unwrap();
if let Ok(result) = module.call_function("fn_0", &[]) {
eprintln!("fn_0 returned: {result:?}");
}
}
}