1use std::sync::Arc;
10
11use tatara_lisp::{caret_run, line_at, span_width_chars, Span};
12use thiserror::Error;
13
14use crate::env::Seal;
15use crate::ffi::Arity;
16
17pub type Result<T> = std::result::Result<T, EvalError>;
18
19#[derive(Debug, Error)]
20pub enum EvalError {
21 #[error("unbound symbol: {name} at {at}")]
22 UnboundSymbol { name: Arc<str>, at: Span },
23
24 #[error("arity mismatch in {fn_name}: expected {expected:?}, got {got} at {at}")]
25 ArityMismatch {
26 fn_name: Arc<str>,
27 expected: Arity,
28 got: usize,
29 at: Span,
30 },
31
32 #[error("type mismatch: expected {expected}, got {got} at {at}")]
33 TypeMismatch {
34 expected: &'static str,
35 got: &'static str,
36 at: Span,
37 },
38
39 #[error("division by zero at {at}")]
40 DivisionByZero { at: Span },
41
42 #[error("macro `{macro_name}` exceeded the expansion limit of {limit} rewrites at {at} — it likely expands to a call to itself")]
48 MacroExpansionLimit {
49 macro_name: Arc<str>,
50 limit: usize,
51 at: Span,
52 },
53
54 #[error("not callable: value of type {value_kind} at {at}")]
55 NotCallable { value_kind: &'static str, at: Span },
56
57 #[error("cannot `set!` `{name}`: {} at {at}", seal.refusal())]
65 SetSealed {
66 name: Arc<str>,
67 seal: Seal,
68 at: Span,
69 },
70
71 #[error("bad special form `{form}`: {reason} at {at}")]
72 BadSpecialForm {
73 form: Arc<str>,
74 reason: String,
75 at: Span,
76 },
77
78 #[error("in native fn {name}: {reason} at {at}")]
79 NativeFn {
80 name: Arc<str>,
81 reason: String,
82 at: Span,
83 },
84
85 #[error("reader error: {0}")]
86 Reader(#[from] tatara_lisp::LispError),
87
88 #[error("halted (host-initiated interrupt)")]
89 Halted,
90
91 #[error("not yet implemented: {0} (Phase 2.3+)")]
92 NotImplemented(&'static str),
93
94 #[error("user error: {value}")]
99 User {
100 value: crate::value::Value,
101 at: Span,
102 },
103}
104
105impl EvalError {
106 pub fn unbound(name: impl Into<Arc<str>>, at: Span) -> Self {
107 Self::UnboundSymbol {
108 name: name.into(),
109 at,
110 }
111 }
112
113 pub fn type_mismatch(expected: &'static str, got: &'static str, at: Span) -> Self {
114 Self::TypeMismatch { expected, got, at }
115 }
116
117 pub fn native_fn(name: impl Into<Arc<str>>, reason: impl Into<String>, at: Span) -> Self {
118 Self::NativeFn {
119 name: name.into(),
120 reason: reason.into(),
121 at,
122 }
123 }
124
125 pub fn bad_form(form: impl Into<Arc<str>>, reason: impl Into<String>, at: Span) -> Self {
126 Self::BadSpecialForm {
127 form: form.into(),
128 reason: reason.into(),
129 at,
130 }
131 }
132
133 #[must_use]
141 pub const fn tag(&self) -> &'static str {
142 match self {
143 Self::UnboundSymbol { .. } => "unbound-symbol",
144 Self::ArityMismatch { .. } => "arity-mismatch",
145 Self::TypeMismatch { .. } => "type-mismatch",
146 Self::DivisionByZero { .. } => "division-by-zero",
147 Self::MacroExpansionLimit { .. } => "macro-expansion-limit",
148 Self::NotCallable { .. } => "not-callable",
149 Self::BadSpecialForm { .. } => "bad-special-form",
150 Self::NativeFn { .. } => "native-fn",
151 Self::SetSealed { .. } => "set-sealed",
152 Self::Reader(_) => "reader",
153 Self::Halted => "halted",
154 Self::NotImplemented(_) => "not-implemented",
155 Self::User { .. } => "user",
156 }
157 }
158
159 pub fn span(&self) -> Option<Span> {
161 match self {
162 Self::UnboundSymbol { at, .. }
163 | Self::ArityMismatch { at, .. }
164 | Self::TypeMismatch { at, .. }
165 | Self::MacroExpansionLimit { at, .. }
166 | Self::DivisionByZero { at }
167 | Self::NotCallable { at, .. }
168 | Self::BadSpecialForm { at, .. }
169 | Self::NativeFn { at, .. }
170 | Self::SetSealed { at, .. }
171 | Self::User { at, .. } => Some(*at),
172 Self::Reader(_) | Self::Halted | Self::NotImplemented(_) => None,
173 }
174 }
175
176 pub fn render(&self, src: &str) -> String {
205 let Some(span) = self.span() else {
206 return self.to_string();
207 };
208 if span.is_synthetic() || span.end > src.len() {
209 return self.to_string();
210 }
211
212 let (line_no, col) = Span::line_col(src, span.start);
213 let line = line_at(src, span.start);
214 let line_num_str = format!("{line_no}");
215 let gutter = " ".repeat(line_num_str.len());
216
217 let caret_line = format!(
227 "{gutter} | {run}",
228 run = caret_run(line, col, span_width_chars(src, span))
229 );
230
231 let summary = self.short_message();
232 format!(
233 "error: {summary}\n at line {line_no}, column {col}\n{line_num_str} | {line}\n{caret_line}",
234 )
235 }
236
237 pub fn short_message(&self) -> String {
239 match self {
240 Self::UnboundSymbol { name, .. } => format!("unbound symbol `{name}`"),
241 Self::SetSealed { name, seal, .. } => {
242 format!("`set!` of sealed binding `{name}` ({seal:?})")
243 }
244 Self::MacroExpansionLimit {
245 macro_name, limit, ..
246 } => format!("macro `{macro_name}` exceeded {limit} expansion steps"),
247 Self::ArityMismatch {
248 fn_name,
249 expected,
250 got,
251 ..
252 } => format!("`{fn_name}` expected {expected:?}, got {got}"),
253 Self::TypeMismatch { expected, got, .. } => {
254 format!("type mismatch: expected {expected}, got {got}")
255 }
256 Self::DivisionByZero { .. } => "division by zero".into(),
257 Self::NotCallable { value_kind, .. } => {
258 format!("value of type {value_kind} is not callable")
259 }
260 Self::BadSpecialForm { form, reason, .. } => {
261 format!("bad `{form}`: {reason}")
262 }
263 Self::NativeFn { name, reason, .. } => format!("in native `{name}`: {reason}"),
264 Self::Reader(e) => format!("reader: {e}"),
265 Self::Halted => "halted".into(),
266 Self::NotImplemented(what) => format!("not yet implemented: {what}"),
267 Self::User { value, .. } => format!("uncaught: {value}"),
268 }
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
277 fn render_slices_the_span_s_own_line_via_the_shared_slicer() {
278 let src = "aaa\nbbb\nccc";
283 let rendered = EvalError::unbound("bbb", Span::new(4, 7)).render(src);
284 assert!(
285 rendered.contains("2 | bbb"),
286 "line 2 must be the rendered snippet, got:\n{rendered}"
287 );
288 assert!(
289 !rendered.contains("aaa") && !rendered.contains("ccc"),
290 "only the span's own line may be rendered, got:\n{rendered}"
291 );
292 }
293
294 #[test]
295 fn render_mirrors_a_tab_indent_into_the_caret_pad() {
296 let src = "\tfoo";
302 let rendered = EvalError::unbound("foo", Span::new(1, 4)).render(src);
303 assert!(
304 rendered.contains("\t^^^"),
305 "caret pad must mirror the source tab, got:\n{rendered:?}"
306 );
307 assert!(
308 !rendered.contains(" ^^^"),
309 "a space-padded run is the pre-lift drift, got:\n{rendered:?}"
310 );
311 }
312
313 #[test]
314 fn render_sizes_the_caret_run_in_chars_not_bytes() {
315 let src = "(+ éé 1)";
319 let start = src.find("éé").expect("fixture contains the symbol");
320 let span = Span::new(start, start + "éé".len());
321 let rendered = EvalError::unbound("éé", span).render(src);
322 assert!(
323 rendered.contains(" ^^\n") || rendered.ends_with(" ^^"),
324 "two chars must draw exactly two carets, got:\n{rendered:?}"
325 );
326 assert!(
327 !rendered.contains("^^^"),
328 "a byte-sized run over-underlines multi-byte source, got:\n{rendered:?}"
329 );
330 }
331
332 #[test]
333 fn render_includes_line_col_and_caret() {
334 let err = EvalError::unbound("foo", Span::new(4, 7));
335 let src = "(+ x foo y)";
336 let rendered = err.render(src);
337 assert!(rendered.contains("unbound symbol `foo`"));
338 assert!(rendered.contains("line 1, column 5"));
339 assert!(rendered.contains("(+ x foo y)"));
340 assert!(rendered.contains("^^^"));
341 }
342
343 #[test]
344 fn render_without_span_falls_back_to_display() {
345 let err = EvalError::Halted;
346 assert!(!err.render("ignored").is_empty());
347 }
348
349 #[test]
350 fn render_synthetic_span_falls_back() {
351 let err = EvalError::unbound("x", Span::synthetic());
352 let rendered = err.render("some source");
353 assert!(!rendered.contains("line"));
355 }
356
357 #[test]
358 fn short_message_for_each_variant() {
359 use crate::ffi::Arity;
360
361 assert!(EvalError::DivisionByZero {
362 at: Span::synthetic(),
363 }
364 .short_message()
365 .contains("division"));
366
367 assert!(EvalError::unbound("foo", Span::synthetic())
368 .short_message()
369 .contains("foo"));
370
371 assert!(EvalError::ArityMismatch {
372 fn_name: "+".into(),
373 expected: Arity::Exact(2),
374 got: 3,
375 at: Span::synthetic(),
376 }
377 .short_message()
378 .contains("got 3"));
379 }
380}