use crate::errors::AlkahestError;
use crate::kernel::{ExprId, ExprPool};
use std::fmt;
pub const MAX_EXPR_DEPTH: u32 = 2048;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DepthLimitError {
pub depth: u32,
pub limit: u32,
}
impl fmt::Display for DepthLimitError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"expression nesting depth {} exceeds the limit of {}; \
recursing over it would overflow the stack",
self.depth, self.limit
)
}
}
impl std::error::Error for DepthLimitError {}
impl AlkahestError for DepthLimitError {
fn code(&self) -> &'static str {
"E-DEPTH-001"
}
fn remediation(&self) -> Option<&'static str> {
Some("rebuild the expression with less nesting (a balanced n-ary Add is shallow where a chain of binary ones is not), or process it in smaller pieces")
}
}
pub fn check_expr_depth(pool: &ExprPool, id: ExprId) -> Result<(), DepthLimitError> {
let depth = pool.depth(id);
if depth > MAX_EXPR_DEPTH {
Err(DepthLimitError {
depth,
limit: MAX_EXPR_DEPTH,
})
} else {
Ok(())
}
}
pub fn check_expr_depths(pool: &ExprPool, ids: &[ExprId]) -> Result<(), DepthLimitError> {
for &id in ids {
check_expr_depth(pool, id)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kernel::Domain;
#[test]
fn depth_is_the_longest_path_not_the_shortest() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
assert_eq!(pool.depth(x), 1);
let s = pool.func("sin", vec![x]);
assert_eq!(pool.depth(s), 2);
let sum = pool.add(vec![s, x]);
assert_eq!(pool.depth(sum), 3);
}
#[test]
fn width_does_not_count_towards_depth() {
let pool = ExprPool::new();
let terms: Vec<_> = (0..10_000).map(|i| pool.integer(i)).collect();
let wide = pool.add(terms);
assert_eq!(pool.depth(wide), 2);
assert!(check_expr_depth(&pool, wide).is_ok());
}
#[test]
fn a_chain_of_binary_adds_is_refused_past_the_limit() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let mut acc = x;
for i in 0..MAX_EXPR_DEPTH {
let k = pool.integer(i);
acc = pool.add(vec![acc, k]);
}
assert_eq!(pool.depth(acc), MAX_EXPR_DEPTH + 1);
let err = check_expr_depth(&pool, acc).expect_err("one past the limit must be refused");
assert_eq!(err.depth, MAX_EXPR_DEPTH + 1);
assert_eq!(err.code(), "E-DEPTH-001");
}
#[test]
fn the_limit_itself_is_accepted() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let mut acc = x;
for _ in 1..MAX_EXPR_DEPTH {
acc = pool.func("sin", vec![acc]);
}
assert_eq!(pool.depth(acc), MAX_EXPR_DEPTH);
assert!(check_expr_depth(&pool, acc).is_ok());
}
#[test]
fn batch_check_reports_the_first_offender() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let mut deep = x;
for _ in 0..=MAX_EXPR_DEPTH {
deep = pool.func("sin", vec![deep]);
}
assert!(check_expr_depths(&pool, &[x, x]).is_ok());
assert!(check_expr_depths(&pool, &[x, deep, x]).is_err());
}
}