Skip to main content

cljrs_runtime/env/
depth.rs

1//! Call-depth cap for [`ExecutionMode::NoGcTransaction`].
2//!
3//! Every interpreted application consumes real Rust stack, so a hostile or
4//! merely runaway transaction could overflow the host thread's stack and abort
5//! the process.  A runtime built in [`ExecutionMode::NoGcTransaction`] routes
6//! its calls through [`call_cljrs_fn`], which refuses to nest deeper than the
7//! limit installed by [`DepthGuard`].
8//!
9//! This used to be a `call_cljrs_fn` function pointer that `cljrs-tx` stored in
10//! `GlobalEnv`.  It is the one call-path override that had a reason to exist,
11//! so it survives the removal of the seam — as an execution mode owned by the
12//! runtime rather than as an arbitrary hook.
13//!
14//! [`ExecutionMode::NoGcTransaction`]: crate::ExecutionMode::NoGcTransaction
15
16use std::cell::Cell;
17
18use cljrs_value::{CljxFn, Value};
19
20use crate::env::env::Env;
21use crate::env::error::{EvalError, EvalResult};
22
23/// Marker text for a depth-cap rejection.  Surfaced as
24/// [`EvalError::Runtime`] because the interpreter's call path has a fixed
25/// error type; `cljrs-tx` matches on it to report a depth overrun.
26pub const DEPTH_EXCEEDED_MSG: &str = "cljrs-tx: transaction call depth exceeded";
27
28thread_local! {
29    /// `(limit, current)` nested-application counter for the running
30    /// invocation; `None` outside one.
31    static CALL_DEPTH: Cell<Option<(u64, u64)>> = const { Cell::new(None) };
32}
33
34/// Installs the call-depth budget for one invocation's dynamic extent.
35///
36/// The budget is thread-local and is cleared when the guard drops, including
37/// on unwind.
38pub struct DepthGuard;
39
40impl DepthGuard {
41    pub fn install(limit: u64) -> Self {
42        CALL_DEPTH.with(|cell| cell.set(Some((limit, 0))));
43        Self
44    }
45}
46
47impl Drop for DepthGuard {
48    fn drop(&mut self) {
49        CALL_DEPTH.with(|cell| cell.set(None));
50    }
51}
52
53/// Tree-walking function application with the transaction depth cap applied.
54///
55/// With no [`DepthGuard`] installed on this thread this is exactly
56/// `crate::interp::apply::call_cljrs_fn`.
57#[allow(clippy::result_large_err)]
58pub fn call_cljrs_fn(f: &CljxFn, args: &[Value], env: &mut Env) -> EvalResult {
59    let Some((limit, depth)) = CALL_DEPTH.with(Cell::get) else {
60        return crate::interp::apply::call_cljrs_fn(f, args, env);
61    };
62    if depth >= limit {
63        return Err(EvalError::Runtime(DEPTH_EXCEEDED_MSG.into()));
64    }
65    CALL_DEPTH.with(|cell| cell.set(Some((limit, depth + 1))));
66    let result = crate::interp::apply::call_cljrs_fn(f, args, env);
67    CALL_DEPTH.with(|cell| {
68        if let Some((limit, current)) = cell.get() {
69            cell.set(Some((limit, current.saturating_sub(1))));
70        }
71    });
72    result
73}