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}