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    #[error("out of range")]
45    OutOfRange,
46
47    #[error("transient already persisted")]
48    TransientAlreadyPersisted,
49
50    #[error("could not parse value")]
51    Parse,
52
53    #[error("thrown exception")]
54    Thrown(Value),
55}
56
57pub type ValueResult<T> = Result<T, ValueError>;
58
59#[derive(Clone, Debug)]
60pub struct ExceptionInfo {
61    pub(crate) error: ValueError,
62    pub(crate) message: String,
63    pub(crate) data: Option<MapValue>,
64    pub(crate) cause: Option<GcPtr<ExceptionInfo>>,
65}
66
67impl ExceptionInfo {
68    pub fn new(
69        error: ValueError,
70        message: String,
71        data: Option<MapValue>,
72        cause: Option<GcPtr<ExceptionInfo>>,
73    ) -> Self {
74        Self {
75            error,
76            message,
77            data: data.as_ref().cloned(),
78            cause: cause.as_ref().cloned(),
79        }
80    }
81
82    /// Short name of the underlying error kind (`"WrongType"`, `"Other"`,
83    /// …) — the `:type` of this exception's `:via` entry.
84    pub fn type_name(&self) -> &'static str {
85        match self.error {
86            ValueError::WrongType { .. } => "WrongType",
87            ValueError::IndexOutOfBounds { .. } => "IndexOutOfBounds",
88            ValueError::ArityError { .. } => "ArityError",
89            ValueError::NotCallable { .. } => "NotCallable",
90            ValueError::OddMap { .. } => "OddMap",
91            ValueError::Unsupported => "Unsupported",
92            ValueError::Other(_) => "Other",
93            ValueError::GasExhausted => "GasExhausted",
94            ValueError::OutOfRange => "OutOfRange",
95            ValueError::TransientAlreadyPersisted => "TransientAlreadyPersisted",
96            ValueError::Parse => "ParseError",
97            ValueError::Thrown(_) => "Thrown",
98        }
99    }
100
101    /// This exception alone (causes excluded) as a `Throwable->map` `:via`
102    /// entry: `{:type .. :message .. :data ..}`.
103    pub fn to_via_map(&self) -> ValueResult<Value> {
104        let map = TransientMap::new();
105        map.assoc(
106            Value::keyword(Keyword::simple("type")),
107            Value::string(self.type_name()),
108        )?;
109        map.assoc(
110            Value::keyword(Keyword::simple("message")),
111            Value::string(&self.message),
112        )?;
113        if let Some(info) = self.data.as_ref() {
114            map.assoc(
115                Value::keyword(Keyword::simple("data")),
116                Value::Map(info.clone()),
117            )?;
118        }
119        // TODO, add :at (source location)
120        Ok(Value::Map(MapValue::Hash(GcPtr::new(map.persistent()?))))
121    }
122
123    pub fn to_map(&self) -> ValueResult<Value> {
124        let map = TransientMap::new();
125        map.assoc(
126            Value::keyword(Keyword::simple("cause")),
127            self.cause
128                .as_ref()
129                .map(|c| Value::Str(GcPtr::new(c.get().message.to_string())))
130                .unwrap_or(Value::Str(GcPtr::new(self.message.to_string()))),
131        )?;
132        if let Some(info) = self.data.as_ref() {
133            map.assoc(
134                Value::keyword(Keyword::simple("data")),
135                Value::Map(info.clone()),
136            )?;
137        }
138        let via = TransientVector::new();
139        via.append(self.to_via_map()?);
140        let mut cur = self.cause.as_ref();
141        while let Some(e) = cur {
142            via.append(e.get().to_via_map()?);
143            cur = e.get().cause.as_ref();
144        }
145        map.assoc(
146            Value::keyword(Keyword::simple("via")),
147            Value::Vector(GcPtr::new(via.persistent()?)),
148        );
149        let backtrace = Backtrace::capture();
150        if matches!(backtrace.status(), BacktraceStatus::Captured) {
151            // TODO, turn frames() into vector once stable?
152            map.assoc(
153                Value::keyword(Keyword::simple("trace")),
154                Value::string(format!("{}", backtrace)),
155            )?;
156        }
157        Ok(Value::Map(MapValue::Hash(GcPtr::new(map.persistent()?))))
158    }
159
160    pub fn message(&self) -> String {
161        self.message.to_string()
162    }
163
164    pub fn data(&self) -> Option<MapValue> {
165        self.data.as_ref().cloned()
166    }
167
168    pub fn cause(&self) -> Option<GcPtr<ExceptionInfo>> {
169        self.cause.as_ref().cloned()
170    }
171}
172
173impl Trace for ExceptionInfo {
174    fn trace(&self, visitor: &mut MarkVisitor) {
175        if let Some(cause) = self.cause.as_ref() {
176            visitor.visit(cause);
177        }
178    }
179
180    fn gc_size_extra(&self) -> usize {
181        self.message.capacity()
182    }
183}
184
185impl ClojureHash for ExceptionInfo {
186    fn clojure_hash(&self) -> u32 {
187        let msg_hash = hash_string(self.message.as_ref());
188        let cause_hash = self
189            .cause
190            .as_ref()
191            .map(|c| c.get().clojure_hash())
192            .unwrap_or(0);
193        hash_combine_ordered(msg_hash, cause_hash)
194    }
195}