Skip to main content

blue_lang_runtime/
errors.rs

1use std::sync::Arc;
2
3use tatara_lisp_eval::ffi::{Arity, Caller};
4use tatara_lisp_eval::value::{ErrorObj, MapKey};
5use tatara_lisp_eval::{EvalError, Interpreter, Value};
6
7pub const RAISE_AT: &str = "blue-raise-at";
8
9const DATA: &str = "data";
10const CAUSE: &str = "cause";
11const TRACE: &str = "trace";
12
13fn keyword(s: &str) -> Value {
14    Value::Keyword(Arc::from(s))
15}
16
17fn data_key(k: &Value) -> Option<&str> {
18    match k {
19        Value::Keyword(s) => Some(s),
20        _ => None,
21    }
22}
23
24fn raised_by_blue(e: &ErrorObj) -> bool {
25    !e.data.is_empty()
26        && e.data
27            .iter()
28            .all(|(k, _)| matches!(data_key(k), Some(DATA | CAUSE | TRACE)))
29}
30
31fn entry<'a>(e: &'a ErrorObj, name: &str) -> Option<&'a Value> {
32    e.data
33        .iter()
34        .find(|(k, _)| data_key(k) == Some(name))
35        .map(|(_, v)| v)
36}
37
38#[must_use]
39pub fn error_data(e: &ErrorObj) -> Value {
40    if raised_by_blue(e) {
41        return entry(e, DATA).cloned().unwrap_or(Value::Nil);
42    }
43    if e.data.is_empty() {
44        return Value::Nil;
45    }
46    Value::map(
47        e.data
48            .iter()
49            .filter_map(|(k, v)| MapKey::from_value(k).map(|key| (key, v.clone()))),
50    )
51}
52
53#[must_use]
54pub fn error_cause(e: &ErrorObj) -> Value {
55    if raised_by_blue(e) {
56        return entry(e, CAUSE).cloned().unwrap_or(Value::Nil);
57    }
58    Value::Nil
59}
60
61#[must_use]
62pub fn error_trace(e: &ErrorObj) -> Vec<String> {
63    if !raised_by_blue(e) {
64        return Vec::new();
65    }
66    match entry(e, TRACE) {
67        Some(Value::List(xs)) => xs
68            .iter()
69            .filter_map(|x| match x {
70                Value::Str(s) => Some(s.to_string()),
71                _ => None,
72            })
73            .collect(),
74        _ => Vec::new(),
75    }
76}
77
78#[must_use]
79pub fn describe(e: &ErrorObj) -> String {
80    let mut out = format!("{}: {}", e.tag, e.message);
81    if let Some(at) = error_trace(e).first() {
82        out.push_str(&format!(" (at {at})"));
83    }
84    out
85}
86
87#[must_use]
88pub fn chain(value: &Value) -> Vec<String> {
89    let mut out = Vec::new();
90    let mut at = value.clone();
91    while let Value::Error(e) = at {
92        out.push(describe(&e));
93        if out.len() > 64 {
94            break;
95        }
96        at = error_cause(&e);
97    }
98    out
99}
100
101fn build(
102    kind: &Value,
103    message: &Value,
104    opts: Option<&Value>,
105    trace: Option<String>,
106    sp: tatara_lisp::Span,
107) -> Result<Value, EvalError> {
108    let tag: Arc<str> = match kind {
109        Value::Keyword(s) | Value::Symbol(s) => s.clone(),
110        other => {
111            return Err(EvalError::type_mismatch(
112                "an error kind such as :not_found",
113                other.type_name(),
114                sp,
115            ))
116        }
117    };
118    let message: Arc<str> = match message {
119        Value::Str(s) => s.clone(),
120        other => {
121            return Err(EvalError::type_mismatch(
122                "a message string",
123                other.type_name(),
124                sp,
125            ))
126        }
127    };
128    let mut data = Vec::new();
129    match opts {
130        None | Some(Value::Nil) => {}
131        Some(Value::Map(m)) => {
132            for (k, v) in m.iter() {
133                match k {
134                    MapKey::Keyword(name) | MapKey::Symbol(name)
135                        if matches!(&**name, DATA | CAUSE) =>
136                    {
137                        data.push((keyword(name), v.clone()));
138                    }
139                    other => {
140                        return Err(EvalError::native_fn(
141                            "raise",
142                            format!(
143                                "its options are {{data: …, cause: …}}, and `{}` is neither",
144                                crate::literal::render_value(&other.to_value())
145                            ),
146                            sp,
147                        ))
148                    }
149                }
150            }
151        }
152        Some(other) => {
153            return Err(EvalError::type_mismatch(
154                "raise's options {data: …, cause: …}",
155                other.type_name(),
156                sp,
157            ))
158        }
159    }
160    if let Some(at) = trace {
161        data.push((keyword(TRACE), Value::list(vec![Value::Str(Arc::from(at))])));
162    }
163    if data.is_empty() {
164        data.push((keyword(DATA), Value::Nil));
165    }
166    Ok(Value::Error(Arc::new(ErrorObj { tag, message, data })))
167}
168
169fn raise_with(
170    args: &[Value],
171    trace: Option<String>,
172    sp: tatara_lisp::Span,
173) -> Result<Value, EvalError> {
174    let value = match args {
175        [single] => single.clone(),
176        [kind, message] => build(kind, message, None, trace, sp)?,
177        [kind, message, opts] => build(kind, message, Some(opts), trace, sp)?,
178        _ => {
179            return Err(EvalError::native_fn(
180                "raise",
181                "write raise(:kind, \"message\"), raise(:kind, \"message\", {data: …, cause: e}) or raise(e)",
182                sp,
183            ))
184        }
185    };
186    Err(EvalError::User { value, at: sp })
187}
188
189const NIL_TEST: &str = "nil?";
190
191fn field(v: &Value, name: &str, sp: tatara_lisp::Span) -> Result<Value, EvalError> {
192    let missing = |what: String| Err(EvalError::native_fn(format!(".{name}"), what, sp));
193    if name == NIL_TEST
194        && !matches!(v, Value::Map(m) if m.iter().any(|(k, _)| matches!(k, MapKey::Keyword(s) | MapKey::Str(s) if &**s == name)))
195    {
196        return Ok(Value::Bool(matches!(v, Value::Nil)));
197    }
198    match v {
199        Value::Map(m) => {
200            let found = m
201                .iter()
202                .find(|(k, _)| matches!(k, MapKey::Keyword(s) | MapKey::Str(s) if &**s == name))
203                .map(|(_, v)| v.clone());
204            match found {
205                Some(x) => Ok(x),
206                None => {
207                    let mut fields: Vec<String> = m
208                        .iter()
209                        .filter_map(|(k, _)| match k {
210                            MapKey::Keyword(s) | MapKey::Str(s) => Some(s.to_string()),
211                            _ => None,
212                        })
213                        .collect();
214                    fields.sort();
215                    missing(format!(
216                        "the record has no field `{name}` (its fields: {}); get(m, :{name}) reads a field that may be absent",
217                        if fields.is_empty() { "none".to_string() } else { fields.join(", ") }
218                    ))
219                }
220            }
221        }
222        Value::Error(e) => Ok(match name {
223            "kind" => Value::Keyword(e.tag.clone()),
224            "message" => Value::Str(e.message.clone()),
225            DATA => error_data(e),
226            CAUSE => error_cause(e),
227            TRACE => Value::list(
228                error_trace(e)
229                    .into_iter()
230                    .map(|s| Value::Str(Arc::from(s))),
231            ),
232            other => {
233                return missing(format!(
234                    "an error's fields are kind, message, data, cause and trace, not `{other}`"
235                ))
236            }
237        }),
238        Value::List(items) => {
239            let pairs = !items.is_empty()
240                && items.iter().all(|i| {
241                    matches!(i, Value::List(p) if p.len() == 2 && matches!(p[0], Value::Keyword(_) | Value::Str(_)))
242                });
243            if pairs {
244                for i in items.iter() {
245                    if let Value::List(p) = i {
246                        if matches!(&p[0], Value::Keyword(k) | Value::Str(k) if &**k == name) {
247                            return Ok(p[1].clone());
248                        }
249                    }
250                }
251                return missing(format!("the record has no field `{name}`"));
252            }
253            missing(format!(
254                "`.{name}` reads a record's field, and a list has none: to call `{name}` on it, write `{name}(xs)`"
255            ))
256        }
257        other => missing(format!(
258            "`.{name}` reads a record's field, and {} has none: to call `{name}` on it, write `{name}(x)`",
259            other.type_name()
260        )),
261    }
262}
263
264fn kind_matches(v: &Value, kinds: &[Value]) -> bool {
265    let tag: Option<&str> = match v {
266        Value::Error(e) => Some(&e.tag),
267        _ => None,
268    };
269    kinds.iter().any(|k| match (k, tag) {
270        (Value::Keyword(k), Some(t)) => &**k == t,
271        _ => false,
272    })
273}
274
275pub fn install<H: 'static>(interp: &mut Interpreter<H>) {
276    interp.register_fn("raise", Arity::Range(1, 3), |a: &[Value], _h: &mut H, s| {
277        raise_with(a, None, s)
278    });
279    interp.register_fn(
280        RAISE_AT,
281        Arity::Range(2, 4),
282        |a: &[Value], _h: &mut H, s| {
283            let at = match &a[0] {
284                Value::Str(x) => Some(x.to_string()),
285                _ => None,
286            };
287            raise_with(&a[1..], at, s)
288        },
289    );
290    interp.register_fn(
291        blue_lang_syntax::LOWERED_RERAISE,
292        Arity::Exact(1),
293        |a: &[Value], _h: &mut H, s| {
294            Err(EvalError::User {
295                value: a[0].clone(),
296                at: s,
297            })
298        },
299    );
300    interp.register_fn(
301        blue_lang_syntax::LOWERED_ERROR_MATCH,
302        Arity::AtLeast(2),
303        |a: &[Value], _h: &mut H, _s| Ok(Value::Bool(kind_matches(&a[0], &a[1..]))),
304    );
305    interp.register_fn(
306        blue_lang_syntax::LOWERED_FIELD,
307        Arity::Exact(2),
308        |a: &[Value], _h: &mut H, s| match &a[1] {
309            Value::Keyword(k) => field(&a[0], k, s),
310            other => Err(EvalError::type_mismatch(
311                "a field name",
312                other.type_name(),
313                s,
314            )),
315        },
316    );
317    interp.register_fn(
318        blue_lang_syntax::LOWERED_ATTEMPTED,
319        Arity::Exact(1),
320        |a: &[Value], _h: &mut H, _s| Ok(Value::list(vec![keyword("ok"), a[0].clone()])),
321    );
322    interp.register_fn(
323        blue_lang_syntax::LOWERED_FAILED,
324        Arity::Exact(1),
325        |a: &[Value], _h: &mut H, _s| Ok(Value::list(vec![keyword("raised"), a[0].clone()])),
326    );
327    interp.register_higher_order_fn(
328        blue_lang_syntax::LOWERED_ENSURE,
329        Arity::Exact(2),
330        |a: &[Value], host: &mut H, caller: &Caller<H>, s| {
331            caller.apply_value(&a[1], Vec::new(), host, s)?;
332            match &a[0] {
333                Value::List(p) if p.len() == 2 => match &p[0] {
334                    Value::Keyword(k) if &**k == "raised" => Err(EvalError::User {
335                        value: p[1].clone(),
336                        at: s,
337                    }),
338                    _ => Ok(p[1].clone()),
339                },
340                other => Ok(other.clone()),
341            }
342        },
343    );
344}