Files
WAFER/crates/core/src/consolidate.rs
T
Oleksandr Kozachuk cda296aab5 feat(codegen): stack under/overflow guards in compiled words (WS-007)
Compiled code could silently move dsp/rsp/fsp out of their stack
regions (e.g. DROP on an empty stack), corrupting later pushes with
no diagnostic -- the addresses stay inside valid linear memory, so
nothing could trap. Host-side checks cannot catch it.

- Guards are emitted at the sp-adjustment choke points (dsp_inc/
  dsp_dec, fsp_inc/fsp_dec, rpush/rpop/rpeek, peek, TwoDup/TwoDrop,
  promoted prologue/epilogue -- DROP never loads its value, so
  guarding pop() alone is not enough). On fault: write the code to
  SYSVAR_FAULT_CODE, call _STACK_FAULT_, which THROWs it -- so
  guards are CATCHable and print standard messages (-3/-4/-5/-6/
  -44/-45).
- The batch/consolidated compile path (all boot primitives) and the
  export path are wired too; a thread-local carries the fault index
  into the shared emission helpers.
- Config: codegen.stack_guards, default ON. `wafer build` output
  defaults OFF (production artifact); WAFER_STACK_GUARDS=0|1
  overrides either. Perf comparison lanes run unguarded.
- Measured overhead in release loops: within noise (never-taken
  branches).
- toolstest.fth baseline 37 -> 38: line 368's bare interpreted `R>`
  used to underflow silently and count as passing; the guard now
  correctly reports -6.
2026-08-05 17:00:22 +02:00

170 lines
5.7 KiB
Rust

//! Consolidation recompiler: merge all JIT-compiled words into a single WASM module.
//!
//! After interactive development, `CONSOLIDATE` recompiles everything:
//! - All `call_indirect` replaced with direct `call`
//! - Single WASM module output for maximum performance
//!
//! The implementation lives across two places:
//! - `codegen::compile_consolidated_module()` generates the multi-function WASM module
//! - `outer::ForthVM::consolidate()` orchestrates collection, compilation, and table update
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use crate::codegen::compile_consolidated_module;
use crate::dictionary::WordId;
use crate::ir::IrOp;
#[test]
fn consolidated_module_validates_empty() {
// Empty word list should produce nothing (but we guard against this at call site)
let words = vec![];
let map = HashMap::new();
let result = compile_consolidated_module(&words, &map, 16, None);
// Empty is valid -- should produce a valid module with no functions
assert!(result.is_ok());
}
#[test]
fn consolidated_module_validates_single_word() {
let words = vec![(WordId(1), vec![IrOp::PushI32(42)])];
let mut map = HashMap::new();
map.insert(WordId(1), 1u32); // function index 1 (after emit import)
let result = compile_consolidated_module(&words, &map, 16, None);
assert!(result.is_ok());
}
#[test]
fn consolidated_module_validates_multiple_words() {
let words = vec![
(WordId(1), vec![IrOp::PushI32(1)]),
(WordId(2), vec![IrOp::PushI32(2), IrOp::Add]),
(
WordId(3),
vec![IrOp::Call(WordId(1)), IrOp::Call(WordId(2))],
),
];
let mut map = HashMap::new();
map.insert(WordId(1), 1u32);
map.insert(WordId(2), 2u32);
map.insert(WordId(3), 3u32);
let result = compile_consolidated_module(&words, &map, 16, None);
assert!(result.is_ok());
}
#[test]
fn consolidated_module_validates_external_call() {
// Word 3 calls WordId(99) which is NOT in the module -- should use call_indirect
let words = vec![(WordId(3), vec![IrOp::Call(WordId(99))])];
let mut map = HashMap::new();
map.insert(WordId(3), 1u32);
let result = compile_consolidated_module(&words, &map, 256, None);
assert!(result.is_ok());
}
#[test]
fn consolidated_module_validates_tail_call() {
let words = vec![
(WordId(1), vec![IrOp::PushI32(1)]),
(WordId(2), vec![IrOp::TailCall(WordId(1))]),
];
let mut map = HashMap::new();
map.insert(WordId(1), 1u32);
map.insert(WordId(2), 2u32);
let result = compile_consolidated_module(&words, &map, 16, None);
assert!(result.is_ok());
}
#[test]
fn consolidated_module_validates_control_flow_with_calls() {
// IF body contains a call to a consolidated word
let words = vec![
(WordId(1), vec![IrOp::PushI32(1)]),
(
WordId(2),
vec![
IrOp::PushI32(1),
IrOp::If {
then_body: vec![IrOp::Call(WordId(1))],
else_body: Some(vec![IrOp::PushI32(0)]),
},
],
),
];
let mut map = HashMap::new();
map.insert(WordId(1), 1u32);
map.insert(WordId(2), 2u32);
let result = compile_consolidated_module(&words, &map, 16, None);
assert!(result.is_ok());
}
#[test]
fn consolidated_module_validates_loop_with_calls() {
// DO LOOP body contains a call to a consolidated word
let words = vec![
(WordId(1), vec![IrOp::PushI32(1), IrOp::Add]),
(
WordId(2),
vec![
IrOp::PushI32(0),
IrOp::PushI32(3),
IrOp::PushI32(0),
IrOp::DoLoop {
body: vec![IrOp::Call(WordId(1))],
is_plus_loop: false,
},
],
),
];
let mut map = HashMap::new();
map.insert(WordId(1), 1u32);
map.insert(WordId(2), 2u32);
let result = compile_consolidated_module(&words, &map, 16, None);
assert!(result.is_ok());
}
#[test]
fn consolidated_module_validates_begin_until_with_calls() {
let words = vec![
(WordId(1), vec![IrOp::PushI32(1), IrOp::Sub]),
(
WordId(2),
vec![
IrOp::PushI32(5),
IrOp::BeginUntil {
body: vec![IrOp::Call(WordId(1)), IrOp::Dup, IrOp::ZeroEq],
},
],
),
];
let mut map = HashMap::new();
map.insert(WordId(1), 1u32);
map.insert(WordId(2), 2u32);
let result = compile_consolidated_module(&words, &map, 16, None);
assert!(result.is_ok());
}
#[test]
fn consolidated_module_validates_begin_while_repeat_with_calls() {
let words = vec![
(WordId(1), vec![IrOp::PushI32(1), IrOp::Sub]),
(
WordId(2),
vec![
IrOp::PushI32(3),
IrOp::BeginWhileRepeat {
test: vec![IrOp::Dup],
body: vec![IrOp::Call(WordId(1))],
},
],
),
];
let mut map = HashMap::new();
map.insert(WordId(1), 1u32);
map.insert(WordId(2), 2u32);
let result = compile_consolidated_module(&words, &map, 16, None);
assert!(result.is_ok());
}
}