Runtime abstraction + browser REPL

Decouple ForthVM from wasmtime via a Runtime trait so the same outer
interpreter, compiler, and 200+ word definitions work on both native
(wasmtime) and browser (js-sys WebAssembly API) backends.

Runtime trait (runtime.rs):
- HostAccess trait for memory/global ops inside host function closures
- HostFn type: Box<dyn Fn(&mut dyn HostAccess) -> Result<()>>
- Runtime trait: memory, globals, table, instantiate, call, register

NativeRuntime (runtime_native.rs):
- Wraps wasmtime Engine/Store/Memory/Table/Global/Func
- CallerHostAccess bridges HostAccess to wasmtime Caller API
- Feature-gated behind "native" (default)

outer.rs refactor:
- ForthVM<R: Runtime> — generic over execution backend
- All 87 host functions converted from Func::new closures to HostFn
- All memory access via rt.mem_read/write_*, global access via rt.get/set_*
- Zero logic changes — pure API conversion

wafer-core feature gates:
- default = ["native"] includes wasmtime + all native modules
- Without "native": pure Rust only (outer, codegen, optimizer, dictionary)

Browser REPL (crates/web):
- WebRuntime: js-sys WebAssembly.Memory/Table/Global/Module/Instance
- WaferRepl: wasm-bindgen entry point (evaluate, data_stack, reset)
- WebAssembly.Function with Safari fallback (wrapper module)
- Frontend: dark terminal UI, word panel, init code editor, history
- Build: wasm-pack build --target web

All 452 tests pass (431 unit + 1 benchmark + 9 comparison + 11 compliance).
This commit is contained in:
2026-04-13 10:06:37 +02:00
parent d24fa59e43
commit 246e21fb0f
20 changed files with 3576 additions and 2707 deletions
+5 -1
View File
@@ -8,10 +8,14 @@ license.workspace = true
[lints]
workspace = true
[features]
default = ["native"]
native = ["dep:wasmtime"]
[dependencies]
wasm-encoder = { workspace = true }
wasmparser = { workspace = true }
wasmtime = { workspace = true }
wasmtime = { workspace = true, optional = true }
anyhow = { workspace = true }
thiserror = { workspace = true }
+1 -1
View File
@@ -3059,7 +3059,7 @@ fn compile_multi_word_module(
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
#[cfg(all(test, feature = "native"))]
mod tests {
use super::*;
use crate::dictionary::WordId;
+6 -4
View File
@@ -10,6 +10,7 @@ use crate::codegen::{ExportSections, compile_exportable_module};
use crate::dictionary::WordId;
use crate::ir::IrOp;
use crate::outer::ForthVM;
use crate::runtime::Runtime;
/// Configuration for `wafer build`.
pub struct ExportConfig {
@@ -39,14 +40,14 @@ pub struct ExportMetadata {
///
/// Returns the raw `.wasm` bytes ready to write to a file, plus the metadata.
pub fn export_module(
vm: &mut ForthVM,
vm: &mut ForthVM<impl Runtime>,
config: &ExportConfig,
) -> anyhow::Result<(Vec<u8>, ExportMetadata)> {
let mut words = vm.ir_words();
// Determine the entry point.
// Priority: --entry flag > MAIN word > recorded top-level execution.
let toplevel = vm.toplevel_ir();
let toplevel = vm.toplevel_ir().to_vec();
let entry_word_id = if let Some(ref name) = config.entry_word {
Some(
vm.resolve_word(name)
@@ -58,7 +59,7 @@ pub fn export_module(
// Synthesize a _start word from recorded top-level execution.
// Pick a WordId that won't collide (one past the current table size).
let start_id = WordId(vm.current_table_size());
words.push((start_id, toplevel.to_vec()));
words.push((start_id, toplevel.clone()));
Some(start_id)
} else {
None
@@ -361,8 +362,9 @@ mod tests {
fn roundtrip(source: &str) -> String {
use crate::outer::ForthVM;
use crate::runner::run_wasm_bytes;
use crate::runtime_native::NativeRuntime;
let mut vm = ForthVM::new().unwrap();
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
vm.set_recording(true);
vm.evaluate(source).unwrap();
+15 -3
View File
@@ -16,13 +16,25 @@
pub mod codegen;
pub mod config;
pub mod consolidate;
pub mod dictionary;
pub mod error;
pub mod export;
pub mod ir;
pub mod js_loader;
pub mod memory;
pub mod optimizer;
pub mod runtime;
// Outer interpreter: runtime-agnostic, works with any Runtime impl
#[allow(trivial_numeric_casts, clippy::unnecessary_cast)]
pub mod outer;
// Modules requiring the native wasmtime runtime
#[cfg(feature = "native")]
pub mod consolidate;
#[cfg(feature = "native")]
pub mod export;
#[cfg(feature = "native")]
pub mod js_loader;
#[cfg(feature = "native")]
pub mod runner;
#[cfg(feature = "native")]
pub mod runtime_native;
+1531 -2646
View File
File diff suppressed because it is too large Load Diff
+152
View File
@@ -0,0 +1,152 @@
//! Runtime abstraction for WASM execution.
//!
//! The [`Runtime`] trait decouples the Forth VM from any specific WASM engine.
//! Two implementations exist:
//! - `NativeRuntime` (wasmtime) — for CLI and native tests
//! - `WebRuntime` (`js_sys`) — for browser REPL
//!
//! The [`HostAccess`] trait provides memory and global access to host function
//! callbacks, abstracting over wasmtime's `Caller` and browser's `js_sys` APIs.
use std::sync::{Arc, Mutex};
/// Access to WASM memory and globals from within a host function callback.
///
/// Both wasmtime (via `Caller`) and browser (via `js_sys`) implement this trait,
/// allowing host function logic to be shared across runtimes.
pub trait HostAccess {
/// Read a 32-bit integer from linear memory at `addr` (little-endian).
fn mem_read_i32(&mut self, addr: u32) -> i32;
/// Write a 32-bit integer to linear memory at `addr` (little-endian).
fn mem_write_i32(&mut self, addr: u32, val: i32);
/// Read a single byte from linear memory.
fn mem_read_u8(&mut self, addr: u32) -> u8;
/// Write a single byte to linear memory.
fn mem_write_u8(&mut self, addr: u32, val: u8);
/// Read a slice of bytes from linear memory.
fn mem_read_slice(&mut self, addr: u32, len: usize) -> Vec<u8>;
/// Write a slice of bytes to linear memory.
fn mem_write_slice(&mut self, addr: u32, data: &[u8]);
/// Total size of linear memory in bytes.
fn mem_len(&mut self) -> usize;
/// Read the data stack pointer global.
fn get_dsp(&mut self) -> u32;
/// Write the data stack pointer global.
fn set_dsp(&mut self, val: u32);
/// Read the return stack pointer global.
fn get_rsp(&mut self) -> u32;
/// Write the return stack pointer global.
fn set_rsp(&mut self, val: u32);
/// Read the float stack pointer global.
fn get_fsp(&mut self) -> u32;
/// Write the float stack pointer global.
fn set_fsp(&mut self, val: u32);
/// Call a function in the shared table by index.
/// Needed by CATCH to invoke the xt it receives.
fn call_func(&mut self, fn_index: u32) -> anyhow::Result<()>;
}
/// Host function callback type.
///
/// A boxed closure that receives mutable [`HostAccess`] for memory/global ops.
/// Captures shared state (e.g. output buffer) via `Arc<Mutex<...>>`.
pub type HostFn = Box<dyn Fn(&mut dyn HostAccess) -> anyhow::Result<()> + Send + Sync>;
/// Abstraction over a WASM execution runtime.
///
/// Provides memory access, global management, module instantiation,
/// function execution, and host function registration.
pub trait Runtime: Sized {
/// Create a new runtime with shared linear memory, function table,
/// stack pointer globals, and an `emit` host function wired to `output`.
fn new(
memory_pages: u32,
table_size: u32,
dsp_init: u32,
rsp_init: u32,
fsp_init: u32,
output: Arc<Mutex<String>>,
) -> anyhow::Result<Self>;
// -- Linear memory access --
/// Read a 32-bit integer from linear memory at `addr` (little-endian).
fn mem_read_i32(&mut self, addr: u32) -> i32;
/// Write a 32-bit integer to linear memory at `addr` (little-endian).
fn mem_write_i32(&mut self, addr: u32, val: i32);
/// Read a single byte from linear memory.
fn mem_read_u8(&mut self, addr: u32) -> u8;
/// Write a single byte to linear memory.
fn mem_write_u8(&mut self, addr: u32, val: u8);
/// Read a slice of bytes from linear memory.
fn mem_read_slice(&mut self, addr: u32, len: usize) -> Vec<u8>;
/// Write a slice of bytes to linear memory.
fn mem_write_slice(&mut self, addr: u32, data: &[u8]);
/// Total size of linear memory in bytes.
fn mem_len(&mut self) -> usize;
// -- Globals --
/// Read the data stack pointer global.
fn get_dsp(&mut self) -> u32;
/// Write the data stack pointer global.
fn set_dsp(&mut self, val: u32);
/// Read the return stack pointer global.
fn get_rsp(&mut self) -> u32;
/// Write the return stack pointer global.
fn set_rsp(&mut self, val: u32);
/// Read the float stack pointer global.
fn get_fsp(&mut self) -> u32;
/// Write the float stack pointer global.
fn set_fsp(&mut self, val: u32);
// -- Function table --
/// Current number of entries in the function table.
fn table_size(&mut self) -> u32;
/// Grow the table if needed so that index `needed` is valid.
fn ensure_table_size(&mut self, needed: u32) -> anyhow::Result<()>;
// -- Compilation and execution --
/// Compile WASM bytes into a module, instantiate it with shared imports
/// (memory, table, globals, emit), and install the exported function
/// at `fn_index` in the shared table.
fn instantiate_and_install(&mut self, wasm_bytes: &[u8], fn_index: u32) -> anyhow::Result<()>;
/// Call the function at `fn_index` in the shared table.
fn call_func(&mut self, fn_index: u32) -> anyhow::Result<()>;
// -- Host functions --
/// Register a void→void host function at `fn_index` in the shared table.
///
/// The callback receives a [`HostAccess`] for memory and global operations.
/// It may also capture shared state via `Arc<Mutex<...>>`.
fn register_host_func(&mut self, fn_index: u32, f: HostFn) -> anyhow::Result<()>;
}
+328
View File
@@ -0,0 +1,328 @@
//! Native runtime implementation using wasmtime.
use std::sync::{Arc, Mutex};
use wasmtime::{
Engine, Func, FuncType, Global, Instance, Memory, Module, Mutability, Ref, RefType, Store,
Table, Val, ValType,
};
use crate::runtime::{HostAccess, HostFn, Runtime};
/// Host-side state accessible from WASM callbacks.
struct NativeVmHost {
#[allow(dead_code)]
output: Arc<Mutex<String>>,
}
/// [`HostAccess`] implementation for wasmtime, wrapping a `Caller`.
struct CallerHostAccess<'a, 'b> {
caller: &'a mut wasmtime::Caller<'b, NativeVmHost>,
memory: Memory,
table: Table,
dsp: Global,
rsp: Global,
fsp: Global,
}
impl HostAccess for CallerHostAccess<'_, '_> {
fn mem_read_i32(&mut self, addr: u32) -> i32 {
let data = self.memory.data(&self.caller);
let a = addr as usize;
i32::from_le_bytes(data[a..a + 4].try_into().unwrap())
}
fn mem_write_i32(&mut self, addr: u32, val: i32) {
let a = addr as usize;
let bytes = val.to_le_bytes();
self.memory.data_mut(&mut *self.caller)[a..a + 4].copy_from_slice(&bytes);
}
fn mem_read_u8(&mut self, addr: u32) -> u8 {
self.memory.data(&self.caller)[addr as usize]
}
fn mem_write_u8(&mut self, addr: u32, val: u8) {
self.memory.data_mut(&mut *self.caller)[addr as usize] = val;
}
fn mem_read_slice(&mut self, addr: u32, len: usize) -> Vec<u8> {
let a = addr as usize;
self.memory.data(&self.caller)[a..a + len].to_vec()
}
fn mem_write_slice(&mut self, addr: u32, data: &[u8]) {
let a = addr as usize;
self.memory.data_mut(&mut *self.caller)[a..a + data.len()].copy_from_slice(data);
}
fn mem_len(&mut self) -> usize {
self.memory.data(&self.caller).len()
}
fn get_dsp(&mut self) -> u32 {
self.dsp.get(&mut *self.caller).unwrap_i32() as u32
}
fn set_dsp(&mut self, val: u32) {
self.dsp
.set(&mut *self.caller, Val::I32(val as i32))
.unwrap();
}
fn get_rsp(&mut self) -> u32 {
self.rsp.get(&mut *self.caller).unwrap_i32() as u32
}
fn set_rsp(&mut self, val: u32) {
self.rsp
.set(&mut *self.caller, Val::I32(val as i32))
.unwrap();
}
fn get_fsp(&mut self) -> u32 {
self.fsp.get(&mut *self.caller).unwrap_i32() as u32
}
fn set_fsp(&mut self, val: u32) {
self.fsp
.set(&mut *self.caller, Val::I32(val as i32))
.unwrap();
}
fn call_func(&mut self, fn_index: u32) -> anyhow::Result<()> {
let func_ref = self
.table
.get(&mut *self.caller, fn_index as u64)
.ok_or_else(|| anyhow::anyhow!("call_func: invalid index {fn_index}"))?;
let func = *func_ref
.unwrap_func()
.ok_or_else(|| anyhow::anyhow!("call_func: null funcref {fn_index}"))?;
func.call(&mut *self.caller, &[], &mut [])?;
Ok(())
}
}
/// Wasmtime-based native runtime.
pub struct NativeRuntime {
engine: Engine,
store: Store<NativeVmHost>,
memory: Memory,
table: Table,
dsp: Global,
rsp: Global,
fsp: Global,
emit_func: Func,
}
impl Runtime for NativeRuntime {
fn new(
memory_pages: u32,
table_size: u32,
dsp_init: u32,
rsp_init: u32,
fsp_init: u32,
output: Arc<Mutex<String>>,
) -> anyhow::Result<Self> {
let mut config = wasmtime::Config::new();
config.cranelift_nan_canonicalization(false);
let engine = Engine::new(&config)?;
let host = NativeVmHost {
output: Arc::clone(&output),
};
let mut store = Store::new(&engine, host);
let memory = Memory::new(&mut store, wasmtime::MemoryType::new(memory_pages, None))?;
let dsp = Global::new(
&mut store,
wasmtime::GlobalType::new(ValType::I32, Mutability::Var),
Val::I32(dsp_init as i32),
)?;
let rsp = Global::new(
&mut store,
wasmtime::GlobalType::new(ValType::I32, Mutability::Var),
Val::I32(rsp_init as i32),
)?;
let fsp = Global::new(
&mut store,
wasmtime::GlobalType::new(ValType::I32, Mutability::Var),
Val::I32(fsp_init as i32),
)?;
let table = Table::new(
&mut store,
wasmtime::TableType::new(RefType::FUNCREF, table_size, None),
Ref::Func(None),
)?;
let out_ref = Arc::clone(&output);
let emit_func = Func::new(
&mut store,
FuncType::new(&engine, [ValType::I32], []),
move |_caller, params, _results| {
let ch = params[0].unwrap_i32() as u8 as char;
out_ref.lock().unwrap().push(ch);
Ok(())
},
);
Ok(NativeRuntime {
engine,
store,
memory,
table,
dsp,
rsp,
fsp,
emit_func,
})
}
// -- Memory --
fn mem_read_i32(&mut self, addr: u32) -> i32 {
let a = addr as usize;
let data = self.memory.data(&self.store);
i32::from_le_bytes(data[a..a + 4].try_into().unwrap())
}
fn mem_write_i32(&mut self, addr: u32, val: i32) {
let a = addr as usize;
let bytes = val.to_le_bytes();
self.memory.data_mut(&mut self.store)[a..a + 4].copy_from_slice(&bytes);
}
fn mem_read_u8(&mut self, addr: u32) -> u8 {
self.memory.data(&self.store)[addr as usize]
}
fn mem_write_u8(&mut self, addr: u32, val: u8) {
self.memory.data_mut(&mut self.store)[addr as usize] = val;
}
fn mem_read_slice(&mut self, addr: u32, len: usize) -> Vec<u8> {
let a = addr as usize;
self.memory.data(&self.store)[a..a + len].to_vec()
}
fn mem_write_slice(&mut self, addr: u32, data: &[u8]) {
let a = addr as usize;
self.memory.data_mut(&mut self.store)[a..a + data.len()].copy_from_slice(data);
}
fn mem_len(&mut self) -> usize {
self.memory.data(&self.store).len()
}
// -- Globals --
fn get_dsp(&mut self) -> u32 {
self.dsp.get(&mut self.store).unwrap_i32() as u32
}
fn set_dsp(&mut self, val: u32) {
self.dsp.set(&mut self.store, Val::I32(val as i32)).unwrap();
}
fn get_rsp(&mut self) -> u32 {
self.rsp.get(&mut self.store).unwrap_i32() as u32
}
fn set_rsp(&mut self, val: u32) {
self.rsp.set(&mut self.store, Val::I32(val as i32)).unwrap();
}
fn get_fsp(&mut self) -> u32 {
self.fsp.get(&mut self.store).unwrap_i32() as u32
}
fn set_fsp(&mut self, val: u32) {
self.fsp.set(&mut self.store, Val::I32(val as i32)).unwrap();
}
// -- Table --
fn table_size(&mut self) -> u32 {
self.table.size(&self.store) as u32
}
fn ensure_table_size(&mut self, needed: u32) -> anyhow::Result<()> {
let current = self.table.size(&self.store) as u32;
if needed >= current {
let grow = (needed - current + 64) as u64;
self.table.grow(&mut self.store, grow, Ref::Func(None))?;
}
Ok(())
}
// -- Compilation and execution --
fn instantiate_and_install(&mut self, wasm_bytes: &[u8], fn_index: u32) -> anyhow::Result<()> {
self.ensure_table_size(fn_index)?;
let module = Module::new(&self.engine, wasm_bytes)?;
let instance = Instance::new(
&mut self.store,
&module,
&[
self.emit_func.into(),
self.memory.into(),
self.dsp.into(),
self.rsp.into(),
self.fsp.into(),
self.table.into(),
],
)?;
// Single-word modules export "fn"; multi-word (consolidated/batch)
// modules use the element section to place functions in the table.
if let Some(func) = instance.get_func(&mut self.store, "fn") {
self.table
.set(&mut self.store, fn_index as u64, Ref::Func(Some(func)))?;
}
Ok(())
}
fn call_func(&mut self, fn_index: u32) -> anyhow::Result<()> {
let r = self
.table
.get(&mut self.store, fn_index as u64)
.ok_or_else(|| anyhow::anyhow!("word {fn_index} not in function table"))?;
let func = *r
.unwrap_func()
.ok_or_else(|| anyhow::anyhow!("word {fn_index} is null funcref"))?;
func.call(&mut self.store, &[], &mut [])?;
Ok(())
}
// -- Host functions --
fn register_host_func(&mut self, fn_index: u32, f: HostFn) -> anyhow::Result<()> {
let mem = self.memory;
let tbl = self.table;
let dsp = self.dsp;
let rsp = self.rsp;
let fsp = self.fsp;
let func = Func::new(
&mut self.store,
FuncType::new(&self.engine, [], []),
move |mut caller, _params, _results| {
let mut ctx = CallerHostAccess {
caller: &mut caller,
memory: mem,
table: tbl,
dsp,
rsp,
fsp,
};
f(&mut ctx).map_err(|e| wasmtime::Error::msg(e.to_string()))
},
);
self.ensure_table_size(fn_index)?;
self.table
.set(&mut self.store, fn_index as u64, Ref::Func(Some(func)))?;
Ok(())
}
}
+7 -4
View File
@@ -6,6 +6,7 @@
use std::time::Instant;
use wafer_core::config::WaferConfig;
use wafer_core::outer::ForthVM;
use wafer_core::runtime_native::NativeRuntime;
// -----------------------------------------------------------------------
// Benchmark definitions
@@ -203,7 +204,8 @@ struct BenchResult {
fn run_benchmark(config: &WaferConfig, bench: &Benchmark) -> BenchResult {
// Compile
let compile_start = Instant::now();
let mut vm = ForthVM::new_with_config(config.clone()).expect("VM creation failed");
let mut vm =
ForthVM::<NativeRuntime>::new_with_config(config.clone()).expect("VM creation failed");
for line in bench.define.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
@@ -246,7 +248,8 @@ fn correctness_all_configs() {
for (cfg_name, config) in &configs {
for bench in &benches {
let mut vm = ForthVM::new_with_config(config.clone()).expect("VM creation failed");
let mut vm = ForthVM::<NativeRuntime>::new_with_config(config.clone())
.expect("VM creation failed");
let mut define_ok = true;
for line in bench.define.lines() {
let trimmed = line.trim();
@@ -427,8 +430,8 @@ fn optimization_report() {
let result_all = run_benchmark(&all_config, bench);
// With CONSOLIDATE
let mut vm_consol =
ForthVM::new_with_config(all_config.clone()).expect("VM creation failed");
let mut vm_consol = ForthVM::<NativeRuntime>::new_with_config(all_config.clone())
.expect("VM creation failed");
for line in bench.define.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
+5 -3
View File
@@ -12,6 +12,7 @@ use std::sync::OnceLock;
use wafer_core::config::WaferConfig;
use wafer_core::outer::ForthVM;
use wafer_core::runtime_native::NativeRuntime;
// -----------------------------------------------------------------------
// Gforth discovery (cached)
@@ -74,7 +75,7 @@ struct EngineResult {
/// Run Forth code through WAFER (in-process via `ForthVM`).
fn run_wafer(code: &str) -> EngineResult {
let mut vm = ForthVM::new().expect("Failed to create ForthVM");
let mut vm = ForthVM::<NativeRuntime>::new().expect("Failed to create ForthVM");
let mut output = String::new();
for line in code.lines() {
let trimmed = line.trim();
@@ -99,7 +100,8 @@ fn run_wafer(code: &str) -> EngineResult {
/// Run Forth code through WAFER with all optimizations enabled.
fn run_wafer_optimized(code: &str) -> EngineResult {
let mut vm = ForthVM::new_with_config(WaferConfig::all()).expect("Failed to create ForthVM");
let mut vm = ForthVM::<NativeRuntime>::new_with_config(WaferConfig::all())
.expect("Failed to create ForthVM");
let mut output = String::new();
for line in code.lines() {
let trimmed = line.trim();
@@ -809,7 +811,7 @@ fn performance_report() {
// Verify correctness first
for bench in &benchmarks {
let mut vm = ForthVM::new().expect("VM creation failed");
let mut vm = ForthVM::<NativeRuntime>::new().expect("VM creation failed");
for line in bench.define.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
+8 -7
View File
@@ -5,6 +5,7 @@
//! asserting 0 test failures.
use wafer_core::outer::ForthVM;
use wafer_core::runtime_native::NativeRuntime;
/// Path to the test suite source directory.
const SUITE_DIR: &str = concat!(
@@ -13,7 +14,7 @@ const SUITE_DIR: &str = concat!(
);
/// Load a file and evaluate it line by line, ignoring errors on individual lines.
fn load_file(vm: &mut ForthVM, path: &str) {
fn load_file(vm: &mut ForthVM<NativeRuntime>, path: &str) {
let source = std::fs::read_to_string(path).unwrap_or_else(|_| panic!("Failed to read {path}"));
for line in source.lines() {
let _ = vm.evaluate(line);
@@ -22,8 +23,8 @@ fn load_file(vm: &mut ForthVM, path: &str) {
}
/// Boot a WAFER VM with full prerequisites loaded.
fn boot_with_prerequisites() -> ForthVM {
let mut vm = ForthVM::new().expect("Failed to create ForthVM");
fn boot_with_prerequisites() -> ForthVM<NativeRuntime> {
let mut vm = ForthVM::<NativeRuntime>::new().expect("Failed to create ForthVM");
// Load test framework
load_file(&mut vm, &format!("{SUITE_DIR}/tester.fr"));
@@ -40,7 +41,7 @@ fn boot_with_prerequisites() -> ForthVM {
}
/// Run a test suite file and return the #ERRORS count.
fn run_suite(vm: &mut ForthVM, test_file: &str) -> u32 {
fn run_suite(vm: &mut ForthVM<NativeRuntime>, test_file: &str) -> u32 {
// Reset error counter
let _ = vm.evaluate("DECIMAL 0 #ERRORS !");
vm.take_output();
@@ -74,7 +75,7 @@ fn run_suite(vm: &mut ForthVM, test_file: &str) -> u32 {
#[test]
fn compliance_core() {
let mut vm = ForthVM::new().expect("Failed to create ForthVM");
let mut vm = ForthVM::<NativeRuntime>::new().expect("Failed to create ForthVM");
load_file(&mut vm, &format!("{SUITE_DIR}/tester.fr"));
load_file(&mut vm, &format!("{SUITE_DIR}/core.fr"));
@@ -94,7 +95,7 @@ fn compliance_core_plus() {
fn compliance_core_ext() {
// Core Extensions are loaded as part of prerequisites.
// Run from scratch to get a clean error count.
let mut vm = ForthVM::new().expect("Failed to create ForthVM");
let mut vm = ForthVM::<NativeRuntime>::new().expect("Failed to create ForthVM");
load_file(&mut vm, &format!("{SUITE_DIR}/tester.fr"));
load_file(&mut vm, &format!("{SUITE_DIR}/core.fr"));
let _ = vm.evaluate("DECIMAL");
@@ -162,7 +163,7 @@ fn compliance_search_order() {
fn compliance_string() {
// Run from scratch -- the stringtest includes CoreExt tests that
// cascade failures when run on top of an already-loaded CoreExt suite.
let mut vm = ForthVM::new().expect("Failed to create ForthVM");
let mut vm = ForthVM::<NativeRuntime>::new().expect("Failed to create ForthVM");
load_file(&mut vm, &format!("{SUITE_DIR}/tester.fr"));
load_file(&mut vm, &format!("{SUITE_DIR}/core.fr"));
let _ = vm.evaluate("DECIMAL");