perf(core): typed calling convention for words with a known stack effect
Such a word now compiles to a fast entry (i32 x p) -> (i32 x q) carrying its stack items as WASM values, plus the usual ( -- ) wrapper that keeps the table slot, so EXECUTE / interpreter / host words / CATCH see the unchanged memory ABI. Fib(25) 1035 -> 366 us, 4.3x slower than sf64 -> 1.2x; the default guards-on config 1740 -> 361 us. WS-006.
This commit is contained in:
@@ -5,6 +5,53 @@ All notable changes to WAFER are documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **A typed calling convention for words with a known stack effect.** Such a
|
||||||
|
word now compiles to two entry points: a fast one whose signature is
|
||||||
|
`(i32 x p) -> (i32 x q)`, carrying its stack items as WASM values, and the
|
||||||
|
usual `( -- )` wrapper that moves those items on and off the memory data
|
||||||
|
stack. The wrapper keeps the function-table slot, so `EXECUTE`, the outer
|
||||||
|
interpreter, host words and `CATCH` see exactly the ABI they saw before;
|
||||||
|
only direct calls inside a module take the fast entry.
|
||||||
|
|
||||||
|
This is what the SwiftForth gap was made of. sf64 keeps TOS in `RBX` and
|
||||||
|
the stack pointer in `RBP`, and both survive a `CALL` untouched, so its
|
||||||
|
`FIB` is 16 instructions and ~7 memory touches per node. WAFER kept the
|
||||||
|
whole stack in linear memory and flushed its cached `$dsp` to an imported
|
||||||
|
global before every call: ~36 memory touches per node. The stack simulator
|
||||||
|
that already promoted loop and `IF` bodies into WASM locals refused any
|
||||||
|
body containing a call or an `EXIT` -- exactly the words where the
|
||||||
|
convention cost the most. It now handles both.
|
||||||
|
|
||||||
|
Fibonacci(25) goes from 1035 to 366 µs, 4.3x slower than `sf64` to 1.2x.
|
||||||
|
Loop-heavy benchmarks are unchanged (they were already promoted, and
|
||||||
|
already beat `sf64`). Words that keep the memory convention: anything
|
||||||
|
using `SP@`, `DEPTH`, `EXECUTE`, `>R`/`R>`, floats or locals; anything
|
||||||
|
calling a word that is itself untyped, which in the JIT path means every
|
||||||
|
call except `RECURSE`; mutually recursive words; and words whose effect is
|
||||||
|
not static -- branches that disagree on depth, `EXIT` at the wrong depth,
|
||||||
|
a non-neutral loop body, or a recursion that grows the stack per level.
|
||||||
|
|
||||||
|
`CONSOLIDATE` extends this across words, since it puts them all in one
|
||||||
|
module: the effects are solved to a fixpoint from the leaves outward, and
|
||||||
|
105 of 187 words in a booted dictionary end up typed.
|
||||||
|
|
||||||
|
Stack guards get cheap as a side effect -- they hang off the memory-stack
|
||||||
|
push/pop choke points, and a typed word barely has any. The default
|
||||||
|
guards-on configuration that the REPL and the web build use went from 1631
|
||||||
|
to 365 µs on the same benchmark.
|
||||||
|
|
||||||
|
`WAFER_TYPED_CALLS=0` falls back to the memory-stack convention.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- The Forth 2012 Core suite now also runs against consolidated code
|
||||||
|
(`compliance_core_after_consolidate`). `CONSOLIDATE` had no correctness
|
||||||
|
test at all before -- only benchmarks.
|
||||||
|
|
||||||
## [0.2.6] - 2026-08-07
|
## [0.2.6] - 2026-08-07
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ An optimizing Forth 2012 compiler targeting WebAssembly. WAFER JIT-compiles each
|
|||||||
- **Faster than gforth** on all benchmarks in release mode (2-10x faster)
|
- **Faster than gforth** on all benchmarks in release mode (2-10x faster)
|
||||||
- **JIT compilation** — each `:` definition compiles to its own WASM module
|
- **JIT compilation** — each `:` definition compiles to its own WASM module
|
||||||
- **Self-recursive direct calls** — RECURSE compiles to native `call` instead of `call_indirect`
|
- **Self-recursive direct calls** — RECURSE compiles to native `call` instead of `call_indirect`
|
||||||
|
- **Typed calling convention** — a word with a statically known stack effect passes its stack items as WASM values, so a call keeps them in registers instead of round-tripping through memory
|
||||||
- **Consolidation mode** — recompile all words into a single optimized WASM module
|
- **Consolidation mode** — recompile all words into a single optimized WASM module
|
||||||
- **Interactive REPL** with line editing (rustyline)
|
- **Interactive REPL** with line editing (rustyline)
|
||||||
- **Browser REPL** — runs entirely in the browser via wasm-pack + js-sys
|
- **Browser REPL** — runs entirely in the browser via wasm-pack + js-sys
|
||||||
@@ -79,23 +80,31 @@ git submodule update --init
|
|||||||
|
|
||||||
## Performance
|
## Performance
|
||||||
|
|
||||||
WAFER beats gforth (the GNU Forth reference implementation) on all benchmarks in release mode:
|
WAFER beats gforth (the GNU Forth reference implementation) on all benchmarks in release mode, and is within
|
||||||
|
reach of SwiftForth `sf64`, which compiles to native code:
|
||||||
|
|
||||||
```
|
```
|
||||||
Benchmark WAFER CONSOL gforth WAFER/gf
|
Benchmark WAFER CONSOL gforth sf64 WAFER/gf WAFER/sf
|
||||||
Fibonacci(25) 1629 1535 3422 0.45x
|
Fibonacci(25) 378 359 3238 296 0.11x 1.21x
|
||||||
Factorial(12)x10K 340 339 638 0.53x
|
Factorial(12)x10K 335 320 633 183 0.51x 1.75x
|
||||||
GCD-bench(500) 18 15 30 0.50x
|
GCD-bench(500) 14 15 29 31 0.48x 0.45x
|
||||||
NestedLoops(50) 84 73 720 0.10x
|
NestedLoops(50) 72 69 698 207 0.10x 0.33x
|
||||||
Collatz(2K) 1212 1202 3914 0.31x
|
Collatz(2K) 994 997 3940 668 0.25x 1.49x
|
||||||
```
|
```
|
||||||
|
|
||||||
Times in microseconds. WAFER/gf < 1.0 means WAFER is faster. CONSOL = after `CONSOLIDATE`.
|
Times in microseconds. WAFER/gf < 1.0 means WAFER is faster. CONSOL = after `CONSOLIDATE`.
|
||||||
|
|
||||||
|
A word whose stack effect is statically known gets a **typed entry point**: its stack items travel in and out
|
||||||
|
as WASM values instead of through the memory data stack, so cranelift keeps them in registers across a call
|
||||||
|
the way a native Forth keeps TOS in one. The word also keeps a `( -- )` wrapper, which is what the function
|
||||||
|
table, `EXECUTE` and the outer interpreter reach, so nothing about the memory ABI changes from the outside.
|
||||||
|
Call-heavy code is what this pays for -- Fibonacci went from 4.3x slower than `sf64` to 1.2x. Set
|
||||||
|
`WAFER_TYPED_CALLS=0` to fall back to the memory-stack convention.
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# All tests (~570 currently passing)
|
# All tests (~620 currently passing)
|
||||||
cargo test --workspace
|
cargo test --workspace
|
||||||
|
|
||||||
# Forth 2012 compliance suite
|
# Forth 2012 compliance suite
|
||||||
|
|||||||
@@ -263,7 +263,8 @@ fn cmd_run(file: &str) -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// `WaferConfig` for CLI-created VMs. `WAFER_STACK_GUARDS=0|1` overrides
|
/// `WaferConfig` for CLI-created VMs. `WAFER_STACK_GUARDS=0|1` overrides
|
||||||
/// the per-command default (REPL/file execution on, build off).
|
/// the per-command default (REPL/file execution on, build off);
|
||||||
|
/// `WAFER_TYPED_CALLS=0` falls back to the memory-stack calling convention.
|
||||||
fn vm_config(default_guards: bool) -> wafer_core::config::WaferConfig {
|
fn vm_config(default_guards: bool) -> wafer_core::config::WaferConfig {
|
||||||
let mut cfg = wafer_core::config::WaferConfig::all();
|
let mut cfg = wafer_core::config::WaferConfig::all();
|
||||||
cfg.codegen.stack_guards = match std::env::var("WAFER_STACK_GUARDS").ok().as_deref() {
|
cfg.codegen.stack_guards = match std::env::var("WAFER_STACK_GUARDS").ok().as_deref() {
|
||||||
@@ -271,6 +272,7 @@ fn vm_config(default_guards: bool) -> wafer_core::config::WaferConfig {
|
|||||||
Some(_) => true,
|
Some(_) => true,
|
||||||
None => default_guards,
|
None => default_guards,
|
||||||
};
|
};
|
||||||
|
cfg.codegen.typed_calls = std::env::var("WAFER_TYPED_CALLS").ok().as_deref() != Some("0");
|
||||||
cfg
|
cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+836
-96
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,11 @@ pub struct CodegenOpts {
|
|||||||
/// corrupting stack pointers. On by default; benchmarks and
|
/// corrupting stack pointers. On by default; benchmarks and
|
||||||
/// exported production modules turn it off.
|
/// exported production modules turn it off.
|
||||||
pub stack_guards: bool,
|
pub stack_guards: bool,
|
||||||
|
/// Compile words with a statically known stack effect to a typed entry
|
||||||
|
/// point that carries stack items in WASM values, so a call keeps them
|
||||||
|
/// in registers instead of round-tripping through the memory stack.
|
||||||
|
/// On by default; `WAFER_TYPED_CALLS=0` turns it off.
|
||||||
|
pub typed_calls: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Master configuration for all WAFER optimizations.
|
/// Master configuration for all WAFER optimizations.
|
||||||
@@ -38,6 +43,7 @@ impl WaferConfig {
|
|||||||
codegen: CodegenOpts {
|
codegen: CodegenOpts {
|
||||||
stack_to_local_promotion: true,
|
stack_to_local_promotion: true,
|
||||||
stack_guards: true,
|
stack_guards: true,
|
||||||
|
typed_calls: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -56,6 +62,7 @@ impl WaferConfig {
|
|||||||
codegen: CodegenOpts {
|
codegen: CodegenOpts {
|
||||||
stack_to_local_promotion: false,
|
stack_to_local_promotion: false,
|
||||||
stack_guards: false,
|
stack_guards: false,
|
||||||
|
typed_calls: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ mod tests {
|
|||||||
// Empty word list should produce nothing (but we guard against this at call site)
|
// Empty word list should produce nothing (but we guard against this at call site)
|
||||||
let words = vec![];
|
let words = vec![];
|
||||||
let map = HashMap::new();
|
let map = HashMap::new();
|
||||||
let result = compile_consolidated_module(&words, &map, 16, None);
|
let result = compile_consolidated_module(&words, &map, 16, None, true);
|
||||||
// Empty is valid -- should produce a valid module with no functions
|
// Empty is valid -- should produce a valid module with no functions
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,7 @@ mod tests {
|
|||||||
let words = vec![(WordId(1), vec![IrOp::PushI32(42)])];
|
let words = vec![(WordId(1), vec![IrOp::PushI32(42)])];
|
||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
map.insert(WordId(1), 1u32); // function index 1 (after emit import)
|
map.insert(WordId(1), 1u32); // function index 1 (after emit import)
|
||||||
let result = compile_consolidated_module(&words, &map, 16, None);
|
let result = compile_consolidated_module(&words, &map, 16, None, true);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ mod tests {
|
|||||||
map.insert(WordId(1), 1u32);
|
map.insert(WordId(1), 1u32);
|
||||||
map.insert(WordId(2), 2u32);
|
map.insert(WordId(2), 2u32);
|
||||||
map.insert(WordId(3), 3u32);
|
map.insert(WordId(3), 3u32);
|
||||||
let result = compile_consolidated_module(&words, &map, 16, None);
|
let result = compile_consolidated_module(&words, &map, 16, None, true);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ mod tests {
|
|||||||
let words = vec![(WordId(3), vec![IrOp::Call(WordId(99))])];
|
let words = vec![(WordId(3), vec![IrOp::Call(WordId(99))])];
|
||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
map.insert(WordId(3), 1u32);
|
map.insert(WordId(3), 1u32);
|
||||||
let result = compile_consolidated_module(&words, &map, 256, None);
|
let result = compile_consolidated_module(&words, &map, 256, None, true);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ mod tests {
|
|||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
map.insert(WordId(1), 1u32);
|
map.insert(WordId(1), 1u32);
|
||||||
map.insert(WordId(2), 2u32);
|
map.insert(WordId(2), 2u32);
|
||||||
let result = compile_consolidated_module(&words, &map, 16, None);
|
let result = compile_consolidated_module(&words, &map, 16, None, true);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ mod tests {
|
|||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
map.insert(WordId(1), 1u32);
|
map.insert(WordId(1), 1u32);
|
||||||
map.insert(WordId(2), 2u32);
|
map.insert(WordId(2), 2u32);
|
||||||
let result = compile_consolidated_module(&words, &map, 16, None);
|
let result = compile_consolidated_module(&words, &map, 16, None, true);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ mod tests {
|
|||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
map.insert(WordId(1), 1u32);
|
map.insert(WordId(1), 1u32);
|
||||||
map.insert(WordId(2), 2u32);
|
map.insert(WordId(2), 2u32);
|
||||||
let result = compile_consolidated_module(&words, &map, 16, None);
|
let result = compile_consolidated_module(&words, &map, 16, None, true);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,7 +141,7 @@ mod tests {
|
|||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
map.insert(WordId(1), 1u32);
|
map.insert(WordId(1), 1u32);
|
||||||
map.insert(WordId(2), 2u32);
|
map.insert(WordId(2), 2u32);
|
||||||
let result = compile_consolidated_module(&words, &map, 16, None);
|
let result = compile_consolidated_module(&words, &map, 16, None, true);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ mod tests {
|
|||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
map.insert(WordId(1), 1u32);
|
map.insert(WordId(1), 1u32);
|
||||||
map.insert(WordId(2), 2u32);
|
map.insert(WordId(2), 2u32);
|
||||||
let result = compile_consolidated_module(&words, &map, 16, None);
|
let result = compile_consolidated_module(&words, &map, 16, None, true);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ pub fn export_module(
|
|||||||
table_size,
|
table_size,
|
||||||
&export_sections,
|
&export_sections,
|
||||||
vm.stack_guard_param(),
|
vm.stack_guard_param(),
|
||||||
|
vm.typed_calls(),
|
||||||
)
|
)
|
||||||
.map_err(|e| anyhow::anyhow!("export codegen error: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("export codegen error: {e}"))?;
|
||||||
|
|
||||||
|
|||||||
@@ -2644,6 +2644,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
&local_fn_map,
|
&local_fn_map,
|
||||||
table_size,
|
table_size,
|
||||||
self.stack_guard_param(),
|
self.stack_guard_param(),
|
||||||
|
self.config.codegen.typed_calls,
|
||||||
)
|
)
|
||||||
.map_err(|e| anyhow::anyhow!("consolidation codegen error: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("consolidation codegen error: {e}"))?;
|
||||||
|
|
||||||
@@ -2674,6 +2675,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
&local_fn_map,
|
&local_fn_map,
|
||||||
table_size,
|
table_size,
|
||||||
self.stack_guard_param(),
|
self.stack_guard_param(),
|
||||||
|
self.config.codegen.typed_calls,
|
||||||
)
|
)
|
||||||
.map_err(|e| anyhow::anyhow!("batch compile error: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("batch compile error: {e}"))?;
|
||||||
|
|
||||||
@@ -2966,6 +2968,11 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
.then_some(self.stack_fault_id)
|
.then_some(self.stack_fault_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether words with a known stack effect get a typed entry point.
|
||||||
|
pub(crate) fn typed_calls(&self) -> bool {
|
||||||
|
self.config.codegen.typed_calls
|
||||||
|
}
|
||||||
|
|
||||||
/// Codegen configuration for compiling one word.
|
/// Codegen configuration for compiling one word.
|
||||||
fn codegen_config(&mut self, base_fn_index: u32) -> CodegenConfig {
|
fn codegen_config(&mut self, base_fn_index: u32) -> CodegenConfig {
|
||||||
CodegenConfig {
|
CodegenConfig {
|
||||||
@@ -2973,6 +2980,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
table_size: self.table_size(),
|
table_size: self.table_size(),
|
||||||
stack_to_local_promotion: self.config.codegen.stack_to_local_promotion,
|
stack_to_local_promotion: self.config.codegen.stack_to_local_promotion,
|
||||||
stack_guards: self.stack_guard_param(),
|
stack_guards: self.stack_guard_param(),
|
||||||
|
typed_calls: self.config.codegen.typed_calls,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8180,6 +8188,73 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::runtime_native::NativeRuntime;
|
use crate::runtime_native::NativeRuntime;
|
||||||
|
|
||||||
|
// -- Typed calling convention -------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_typed_word_recursion_matches_the_memory_convention() {
|
||||||
|
// FIB is the shape the typed entry exists for: self-recursive with
|
||||||
|
// an early EXIT, so it is compiled as WASM values in and out.
|
||||||
|
let (stack, _) = eval(
|
||||||
|
": FIB DUP 2 < IF EXIT THEN DUP 1- RECURSE SWAP 2 - RECURSE + ; \
|
||||||
|
0 FIB 1 FIB 2 FIB 10 FIB 25 FIB",
|
||||||
|
);
|
||||||
|
assert_eq!(stack, vec![75025, 55, 1, 1, 0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_typed_word_keeps_the_items_below_its_arguments() {
|
||||||
|
// The wrapper may only move the cells the word declared; anything
|
||||||
|
// deeper has to still be there afterwards.
|
||||||
|
let (stack, _) = eval(": SQ DUP * ; 7 8 9 SQ");
|
||||||
|
assert_eq!(stack, vec![81, 8, 7]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_typed_word_is_reachable_through_execute() {
|
||||||
|
// EXECUTE goes through the table, which holds the `( -- )` wrapper.
|
||||||
|
let (stack, _) = eval(": SQ DUP * ; 6 ' SQ EXECUTE");
|
||||||
|
assert_eq!(stack, vec![36]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_catch_sees_a_typed_word_underflow() {
|
||||||
|
// A typed word's stack guards live in its wrapper, where the
|
||||||
|
// arguments come off the memory stack. They still THROW -4 rather
|
||||||
|
// than corrupting the stack pointer, and CATCH still reports it.
|
||||||
|
let (stack, _) = eval(": SQ DUP * ; : CHK ['] SQ CATCH ; CHK");
|
||||||
|
assert_eq!(stack, vec![-4]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_catch_restores_the_stack_around_a_caller_of_typed_code() {
|
||||||
|
// T contains THROW, a host word, so T itself keeps the memory
|
||||||
|
// convention and reaches SQ through its wrapper. CATCH restores the
|
||||||
|
// depth it saved across that boundary -- not the contents, so the 3
|
||||||
|
// that SQ squared stays squared. gforth agrees: `1 2 9 5 4`.
|
||||||
|
let (stack, _) = eval(": SQ DUP * ; : T SQ 5 THROW ; : CHK 1 2 3 ['] T CATCH DEPTH ; CHK");
|
||||||
|
assert_eq!(stack, vec![4, 5, 9, 2, 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_typed_word_underflow_still_throws() {
|
||||||
|
// The stack guards for a typed word live in its wrapper, where the
|
||||||
|
// arguments are taken off the memory stack.
|
||||||
|
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
||||||
|
vm.evaluate(": SQ DUP * ;").unwrap();
|
||||||
|
vm.take_output();
|
||||||
|
let err = vm.evaluate("SQ");
|
||||||
|
assert!(err.is_err(), "empty-stack SQ should throw, got {err:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deep_typed_recursion_unwinds_cleanly() {
|
||||||
|
// 10k levels of a typed self-call: results come back in WASM values,
|
||||||
|
// so nothing is left on the memory stack afterwards.
|
||||||
|
let (stack, _) =
|
||||||
|
eval(": COUNT-DOWN DUP 0= IF EXIT THEN 1- RECURSE ; 10000 COUNT-DOWN DEPTH");
|
||||||
|
assert_eq!(stack, vec![1, 0]);
|
||||||
|
}
|
||||||
|
|
||||||
fn eval(input: &str) -> (Vec<i32>, String) {
|
fn eval(input: &str) -> (Vec<i32>, String) {
|
||||||
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
||||||
vm.evaluate(input).unwrap();
|
vm.evaluate(input).unwrap();
|
||||||
|
|||||||
@@ -344,3 +344,32 @@ fn compliance_tools() {
|
|||||||
let errors = run_suite(&mut vm, "toolstest.fth");
|
let errors = run_suite(&mut vm, "toolstest.fth");
|
||||||
assert_eq!(errors, 0, "Programming-Tools: {errors} test failures");
|
assert_eq!(errors, 0, "Programming-Tools: {errors} test failures");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The Forth 2012 Core suite against consolidated code.
|
||||||
|
///
|
||||||
|
/// `CONSOLIDATE` recompiles the whole dictionary into one WASM module, which
|
||||||
|
/// is where cross-word typed calls live: a word with a known stack effect
|
||||||
|
/// gets a fast entry taking and returning its stack items as WASM values,
|
||||||
|
/// and its `() -> ()` wrapper keeps the table slot. Nothing else covers that
|
||||||
|
/// path for correctness, so run the suite on top of it.
|
||||||
|
#[test]
|
||||||
|
fn compliance_core_after_consolidate() {
|
||||||
|
let mut vm = ForthVM::<NativeRuntime>::new().expect("Failed to create ForthVM");
|
||||||
|
let tester_path = format!("{SUITE_DIR}/tester.fr");
|
||||||
|
let f1 = load_file(&mut vm, &tester_path);
|
||||||
|
assert_load_fails_within_baseline(&tester_path, f1);
|
||||||
|
|
||||||
|
vm.evaluate("CONSOLIDATE").expect("CONSOLIDATE failed");
|
||||||
|
vm.take_output();
|
||||||
|
|
||||||
|
let core_path = format!("{SUITE_DIR}/core.fr");
|
||||||
|
let f2 = load_file(&mut vm, &core_path);
|
||||||
|
assert_load_fails_within_baseline(&core_path, f2);
|
||||||
|
|
||||||
|
let _ = vm.evaluate("DECIMAL #ERRORS @");
|
||||||
|
let errors = vm.data_stack().first().copied().unwrap_or(-1);
|
||||||
|
assert_eq!(
|
||||||
|
errors, 0,
|
||||||
|
"Core word set after CONSOLIDATE: {errors} failures"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user