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}