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:
Oleksandr Kozachuk
2026-08-05 17:00:22 +02:00
parent e31407ab58
commit cda296aab5
9 changed files with 324 additions and 140 deletions
+9 -9
View File
@@ -21,7 +21,7 @@ mod tests {
// 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);
let result = compile_consolidated_module(&words, &map, 16, None);
// Empty is valid -- should produce a valid module with no functions
assert!(result.is_ok());
}
@@ -31,7 +31,7 @@ mod tests {
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);
let result = compile_consolidated_module(&words, &map, 16, None);
assert!(result.is_ok());
}
@@ -49,7 +49,7 @@ mod tests {
map.insert(WordId(1), 1u32);
map.insert(WordId(2), 2u32);
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());
}
@@ -59,7 +59,7 @@ mod tests {
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);
let result = compile_consolidated_module(&words, &map, 256, None);
assert!(result.is_ok());
}
@@ -72,7 +72,7 @@ mod tests {
let mut map = HashMap::new();
map.insert(WordId(1), 1u32);
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());
}
@@ -95,7 +95,7 @@ mod tests {
let mut map = HashMap::new();
map.insert(WordId(1), 1u32);
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());
}
@@ -120,7 +120,7 @@ mod tests {
let mut map = HashMap::new();
map.insert(WordId(1), 1u32);
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());
}
@@ -141,7 +141,7 @@ mod tests {
let mut map = HashMap::new();
map.insert(WordId(1), 1u32);
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());
}
@@ -163,7 +163,7 @@ mod tests {
let mut map = HashMap::new();
map.insert(WordId(1), 1u32);
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());
}
}