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
189pub fn raise_kind(kind: &str, message: String, sp: tatara_lisp::Span) -> EvalError {
190 raise_kind_with(kind, message, Value::Nil, sp)
191}
192
193pub fn raise_kind_with(
194 kind: &str,
195 message: String,
196 data: Value,
197 sp: tatara_lisp::Span,
198) -> EvalError {
199 let mut opts = tatara_lisp_eval::Map::new();
200 opts.insert(MapKey::Keyword(Arc::from(DATA)), data);
201 let args = [
202 keyword(kind),
203 Value::Str(message.into()),
204 Value::Map(Arc::new(opts)),
205 ];
206 match raise_with(&args, None, sp) {
207 Err(e) => e,
208 Ok(_) => unreachable!("raise_with always raises"),
209 }
210}
211
212const NIL_TEST: &str = "nil?";
213
214fn field(v: &Value, name: &str, sp: tatara_lisp::Span) -> Result<Value, EvalError> {
215 let missing = |what: String| Err(EvalError::native_fn(format!(".{name}"), what, sp));
216 if name == NIL_TEST
217 && !matches!(v, Value::Map(m) if m.iter().any(|(k, _)| matches!(k, MapKey::Keyword(s) | MapKey::Str(s) if &**s == name)))
218 {
219 return Ok(Value::Bool(matches!(v, Value::Nil)));
220 }
221 match v {
222 Value::Map(m) => {
223 let found = m
224 .iter()
225 .find(|(k, _)| matches!(k, MapKey::Keyword(s) | MapKey::Str(s) if &**s == name))
226 .map(|(_, v)| v.clone());
227 match found {
228 Some(x) => Ok(x),
229 None => {
230 let mut fields: Vec<String> = m
231 .iter()
232 .filter_map(|(k, _)| match k {
233 MapKey::Keyword(s) | MapKey::Str(s) => Some(s.to_string()),
234 _ => None,
235 })
236 .collect();
237 fields.sort();
238 missing(format!(
239 "the record has no field `{name}` (its fields: {}); get(m, :{name}) reads a field that may be absent",
240 if fields.is_empty() { "none".to_string() } else { fields.join(", ") }
241 ))
242 }
243 }
244 }
245 Value::Error(e) => Ok(match name {
246 "kind" => Value::Keyword(e.tag.clone()),
247 "message" => Value::Str(e.message.clone()),
248 DATA => error_data(e),
249 CAUSE => error_cause(e),
250 TRACE => Value::list(
251 error_trace(e)
252 .into_iter()
253 .map(|s| Value::Str(Arc::from(s))),
254 ),
255 other => {
256 return missing(format!(
257 "an error's fields are kind, message, data, cause and trace, not `{other}`"
258 ))
259 }
260 }),
261 Value::List(items) => {
262 let pairs = !items.is_empty()
263 && items.iter().all(|i| {
264 matches!(i, Value::List(p) if p.len() == 2 && matches!(p[0], Value::Keyword(_) | Value::Str(_)))
265 });
266 if pairs {
267 for i in items.iter() {
268 if let Value::List(p) = i {
269 if matches!(&p[0], Value::Keyword(k) | Value::Str(k) if &**k == name) {
270 return Ok(p[1].clone());
271 }
272 }
273 }
274 return missing(format!("the record has no field `{name}`"));
275 }
276 missing(format!(
277 "`.{name}` reads a record's field, and a list has none: to call `{name}` on it, write `{name}(xs)`"
278 ))
279 }
280 other => missing(format!(
281 "`.{name}` reads a record's field, and {} has none: to call `{name}` on it, write `{name}(x)`",
282 other.type_name()
283 )),
284 }
285}
286
287fn kind_matches(v: &Value, kinds: &[Value]) -> bool {
288 let tag: Option<&str> = match v {
289 Value::Error(e) => Some(&e.tag),
290 _ => None,
291 };
292 kinds.iter().any(|k| match (k, tag) {
293 (Value::Keyword(k), Some(t)) => &**k == t,
294 _ => false,
295 })
296}
297
298pub fn install<H: 'static>(interp: &mut Interpreter<H>) {
299 interp.register_fn("raise", Arity::Range(1, 3), |a: &[Value], _h: &mut H, s| {
300 raise_with(a, None, s)
301 });
302 interp.register_fn(
303 RAISE_AT,
304 Arity::Range(2, 4),
305 |a: &[Value], _h: &mut H, s| {
306 let at = match &a[0] {
307 Value::Str(x) => Some(x.to_string()),
308 _ => None,
309 };
310 raise_with(&a[1..], at, s)
311 },
312 );
313 interp.register_fn(
314 blue_lang_syntax::LOWERED_RERAISE,
315 Arity::Exact(1),
316 |a: &[Value], _h: &mut H, s| {
317 Err(EvalError::User {
318 value: a[0].clone(),
319 at: s,
320 })
321 },
322 );
323 interp.register_fn(
324 blue_lang_syntax::LOWERED_ERROR_MATCH,
325 Arity::AtLeast(2),
326 |a: &[Value], _h: &mut H, _s| Ok(Value::Bool(kind_matches(&a[0], &a[1..]))),
327 );
328 interp.register_fn(
329 blue_lang_syntax::LOWERED_FIELD,
330 Arity::Exact(2),
331 |a: &[Value], _h: &mut H, s| match &a[1] {
332 Value::Keyword(k) => field(&a[0], k, s),
333 other => Err(EvalError::type_mismatch(
334 "a field name",
335 other.type_name(),
336 s,
337 )),
338 },
339 );
340 interp.register_fn(
341 blue_lang_syntax::LOWERED_ATTEMPTED,
342 Arity::Exact(1),
343 |a: &[Value], _h: &mut H, _s| Ok(Value::list(vec![keyword("ok"), a[0].clone()])),
344 );
345 interp.register_fn(
346 blue_lang_syntax::LOWERED_FAILED,
347 Arity::Exact(1),
348 |a: &[Value], _h: &mut H, _s| Ok(Value::list(vec![keyword("raised"), a[0].clone()])),
349 );
350 interp.register_higher_order_fn(
351 blue_lang_syntax::LOWERED_ENSURE,
352 Arity::Exact(2),
353 |a: &[Value], host: &mut H, caller: &Caller<H>, s| {
354 caller.apply_value(&a[1], Vec::new(), host, s)?;
355 match &a[0] {
356 Value::List(p) if p.len() == 2 => match &p[0] {
357 Value::Keyword(k) if &**k == "raised" => Err(EvalError::User {
358 value: p[1].clone(),
359 at: s,
360 }),
361 _ => Ok(p[1].clone()),
362 },
363 other => Ok(other.clone()),
364 }
365 },
366 );
367}