Skip to main content

cljrs_runtime/env/
error.rs

1//! Evaluation-time error types.
2
3use cljrs_value::Value;
4use cljrs_value::ValueError;
5
6#[derive(Debug, thiserror::Error)]
7pub enum EvalError {
8    #[error("runtime error: {0}")]
9    Runtime(String),
10
11    /// The cooperative execution-credit budget was exhausted.
12    #[error("gas exhausted")]
13    GasExhausted,
14
15    /// Execution was cancelled; the gas budget was not exhausted.
16    #[error("execution cancelled")]
17    Cancelled,
18
19    /// The process memory governor refused a governed allocation (strict
20    /// mode), and a collection did not bring committed memory back within
21    /// the hard limit.  Fixed-size; building it never allocates.  Like
22    /// `GasExhausted`, `try`/`catch` does not intercept it.
23    #[error("{0}")]
24    MemoryLimitExceeded(cljrs_gc::MemoryLimitExceeded),
25
26    /// An operation attempted to use a capability unavailable to an isolated
27    /// transaction function.
28    #[error("effect forbidden in transaction function: {0}")]
29    ForbiddenEffect(String),
30
31    #[error("unbound symbol: {0}")]
32    UnboundSymbol(String),
33
34    #[error("arity error calling {name}: expected {expected}, got {got}")]
35    Arity {
36        name: String,
37        expected: String,
38        got: usize,
39    },
40
41    #[error("not callable: {0}")]
42    NotCallable(String),
43
44    /// A value thrown via `throw` or `ex-info`.
45    #[error("{0}")]
46    Thrown(Value),
47
48    #[error("read error: {0}")]
49    Read(#[from] cljrs_types::error::CljxError),
50
51    /// Internal signal for `recur` — caught by the loop/fn trampoline.
52    /// Never propagated to user code.
53    #[doc(hidden)]
54    #[error("internal: recur outside loop or fn")]
55    Recur(Vec<Value>),
56
57    #[error(
58        "commit {commit:?} failed signature verification — \
59         refusing to execute versioned symbol (enable GPG/SSH trust or disable \
60         :verify-commit-signatures): {reason}"
61    )]
62    CommitSignatureVerificationFailed { commit: String, reason: String },
63}
64
65impl EvalError {
66    /// Convert this error into a Clojure error *value* (`Value::Error`).
67    ///
68    /// A `Thrown` value is returned unchanged (preserving its `ex-data` /
69    /// `ex-cause`); any other error is wrapped in a fresh `ExceptionInfo` with
70    /// the error's display string as the message. Used where an error must be
71    /// stored as a value and later re-thrown — e.g. a failed `Future`'s state.
72    pub fn to_error_value(self) -> Value {
73        match self {
74            EvalError::Thrown(v) => v,
75            other => {
76                let msg = other.to_string();
77                let error = match other {
78                    EvalError::Cancelled => ValueError::Cancelled,
79                    EvalError::MemoryLimitExceeded(fault) => {
80                        cljrs_value::ValueError::MemoryLimitExceeded(fault)
81                    }
82                    _ => cljrs_value::ValueError::Other(msg.clone()),
83                };
84                // Error reporting may use the critical memory reserve.
85                Value::Error(cljrs_gc::GcPtr::new_critical(
86                    cljrs_value::ExceptionInfo::new(error, msg, None, None),
87                ))
88            }
89        }
90    }
91}
92
93/// A `Thunk::force` error: cancellation stays typed, so lazy cells do not
94/// cache it; every other failure becomes its message, as before.
95pub fn eval_error_to_thunk_error(error: EvalError) -> ValueError {
96    match error {
97        EvalError::Cancelled => ValueError::Cancelled,
98        other => ValueError::Other(other.to_string()),
99    }
100}
101
102/// A realization error (`LazySeq::try_realize`, `Delay::force`) as a runtime
103/// error, keeping cancellation uncatchable.
104pub fn realize_error_to_eval_error(error: ValueError) -> EvalError {
105    match error {
106        ValueError::Cancelled => EvalError::Cancelled,
107        other => EvalError::Runtime(other.to_string()),
108    }
109}
110
111/// Surface a builtin's `ValueError` to the evaluator as a *catchable* condition.
112///
113/// Internal runtime errors (`IndexOutOfBounds`, `WrongType`, `ArityError`, …)
114/// are normalized into an `EvalError::Thrown(Value::Error(..))` carrying the
115/// original `ValueError` variant and its plain display message — no
116/// `runtime error:` prefix — so that `(catch :default e ..)` /
117/// `(catch Throwable e ..)` bind a value on which `ex-message` / `ex-data`
118/// behave the same as for a user `throw` / `ex-info`. A `ValueError::Thrown`
119/// (a builtin re-throwing a Clojure value) is surfaced as that exact value.
120pub fn value_error_to_eval_error(err: ValueError) -> EvalError {
121    match err {
122        ValueError::Thrown(v) => EvalError::Thrown(v),
123        ValueError::GasExhausted => EvalError::GasExhausted,
124        ValueError::Cancelled => EvalError::Cancelled,
125        ValueError::MemoryLimitExceeded(fault) => {
126            // The error is back on a path that carries it; a copy left in
127            // the runtime-fault slot (see `callback::invoke`) would report
128            // it a second time.
129            let _ = cljrs_gc::governor::take_runtime_fault();
130            EvalError::MemoryLimitExceeded(fault)
131        }
132        other => {
133            let msg = other.to_string();
134            // Error reporting may use the critical memory reserve.
135            EvalError::Thrown(Value::Error(cljrs_gc::GcPtr::new_critical(
136                cljrs_value::ExceptionInfo::new(other, msg, None, None),
137            )))
138        }
139    }
140}
141
142pub type EvalResult<T = Value> = Result<T, EvalError>;