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.
This commit is contained in:
+15
-2
@@ -137,7 +137,8 @@ fn cmd_build(
|
|||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let source = std::fs::read_to_string(file)?;
|
let source = std::fs::read_to_string(file)?;
|
||||||
|
|
||||||
let mut vm = ForthVM::<NativeRuntime>::new()?;
|
// Exported modules are production artifacts: no stack guards by default
|
||||||
|
let mut vm = ForthVM::<NativeRuntime>::new_with_config(vm_config(false))?;
|
||||||
vm.set_recording(true);
|
vm.set_recording(true);
|
||||||
vm.evaluate(&source)?;
|
vm.evaluate(&source)?;
|
||||||
|
|
||||||
@@ -260,9 +261,21 @@ fn cmd_run(file: &str) -> anyhow::Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `WaferConfig` for CLI-created VMs. `WAFER_STACK_GUARDS=0|1` overrides
|
||||||
|
/// the per-command default (REPL/file execution on, build off).
|
||||||
|
fn vm_config(default_guards: bool) -> wafer_core::config::WaferConfig {
|
||||||
|
let mut cfg = wafer_core::config::WaferConfig::all();
|
||||||
|
cfg.codegen.stack_guards = match std::env::var("WAFER_STACK_GUARDS").ok().as_deref() {
|
||||||
|
Some("0") => false,
|
||||||
|
Some(_) => true,
|
||||||
|
None => default_guards,
|
||||||
|
};
|
||||||
|
cfg
|
||||||
|
}
|
||||||
|
|
||||||
/// `wafer` (REPL) or `wafer program.fth` (evaluate and exit)
|
/// `wafer` (REPL) or `wafer program.fth` (evaluate and exit)
|
||||||
fn cmd_eval_or_repl(file: Option<&str>) -> anyhow::Result<()> {
|
fn cmd_eval_or_repl(file: Option<&str>) -> anyhow::Result<()> {
|
||||||
let mut vm = ForthVM::<NativeRuntime>::new()?;
|
let mut vm = ForthVM::<NativeRuntime>::new_with_config(vm_config(true))?;
|
||||||
|
|
||||||
match file {
|
match file {
|
||||||
Some(file) => {
|
Some(file) => {
|
||||||
|
|||||||
+131
-5
@@ -19,7 +19,10 @@ use wasm_encoder::{
|
|||||||
use crate::dictionary::WordId;
|
use crate::dictionary::WordId;
|
||||||
use crate::error::{WaferError, WaferResult};
|
use crate::error::{WaferError, WaferResult};
|
||||||
use crate::ir::IrOp;
|
use crate::ir::IrOp;
|
||||||
use crate::memory::{CELL_SIZE, SYSVAR_LEAVE_FLAG};
|
use crate::memory::{
|
||||||
|
CELL_SIZE, DATA_STACK_BASE, DATA_STACK_TOP, FLOAT_STACK_BASE, FLOAT_STACK_TOP,
|
||||||
|
RETURN_STACK_BASE, RETURN_STACK_TOP, SYSVAR_FAULT_CODE, SYSVAR_LEAVE_FLAG,
|
||||||
|
};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Import indices (order matters: imports numbered sequentially by kind)
|
// Import indices (order matters: imports numbered sequentially by kind)
|
||||||
@@ -94,6 +97,9 @@ pub struct CodegenConfig {
|
|||||||
pub table_size: u32,
|
pub table_size: u32,
|
||||||
/// Enable stack-to-local promotion for straight-line words.
|
/// Enable stack-to-local promotion for straight-line words.
|
||||||
pub stack_to_local_promotion: bool,
|
pub stack_to_local_promotion: bool,
|
||||||
|
/// Table index of the `_STACK_FAULT_` host word; `Some` enables
|
||||||
|
/// stack under/overflow guards in the emitted code.
|
||||||
|
pub stack_guards: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Result of compiling a word to WASM.
|
/// Result of compiling a word to WASM.
|
||||||
@@ -109,16 +115,73 @@ pub struct CompiledModule {
|
|||||||
// Instruction-level helpers (free functions that take &mut Function)
|
// Instruction-level helpers (free functions that take &mut Function)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// Decrement the cached `$dsp` local by `CELL_SIZE`.
|
// Stack-guard emission. The fault word's table index is stashed in a
|
||||||
|
// thread-local by `compile_word` (None = guards off) so the low-level
|
||||||
|
// push/pop helpers can stay plain `&mut Function` free functions
|
||||||
|
// without threading config through every emitter.
|
||||||
|
thread_local! {
|
||||||
|
static GUARD_FAULT: std::cell::Cell<Option<u32>> = const { std::cell::Cell::new(None) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// With guards on: emit `if <cond> { mem[SYSVAR_FAULT_CODE] = code;
|
||||||
|
/// call _STACK_FAULT_ }`. `cond` must leave an i32 boolean on the
|
||||||
|
/// operand stack. The fault host word throws, so the `if` never falls
|
||||||
|
/// through on the failure path.
|
||||||
|
fn emit_guard(f: &mut Function, code: i32, cond: impl FnOnce(&mut Function)) {
|
||||||
|
let Some(fault_idx) = GUARD_FAULT.get() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
cond(f);
|
||||||
|
f.instruction(&Instruction::If(BlockType::Empty))
|
||||||
|
.instruction(&Instruction::I32Const(SYSVAR_FAULT_CODE as i32))
|
||||||
|
.instruction(&Instruction::I32Const(code))
|
||||||
|
.instruction(&Instruction::I32Store(MEM4))
|
||||||
|
.instruction(&Instruction::I32Const(fault_idx as i32))
|
||||||
|
.instruction(&Instruction::CallIndirect {
|
||||||
|
type_index: TYPE_VOID,
|
||||||
|
table_index: TABLE,
|
||||||
|
})
|
||||||
|
.instruction(&Instruction::End);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Guard: data stack has at least `n` cells (else throw -4).
|
||||||
|
fn guard_dsp_underflow(f: &mut Function, n: u32) {
|
||||||
|
emit_guard(f, -4, |f| {
|
||||||
|
f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL))
|
||||||
|
.instruction(&Instruction::I32Const((n * CELL_SIZE) as i32))
|
||||||
|
.instruction(&Instruction::I32Add)
|
||||||
|
.instruction(&Instruction::I32Const(DATA_STACK_TOP as i32))
|
||||||
|
.instruction(&Instruction::I32GtU);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Guard: data stack has room for `n` more cells (else throw -3).
|
||||||
|
fn guard_dsp_overflow(f: &mut Function, n: u32) {
|
||||||
|
emit_guard(f, -3, |f| {
|
||||||
|
f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL))
|
||||||
|
.instruction(&Instruction::I32Const(
|
||||||
|
(DATA_STACK_BASE + n * CELL_SIZE) as i32,
|
||||||
|
))
|
||||||
|
.instruction(&Instruction::I32LtU);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decrement the cached `$dsp` local by `CELL_SIZE` (allocate one cell).
|
||||||
|
/// This is the single choke point for data-stack pushes, so the
|
||||||
|
/// overflow guard lives here.
|
||||||
fn dsp_dec(f: &mut Function) {
|
fn dsp_dec(f: &mut Function) {
|
||||||
|
guard_dsp_overflow(f, 1);
|
||||||
f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL))
|
f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL))
|
||||||
.instruction(&Instruction::I32Const(CELL_SIZE as i32))
|
.instruction(&Instruction::I32Const(CELL_SIZE as i32))
|
||||||
.instruction(&Instruction::I32Sub)
|
.instruction(&Instruction::I32Sub)
|
||||||
.instruction(&Instruction::LocalSet(CACHED_DSP_LOCAL));
|
.instruction(&Instruction::LocalSet(CACHED_DSP_LOCAL));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Increment the cached `$dsp` local by `CELL_SIZE`.
|
/// Increment the cached `$dsp` local by `CELL_SIZE` (free one cell).
|
||||||
|
/// Single choke point for data-stack pops (`DROP` never loads the
|
||||||
|
/// value, so the underflow guard must sit here, not in `pop`).
|
||||||
fn dsp_inc(f: &mut Function) {
|
fn dsp_inc(f: &mut Function) {
|
||||||
|
guard_dsp_underflow(f, 1);
|
||||||
f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL))
|
f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL))
|
||||||
.instruction(&Instruction::I32Const(CELL_SIZE as i32))
|
.instruction(&Instruction::I32Const(CELL_SIZE as i32))
|
||||||
.instruction(&Instruction::I32Add)
|
.instruction(&Instruction::I32Add)
|
||||||
@@ -160,6 +223,7 @@ fn pop_to(f: &mut Function, local: u32) {
|
|||||||
|
|
||||||
/// Read the top of the data stack without popping (value on operand stack).
|
/// Read the top of the data stack without popping (value on operand stack).
|
||||||
fn peek(f: &mut Function) {
|
fn peek(f: &mut Function) {
|
||||||
|
guard_dsp_underflow(f, 1);
|
||||||
f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL))
|
f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL))
|
||||||
.instruction(&Instruction::I32Load(MEM4));
|
.instruction(&Instruction::I32Load(MEM4));
|
||||||
}
|
}
|
||||||
@@ -182,6 +246,13 @@ fn dsp_reload(f: &mut Function) {
|
|||||||
|
|
||||||
/// Push a value from the WASM operand stack onto the return stack via `tmp`.
|
/// Push a value from the WASM operand stack onto the return stack via `tmp`.
|
||||||
fn rpush_via_local(f: &mut Function, tmp: u32) {
|
fn rpush_via_local(f: &mut Function, tmp: u32) {
|
||||||
|
emit_guard(f, -5, |f| {
|
||||||
|
f.instruction(&Instruction::GlobalGet(RSP))
|
||||||
|
.instruction(&Instruction::I32Const(
|
||||||
|
(RETURN_STACK_BASE + CELL_SIZE) as i32,
|
||||||
|
))
|
||||||
|
.instruction(&Instruction::I32LtU);
|
||||||
|
});
|
||||||
f.instruction(&Instruction::LocalSet(tmp));
|
f.instruction(&Instruction::LocalSet(tmp));
|
||||||
// rsp -= CELL_SIZE
|
// rsp -= CELL_SIZE
|
||||||
f.instruction(&Instruction::GlobalGet(RSP))
|
f.instruction(&Instruction::GlobalGet(RSP))
|
||||||
@@ -194,8 +265,18 @@ fn rpush_via_local(f: &mut Function, tmp: u32) {
|
|||||||
.instruction(&Instruction::I32Store(MEM4));
|
.instruction(&Instruction::I32Store(MEM4));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Guard: return stack is non-empty (else throw -6).
|
||||||
|
fn guard_rsp_underflow(f: &mut Function) {
|
||||||
|
emit_guard(f, -6, |f| {
|
||||||
|
f.instruction(&Instruction::GlobalGet(RSP))
|
||||||
|
.instruction(&Instruction::I32Const(RETURN_STACK_TOP as i32))
|
||||||
|
.instruction(&Instruction::I32GeU);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Pop the return stack onto the WASM operand stack.
|
/// Pop the return stack onto the WASM operand stack.
|
||||||
fn rpop(f: &mut Function) {
|
fn rpop(f: &mut Function) {
|
||||||
|
guard_rsp_underflow(f);
|
||||||
f.instruction(&Instruction::GlobalGet(RSP))
|
f.instruction(&Instruction::GlobalGet(RSP))
|
||||||
.instruction(&Instruction::I32Load(MEM4));
|
.instruction(&Instruction::I32Load(MEM4));
|
||||||
// rsp += CELL_SIZE
|
// rsp += CELL_SIZE
|
||||||
@@ -207,6 +288,7 @@ fn rpop(f: &mut Function) {
|
|||||||
|
|
||||||
/// Peek at the top of the return stack (no pop).
|
/// Peek at the top of the return stack (no pop).
|
||||||
fn rpeek(f: &mut Function) {
|
fn rpeek(f: &mut Function) {
|
||||||
|
guard_rsp_underflow(f);
|
||||||
f.instruction(&Instruction::GlobalGet(RSP))
|
f.instruction(&Instruction::GlobalGet(RSP))
|
||||||
.instruction(&Instruction::I32Load(MEM4));
|
.instruction(&Instruction::I32Load(MEM4));
|
||||||
}
|
}
|
||||||
@@ -253,7 +335,9 @@ struct EmitCtx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Decrement the FSP global by 8 (allocate space for one f64).
|
/// Decrement the FSP global by 8 (allocate space for one f64).
|
||||||
|
/// Single choke point for float pushes: overflow guard lives here.
|
||||||
fn fsp_dec(f: &mut Function) {
|
fn fsp_dec(f: &mut Function) {
|
||||||
|
guard_fsp_overflow(f);
|
||||||
f.instruction(&Instruction::GlobalGet(FSP))
|
f.instruction(&Instruction::GlobalGet(FSP))
|
||||||
.instruction(&Instruction::I32Const(8))
|
.instruction(&Instruction::I32Const(8))
|
||||||
.instruction(&Instruction::I32Sub)
|
.instruction(&Instruction::I32Sub)
|
||||||
@@ -261,7 +345,10 @@ fn fsp_dec(f: &mut Function) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Increment the FSP global by 8 (free space for one f64).
|
/// Increment the FSP global by 8 (free space for one f64).
|
||||||
|
/// Single choke point for float pops (`FDROP` never loads the value):
|
||||||
|
/// underflow guard lives here.
|
||||||
fn fsp_inc(f: &mut Function) {
|
fn fsp_inc(f: &mut Function) {
|
||||||
|
guard_fsp_underflow(f);
|
||||||
f.instruction(&Instruction::GlobalGet(FSP))
|
f.instruction(&Instruction::GlobalGet(FSP))
|
||||||
.instruction(&Instruction::I32Const(8))
|
.instruction(&Instruction::I32Const(8))
|
||||||
.instruction(&Instruction::I32Add)
|
.instruction(&Instruction::I32Add)
|
||||||
@@ -278,6 +365,24 @@ fn fpush_via_local(f: &mut Function, tmp: u32) {
|
|||||||
.instruction(&Instruction::F64Store(MEM8));
|
.instruction(&Instruction::F64Store(MEM8));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Guard: float stack has room for one more f64 (else throw -44).
|
||||||
|
fn guard_fsp_overflow(f: &mut Function) {
|
||||||
|
emit_guard(f, -44, |f| {
|
||||||
|
f.instruction(&Instruction::GlobalGet(FSP))
|
||||||
|
.instruction(&Instruction::I32Const((FLOAT_STACK_BASE + 8) as i32))
|
||||||
|
.instruction(&Instruction::I32LtU);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Guard: float stack is non-empty (else throw -45).
|
||||||
|
fn guard_fsp_underflow(f: &mut Function) {
|
||||||
|
emit_guard(f, -45, |f| {
|
||||||
|
f.instruction(&Instruction::GlobalGet(FSP))
|
||||||
|
.instruction(&Instruction::I32Const(FLOAT_STACK_TOP as i32))
|
||||||
|
.instruction(&Instruction::I32GeU);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// Decrement FSP, then store the f64 from local `src` at [FSP].
|
/// Decrement FSP, then store the f64 from local `src` at [FSP].
|
||||||
fn fpush_from_local(f: &mut Function, src: u32) {
|
fn fpush_from_local(f: &mut Function, src: u32) {
|
||||||
fsp_dec(f);
|
fsp_dec(f);
|
||||||
@@ -295,6 +400,7 @@ fn fpop(f: &mut Function) {
|
|||||||
|
|
||||||
/// Load f64 from [FSP] onto the WASM operand stack without popping.
|
/// Load f64 from [FSP] onto the WASM operand stack without popping.
|
||||||
fn fpeek(f: &mut Function) {
|
fn fpeek(f: &mut Function) {
|
||||||
|
guard_fsp_underflow(f);
|
||||||
f.instruction(&Instruction::GlobalGet(FSP))
|
f.instruction(&Instruction::GlobalGet(FSP))
|
||||||
.instruction(&Instruction::F64Load(MEM8));
|
.instruction(&Instruction::F64Load(MEM8));
|
||||||
}
|
}
|
||||||
@@ -796,6 +902,8 @@ fn emit_op(f: &mut Function, op: &IrOp, ctx: &mut EmitCtx) {
|
|||||||
// -- Compound operations -----------------------------------------------
|
// -- Compound operations -----------------------------------------------
|
||||||
IrOp::TwoDup => {
|
IrOp::TwoDup => {
|
||||||
// ( a b -- a b a b )
|
// ( a b -- a b a b )
|
||||||
|
guard_dsp_underflow(f, 2);
|
||||||
|
guard_dsp_overflow(f, 2);
|
||||||
f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL))
|
f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL))
|
||||||
.instruction(&Instruction::I32Load(MEM4)); // b
|
.instruction(&Instruction::I32Load(MEM4)); // b
|
||||||
f.instruction(&Instruction::LocalSet(SCRATCH_BASE));
|
f.instruction(&Instruction::LocalSet(SCRATCH_BASE));
|
||||||
@@ -822,6 +930,7 @@ fn emit_op(f: &mut Function, op: &IrOp, ctx: &mut EmitCtx) {
|
|||||||
|
|
||||||
IrOp::TwoDrop => {
|
IrOp::TwoDrop => {
|
||||||
// ( a b -- )
|
// ( a b -- )
|
||||||
|
guard_dsp_underflow(f, 2);
|
||||||
f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL))
|
f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL))
|
||||||
.instruction(&Instruction::I32Const((CELL_SIZE * 2) as i32))
|
.instruction(&Instruction::I32Const((CELL_SIZE * 2) as i32))
|
||||||
.instruction(&Instruction::I32Add)
|
.instruction(&Instruction::I32Add)
|
||||||
@@ -1545,6 +1654,11 @@ impl StackSim {
|
|||||||
/// Emit the promoted prologue: load `preload` items from the memory stack
|
/// Emit the promoted prologue: load `preload` items from the memory stack
|
||||||
/// into WASM locals.
|
/// into WASM locals.
|
||||||
fn emit_promoted_prologue(f: &mut Function, preload: u32, sim: &mut StackSim) {
|
fn emit_promoted_prologue(f: &mut Function, preload: u32, sim: &mut StackSim) {
|
||||||
|
// One entry check covers the whole promoted word: the caller must
|
||||||
|
// have at least `preload` cells on the data stack.
|
||||||
|
if preload > 0 {
|
||||||
|
guard_dsp_underflow(f, preload);
|
||||||
|
}
|
||||||
// Load items: mem[dsp] = top of stack, mem[dsp+4] = second, etc.
|
// Load items: mem[dsp] = top of stack, mem[dsp+4] = second, etc.
|
||||||
// We load them top-first, then reverse the sim stack so that
|
// We load them top-first, then reverse the sim stack so that
|
||||||
// sim.stack[0] = deepest loaded, sim.stack[last] = top.
|
// sim.stack[0] = deepest loaded, sim.stack[last] = top.
|
||||||
@@ -1575,6 +1689,7 @@ fn emit_promoted_prologue(f: &mut Function, preload: u32, sim: &mut StackSim) {
|
|||||||
fn emit_promoted_epilogue(f: &mut Function, sim: &mut StackSim) {
|
fn emit_promoted_epilogue(f: &mut Function, sim: &mut StackSim) {
|
||||||
let remaining = sim.stack.len() as u32;
|
let remaining = sim.stack.len() as u32;
|
||||||
if remaining > 0 {
|
if remaining > 0 {
|
||||||
|
guard_dsp_overflow(f, remaining);
|
||||||
// Decrement cached DSP for the items we're pushing back
|
// Decrement cached DSP for the items we're pushing back
|
||||||
f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL));
|
f.instruction(&Instruction::LocalGet(CACHED_DSP_LOCAL));
|
||||||
f.instruction(&Instruction::I32Const((remaining * CELL_SIZE) as i32));
|
f.instruction(&Instruction::I32Const((remaining * CELL_SIZE) as i32));
|
||||||
@@ -2407,6 +2522,9 @@ pub fn compile_word(
|
|||||||
body: &[IrOp],
|
body: &[IrOp],
|
||||||
config: &CodegenConfig,
|
config: &CodegenConfig,
|
||||||
) -> WaferResult<CompiledModule> {
|
) -> WaferResult<CompiledModule> {
|
||||||
|
// Arm (or disarm) stack-guard emission for this compilation.
|
||||||
|
GUARD_FAULT.set(config.stack_guards);
|
||||||
|
|
||||||
let mut module = Module::new();
|
let mut module = Module::new();
|
||||||
|
|
||||||
// -- Type section --
|
// -- Type section --
|
||||||
@@ -2871,8 +2989,9 @@ pub fn compile_consolidated_module(
|
|||||||
words: &[(WordId, Vec<IrOp>)],
|
words: &[(WordId, Vec<IrOp>)],
|
||||||
local_fn_map: &HashMap<WordId, u32>,
|
local_fn_map: &HashMap<WordId, u32>,
|
||||||
table_size: u32,
|
table_size: u32,
|
||||||
|
stack_guards: Option<u32>,
|
||||||
) -> WaferResult<Vec<u8>> {
|
) -> WaferResult<Vec<u8>> {
|
||||||
compile_multi_word_module(words, local_fn_map, table_size, None)
|
compile_multi_word_module(words, local_fn_map, table_size, None, stack_guards)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compile an exportable WASM module with embedded memory and metadata.
|
/// Compile an exportable WASM module with embedded memory and metadata.
|
||||||
@@ -2885,8 +3004,9 @@ pub fn compile_exportable_module(
|
|||||||
local_fn_map: &HashMap<WordId, u32>,
|
local_fn_map: &HashMap<WordId, u32>,
|
||||||
table_size: u32,
|
table_size: u32,
|
||||||
export: &ExportSections<'_>,
|
export: &ExportSections<'_>,
|
||||||
|
stack_guards: Option<u32>,
|
||||||
) -> WaferResult<Vec<u8>> {
|
) -> WaferResult<Vec<u8>> {
|
||||||
compile_multi_word_module(words, local_fn_map, table_size, Some(export))
|
compile_multi_word_module(words, local_fn_map, table_size, Some(export), stack_guards)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Internal: build a multi-word WASM module. When `export` is `Some`, adds
|
/// Internal: build a multi-word WASM module. When `export` is `Some`, adds
|
||||||
@@ -2896,7 +3016,11 @@ fn compile_multi_word_module(
|
|||||||
local_fn_map: &HashMap<WordId, u32>,
|
local_fn_map: &HashMap<WordId, u32>,
|
||||||
table_size: u32,
|
table_size: u32,
|
||||||
export: Option<&ExportSections<'_>>,
|
export: Option<&ExportSections<'_>>,
|
||||||
|
stack_guards: Option<u32>,
|
||||||
) -> WaferResult<Vec<u8>> {
|
) -> WaferResult<Vec<u8>> {
|
||||||
|
// Arm (or disarm) stack-guard emission for this module.
|
||||||
|
GUARD_FAULT.set(stack_guards);
|
||||||
|
|
||||||
let has_data = export.is_some_and(|e| !e.memory_snapshot.is_empty());
|
let has_data = export.is_some_and(|e| !e.memory_snapshot.is_empty());
|
||||||
let mut module = Module::new();
|
let mut module = Module::new();
|
||||||
|
|
||||||
@@ -3125,6 +3249,7 @@ mod tests {
|
|||||||
base_fn_index: 0,
|
base_fn_index: 0,
|
||||||
table_size: 16,
|
table_size: 16,
|
||||||
stack_to_local_promotion: true,
|
stack_to_local_promotion: true,
|
||||||
|
stack_guards: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3349,6 +3474,7 @@ mod tests {
|
|||||||
base_fn_index: 7,
|
base_fn_index: 7,
|
||||||
table_size: 16,
|
table_size: 16,
|
||||||
stack_to_local_promotion: true,
|
stack_to_local_promotion: true,
|
||||||
|
stack_guards: None,
|
||||||
};
|
};
|
||||||
let m = compile_word("t", &[IrOp::PushI32(1)], &cfg).unwrap();
|
let m = compile_word("t", &[IrOp::PushI32(1)], &cfg).unwrap();
|
||||||
assert_eq!(m.fn_index, 7);
|
assert_eq!(m.fn_index, 7);
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ use crate::optimizer::OptConfig;
|
|||||||
pub struct CodegenOpts {
|
pub struct CodegenOpts {
|
||||||
/// Enable stack-to-local promotion for straight-line words.
|
/// Enable stack-to-local promotion for straight-line words.
|
||||||
pub stack_to_local_promotion: bool,
|
pub stack_to_local_promotion: bool,
|
||||||
|
/// Emit stack under/overflow guards in compiled words. Faults throw
|
||||||
|
/// standard codes (-3/-4/-5/-6/-44/-45) instead of silently
|
||||||
|
/// corrupting stack pointers. On by default; benchmarks and
|
||||||
|
/// exported production modules turn it off.
|
||||||
|
pub stack_guards: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Master configuration for all WAFER optimizations.
|
/// Master configuration for all WAFER optimizations.
|
||||||
@@ -32,6 +37,7 @@ impl WaferConfig {
|
|||||||
},
|
},
|
||||||
codegen: CodegenOpts {
|
codegen: CodegenOpts {
|
||||||
stack_to_local_promotion: true,
|
stack_to_local_promotion: true,
|
||||||
|
stack_guards: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,6 +55,7 @@ impl WaferConfig {
|
|||||||
},
|
},
|
||||||
codegen: CodegenOpts {
|
codegen: CodegenOpts {
|
||||||
stack_to_local_promotion: false,
|
stack_to_local_promotion: false,
|
||||||
|
stack_guards: 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);
|
let result = compile_consolidated_module(&words, &map, 16, None);
|
||||||
// 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);
|
let result = compile_consolidated_module(&words, &map, 16, None);
|
||||||
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);
|
let result = compile_consolidated_module(&words, &map, 16, None);
|
||||||
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);
|
let result = compile_consolidated_module(&words, &map, 256, None);
|
||||||
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);
|
let result = compile_consolidated_module(&words, &map, 16, None);
|
||||||
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);
|
let result = compile_consolidated_module(&words, &map, 16, None);
|
||||||
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);
|
let result = compile_consolidated_module(&words, &map, 16, None);
|
||||||
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);
|
let result = compile_consolidated_module(&words, &map, 16, None);
|
||||||
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);
|
let result = compile_consolidated_module(&words, &map, 16, None);
|
||||||
assert!(result.is_ok());
|
assert!(result.is_ok());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,8 +120,14 @@ pub fn export_module(
|
|||||||
metadata_json: metadata_json.as_bytes(),
|
metadata_json: metadata_json.as_bytes(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let wasm_bytes = compile_exportable_module(&words, &local_fn_map, table_size, &export_sections)
|
let wasm_bytes = compile_exportable_module(
|
||||||
.map_err(|e| anyhow::anyhow!("export codegen error: {e}"))?;
|
&words,
|
||||||
|
&local_fn_map,
|
||||||
|
table_size,
|
||||||
|
&export_sections,
|
||||||
|
vm.stack_guard_param(),
|
||||||
|
)
|
||||||
|
.map_err(|e| anyhow::anyhow!("export codegen error: {e}"))?;
|
||||||
|
|
||||||
Ok((wasm_bytes, metadata))
|
Ok((wasm_bytes, metadata))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ pub const SYSVAR_NUM_TIB: u32 = SYSVAR_BASE + 24;
|
|||||||
pub const SYSVAR_HLD: u32 = SYSVAR_BASE + 28;
|
pub const SYSVAR_HLD: u32 = SYSVAR_BASE + 28;
|
||||||
/// LEAVE flag: nonzero when LEAVE has been called inside a DO loop.
|
/// LEAVE flag: nonzero when LEAVE has been called inside a DO loop.
|
||||||
pub const SYSVAR_LEAVE_FLAG: u32 = SYSVAR_BASE + 32;
|
pub const SYSVAR_LEAVE_FLAG: u32 = SYSVAR_BASE + 32;
|
||||||
|
/// Throw code left by a compiled stack-guard fault for `_STACK_FAULT_`.
|
||||||
|
pub const SYSVAR_FAULT_CODE: u32 = SYSVAR_BASE + 36;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|||||||
+144
-121
@@ -24,8 +24,8 @@ use crate::ir::IrOp;
|
|||||||
use crate::memory::HASH_SCRATCH_BASE;
|
use crate::memory::HASH_SCRATCH_BASE;
|
||||||
use crate::memory::{
|
use crate::memory::{
|
||||||
CELL_SIZE, DATA_STACK_TOP, FLOAT_SIZE, FLOAT_STACK_BASE, FLOAT_STACK_TOP, INPUT_BUFFER_BASE,
|
CELL_SIZE, DATA_STACK_TOP, FLOAT_SIZE, FLOAT_STACK_BASE, FLOAT_STACK_TOP, INPUT_BUFFER_BASE,
|
||||||
INPUT_BUFFER_SIZE, RETURN_STACK_TOP, SYSVAR_BASE_VAR, SYSVAR_HERE, SYSVAR_LEAVE_FLAG,
|
INPUT_BUFFER_SIZE, RETURN_STACK_TOP, SYSVAR_BASE_VAR, SYSVAR_FAULT_CODE, SYSVAR_HERE,
|
||||||
SYSVAR_NUM_TIB, SYSVAR_STATE, SYSVAR_TO_IN,
|
SYSVAR_LEAVE_FLAG, SYSVAR_NUM_TIB, SYSVAR_STATE, SYSVAR_TO_IN,
|
||||||
};
|
};
|
||||||
use crate::optimizer::optimize;
|
use crate::optimizer::optimize;
|
||||||
|
|
||||||
@@ -270,6 +270,8 @@ pub struct ForthVM<R: Runtime> {
|
|||||||
marker_states: HashMap<u32, MarkerState>,
|
marker_states: HashMap<u32, MarkerState>,
|
||||||
/// EMPTY's rollback target: boot state, or wherever GILD re-baselined.
|
/// EMPTY's rollback target: boot state, or wherever GILD re-baselined.
|
||||||
gild_state: Option<Box<MarkerState>>,
|
gild_state: Option<Box<MarkerState>>,
|
||||||
|
/// Table index of `_STACK_FAULT_` (compiled stack-guard target).
|
||||||
|
stack_fault_id: u32,
|
||||||
/// Pending MARKER restore: after a marker word executes, restore this state
|
/// Pending MARKER restore: after a marker word executes, restore this state
|
||||||
pending_marker_restore: Arc<Mutex<Option<u32>>>,
|
pending_marker_restore: Arc<Mutex<Option<u32>>>,
|
||||||
/// Conditional compilation skip depth: >0 means we're skipping tokens for [IF]/[ELSE]
|
/// Conditional compilation skip depth: >0 means we're skipping tokens for [IF]/[ELSE]
|
||||||
@@ -350,6 +352,7 @@ fn throw_message(code: i32) -> Option<&'static str> {
|
|||||||
-31 => "Word not defined by CREATE",
|
-31 => "Word not defined by CREATE",
|
||||||
-42 => "Floating-point divide by zero",
|
-42 => "Floating-point divide by zero",
|
||||||
-43 => "Floating-point result out of range",
|
-43 => "Floating-point result out of range",
|
||||||
|
-44 => "Floating-point stack overflow",
|
||||||
-45 => "Floating-point stack underflow",
|
-45 => "Floating-point stack underflow",
|
||||||
-56 => "QUIT",
|
-56 => "QUIT",
|
||||||
_ => return None,
|
_ => return None,
|
||||||
@@ -469,6 +472,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
recording_toplevel: false,
|
recording_toplevel: false,
|
||||||
marker_states: HashMap::new(),
|
marker_states: HashMap::new(),
|
||||||
gild_state: None,
|
gild_state: None,
|
||||||
|
stack_fault_id: 0,
|
||||||
pending_marker_restore: Arc::new(Mutex::new(None)),
|
pending_marker_restore: Arc::new(Mutex::new(None)),
|
||||||
conditional_skip_depth: 0,
|
conditional_skip_depth: 0,
|
||||||
next_block_label: 0,
|
next_block_label: 0,
|
||||||
@@ -2298,11 +2302,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
self.ir_bodies.insert(word_id, ir.clone());
|
self.ir_bodies.insert(word_id, ir.clone());
|
||||||
|
|
||||||
// Compile to WASM
|
// Compile to WASM
|
||||||
let config = CodegenConfig {
|
let config = self.codegen_config(word_id.0);
|
||||||
base_fn_index: word_id.0,
|
|
||||||
table_size: self.table_size(),
|
|
||||||
stack_to_local_promotion: self.config.codegen.stack_to_local_promotion,
|
|
||||||
};
|
|
||||||
let compiled =
|
let compiled =
|
||||||
compile_word(&name, &ir, &config).map_err(|e| anyhow::anyhow!("codegen error: {e}"))?;
|
compile_word(&name, &ir, &config).map_err(|e| anyhow::anyhow!("codegen error: {e}"))?;
|
||||||
|
|
||||||
@@ -2361,8 +2361,13 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
let table_size = self.table_size();
|
let table_size = self.table_size();
|
||||||
|
|
||||||
// Compile the consolidated module
|
// Compile the consolidated module
|
||||||
let module_bytes = compile_consolidated_module(&words, &local_fn_map, table_size)
|
let module_bytes = compile_consolidated_module(
|
||||||
.map_err(|e| anyhow::anyhow!("consolidation codegen error: {e}"))?;
|
&words,
|
||||||
|
&local_fn_map,
|
||||||
|
table_size,
|
||||||
|
self.stack_guard_param(),
|
||||||
|
)
|
||||||
|
.map_err(|e| anyhow::anyhow!("consolidation codegen error: {e}"))?;
|
||||||
|
|
||||||
// Instantiate: the element section in the module handles table placement
|
// Instantiate: the element section in the module handles table placement
|
||||||
// We use fn_index=0 since the element section has the correct offsets
|
// We use fn_index=0 since the element section has the correct offsets
|
||||||
@@ -2386,8 +2391,13 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
self.ensure_table_size(self.next_table_index)?;
|
self.ensure_table_size(self.next_table_index)?;
|
||||||
let table_size = self.table_size();
|
let table_size = self.table_size();
|
||||||
|
|
||||||
let module_bytes = compile_consolidated_module(&words, &local_fn_map, table_size)
|
let module_bytes = compile_consolidated_module(
|
||||||
.map_err(|e| anyhow::anyhow!("batch compile error: {e}"))?;
|
&words,
|
||||||
|
&local_fn_map,
|
||||||
|
table_size,
|
||||||
|
self.stack_guard_param(),
|
||||||
|
)
|
||||||
|
.map_err(|e| anyhow::anyhow!("batch compile error: {e}"))?;
|
||||||
|
|
||||||
self.total_module_bytes += module_bytes.len() as u64;
|
self.total_module_bytes += module_bytes.len() as u64;
|
||||||
// Instantiate: the element section in the module handles table placement
|
// Instantiate: the element section in the module handles table placement
|
||||||
@@ -2644,6 +2654,24 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
// Primitive registration
|
// Primitive registration
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// `_STACK_FAULT_` table index when stack guards are enabled.
|
||||||
|
pub(crate) fn stack_guard_param(&self) -> Option<u32> {
|
||||||
|
self.config
|
||||||
|
.codegen
|
||||||
|
.stack_guards
|
||||||
|
.then_some(self.stack_fault_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Codegen configuration for compiling one word.
|
||||||
|
fn codegen_config(&mut self, base_fn_index: u32) -> CodegenConfig {
|
||||||
|
CodegenConfig {
|
||||||
|
base_fn_index,
|
||||||
|
table_size: self.table_size(),
|
||||||
|
stack_to_local_promotion: self.config.codegen.stack_to_local_promotion,
|
||||||
|
stack_guards: self.stack_guard_param(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Register a primitive word by compiling its IR body and installing it.
|
/// Register a primitive word by compiling its IR body and installing it.
|
||||||
fn register_primitive(
|
fn register_primitive(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -2666,11 +2694,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
// Defer WASM compilation for batch processing
|
// Defer WASM compilation for batch processing
|
||||||
self.deferred_ir.push((word_id, ir_body));
|
self.deferred_ir.push((word_id, ir_body));
|
||||||
} else {
|
} else {
|
||||||
let config = CodegenConfig {
|
let config = self.codegen_config(word_id.0);
|
||||||
base_fn_index: word_id.0,
|
|
||||||
table_size: self.table_size(),
|
|
||||||
stack_to_local_promotion: self.config.codegen.stack_to_local_promotion,
|
|
||||||
};
|
|
||||||
let compiled = compile_word(name, &ir_body, &config)
|
let compiled = compile_word(name, &ir_body, &config)
|
||||||
.map_err(|e| anyhow::anyhow!("codegen error for {name}: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("codegen error for {name}: {e}"))?;
|
||||||
self.instantiate_and_install(&compiled, word_id)?;
|
self.instantiate_and_install(&compiled, word_id)?;
|
||||||
@@ -2709,6 +2733,19 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
fn register_primitives(&mut self) -> anyhow::Result<()> {
|
fn register_primitives(&mut self) -> anyhow::Result<()> {
|
||||||
self.batch_mode = true;
|
self.batch_mode = true;
|
||||||
|
|
||||||
|
// _STACK_FAULT_ must exist before any guarded word is compiled:
|
||||||
|
// compiled guards write a throw code to SYSVAR_FAULT_CODE and
|
||||||
|
// call this word, which converts it into a Forth THROW.
|
||||||
|
let throw_code = Arc::clone(&self.throw_code);
|
||||||
|
let func: HostFn = Box::new(move |ctx: &mut dyn HostAccess| {
|
||||||
|
let code = ctx.mem_read_i32(SYSVAR_FAULT_CODE);
|
||||||
|
*throw_code.lock().unwrap() = Some(code);
|
||||||
|
Err(anyhow::anyhow!("forth-throw"))
|
||||||
|
});
|
||||||
|
self.stack_fault_id = self
|
||||||
|
.register_host_primitive("_STACK_FAULT_", false, func)?
|
||||||
|
.0;
|
||||||
|
|
||||||
// -- Stack manipulation --
|
// -- Stack manipulation --
|
||||||
self.register_primitive("DUP", false, vec![IrOp::Dup])?;
|
self.register_primitive("DUP", false, vec![IrOp::Dup])?;
|
||||||
self.register_primitive("DROP", false, vec![IrOp::Drop])?;
|
self.register_primitive("DROP", false, vec![IrOp::Drop])?;
|
||||||
@@ -3192,11 +3229,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
// Compile a tiny word that pushes the variable's address
|
// Compile a tiny word that pushes the variable's address
|
||||||
let ir_body = vec![IrOp::PushI32(var_addr as i32)];
|
let ir_body = vec![IrOp::PushI32(var_addr as i32)];
|
||||||
self.ir_bodies.insert(word_id, ir_body.clone());
|
self.ir_bodies.insert(word_id, ir_body.clone());
|
||||||
let config = CodegenConfig {
|
let config = self.codegen_config(word_id.0);
|
||||||
base_fn_index: word_id.0,
|
|
||||||
table_size: self.table_size(),
|
|
||||||
stack_to_local_promotion: self.config.codegen.stack_to_local_promotion,
|
|
||||||
};
|
|
||||||
let compiled = compile_word(&name, &ir_body, &config)
|
let compiled = compile_word(&name, &ir_body, &config)
|
||||||
.map_err(|e| anyhow::anyhow!("codegen error for VARIABLE {name}: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("codegen error for VARIABLE {name}: {e}"))?;
|
||||||
|
|
||||||
@@ -3224,11 +3257,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
// Compile a word that pushes the constant value
|
// Compile a word that pushes the constant value
|
||||||
let ir_body = vec![IrOp::PushI32(value)];
|
let ir_body = vec![IrOp::PushI32(value)];
|
||||||
self.ir_bodies.insert(word_id, ir_body.clone());
|
self.ir_bodies.insert(word_id, ir_body.clone());
|
||||||
let config = CodegenConfig {
|
let config = self.codegen_config(word_id.0);
|
||||||
base_fn_index: word_id.0,
|
|
||||||
table_size: self.table_size(),
|
|
||||||
stack_to_local_promotion: self.config.codegen.stack_to_local_promotion,
|
|
||||||
};
|
|
||||||
let compiled = compile_word(&name, &ir_body, &config)
|
let compiled = compile_word(&name, &ir_body, &config)
|
||||||
.map_err(|e| anyhow::anyhow!("codegen error for CONSTANT {name}: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("codegen error for CONSTANT {name}: {e}"))?;
|
||||||
|
|
||||||
@@ -3261,11 +3290,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
// Compile a word that pushes the pfa
|
// Compile a word that pushes the pfa
|
||||||
let ir_body = vec![IrOp::PushI32(pfa as i32)];
|
let ir_body = vec![IrOp::PushI32(pfa as i32)];
|
||||||
self.ir_bodies.insert(word_id, ir_body.clone());
|
self.ir_bodies.insert(word_id, ir_body.clone());
|
||||||
let config = CodegenConfig {
|
let config = self.codegen_config(word_id.0);
|
||||||
base_fn_index: word_id.0,
|
|
||||||
table_size: self.table_size(),
|
|
||||||
stack_to_local_promotion: self.config.codegen.stack_to_local_promotion,
|
|
||||||
};
|
|
||||||
let compiled = compile_word(&name, &ir_body, &config)
|
let compiled = compile_word(&name, &ir_body, &config)
|
||||||
.map_err(|e| anyhow::anyhow!("codegen error for CREATE {name}: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("codegen error for CREATE {name}: {e}"))?;
|
||||||
|
|
||||||
@@ -3307,11 +3332,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
// Compile a word that fetches from the value's address
|
// Compile a word that fetches from the value's address
|
||||||
let ir_body = vec![IrOp::PushI32(val_addr as i32), IrOp::Fetch];
|
let ir_body = vec![IrOp::PushI32(val_addr as i32), IrOp::Fetch];
|
||||||
self.ir_bodies.insert(word_id, ir_body.clone());
|
self.ir_bodies.insert(word_id, ir_body.clone());
|
||||||
let config = CodegenConfig {
|
let config = self.codegen_config(word_id.0);
|
||||||
base_fn_index: word_id.0,
|
|
||||||
table_size: self.table_size(),
|
|
||||||
stack_to_local_promotion: self.config.codegen.stack_to_local_promotion,
|
|
||||||
};
|
|
||||||
let compiled = compile_word(&name, &ir_body, &config)
|
let compiled = compile_word(&name, &ir_body, &config)
|
||||||
.map_err(|e| anyhow::anyhow!("codegen error for VALUE {name}: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("codegen error for VALUE {name}: {e}"))?;
|
||||||
|
|
||||||
@@ -3349,11 +3370,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
// Compile a word that fetches the xt and executes it
|
// Compile a word that fetches the xt and executes it
|
||||||
let ir_body = vec![IrOp::PushI32(defer_addr as i32), IrOp::Fetch, IrOp::Execute];
|
let ir_body = vec![IrOp::PushI32(defer_addr as i32), IrOp::Fetch, IrOp::Execute];
|
||||||
self.ir_bodies.insert(word_id, ir_body.clone());
|
self.ir_bodies.insert(word_id, ir_body.clone());
|
||||||
let config = CodegenConfig {
|
let config = self.codegen_config(word_id.0);
|
||||||
base_fn_index: word_id.0,
|
|
||||||
table_size: self.table_size(),
|
|
||||||
stack_to_local_promotion: self.config.codegen.stack_to_local_promotion,
|
|
||||||
};
|
|
||||||
let compiled = compile_word(&name, &ir_body, &config)
|
let compiled = compile_word(&name, &ir_body, &config)
|
||||||
.map_err(|e| anyhow::anyhow!("codegen error for DEFER {name}: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("codegen error for DEFER {name}: {e}"))?;
|
||||||
|
|
||||||
@@ -3386,11 +3403,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
|
|
||||||
let ir_body = vec![IrOp::Call(word_id)];
|
let ir_body = vec![IrOp::Call(word_id)];
|
||||||
self.ir_bodies.insert(new_word_id, ir_body.clone());
|
self.ir_bodies.insert(new_word_id, ir_body.clone());
|
||||||
let config = CodegenConfig {
|
let config = self.codegen_config(new_word_id.0);
|
||||||
base_fn_index: new_word_id.0,
|
|
||||||
table_size: self.table_size(),
|
|
||||||
stack_to_local_promotion: self.config.codegen.stack_to_local_promotion,
|
|
||||||
};
|
|
||||||
let compiled = compile_word(&new_name, &ir_body, &config)
|
let compiled = compile_word(&new_name, &ir_body, &config)
|
||||||
.map_err(|e| anyhow::anyhow!("codegen error for SYNONYM: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("codegen error for SYNONYM: {e}"))?;
|
||||||
self.instantiate_and_install(&compiled, new_word_id)?;
|
self.instantiate_and_install(&compiled, new_word_id)?;
|
||||||
@@ -3438,11 +3451,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
// Compile a word that pushes the buffer address
|
// Compile a word that pushes the buffer address
|
||||||
let ir_body = vec![IrOp::PushI32(buf_addr as i32)];
|
let ir_body = vec![IrOp::PushI32(buf_addr as i32)];
|
||||||
self.ir_bodies.insert(word_id, ir_body.clone());
|
self.ir_bodies.insert(word_id, ir_body.clone());
|
||||||
let config = CodegenConfig {
|
let config = self.codegen_config(word_id.0);
|
||||||
base_fn_index: word_id.0,
|
|
||||||
table_size: self.table_size(),
|
|
||||||
stack_to_local_promotion: self.config.codegen.stack_to_local_promotion,
|
|
||||||
};
|
|
||||||
let compiled = compile_word(&name, &ir_body, &config)
|
let compiled = compile_word(&name, &ir_body, &config)
|
||||||
.map_err(|e| anyhow::anyhow!("codegen error for BUFFER: {name}: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("codegen error for BUFFER: {name}: {e}"))?;
|
||||||
|
|
||||||
@@ -3530,11 +3539,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
.ok_or_else(|| anyhow::anyhow!("_MARKER_RESTORE_ not found"))?;
|
.ok_or_else(|| anyhow::anyhow!("_MARKER_RESTORE_ not found"))?;
|
||||||
let ir_body = vec![IrOp::PushI32(word_id.0 as i32), IrOp::Call(restore_id)];
|
let ir_body = vec![IrOp::PushI32(word_id.0 as i32), IrOp::Call(restore_id)];
|
||||||
self.ir_bodies.insert(word_id, ir_body.clone());
|
self.ir_bodies.insert(word_id, ir_body.clone());
|
||||||
let config = CodegenConfig {
|
let config = self.codegen_config(word_id.0);
|
||||||
base_fn_index: word_id.0,
|
|
||||||
table_size: self.table_size(),
|
|
||||||
stack_to_local_promotion: self.config.codegen.stack_to_local_promotion,
|
|
||||||
};
|
|
||||||
let compiled = compile_word(&name, &ir_body, &config)
|
let compiled = compile_word(&name, &ir_body, &config)
|
||||||
.map_err(|e| anyhow::anyhow!("codegen error for MARKER {name}: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("codegen error for MARKER {name}: {e}"))?;
|
||||||
|
|
||||||
@@ -4417,11 +4422,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
let word_id = WordId(fn_index);
|
let word_id = WordId(fn_index);
|
||||||
|
|
||||||
// Compile and replace
|
// Compile and replace
|
||||||
let config = CodegenConfig {
|
let config = self.codegen_config(word_id.0);
|
||||||
base_fn_index: word_id.0,
|
|
||||||
table_size: self.table_size(),
|
|
||||||
stack_to_local_promotion: self.config.codegen.stack_to_local_promotion,
|
|
||||||
};
|
|
||||||
let name = self
|
let name = self
|
||||||
.dictionary
|
.dictionary
|
||||||
.word_name(latest)
|
.word_name(latest)
|
||||||
@@ -4511,11 +4512,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let second_ir = std::mem::take(&mut self.compiling_ir);
|
let second_ir = std::mem::take(&mut self.compiling_ir);
|
||||||
let config = CodegenConfig {
|
let config = self.codegen_config(second_word_id.0);
|
||||||
base_fn_index: second_word_id.0,
|
|
||||||
table_size: self.table_size(),
|
|
||||||
stack_to_local_promotion: self.config.codegen.stack_to_local_promotion,
|
|
||||||
};
|
|
||||||
let compiled = compile_word("_does_action2_", &second_ir, &config)
|
let compiled = compile_word("_does_action2_", &second_ir, &config)
|
||||||
.map_err(|e| anyhow::anyhow!("codegen error for DOES> body 2: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("codegen error for DOES> body 2: {e}"))?;
|
||||||
self.instantiate_and_install(&compiled, second_word_id)?;
|
self.instantiate_and_install(&compiled, second_word_id)?;
|
||||||
@@ -4573,11 +4570,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let does_ir = std::mem::take(&mut self.compiling_ir);
|
let does_ir = std::mem::take(&mut self.compiling_ir);
|
||||||
let config = CodegenConfig {
|
let config = self.codegen_config(does_word_id.0);
|
||||||
base_fn_index: does_word_id.0,
|
|
||||||
table_size: self.table_size(),
|
|
||||||
stack_to_local_promotion: self.config.codegen.stack_to_local_promotion,
|
|
||||||
};
|
|
||||||
let compiled = compile_word("_does_action_", &does_ir, &config)
|
let compiled = compile_word("_does_action_", &does_ir, &config)
|
||||||
.map_err(|e| anyhow::anyhow!("codegen error for DOES> body: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("codegen error for DOES> body: {e}"))?;
|
||||||
self.instantiate_and_install(&compiled, does_word_id)?;
|
self.instantiate_and_install(&compiled, does_word_id)?;
|
||||||
@@ -4603,11 +4596,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
|
|
||||||
// Compile the defining word as a no-op (the actual work is done
|
// Compile the defining word as a no-op (the actual work is done
|
||||||
// by the outer interpreter when it detects the does-definition).
|
// by the outer interpreter when it detects the does-definition).
|
||||||
let config = CodegenConfig {
|
let config = self.codegen_config(defining_word_id.0);
|
||||||
base_fn_index: defining_word_id.0,
|
|
||||||
table_size: self.table_size(),
|
|
||||||
stack_to_local_promotion: self.config.codegen.stack_to_local_promotion,
|
|
||||||
};
|
|
||||||
let compiled = compile_word(&defining_name, &[], &config)
|
let compiled = compile_word(&defining_name, &[], &config)
|
||||||
.map_err(|e| anyhow::anyhow!("codegen error for defining word: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("codegen error for defining word: {e}"))?;
|
||||||
self.instantiate_and_install(&compiled, defining_word_id)?;
|
self.instantiate_and_install(&compiled, defining_word_id)?;
|
||||||
@@ -4664,11 +4653,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
// Temporarily install a "push PFA" word (will be patched later)
|
// Temporarily install a "push PFA" word (will be patched later)
|
||||||
let ir_body = vec![IrOp::PushI32(pfa as i32)];
|
let ir_body = vec![IrOp::PushI32(pfa as i32)];
|
||||||
self.ir_bodies.insert(new_word_id, ir_body.clone());
|
self.ir_bodies.insert(new_word_id, ir_body.clone());
|
||||||
let config = CodegenConfig {
|
let config = self.codegen_config(new_word_id.0);
|
||||||
base_fn_index: new_word_id.0,
|
|
||||||
table_size: self.table_size(),
|
|
||||||
stack_to_local_promotion: self.config.codegen.stack_to_local_promotion,
|
|
||||||
};
|
|
||||||
let compiled = compile_word(&name, &ir_body, &config)
|
let compiled = compile_word(&name, &ir_body, &config)
|
||||||
.map_err(|e| anyhow::anyhow!("codegen: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("codegen: {e}"))?;
|
||||||
self.instantiate_and_install(&compiled, new_word_id)?;
|
self.instantiate_and_install(&compiled, new_word_id)?;
|
||||||
@@ -4687,11 +4672,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
let tmp_word_id = WordId(tmp_fn_idx);
|
let tmp_word_id = WordId(tmp_fn_idx);
|
||||||
self.next_table_index = self.next_table_index.max(tmp_fn_idx + 1);
|
self.next_table_index = self.next_table_index.max(tmp_fn_idx + 1);
|
||||||
|
|
||||||
let config = CodegenConfig {
|
let config = self.codegen_config(tmp_word_id.0);
|
||||||
base_fn_index: tmp_word_id.0,
|
|
||||||
table_size: self.table_size(),
|
|
||||||
stack_to_local_promotion: self.config.codegen.stack_to_local_promotion,
|
|
||||||
};
|
|
||||||
let compiled = compile_word("_create_part_", &create_ir, &config)
|
let compiled = compile_word("_create_part_", &create_ir, &config)
|
||||||
.map_err(|e| anyhow::anyhow!("codegen: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("codegen: {e}"))?;
|
||||||
self.instantiate_and_install(&compiled, tmp_word_id)?;
|
self.instantiate_and_install(&compiled, tmp_word_id)?;
|
||||||
@@ -4700,11 +4681,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
// Step 4: Patch the new word to push PFA and call does-action
|
// Step 4: Patch the new word to push PFA and call does-action
|
||||||
self.refresh_user_here();
|
self.refresh_user_here();
|
||||||
let patched_ir = vec![IrOp::PushI32(pfa as i32), IrOp::Call(does_action_id)];
|
let patched_ir = vec![IrOp::PushI32(pfa as i32), IrOp::Call(does_action_id)];
|
||||||
let config = CodegenConfig {
|
let config = self.codegen_config(new_word_id.0);
|
||||||
base_fn_index: new_word_id.0,
|
|
||||||
table_size: self.table_size(),
|
|
||||||
stack_to_local_promotion: self.config.codegen.stack_to_local_promotion,
|
|
||||||
};
|
|
||||||
let compiled = compile_word(&name, &patched_ir, &config)
|
let compiled = compile_word(&name, &patched_ir, &config)
|
||||||
.map_err(|e| anyhow::anyhow!("DOES> patch codegen: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("DOES> patch codegen: {e}"))?;
|
||||||
self.instantiate_and_install(&compiled, new_word_id)?;
|
self.instantiate_and_install(&compiled, new_word_id)?;
|
||||||
@@ -4728,11 +4705,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||||
|
|
||||||
let patched_ir = vec![IrOp::PushI32(pfa as i32), IrOp::Call(does_action_id)];
|
let patched_ir = vec![IrOp::PushI32(pfa as i32), IrOp::Call(does_action_id)];
|
||||||
let config = CodegenConfig {
|
let config = self.codegen_config(target_word_id.0);
|
||||||
base_fn_index: target_word_id.0,
|
|
||||||
table_size: self.table_size(),
|
|
||||||
stack_to_local_promotion: self.config.codegen.stack_to_local_promotion,
|
|
||||||
};
|
|
||||||
let compiled = compile_word(&name, &patched_ir, &config)
|
let compiled = compile_word(&name, &patched_ir, &config)
|
||||||
.map_err(|e| anyhow::anyhow!("DOES> patch codegen: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("DOES> patch codegen: {e}"))?;
|
||||||
self.instantiate_and_install(&compiled, target_word_id)?;
|
self.instantiate_and_install(&compiled, target_word_id)?;
|
||||||
@@ -5429,11 +5402,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||||
|
|
||||||
let patched_ir = vec![IrOp::PushI32(pfa as i32), IrOp::Call(WordId(action_id))];
|
let patched_ir = vec![IrOp::PushI32(pfa as i32), IrOp::Call(WordId(action_id))];
|
||||||
let config = CodegenConfig {
|
let config = self.codegen_config(target_word_id.0);
|
||||||
base_fn_index: target_word_id.0,
|
|
||||||
table_size: self.table_size(),
|
|
||||||
stack_to_local_promotion: self.config.codegen.stack_to_local_promotion,
|
|
||||||
};
|
|
||||||
let compiled = compile_word(&name, &patched_ir, &config)
|
let compiled = compile_word(&name, &patched_ir, &config)
|
||||||
.map_err(|e| anyhow::anyhow!("runtime DOES> patch codegen: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("runtime DOES> patch codegen: {e}"))?;
|
||||||
self.instantiate_and_install(&compiled, target_word_id)?;
|
self.instantiate_and_install(&compiled, target_word_id)?;
|
||||||
@@ -6515,11 +6484,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
|
|
||||||
let ir = vec![IrOp::PushI32(lo), IrOp::PushI32(hi)];
|
let ir = vec![IrOp::PushI32(lo), IrOp::PushI32(hi)];
|
||||||
self.ir_bodies.insert(word_id, ir.clone());
|
self.ir_bodies.insert(word_id, ir.clone());
|
||||||
let config = CodegenConfig {
|
let config = self.codegen_config(word_id.0);
|
||||||
base_fn_index: word_id.0,
|
|
||||||
table_size: self.table_size(),
|
|
||||||
stack_to_local_promotion: self.config.codegen.stack_to_local_promotion,
|
|
||||||
};
|
|
||||||
let compiled = compile_word(&name, &ir, &config)
|
let compiled = compile_word(&name, &ir, &config)
|
||||||
.map_err(|e| anyhow::anyhow!("2CONSTANT codegen: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("2CONSTANT codegen: {e}"))?;
|
||||||
self.instantiate_and_install(&compiled, word_id)?;
|
self.instantiate_and_install(&compiled, word_id)?;
|
||||||
@@ -6545,11 +6510,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
|
|
||||||
let ir = vec![IrOp::PushI32(addr as i32)];
|
let ir = vec![IrOp::PushI32(addr as i32)];
|
||||||
self.ir_bodies.insert(word_id, ir.clone());
|
self.ir_bodies.insert(word_id, ir.clone());
|
||||||
let config = CodegenConfig {
|
let config = self.codegen_config(word_id.0);
|
||||||
base_fn_index: word_id.0,
|
|
||||||
table_size: self.table_size(),
|
|
||||||
stack_to_local_promotion: self.config.codegen.stack_to_local_promotion,
|
|
||||||
};
|
|
||||||
let compiled = compile_word(&name, &ir, &config)
|
let compiled = compile_word(&name, &ir, &config)
|
||||||
.map_err(|e| anyhow::anyhow!("2VARIABLE codegen: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("2VARIABLE codegen: {e}"))?;
|
||||||
self.instantiate_and_install(&compiled, word_id)?;
|
self.instantiate_and_install(&compiled, word_id)?;
|
||||||
@@ -6588,11 +6549,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
IrOp::Fetch,
|
IrOp::Fetch,
|
||||||
];
|
];
|
||||||
self.ir_bodies.insert(word_id, ir.clone());
|
self.ir_bodies.insert(word_id, ir.clone());
|
||||||
let config = CodegenConfig {
|
let config = self.codegen_config(word_id.0);
|
||||||
base_fn_index: word_id.0,
|
|
||||||
table_size: self.table_size(),
|
|
||||||
stack_to_local_promotion: self.config.codegen.stack_to_local_promotion,
|
|
||||||
};
|
|
||||||
let compiled = compile_word(&name, &ir, &config)
|
let compiled = compile_word(&name, &ir, &config)
|
||||||
.map_err(|e| anyhow::anyhow!("2VALUE codegen: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("2VALUE codegen: {e}"))?;
|
||||||
self.instantiate_and_install(&compiled, word_id)?;
|
self.instantiate_and_install(&compiled, word_id)?;
|
||||||
@@ -7342,11 +7299,7 @@ impl<R: Runtime> ForthVM<R> {
|
|||||||
// Compile a word that pushes the address onto the DATA stack
|
// Compile a word that pushes the address onto the DATA stack
|
||||||
let ir_body = vec![IrOp::PushI32(addr as i32)];
|
let ir_body = vec![IrOp::PushI32(addr as i32)];
|
||||||
self.ir_bodies.insert(word_id, ir_body.clone());
|
self.ir_bodies.insert(word_id, ir_body.clone());
|
||||||
let config = CodegenConfig {
|
let config = self.codegen_config(word_id.0);
|
||||||
base_fn_index: word_id.0,
|
|
||||||
table_size: self.table_size(),
|
|
||||||
stack_to_local_promotion: self.config.codegen.stack_to_local_promotion,
|
|
||||||
};
|
|
||||||
let compiled = compile_word(&name, &ir_body, &config)
|
let compiled = compile_word(&name, &ir_body, &config)
|
||||||
.map_err(|e| anyhow::anyhow!("codegen error for FVARIABLE {name}: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("codegen error for FVARIABLE {name}: {e}"))?;
|
||||||
|
|
||||||
@@ -9081,6 +9034,76 @@ mod tests {
|
|||||||
assert_eq!(vm.data_stack(), vec![1]);
|
assert_eq!(vm.data_stack(), vec![1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stack_guard_underflow_interpreted() {
|
||||||
|
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
||||||
|
let e = vm.evaluate("DROP").unwrap_err();
|
||||||
|
assert_eq!(e.to_string(), "Stack underflow (throw -4)");
|
||||||
|
// Stack machinery intact afterwards
|
||||||
|
vm.evaluate("7 .").unwrap();
|
||||||
|
assert_eq!(vm.take_output(), "7 ");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stack_guard_underflow_in_compiled_word() {
|
||||||
|
// Straight-line word (promoted path) and mid-word underflow
|
||||||
|
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
||||||
|
vm.evaluate(": D2 DROP DROP ;").unwrap();
|
||||||
|
let e = vm.evaluate("1 D2").unwrap_err();
|
||||||
|
assert_eq!(e.to_string(), "Stack underflow (throw -4)");
|
||||||
|
// The guard fires BEFORE corruption: the 1 must still be there
|
||||||
|
assert_eq!(vm.data_stack(), vec![1]);
|
||||||
|
// Non-promoted (contains a call): same guard. Fresh VM — the
|
||||||
|
// failed D2 call above deliberately left its stack untouched.
|
||||||
|
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
||||||
|
vm.evaluate(": D3 DUP . DROP DROP ;").unwrap();
|
||||||
|
let e = vm.evaluate("2 D3").unwrap_err();
|
||||||
|
assert_eq!(e.to_string(), "Stack underflow (throw -4)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stack_guard_overflow_compiled_loop() {
|
||||||
|
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
||||||
|
vm.evaluate(": FLOOD BEGIN 1 AGAIN ;").unwrap();
|
||||||
|
let e = vm.evaluate("FLOOD").unwrap_err();
|
||||||
|
assert_eq!(e.to_string(), "Stack overflow (throw -3)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stack_guard_float_underflow() {
|
||||||
|
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
||||||
|
vm.evaluate(": FD FDROP ;").unwrap();
|
||||||
|
let e = vm.evaluate("FD").unwrap_err();
|
||||||
|
assert_eq!(e.to_string(), "Floating-point stack underflow (throw -45)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stack_guard_return_stack_underflow() {
|
||||||
|
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
||||||
|
vm.evaluate(": RU R> DROP ;").unwrap();
|
||||||
|
// R> with nothing user-pushed underflows the return stack
|
||||||
|
let e = vm.evaluate("RU").unwrap_err();
|
||||||
|
assert_eq!(e.to_string(), "Return stack underflow (throw -6)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stack_guard_catchable() {
|
||||||
|
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
||||||
|
vm.evaluate(": BAD DROP ;").unwrap();
|
||||||
|
vm.evaluate("' BAD CATCH").unwrap();
|
||||||
|
assert_eq!(vm.data_stack(), vec![-4]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_stack_guards_off_config() {
|
||||||
|
let mut cfg = crate::config::WaferConfig::all();
|
||||||
|
cfg.codegen.stack_guards = false;
|
||||||
|
let mut vm = ForthVM::<NativeRuntime>::new_with_config(cfg).unwrap();
|
||||||
|
// Compiled DROP underflows silently (documented unguarded mode)
|
||||||
|
vm.evaluate(": D1 DROP ;").unwrap();
|
||||||
|
assert!(vm.evaluate("D1").is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_uncaught_throw_has_message() {
|
fn test_uncaught_throw_has_message() {
|
||||||
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
let mut vm = ForthVM::<NativeRuntime>::new().unwrap();
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ fn find_sf64() -> Option<&'static str> {
|
|||||||
/// Spawn `binary`, write `input` to its stdin, and collect the output.
|
/// Spawn `binary`, write `input` to its stdin, and collect the output.
|
||||||
fn run_via_stdin(binary: &str, input: &str) -> Option<std::process::Output> {
|
fn run_via_stdin(binary: &str, input: &str) -> Option<std::process::Output> {
|
||||||
Command::new(binary)
|
Command::new(binary)
|
||||||
|
// Perf lanes measure unguarded code (only the wafer binary reads this)
|
||||||
|
.env("WAFER_STACK_GUARDS", "0")
|
||||||
.stdin(std::process::Stdio::piped())
|
.stdin(std::process::Stdio::piped())
|
||||||
.stdout(std::process::Stdio::piped())
|
.stdout(std::process::Stdio::piped())
|
||||||
.stderr(std::process::Stdio::piped())
|
.stderr(std::process::Stdio::piped())
|
||||||
|
|||||||
@@ -105,8 +105,13 @@ fn expected_load_failures(path: &str) -> u32 {
|
|||||||
// TRAVERSE-WORDLIST / NAME>COMPILE / NAME>INTERPRET blocks leak as
|
// TRAVERSE-WORDLIST / NAME>COMPILE / NAME>INTERPRET blocks leak as
|
||||||
// unknown-word errors. Fix the SOURCE/`>IN` interaction with
|
// unknown-word errors. Fix the SOURCE/`>IN` interaction with
|
||||||
// line-mode input and drop this to 0.
|
// line-mode input and drop this to 0.
|
||||||
|
//
|
||||||
|
// The 38th: line 368 `R> DROP TRUE` runs interpreted (its enclosing
|
||||||
|
// definition aborted on the missing NAME?), and the bare `R>` used
|
||||||
|
// to underflow the return stack silently; stack guards now report
|
||||||
|
// it as "Return stack underflow (throw -6)".
|
||||||
if path.ends_with("/toolstest.fth") {
|
if path.ends_with("/toolstest.fth") {
|
||||||
return 37;
|
return 38;
|
||||||
}
|
}
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user