Skip to main content

cljrs_value/
error.rs

1#![allow(unused)]
2
3use crate::collections::{TransientMap, TransientVector};
4use crate::hash::{hash_combine_ordered, hash_string};
5use crate::{ClojureHash, Keyword, MapValue, PersistentArrayMap, Value};
6use cljrs_gc::{GcPtr, GcVisitor, MarkVisitor, Trace};
7use std::backtrace::{Backtrace, BacktraceStatus};
8
9/// Value-level errors: type mismatches, arity errors, out-of-bounds, etc.
10///
11/// These are deliberately free of miette/NamedSource so they can be
12/// constructed without source-location context.  The evaluator wraps them
13/// in `CljxError::EvalError` when it has a span.
14#[derive(Debug, thiserror::Error, Clone)]
15pub enum ValueError {
16    #[error("wrong type: expected {expected}, got {got}")]
17    WrongType { expected: &'static str, got: String },
18
19    #[error("index out of bounds: {idx} >= {count}")]
20    IndexOutOfBounds { idx: usize, count: usize },
21
22    #[error("arity error: {name} expects {expected}, got {got}")]
23    ArityError {
24        name: String,
25        expected: String,
26        got: usize,
27    },
28
29    #[error("cannot call non-function value: {value}")]
30    NotCallable { value: String },
31
32    #[error("map must have an even number of forms, got {count}")]
33    OddMap { count: usize },
34
35    #[error("this feature is not yet supported")]
36    Unsupported,
37
38    #[error("{0}")]
39    Other(String),
40
41    #[error("gas exhausted")]
42    GasExhausted,
43
44    /// Execution was cancelled; the gas budget was not exhausted.
45    ///
46    /// Reserved for fiber cancellation (`parking::catch_cancelled` and
47    /// cancelled safepoints). It is uncatchable inside the cancelled
48    /// computation, but a future it settles is treated like `future-cancel`:
49    /// awaiters get a catchable exception, and an unread one is not reported
50    /// unless `CLJRS_WARN_INTERRUPTED_FUTURES` is enabled.
51    /// Native code must not return it for other failures.
52    #[error("execution cancelled")]
53    Cancelled,
54
55    /// The process memory governor refused a governed allocation (strict
56    /// mode).  Like `GasExhausted`, it is not catchable by Clojure code.
57    #[error("{0}")]
58    MemoryLimitExceeded(cljrs_gc::MemoryLimitExceeded),
59
60    #[error("out of range")]
61    OutOfRange,
62
63    #[error("transient already persisted")]
64    TransientAlreadyPersisted,
65
66    #[error("could not parse value")]
67    Parse,
68
69    #[error("thrown exception")]
70    Thrown(Value),
71}
72
73pub type ValueResult<T> = Result<T, ValueError>;
74
75#[derive(Clone, Debug)]
76pub struct ExceptionInfo {
77    pub(crate) error: ValueError,
78    pub(crate) message: String,
79    pub(crate) data: Option<MapValue>,
80    pub(crate) cause: Option<GcPtr<ExceptionInfo>>,
81}
82
83impl ExceptionInfo {
84    pub fn new(
85        error: ValueError,
86        message: String,
87        data: Option<MapValue>,
88        cause: Option<GcPtr<ExceptionInfo>>,
89    ) -> Self {
90        Self {
91            error,
92            message,
93            data: data.as_ref().cloned(),
94            cause: cause.as_ref().cloned(),
95        }
96    }
97
98    /// Short name of the underlying error kind (`"WrongType"`, `"Other"`,
99    /// …) — the `:type` of this exception's `:via` entry.
100    pub fn type_name(&self) -> &'static str {
101        match self.error {
102            ValueError::WrongType { .. } => "WrongType",
103            ValueError::IndexOutOfBounds { .. } => "IndexOutOfBounds",
104            ValueError::ArityError { .. } => "ArityError",
105            ValueError::NotCallable { .. } => "NotCallable",
106            ValueError::OddMap { .. } => "OddMap",
107            ValueError::Unsupported => "Unsupported",
108            ValueError::Other(_) => "Other",
109            ValueError::GasExhausted => "GasExhausted",
110            ValueError::Cancelled => "Cancelled",
111            ValueError::MemoryLimitExceeded(_) => "MemoryLimitExceeded",
112            ValueError::OutOfRange => "OutOfRange",
113            ValueError::TransientAlreadyPersisted => "TransientAlreadyPersisted",
114            ValueError::Parse => "ParseError",
115            ValueError::Thrown(_) => "Thrown",
116        }
117    }
118
119    /// This exception alone (causes excluded) as a `Throwable->map` `:via`
120    /// entry: `{:type .. :message .. :data ..}`.
121    pub fn to_via_map(&self) -> ValueResult<Value> {
122        let map = TransientMap::new();
123        map.assoc(
124            Value::keyword(Keyword::simple("type")),
125            Value::string(self.type_name()),
126        )?;
127        map.assoc(
128            Value::keyword(Keyword::simple("message")),
129            Value::string(&self.message),
130        )?;
131        if let Some(info) = self.data.as_ref() {
132            map.assoc(
133                Value::keyword(Keyword::simple("data")),
134                Value::Map(info.clone()),
135            )?;
136        }
137        // TODO, add :at (source location)
138        Ok(Value::Map(MapValue::Hash(GcPtr::new(map.persistent()?))))
139    }
140
141    pub fn to_map(&self) -> ValueResult<Value> {
142        let map = TransientMap::new();
143        map.assoc(
144            Value::keyword(Keyword::simple("cause")),
145            self.cause
146                .as_ref()
147                .map(|c| Value::Str(GcPtr::new(c.get().message.to_string())))
148                .unwrap_or(Value::Str(GcPtr::new(self.message.to_string()))),
149        )?;
150        if let Some(info) = self.data.as_ref() {
151            map.assoc(
152                Value::keyword(Keyword::simple("data")),
153                Value::Map(info.clone()),
154            )?;
155        }
156        let via = TransientVector::new();
157        via.append(self.to_via_map()?);
158        let mut cur = self.cause.as_ref();
159        while let Some(e) = cur {
160            via.append(e.get().to_via_map()?);
161            cur = e.get().cause.as_ref();
162        }
163        map.assoc(
164            Value::keyword(Keyword::simple("via")),
165            Value::Vector(GcPtr::new(via.persistent()?)),
166        );
167        let backtrace = Backtrace::capture();
168        if matches!(backtrace.status(), BacktraceStatus::Captured) {
169            // TODO, turn frames() into vector once stable?
170            map.assoc(
171                Value::keyword(Keyword::simple("trace")),
172                Value::string(format!("{}", backtrace)),
173            )?;
174        }
175        Ok(Value::Map(MapValue::Hash(GcPtr::new(map.persistent()?))))
176    }
177
178    pub fn message(&self) -> String {
179        self.message.to_string()
180    }
181
182    pub fn data(&self) -> Option<MapValue> {
183        self.data.as_ref().cloned()
184    }
185
186    pub fn cause(&self) -> Option<GcPtr<ExceptionInfo>> {
187        self.cause.as_ref().cloned()
188    }
189}
190
191impl Trace for ExceptionInfo {
192    fn trace(&self, visitor: &mut MarkVisitor) {
193        if let Some(data) = &self.data {
194            data.trace(visitor);
195        }
196        if let ValueError::Thrown(value) = &self.error {
197            value.trace(visitor);
198        }
199        if let Some(cause) = self.cause.as_ref() {
200            visitor.visit(cause);
201        }
202    }
203
204    fn gc_size_extra(&self) -> usize {
205        self.message.capacity()
206    }
207}
208
209impl ClojureHash for ExceptionInfo {
210    fn clojure_hash(&self) -> u32 {
211        let msg_hash = hash_string(self.message.as_ref());
212        let cause_hash = self
213            .cause
214            .as_ref()
215            .map(|c| c.get().clojure_hash())
216            .unwrap_or(0);
217        hash_combine_ordered(msg_hash, cause_hash)
218    }
219}