#[cfg(not(feature = "std"))]
use alloc::{
collections::{BTreeSet, VecDeque},
vec::Vec,
};
#[cfg(feature = "std")]
use std::collections::{BTreeSet, HashMap, VecDeque};
#[cfg(not(feature = "std"))]
use hashbrown::HashMap;
use crate::Program;
use crate::VerifierError;
use crate::bytecode::{Instruction, InstructionStream, JumpTable, StackEffect};
use crate::dataflow::analysis::Analysis;
use crate::dataflow::cfg::Cfg;
use crate::dataflow::direction::Forward;
use crate::dataflow::lattice::Lattice;
use crate::dataflow::solver::solve;
const STACK_LIMIT: usize = 8192;
pub type BlockId = usize;
#[derive(Clone, Debug)]
pub struct BlockEffect {
pub min_input: usize,
pub output: OutputKind,
}
#[derive(Clone, Debug)]
pub enum OutputKind {
Delta(i32),
Fixed(usize),
Underflow,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum DepthValue {
Top,
Known(usize),
Underflow,
}
impl Lattice for DepthValue {
fn top() -> Self {
Self::Top
}
fn meet(&self, other: &Self) -> Self {
match (self, other) {
(Self::Top, x) | (x, Self::Top) => x.clone(),
(Self::Underflow, _) | (_, Self::Underflow) => Self::Underflow,
(Self::Known(a), Self::Known(b)) => Self::Known(*a.min(b)),
}
}
}
pub struct StackDepthAnalysis {
effects: HashMap<BlockId, BlockEffect>,
}
impl Analysis for StackDepthAnalysis {
type Value = DepthValue;
type Node = BlockId;
type Dir = Forward;
fn boundary_value(&self) -> DepthValue {
DepthValue::Known(0)
}
fn transfer(&self, node: &BlockId, input: &DepthValue) -> DepthValue {
let effect = self.effects.get(node);
debug_assert!(effect.is_some(), "every CFG node must have a BlockEffect");
apply_effect(input.clone(), effect)
}
}
fn apply_effect(input: DepthValue, effect: Option<&BlockEffect>) -> DepthValue {
let Some(effect) = effect else {
return input;
};
match input {
DepthValue::Top => DepthValue::Top,
DepthValue::Underflow => DepthValue::Underflow,
DepthValue::Known(depth) => {
if depth < effect.min_input {
return DepthValue::Underflow;
}
match &effect.output {
OutputKind::Underflow => DepthValue::Underflow,
OutputKind::Delta(d) => {
let out = depth.saturating_add_signed(isize::try_from(*d).unwrap_or(0));
DepthValue::Known(out)
}
OutputKind::Fixed(k) => DepthValue::Known(*k),
}
}
}
}
#[derive(Debug)]
enum BlockTerminatorKind {
FallThrough,
Jump {
label: u16,
},
CondJump {
label: u16,
},
LoopOpener,
LoopCloser,
Halt,
}
pub(crate) struct CfgContext {
pub(crate) cfg: Cfg<BlockId>,
pub(crate) effects: HashMap<BlockId, BlockEffect>,
pub(crate) loop_regions: Vec<LoopRegion>,
}
pub(crate) struct LoopRegion {
opener_pos: usize,
opener_block: BlockId,
body_start: BlockId,
after_next: BlockId,
}
fn compute_block_effect(
code: &[u8],
block_start: usize,
block_end: usize,
) -> (BlockEffect, BlockTerminatorKind, usize) {
let default = (
BlockEffect {
min_input: 0,
output: OutputKind::Delta(0),
},
BlockTerminatorKind::FallThrough,
block_start,
);
let block_code = match code.get(block_start..block_end) {
Some(s) if !s.is_empty() => s,
_ => return default,
};
let mut depth: i32 = 0;
let mut min_input: usize = 0;
let mut fixed: bool = false;
let mut static_underflow: bool = false;
let mut kind = BlockTerminatorKind::FallThrough;
let mut term_pos: usize = block_start;
let mut stream = InstructionStream::new(block_code);
while let Some(item) = stream.next_instruction() {
let Ok((rel_pos, _, instr)) = item else { break };
let abs_pos = block_start + rel_pos;
match instr.stack_effect() {
StackEffect::Reset => {
if !fixed && depth < 0 {
min_input = min_input.max(depth.unsigned_abs() as usize);
}
fixed = true;
static_underflow = false;
depth = 0;
}
StackEffect::Delta(d) => {
let d = i32::from(d);
depth += d;
if !fixed {
if depth < 0 {
min_input = min_input.max(depth.unsigned_abs() as usize);
}
} else if depth < 0 {
static_underflow = true;
}
}
}
let new_kind = match &instr {
Instruction::Jump1 { label } => Some(BlockTerminatorKind::Jump {
label: u16::from(*label),
}),
Instruction::Jump2 { label } => Some(BlockTerminatorKind::Jump { label: *label }),
Instruction::JumpI1 { label } => Some(BlockTerminatorKind::CondJump {
label: u16::from(*label),
}),
Instruction::JumpI2 { label } => Some(BlockTerminatorKind::CondJump { label: *label }),
Instruction::Halt {} => Some(BlockTerminatorKind::Halt),
Instruction::Range {} | Instruction::Iter { .. } => {
Some(BlockTerminatorKind::LoopOpener)
}
Instruction::Next {} => Some(BlockTerminatorKind::LoopCloser),
_ => None,
};
if let Some(k) = new_kind {
kind = k;
term_pos = abs_pos;
}
}
let output = if static_underflow {
OutputKind::Underflow
} else if fixed {
OutputKind::Fixed(usize::try_from(depth).unwrap_or(0))
} else {
OutputKind::Delta(depth)
};
(BlockEffect { min_input, output }, kind, term_pos)
}
fn simulate_body(
body_start: BlockId,
entry: DepthValue,
after_next: BlockId,
effects: &HashMap<BlockId, BlockEffect>,
cfg: &Cfg<BlockId>,
) -> DepthValue {
let mut depth_in: HashMap<BlockId, DepthValue> = HashMap::new();
let mut worklist: VecDeque<BlockId> = VecDeque::new();
let _ = depth_in.insert(body_start, entry);
worklist.push_back(body_start);
let mut exit_depth = DepthValue::Top;
while let Some(block) = worklist.pop_front() {
let d_in = depth_in.get(&block).cloned().unwrap_or(DepthValue::Top);
let d_out = apply_effect(d_in, effects.get(&block));
if cfg.successors(&block).contains(&body_start) {
exit_depth = exit_depth.meet(&d_out);
}
for &succ in cfg.successors(&block) {
if succ == body_start || succ == after_next {
continue;
}
let current = depth_in.get(&succ).cloned().unwrap_or(DepthValue::Top);
let merged = current.meet(&d_out);
if merged != current {
let _ = depth_in.insert(succ, merged);
worklist.push_back(succ);
}
}
}
exit_depth
}
#[expect(
clippy::too_many_lines,
reason = "two-pass CFG construction; splitting the passes into separate functions would obscure the algorithm"
)]
pub(crate) fn build_cfg(code: &[u8], jump_table: &JumpTable) -> Option<CfgContext> {
if code.is_empty() {
return None;
}
let mut leaders: BTreeSet<usize> = BTreeSet::new();
let _ = leaders.insert(0);
let mut opener_info: HashMap<usize, (usize, usize)> = HashMap::new();
let mut next_back: HashMap<usize, usize> = HashMap::new();
let mut loop_stack: Vec<(usize, usize)> = Vec::new(); let mut halt_block: Option<usize> = None;
let mut cur_block: usize = 0;
let mut stream = InstructionStream::new(code);
while let Some(item) = stream.next_instruction() {
let Ok((pos, _, instr)) = item else { break };
let after = stream.pos();
if leaders.contains(&pos) {
cur_block = pos;
}
match &instr {
Instruction::Target {} => {
let _ = leaders.insert(pos);
}
Instruction::Jump1 { .. }
| Instruction::Jump2 { .. }
| Instruction::JumpI1 { .. }
| Instruction::JumpI2 { .. } => {
let _ = leaders.insert(after);
}
Instruction::Range {} | Instruction::Iter { .. } => {
let _ = leaders.insert(after);
loop_stack.push((pos, after));
}
Instruction::Next {} => {
let _ = leaders.insert(after);
if let Some((opener_pos, body_start)) = loop_stack.pop() {
let _ = next_back.insert(pos, body_start);
let _ = opener_info.insert(opener_pos, (body_start, after));
}
}
Instruction::Halt {} => {
halt_block = Some(cur_block);
}
_ => {}
}
}
let leaders_sorted: Vec<usize> = leaders.into_iter().collect();
let exit_block = halt_block.unwrap_or_else(|| *leaders_sorted.last().unwrap_or(&0));
let mut effects: HashMap<BlockId, BlockEffect> = HashMap::new();
let mut cfg_builder = Cfg::builder(0usize, exit_block);
let mut loop_regions: Vec<LoopRegion> = Vec::new();
for (idx, &block_start) in leaders_sorted.iter().enumerate() {
let block_end = leaders_sorted.get(idx + 1).copied().unwrap_or(code.len());
let (effect, term_kind, term_pos) = compute_block_effect(code, block_start, block_end);
let _ = effects.insert(block_start, effect);
match term_kind {
BlockTerminatorKind::FallThrough => {
if block_end < code.len() {
cfg_builder = cfg_builder.edge(block_start, block_end);
}
}
BlockTerminatorKind::Jump { label } => {
if let Some(target) = jump_table.get(label) {
cfg_builder = cfg_builder.edge(block_start, target);
}
}
BlockTerminatorKind::CondJump { label } => {
if let Some(target) = jump_table.get(label) {
cfg_builder = cfg_builder.edge(block_start, target);
}
if block_end < code.len() {
cfg_builder = cfg_builder.edge(block_start, block_end);
}
}
BlockTerminatorKind::LoopOpener => {
if let Some(&(body_start, after_next)) = opener_info.get(&term_pos) {
cfg_builder = cfg_builder.edge(block_start, body_start);
cfg_builder = cfg_builder.edge(block_start, after_next);
loop_regions.push(LoopRegion {
opener_pos: term_pos,
opener_block: block_start,
body_start,
after_next,
});
} else {
cfg_builder = cfg_builder.edge(block_start, block_end);
}
}
BlockTerminatorKind::LoopCloser => {
if let Some(&body_start) = next_back.get(&term_pos) {
cfg_builder = cfg_builder.edge(block_start, body_start);
}
if block_end < code.len() {
cfg_builder = cfg_builder.edge(block_start, block_end);
}
}
BlockTerminatorKind::Halt => {}
}
}
Some(CfgContext {
cfg: cfg_builder.build(),
effects,
loop_regions,
})
}
pub(crate) fn check_stack_depth_with_context(ctx: CfgContext) -> Result<(), VerifierError> {
let CfgContext {
cfg,
effects,
loop_regions,
} = ctx;
let analysis = StackDepthAnalysis { effects };
let result = solve(&analysis, &cfg);
for region in &loop_regions {
let entry = result
.after(®ion.opener_block)
.cloned()
.unwrap_or(DepthValue::Top);
let exit = simulate_body(
region.body_start,
entry.clone(),
region.after_next,
&analysis.effects,
&cfg,
);
if let (DepthValue::Known(e), DepthValue::Known(x)) = (&entry, &exit)
&& e != x
{
return Err(VerifierError::LoopStackImbalance {
opener: region.opener_pos,
entry: *e,
exit: *x,
});
}
}
let mut sorted_join_blocks: Vec<BlockId> = cfg.nodes().to_vec();
sorted_join_blocks.sort_unstable();
for &block in &sorted_join_blocks {
let preds = cfg.predecessors(&block);
if preds.len() < 2 {
continue;
}
match result.before(&block) {
None | Some(DepthValue::Top) => continue,
_ => {}
}
let mut known = preds.iter().filter_map(|p| {
if let Some(DepthValue::Known(d)) = result.after(p) {
Some(*d)
} else {
None
}
});
let Some(first) = known.next() else { continue };
for other in known {
if other != first {
return Err(VerifierError::StackDepthMismatch {
target_offset: block,
depth_a: first,
depth_b: other,
});
}
}
}
for &node in cfg.nodes() {
let before = result.before(&node).cloned().unwrap_or(DepthValue::Top);
let after = result.after(&node).cloned().unwrap_or(DepthValue::Top);
if before == DepthValue::Top {
continue;
}
match &before {
DepthValue::Underflow => {
return Err(VerifierError::StackUnderflow { offset: node });
}
DepthValue::Known(d) if *d > STACK_LIMIT => {
return Err(VerifierError::StackOverflowRisk {
offset: node,
depth: *d,
});
}
_ => {}
}
match &after {
DepthValue::Underflow => {
return Err(VerifierError::StackUnderflow { offset: node });
}
DepthValue::Known(d) if *d > STACK_LIMIT => {
return Err(VerifierError::StackOverflowRisk {
offset: node,
depth: *d,
});
}
_ => {}
}
}
Ok(())
}
pub fn check_stack_depth(program: &Program) -> Result<(), VerifierError> {
let code = program.code();
if code.is_empty() {
return Ok(());
}
let Some(ctx) = build_cfg(code, program.jump_table()) else {
return Ok(());
};
check_stack_depth_with_context(ctx)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::InstructionBuilder;
fn prog(build: impl FnOnce(&mut InstructionBuilder)) -> Program {
let mut b = InstructionBuilder::new();
build(&mut b);
b.build().unwrap()
}
#[test]
fn empty_program_ok() {
assert!(check_stack_depth(&Program::new(vec![])).is_ok());
}
#[test]
fn halt_only_ok() {
let p = prog(|b| {
let _ = b.emit_halt();
});
assert!(check_stack_depth(&p).is_ok());
}
#[test]
fn push_halt_ok() {
let p = prog(|b| {
let _ = b.emit_push(42).emit_halt();
});
assert!(check_stack_depth(&p).is_ok());
}
#[test]
fn push_pop_halt_ok() {
let p = prog(|b| {
let _ = b.emit_push(1).emit_pop().emit_halt();
});
assert!(check_stack_depth(&p).is_ok());
}
#[test]
fn pop_on_empty_stack_underflow() {
let p = prog(|b| {
let _ = b.emit_pop().emit_halt();
});
let err = check_stack_depth(&p).unwrap_err();
assert_eq!(err.variant_name(), "StackUnderflow");
}
#[test]
fn static_underflow_after_sclr() {
let p = prog(|b| {
let _ = b.emit_sclr().emit_pop().emit_halt();
});
let err = check_stack_depth(&p).unwrap_err();
assert_eq!(err.variant_name(), "StackUnderflow");
}
#[test]
fn sclr_then_push_ok() {
let p = prog(|b| {
let _ = b.emit_sclr().emit_push(1).emit_halt();
});
assert!(check_stack_depth(&p).is_ok());
}
#[test]
fn cond_branch_depth_mismatch_caught() {
let mut b = InstructionBuilder::new();
let target = b.label();
let _ = b
.emit_push(1)
.emit_jump_if(target) .emit_push(99) .place(target)
.unwrap()
.emit_pop()
.emit_halt();
let p = b.build().unwrap();
let err = check_stack_depth(&p).unwrap_err();
assert_eq!(err.variant_name(), "StackDepthMismatch");
}
#[test]
fn cond_branch_both_paths_same_depth_ok() {
let mut b = InstructionBuilder::new();
let target = b.label();
let _ = b
.emit_push(1)
.emit_jump_if(target) .emit_push(2) .emit_pop() .place(target)
.unwrap()
.emit_halt();
let p = b.build().unwrap();
assert!(check_stack_depth(&p).is_ok());
}
#[test]
fn stack_depth_mismatch_at_join() {
let mut b = InstructionBuilder::new();
let target = b.label();
let _ = b
.emit_push(5)
.emit_jump_if(target) .emit_push(7) .place(target)
.unwrap()
.emit_halt();
let p = b.build().unwrap();
let err = check_stack_depth(&p).unwrap_err();
assert_eq!(err.variant_name(), "StackDepthMismatch");
}
#[test]
fn stack_neutral_loop_ok() {
let p = prog(|b| {
let _ = b
.emit_push(0) .emit_push(3) .emit_range() .emit_push(1) .emit_pop() .emit_next()
.emit_halt();
});
assert!(check_stack_depth(&p).is_ok());
}
#[test]
fn loop_body_push_causes_imbalance() {
let p = prog(|b| {
let _ = b
.emit_push(0)
.emit_push(3)
.emit_range()
.emit_push(99) .emit_next()
.emit_halt();
});
let err = check_stack_depth(&p).unwrap_err();
assert_eq!(err.variant_name(), "LoopStackImbalance");
}
#[test]
fn sclr_inside_loop_body_causes_imbalance() {
let p = prog(|b| {
let _ = b
.emit_push(1) .emit_push(0)
.emit_push(3)
.emit_range()
.emit_sclr() .emit_next()
.emit_halt();
});
let err = check_stack_depth(&p).unwrap_err();
assert_eq!(err.variant_name(), "LoopStackImbalance");
}
#[test]
fn loop_body_pop_causes_imbalance() {
let p = prog(|b| {
let _ = b
.emit_push(1) .emit_push(0)
.emit_push(3)
.emit_range() .emit_pop() .emit_next() .emit_halt();
});
let err = check_stack_depth(&p).unwrap_err();
assert_eq!(err.variant_name(), "LoopStackImbalance");
}
#[test]
fn depth_value_meet_identity() {
let x = DepthValue::Known(5);
assert_eq!(DepthValue::Top.meet(&x), x);
assert_eq!(x.meet(&DepthValue::Top), x);
}
#[test]
fn depth_value_meet_known_takes_min() {
let a = DepthValue::Known(3);
let b = DepthValue::Known(7);
assert_eq!(a.meet(&b), DepthValue::Known(3));
}
#[test]
fn depth_value_underflow_absorbs() {
let k = DepthValue::Known(10);
assert_eq!(DepthValue::Underflow.meet(&k), DepthValue::Underflow);
assert_eq!(k.meet(&DepthValue::Underflow), DepthValue::Underflow);
}
}