Files
WAFER/crates/core/src/runner.rs
T
ok2 d24fa59e43 Update all dependencies to latest versions
wasmtime 31→43, wasm-encoder/wasmparser 0.228→0.246, rustyline 15→18.

API migrations: F64Const now takes Ieee64 wrapper, wasmtime has own
Error type (wasmtime::bail! in host closures), cache_config_load_default
removed. Add performance regression limits to benchmark tests.
2026-04-12 18:36:48 +02:00

403 lines
16 KiB
Rust

//! WASM runner: execute a pre-compiled `.wasm` module produced by `wafer build`.
//!
//! Provides the six imports the module expects (emit, memory, dsp, rsp, fsp,
//! table) and registers host-function stubs for known Forth words.
use std::sync::{Arc, Mutex};
use wasmtime::{
Engine, Func, FuncType, Global, GlobalType, Memory, MemoryType, Module, Ref, Store, Table,
TableType, Val, ValType,
};
use crate::export::{ExportMetadata, deserialize_metadata};
use crate::memory::{CELL_SIZE, DATA_STACK_TOP, SYSVAR_BASE_VAR};
/// Host state for the runner (currently unused by wasmtime `Store` but
/// required as the generic parameter).
struct RunnerHost {}
/// Create a wasmtime engine with the standard WAFER configuration.
fn make_engine() -> anyhow::Result<Engine> {
let mut config = wasmtime::Config::new();
config.cranelift_nan_canonicalization(false);
Ok(Engine::new(&config)?)
}
/// Execute a pre-compiled `.wasm` module from a file path.
pub fn run_wasm_file(path: &str) -> anyhow::Result<String> {
let wasm_bytes = std::fs::read(path)?;
run_wasm_bytes(&wasm_bytes)
}
/// Execute WASM bytes directly (used by tests and the CLI).
pub fn run_wasm_bytes(wasm_bytes: &[u8]) -> anyhow::Result<String> {
let metadata_json = extract_custom_section(wasm_bytes, "wafer")?;
let metadata = deserialize_metadata(&metadata_json)?;
let engine = make_engine()?;
let module = Module::new(&engine, wasm_bytes)?;
run_module(&engine, module, &metadata)
}
/// Execute an AOT-precompiled module with separate metadata.
///
/// The `precompiled` bytes must have been produced by
/// `Engine::precompile_module` with a compatible wasmtime version
/// and platform.
#[allow(unsafe_code)]
pub fn run_precompiled_bytes(precompiled: &[u8], metadata_json: &str) -> anyhow::Result<String> {
let metadata = deserialize_metadata(metadata_json)?;
let engine = make_engine()?;
// SAFETY: precompiled bytes are produced by wafer build --native using
// the same wasmtime version. The caller guarantees compatibility.
let module = unsafe { Module::deserialize(&engine, precompiled)? };
run_module(&engine, module, &metadata)
}
/// Shared runner logic: given a compiled module and metadata, set up the
/// six WASM imports, register host functions, call `_start`, return output.
fn run_module(
engine: &Engine,
module: Module,
metadata: &ExportMetadata,
) -> anyhow::Result<String> {
let output = Arc::new(Mutex::new(String::new()));
let mut store = Store::new(engine, RunnerHost {});
// Create the 6 imports the module expects.
let memory_pages = metadata.memory_size.div_ceil(65536).max(16);
let memory = Memory::new(&mut store, MemoryType::new(memory_pages, None))?;
let dsp = Global::new(
&mut store,
GlobalType::new(ValType::I32, wasmtime::Mutability::Var),
Val::I32(metadata.dsp_init as i32),
)?;
let rsp = Global::new(
&mut store,
GlobalType::new(ValType::I32, wasmtime::Mutability::Var),
Val::I32(metadata.rsp_init as i32),
)?;
let fsp = Global::new(
&mut store,
GlobalType::new(ValType::I32, wasmtime::Mutability::Var),
Val::I32(metadata.fsp_init as i32),
)?;
// Determine table size from the module's imports.
let table_min: u32 = module
.imports()
.find(|i| i.name() == "table")
.and_then(|i| i.ty().table().map(|t| t.minimum() as u32))
.unwrap_or(256);
let table = Table::new(
&mut store,
TableType::new(wasmtime::RefType::FUNCREF, table_min, None),
Ref::Func(None),
)?;
// Create the emit function.
let out_ref = Arc::clone(&output);
let emit_func = Func::new(
&mut store,
FuncType::new(engine, [ValType::I32], []),
move |_caller, params, _results| {
let code = params[0].unwrap_i32();
if let Some(ch) = char::from_u32(code as u32) {
out_ref.lock().unwrap().push(ch);
}
Ok(())
},
);
// Instantiate the module.
let instance = wasmtime::Instance::new(
&mut store,
&module,
&[
emit_func.into(),
memory.into(),
dsp.into(),
rsp.into(),
fsp.into(),
table.into(),
],
)?;
// Register host functions in the table at the metadata-specified indices.
for (idx, name) in &metadata.host_functions {
let func = create_host_func(&mut store, engine, memory, dsp, &output, name);
table.set(&mut store, *idx as u64, Ref::Func(Some(func)))?;
}
// Call _start if it exists.
if let Some(start) = instance.get_func(&mut store, "_start") {
start.call(&mut store, &[], &mut [])?;
}
let result = output.lock().unwrap().clone();
Ok(result)
}
/// Create a host function implementation for a known Forth word.
fn create_host_func(
store: &mut Store<RunnerHost>,
engine: &Engine,
memory: Memory,
dsp: Global,
output: &Arc<Mutex<String>>,
name: &str,
) -> Func {
let void_type = FuncType::new(engine, [], []);
match name {
"." => {
// ( n -- ) print number followed by space
let out = Arc::clone(output);
Func::new(store, void_type, move |mut caller, _params, _results| {
let sp = dsp.get(&mut caller).unwrap_i32() as u32;
// Read all values from memory before mutable borrow.
let (n, base) = {
let data = memory.data(&caller);
let n =
i32::from_le_bytes(data[sp as usize..sp as usize + 4].try_into().unwrap());
let base = u32::from_le_bytes(
data[SYSVAR_BASE_VAR as usize..SYSVAR_BASE_VAR as usize + 4]
.try_into()
.unwrap(),
);
(n, base)
};
dsp.set(&mut caller, Val::I32((sp + CELL_SIZE) as i32))?;
let s = if base == 16 {
format!("{n:X} ")
} else {
format!("{n} ")
};
out.lock().unwrap().push_str(&s);
Ok(())
})
}
"U." => {
// ( u -- ) print unsigned number followed by space
let out = Arc::clone(output);
Func::new(store, void_type, move |mut caller, _params, _results| {
let sp = dsp.get(&mut caller).unwrap_i32() as u32;
let (n, base) = {
let data = memory.data(&caller);
let n =
u32::from_le_bytes(data[sp as usize..sp as usize + 4].try_into().unwrap());
let base = u32::from_le_bytes(
data[SYSVAR_BASE_VAR as usize..SYSVAR_BASE_VAR as usize + 4]
.try_into()
.unwrap(),
);
(n, base)
};
dsp.set(&mut caller, Val::I32((sp + CELL_SIZE) as i32))?;
let s = if base == 16 {
format!("{n:X} ")
} else {
format!("{n} ")
};
out.lock().unwrap().push_str(&s);
Ok(())
})
}
"TYPE" => {
// ( c-addr u -- ) output u characters from memory at c-addr
let out = Arc::clone(output);
Func::new(store, void_type, move |mut caller, _params, _results| {
let sp = dsp.get(&mut caller).unwrap_i32() as u32;
let text = {
let data = memory.data(&caller);
let len =
i32::from_le_bytes(data[sp as usize..sp as usize + 4].try_into().unwrap())
as u32;
let addr = i32::from_le_bytes(
data[sp as usize + 4..sp as usize + 8].try_into().unwrap(),
) as u32;
let end = (addr + len) as usize;
String::from_utf8_lossy(&data[addr as usize..end]).to_string()
};
dsp.set(&mut caller, Val::I32((sp + 2 * CELL_SIZE) as i32))?;
out.lock().unwrap().push_str(&text);
Ok(())
})
}
"SPACES" => {
// ( n -- ) output n spaces
let out = Arc::clone(output);
Func::new(store, void_type, move |mut caller, _params, _results| {
let sp = dsp.get(&mut caller).unwrap_i32() as u32;
let n = {
let data = memory.data(&caller);
i32::from_le_bytes(data[sp as usize..sp as usize + 4].try_into().unwrap())
};
dsp.set(&mut caller, Val::I32((sp + CELL_SIZE) as i32))?;
if n > 0 {
let spaces: String = std::iter::repeat_n(' ', n as usize).collect();
out.lock().unwrap().push_str(&spaces);
}
Ok(())
})
}
".S" => {
// ( -- ) print stack contents non-destructively (no mutable borrow needed)
let out = Arc::clone(output);
Func::new(store, void_type, move |mut caller, _params, _results| {
let sp = dsp.get(&mut caller).unwrap_i32() as u32;
let data = memory.data(&caller);
let depth = (DATA_STACK_TOP - sp) / CELL_SIZE;
let mut buf = format!("<{depth}> ");
let mut addr = DATA_STACK_TOP - CELL_SIZE;
while addr >= sp {
let n = i32::from_le_bytes(
data[addr as usize..addr as usize + 4].try_into().unwrap(),
);
buf.push_str(&format!("{n} "));
if addr < CELL_SIZE {
break;
}
addr -= CELL_SIZE;
}
out.lock().unwrap().push_str(&buf);
Ok(())
})
}
"M*" => {
// ( n1 n2 -- d ) signed multiply producing double-cell result
Func::new(store, void_type, move |mut caller, _params, _results| {
let sp = dsp.get(&mut caller).unwrap_i32() as u32;
let (n1, n2) = {
let data = memory.data(&caller);
let n2 =
i32::from_le_bytes(data[sp as usize..sp as usize + 4].try_into().unwrap())
as i64;
let n1 = i32::from_le_bytes(
data[sp as usize + 4..sp as usize + 8].try_into().unwrap(),
) as i64;
(n1, n2)
};
let result = n1 * n2;
let lo = result as i32;
let hi = (result >> 32) as i32;
let data = memory.data_mut(&mut caller);
data[sp as usize + 4..sp as usize + 8].copy_from_slice(&lo.to_le_bytes());
data[sp as usize..sp as usize + 4].copy_from_slice(&hi.to_le_bytes());
Ok(())
})
}
"UM*" => {
// ( u1 u2 -- ud ) unsigned multiply producing double-cell result
Func::new(store, void_type, move |mut caller, _params, _results| {
let sp = dsp.get(&mut caller).unwrap_i32() as u32;
let (u1, u2) = {
let data = memory.data(&caller);
let u2 =
u32::from_le_bytes(data[sp as usize..sp as usize + 4].try_into().unwrap())
as u64;
let u1 = u32::from_le_bytes(
data[sp as usize + 4..sp as usize + 8].try_into().unwrap(),
) as u64;
(u1, u2)
};
let result = u1 * u2;
let lo = result as u32;
let hi = (result >> 32) as u32;
let data = memory.data_mut(&mut caller);
data[sp as usize + 4..sp as usize + 8].copy_from_slice(&lo.to_le_bytes());
data[sp as usize..sp as usize + 4].copy_from_slice(&hi.to_le_bytes());
Ok(())
})
}
"UM/MOD" => {
// ( ud u -- rem quot ) unsigned double-cell divide
Func::new(store, void_type, move |mut caller, _params, _results| {
let sp = dsp.get(&mut caller).unwrap_i32() as u32;
let (dividend, divisor) = {
let data = memory.data(&caller);
let divisor =
u32::from_le_bytes(data[sp as usize..sp as usize + 4].try_into().unwrap())
as u64;
let hi = u32::from_le_bytes(
data[sp as usize + 4..sp as usize + 8].try_into().unwrap(),
) as u64;
let lo = u32::from_le_bytes(
data[sp as usize + 8..sp as usize + 12].try_into().unwrap(),
) as u64;
((hi << 32) | lo, divisor)
};
if divisor == 0 {
wasmtime::bail!("division by zero");
}
let quot = (dividend / divisor) as u32;
let rem = (dividend % divisor) as u32;
let new_sp = sp + CELL_SIZE;
let data = memory.data_mut(&mut caller);
data[new_sp as usize + 4..new_sp as usize + 8]
.copy_from_slice(&(rem as i32).to_le_bytes());
data[new_sp as usize..new_sp as usize + 4]
.copy_from_slice(&(quot as i32).to_le_bytes());
dsp.set(&mut caller, Val::I32(new_sp as i32))?;
Ok(())
})
}
"DEPTH" => {
// ( -- n ) push current stack depth
Func::new(store, void_type, move |mut caller, _params, _results| {
let sp = dsp.get(&mut caller).unwrap_i32() as u32;
let depth = (DATA_STACK_TOP - sp) / CELL_SIZE;
let new_sp = sp - CELL_SIZE;
memory.data_mut(&mut caller)[new_sp as usize..new_sp as usize + 4]
.copy_from_slice(&(depth as i32).to_le_bytes());
dsp.set(&mut caller, Val::I32(new_sp as i32))?;
Ok(())
})
}
_ => {
// Unimplemented host function: trap with a clear message.
let name_owned = name.to_string();
Func::new(store, void_type, move |_caller, _params, _results| {
wasmtime::bail!("host function '{name_owned}' is not available in standalone mode")
})
}
}
}
/// Extract a named custom section from raw WASM bytes.
fn extract_custom_section(wasm_bytes: &[u8], section_name: &str) -> anyhow::Result<String> {
let parsed = wasmparser::Parser::new(0).parse_all(wasm_bytes);
for payload in parsed {
if let wasmparser::Payload::CustomSection(reader) = payload?
&& reader.name() == section_name
{
return Ok(String::from_utf8_lossy(reader.data()).to_string());
}
}
anyhow::bail!("no '{section_name}' custom section found in WASM module")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extract_missing_section_is_error() {
// Minimal valid WASM module (magic + version only won't validate,
// but we can test with a trivial module).
let empty_module = wasm_encoder::Module::new().finish();
let result = extract_custom_section(&empty_module, "wafer");
assert!(result.is_err());
}
}