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