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}
33cljrs_gc::mutator_slot!("cljrs-runtime/CALL_DEPTH", cell CALL_DEPTH: Option<(u64, u64)>);
34
35/// Installs the call-depth budget for one invocation's dynamic extent.
36///
37/// The budget is thread-local and is cleared when the guard drops, including
38/// on unwind.
39pub struct DepthGuard;
40
41impl DepthGuard {
42    pub fn install(limit: u64) -> Self {
43        CALL_DEPTH.with(|cell| cell.set(Some((limit, 0))));
44        Self
45    }
46}
47
48impl Drop for DepthGuard {
49    fn drop(&mut self) {
50        CALL_DEPTH.with(|cell| cell.set(None));
51    }
52}
53
54/// Tree-walking function application with the transaction depth cap applied.
55///
56/// With no [`DepthGuard`] installed on this thread this is exactly
57/// `crate::interp::apply::call_cljrs_fn`.
58#[allow(clippy::result_large_err)]
59pub fn call_cljrs_fn(f: &CljxFn, args: &[Value], env: &mut Env) -> EvalResult {
60    let Some((limit, depth)) = CALL_DEPTH.with(Cell::get) else {
61        return crate::interp::apply::call_cljrs_fn(f, args, env);
62    };
63    if depth >= limit {
64        return Err(EvalError::Runtime(DEPTH_EXCEEDED_MSG.into()));
65    }
66    CALL_DEPTH.with(|cell| cell.set(Some((limit, depth + 1))));
67    let result = crate::interp::apply::call_cljrs_fn(f, args, env);
68    CALL_DEPTH.with(|cell| {
69        if let Some((limit, current)) = cell.get() {
70            cell.set(Some((limit, current.saturating_sub(1))));
71        }
72    });
73    result
74}