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>;