1use std::sync::Arc;
10
11use tatara_lisp::Span;
12use thiserror::Error;
13
14use crate::ffi::Arity;
15
16pub type Result<T> = std::result::Result<T, EvalError>;
17
18#[derive(Debug, Error)]
19pub enum EvalError {
20 #[error("unbound symbol: {name} at {at}")]
21 UnboundSymbol { name: Arc<str>, at: Span },
22
23 #[error("arity mismatch in {fn_name}: expected {expected:?}, got {got} at {at}")]
24 ArityMismatch {
25 fn_name: Arc<str>,
26 expected: Arity,
27 got: usize,
28 at: Span,
29 },
30
31 #[error("type mismatch: expected {expected}, got {got} at {at}")]
32 TypeMismatch {
33 expected: &'static str,
34 got: &'static str,
35 at: Span,
36 },
37
38 #[error("division by zero at {at}")]
39 DivisionByZero { at: Span },
40
41 #[error("not callable: value of type {value_kind} at {at}")]
42 NotCallable { value_kind: &'static str, at: Span },
43
44 #[error("bad special form `{form}`: {reason} at {at}")]
45 BadSpecialForm {
46 form: Arc<str>,
47 reason: String,
48 at: Span,
49 },
50
51 #[error("in native fn {name}: {reason} at {at}")]
52 NativeFn {
53 name: Arc<str>,
54 reason: String,
55 at: Span,
56 },
57
58 #[error("reader error: {0}")]
59 Reader(#[from] tatara_lisp::LispError),
60
61 #[error("halted (host-initiated interrupt)")]
62 Halted,
63
64 #[error("not yet implemented: {0} (Phase 2.3+)")]
65 NotImplemented(&'static str),
66
67 #[error("user error: {value}")]
72 User {
73 value: crate::value::Value,
74 at: Span,
75 },
76}
77
78impl EvalError {
79 pub fn unbound(name: impl Into<Arc<str>>, at: Span) -> Self {
80 Self::UnboundSymbol {
81 name: name.into(),
82 at,
83 }
84 }
85
86 pub fn type_mismatch(expected: &'static str, got: &'static str, at: Span) -> Self {
87 Self::TypeMismatch { expected, got, at }
88 }
89
90 pub fn native_fn(name: impl Into<Arc<str>>, reason: impl Into<String>, at: Span) -> Self {
91 Self::NativeFn {
92 name: name.into(),
93 reason: reason.into(),
94 at,
95 }
96 }
97
98 pub fn bad_form(form: impl Into<Arc<str>>, reason: impl Into<String>, at: Span) -> Self {
99 Self::BadSpecialForm {
100 form: form.into(),
101 reason: reason.into(),
102 at,
103 }
104 }
105
106 pub fn span(&self) -> Option<Span> {
108 match self {
109 Self::UnboundSymbol { at, .. }
110 | Self::ArityMismatch { at, .. }
111 | Self::TypeMismatch { at, .. }
112 | Self::DivisionByZero { at }
113 | Self::NotCallable { at, .. }
114 | Self::BadSpecialForm { at, .. }
115 | Self::NativeFn { at, .. }
116 | Self::User { at, .. } => Some(*at),
117 Self::Reader(_) | Self::Halted | Self::NotImplemented(_) => None,
118 }
119 }
120
121 pub fn render(&self, src: &str) -> String {
129 let Some(span) = self.span() else {
130 return self.to_string();
131 };
132 if span.is_synthetic() || span.end > src.len() {
133 return self.to_string();
134 }
135
136 let (line_no, col) = Span::line_col(src, span.start);
137 let line = find_line(src, span.start);
138 let line_num_str = format!("{line_no}");
139 let gutter = " ".repeat(line_num_str.len());
140
141 let col_offset = col.saturating_sub(1);
142 let len = (span.end - span.start).max(1);
143 let caret_line = format!(
144 "{gutter} | {blanks}{carets}",
145 blanks = " ".repeat(col_offset),
146 carets = "^".repeat(len)
147 );
148
149 let summary = self.short_message();
150 format!(
151 "error: {summary}\n at line {line_no}, column {col}\n{line_num_str} | {line}\n{caret_line}",
152 )
153 }
154
155 pub fn short_message(&self) -> String {
157 match self {
158 Self::UnboundSymbol { name, .. } => format!("unbound symbol `{name}`"),
159 Self::ArityMismatch {
160 fn_name,
161 expected,
162 got,
163 ..
164 } => format!("`{fn_name}` expected {expected:?}, got {got}"),
165 Self::TypeMismatch { expected, got, .. } => {
166 format!("type mismatch: expected {expected}, got {got}")
167 }
168 Self::DivisionByZero { .. } => "division by zero".into(),
169 Self::NotCallable { value_kind, .. } => {
170 format!("value of type {value_kind} is not callable")
171 }
172 Self::BadSpecialForm { form, reason, .. } => {
173 format!("bad `{form}`: {reason}")
174 }
175 Self::NativeFn { name, reason, .. } => format!("in native `{name}`: {reason}"),
176 Self::Reader(e) => format!("reader: {e}"),
177 Self::Halted => "halted".into(),
178 Self::NotImplemented(what) => format!("not yet implemented: {what}"),
179 Self::User { value, .. } => format!("uncaught: {value}"),
180 }
181 }
182}
183
184fn find_line(src: &str, pos: usize) -> &str {
186 let start = src[..pos].rfind('\n').map(|i| i + 1).unwrap_or(0);
187 let end = src[pos..].find('\n').map(|i| pos + i).unwrap_or(src.len());
188 &src[start..end]
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 #[test]
196 fn find_line_single_line() {
197 let src = "foo bar baz";
198 assert_eq!(find_line(src, 5), "foo bar baz");
199 }
200
201 #[test]
202 fn find_line_multi_line() {
203 let src = "aaa\nbbb\nccc";
204 assert_eq!(find_line(src, 0), "aaa");
205 assert_eq!(find_line(src, 4), "bbb");
206 assert_eq!(find_line(src, 8), "ccc");
207 }
208
209 #[test]
210 fn render_includes_line_col_and_caret() {
211 let err = EvalError::unbound("foo", Span::new(4, 7));
212 let src = "(+ x foo y)";
213 let rendered = err.render(src);
214 assert!(rendered.contains("unbound symbol `foo`"));
215 assert!(rendered.contains("line 1, column 5"));
216 assert!(rendered.contains("(+ x foo y)"));
217 assert!(rendered.contains("^^^"));
218 }
219
220 #[test]
221 fn render_without_span_falls_back_to_display() {
222 let err = EvalError::Halted;
223 assert!(!err.render("ignored").is_empty());
224 }
225
226 #[test]
227 fn render_synthetic_span_falls_back() {
228 let err = EvalError::unbound("x", Span::synthetic());
229 let rendered = err.render("some source");
230 assert!(!rendered.contains("line"));
232 }
233
234 #[test]
235 fn short_message_for_each_variant() {
236 use crate::ffi::Arity;
237
238 assert!(EvalError::DivisionByZero {
239 at: Span::synthetic(),
240 }
241 .short_message()
242 .contains("division"));
243
244 assert!(EvalError::unbound("foo", Span::synthetic())
245 .short_message()
246 .contains("foo"));
247
248 assert!(EvalError::ArityMismatch {
249 fn_name: "+".into(),
250 expected: Arity::Exact(2),
251 got: 3,
252 at: Span::synthetic(),
253 }
254 .short_message()
255 .contains("got 3"));
256 }
257}