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    fn to_via_map(&self) -> ValueResult<Value> {
83        let map = TransientMap::new();
84        map.assoc(
85            Value::keyword(Keyword::simple("type")),
86            Value::string(match self.error {
87                ValueError::WrongType { .. } => "WrongType",
88                ValueError::IndexOutOfBounds { .. } => "IndexOutOfBounds",
89                ValueError::ArityError { .. } => "ArityError",
90                ValueError::NotCallable { .. } => "NotCallable",
91                ValueError::OddMap { .. } => "OddMap",
92                ValueError::Unsupported => "Unsupported",
93                ValueError::Other(_) => "Other",
94                ValueError::GasExhausted => "GasExhausted",
95                ValueError::OutOfRange => "OutOfRange",
96                ValueError::TransientAlreadyPersisted => "TransientAlreadyPersisted",
97                ValueError::Parse => "ParseError",
98                ValueError::Thrown(_) => "Thrown",
99            }),
100        )?;
101        map.assoc(
102            Value::keyword(Keyword::simple("message")),
103            Value::string(&self.message),
104        )?;
105        if let Some(info) = self.data.as_ref() {
106            map.assoc(
107                Value::keyword(Keyword::simple("data")),
108                Value::Map(info.clone()),
109            )?;
110        }
111        // TODO, add :at (source location)
112        Ok(Value::Map(MapValue::Hash(GcPtr::new(map.persistent()?))))
113    }
114
115    pub fn to_map(&self) -> ValueResult<Value> {
116        let map = TransientMap::new();
117        map.assoc(
118            Value::keyword(Keyword::simple("cause")),
119            self.cause
120                .as_ref()
121                .map(|c| Value::Str(GcPtr::new(c.get().message.to_string())))
122                .unwrap_or(Value::Str(GcPtr::new(self.message.to_string()))),
123        )?;
124        if let Some(info) = self.data.as_ref() {
125            map.assoc(
126                Value::keyword(Keyword::simple("data")),
127                Value::Map(info.clone()),
128            )?;
129        }
130        let via = TransientVector::new();
131        via.append(self.to_via_map()?);
132        let mut cur = self.cause.as_ref();
133        while let Some(e) = cur {
134            via.append(e.get().to_via_map()?);
135            cur = e.get().cause.as_ref();
136        }
137        map.assoc(
138            Value::keyword(Keyword::simple("via")),
139            Value::Vector(GcPtr::new(via.persistent()?)),
140        );
141        let backtrace = Backtrace::capture();
142        if matches!(backtrace.status(), BacktraceStatus::Captured) {
143            // TODO, turn frames() into vector once stable?
144            map.assoc(
145                Value::keyword(Keyword::simple("trace")),
146                Value::string(format!("{}", backtrace)),
147            )?;
148        }
149        Ok(Value::Map(MapValue::Hash(GcPtr::new(map.persistent()?))))
150    }
151
152    pub fn message(&self) -> String {
153        self.message.to_string()
154    }
155
156    pub fn data(&self) -> Option<MapValue> {
157        self.data.as_ref().cloned()
158    }
159
160    pub fn cause(&self) -> Option<GcPtr<ExceptionInfo>> {
161        self.cause.as_ref().cloned()
162    }
163}
164
165impl Trace for ExceptionInfo {
166    fn trace(&self, visitor: &mut MarkVisitor) {
167        if let Some(cause) = self.cause.as_ref() {
168            visitor.visit(cause);
169        }
170    }
171
172    fn gc_size_extra(&self) -> usize {
173        self.message.capacity()
174    }
175}
176
177impl ClojureHash for ExceptionInfo {
178    fn clojure_hash(&self) -> u32 {
179        let msg_hash = hash_string(self.message.as_ref());
180        let cause_hash = self
181            .cause
182            .as_ref()
183            .map(|c| c.get().clojure_hash())
184            .unwrap_or(0);
185        hash_combine_ordered(msg_hash, cause_hash)
186    }
187}