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