1use std::collections::HashMap;
2use std::fmt;
3
4use crate::value::Value;
5
6#[macro_export]
16macro_rules! check_arity {
17 ($args:expr, $name:expr, $exact:literal) => {
18 if $args.len() != $exact {
19 return Err($crate::SemaError::arity(
20 $name,
21 stringify!($exact),
22 $args.len(),
23 ));
24 }
25 };
26 ($args:expr, $name:expr, $lo:literal ..= $hi:literal) => {
27 if $args.len() < $lo || $args.len() > $hi {
28 return Err($crate::SemaError::arity(
29 $name,
30 concat!(stringify!($lo), "-", stringify!($hi)),
31 $args.len(),
32 ));
33 }
34 };
35 ($args:expr, $name:expr, $lo:literal ..) => {
36 if $args.len() < $lo {
37 return Err($crate::SemaError::arity(
38 $name,
39 concat!(stringify!($lo), "+"),
40 $args.len(),
41 ));
42 }
43 };
44}
45
46#[derive(Debug, Clone, Copy)]
47pub struct Span {
48 pub line: usize,
49 pub col: usize,
50 pub end_line: usize,
51 pub end_col: usize,
52}
53
54impl Span {
55 pub fn point(line: usize, col: usize) -> Self {
57 Span {
58 line,
59 col,
60 end_line: line,
61 end_col: col,
62 }
63 }
64
65 pub fn new(line: usize, col: usize, end_line: usize, end_col: usize) -> Self {
67 Span {
68 line,
69 col,
70 end_line,
71 end_col,
72 }
73 }
74
75 pub fn to(self, other: &Span) -> Span {
77 Span {
78 line: self.line,
79 col: self.col,
80 end_line: other.end_line,
81 end_col: other.end_col,
82 }
83 }
84
85 pub fn with_end(self, end_line: usize, end_col: usize) -> Span {
87 Span {
88 line: self.line,
89 col: self.col,
90 end_line,
91 end_col,
92 }
93 }
94}
95
96impl fmt::Display for Span {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 write!(f, "{}:{}", self.line, self.col)
99 }
100}
101
102#[derive(Debug, Clone)]
104pub struct CallFrame {
105 pub name: String,
106 pub file: Option<std::path::PathBuf>,
107 pub span: Option<Span>,
108}
109
110#[derive(Debug, Clone)]
112pub struct StackTrace(pub Vec<CallFrame>);
113
114impl fmt::Display for StackTrace {
115 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116 for frame in &self.0 {
117 write!(f, " at {}", frame.name)?;
118 match (&frame.file, &frame.span) {
119 (Some(file), Some(span)) => writeln!(f, " ({}:{span})", file.display())?,
120 (Some(file), None) => writeln!(f, " ({})", file.display())?,
121 (None, Some(span)) => writeln!(f, " (<input>:{span})")?,
122 (None, None) => writeln!(f)?,
123 }
124 }
125 Ok(())
126 }
127}
128
129pub type SpanMap = HashMap<usize, Span>;
131
132#[derive(Debug, Clone, thiserror::Error)]
133pub enum SemaError {
134 #[error("Reader error at {span}: {message}")]
135 Reader { message: String, span: Span },
136
137 #[error("Eval error: {0}")]
138 Eval(String),
139
140 #[error("Type error: expected {expected}, got {got}")]
141 Type { expected: String, got: String },
142
143 #[error("Arity error: {name} expects {expected} args, got {got}")]
144 Arity {
145 name: String,
146 expected: String,
147 got: usize,
148 },
149
150 #[error("Unbound variable: {0}")]
151 Unbound(String),
152
153 #[error("LLM error: {0}")]
154 Llm(String),
155
156 #[error("IO error: {0}")]
157 Io(String),
158
159 #[error("Permission denied: {function} requires '{capability}' capability")]
160 PermissionDenied {
161 function: String,
162 capability: String,
163 },
164
165 #[error("Permission denied: {function} — path '{path}' is outside allowed directories")]
166 PathDenied { function: String, path: String },
167
168 #[error("User exception: {0}")]
169 UserException(Value),
170
171 #[error("{inner}")]
172 WithTrace {
173 inner: Box<SemaError>,
174 trace: StackTrace,
175 },
176
177 #[error("{inner}")]
178 WithContext {
179 inner: Box<SemaError>,
180 hint: Option<String>,
181 note: Option<String>,
182 },
183}
184
185impl SemaError {
186 pub fn eval(msg: impl Into<String>) -> Self {
187 SemaError::Eval(msg.into())
188 }
189
190 pub fn type_error(expected: impl Into<String>, got: impl Into<String>) -> Self {
191 SemaError::Type {
192 expected: expected.into(),
193 got: got.into(),
194 }
195 }
196
197 pub fn arity(name: impl Into<String>, expected: impl Into<String>, got: usize) -> Self {
198 SemaError::Arity {
199 name: name.into(),
200 expected: expected.into(),
201 got,
202 }
203 }
204
205 pub fn with_hint(self, hint: impl Into<String>) -> Self {
207 match self {
208 SemaError::WithContext { inner, note, .. } => SemaError::WithContext {
209 inner,
210 hint: Some(hint.into()),
211 note,
212 },
213 other => SemaError::WithContext {
214 inner: Box::new(other),
215 hint: Some(hint.into()),
216 note: None,
217 },
218 }
219 }
220
221 pub fn with_note(self, note: impl Into<String>) -> Self {
223 match self {
224 SemaError::WithContext { inner, hint, .. } => SemaError::WithContext {
225 inner,
226 hint,
227 note: Some(note.into()),
228 },
229 other => SemaError::WithContext {
230 inner: Box::new(other),
231 hint: None,
232 note: Some(note.into()),
233 },
234 }
235 }
236
237 pub fn hint(&self) -> Option<&str> {
239 match self {
240 SemaError::WithContext { hint, .. } => hint.as_deref(),
241 SemaError::WithTrace { inner, .. } => inner.hint(),
242 _ => None,
243 }
244 }
245
246 pub fn note(&self) -> Option<&str> {
248 match self {
249 SemaError::WithContext { note, .. } => note.as_deref(),
250 SemaError::WithTrace { inner, .. } => inner.note(),
251 _ => None,
252 }
253 }
254
255 pub fn with_stack_trace(self, trace: StackTrace) -> Self {
257 if trace.0.is_empty() {
258 return self;
259 }
260 match self {
261 SemaError::WithTrace { .. } => self,
262 SemaError::WithContext { inner, hint, note } => SemaError::WithContext {
263 inner: Box::new(inner.with_stack_trace(trace)),
264 hint,
265 note,
266 },
267 other => SemaError::WithTrace {
268 inner: Box::new(other),
269 trace,
270 },
271 }
272 }
273
274 pub fn stack_trace(&self) -> Option<&StackTrace> {
275 match self {
276 SemaError::WithTrace { trace, .. } => Some(trace),
277 SemaError::WithContext { inner, .. } => inner.stack_trace(),
278 _ => None,
279 }
280 }
281
282 pub fn inner(&self) -> &SemaError {
283 match self {
284 SemaError::WithTrace { inner, .. } => inner.inner(),
285 SemaError::WithContext { inner, .. } => inner.inner(),
286 other => other,
287 }
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294 use crate::Value;
295
296 #[test]
298 fn span_display() {
299 let span = Span::point(1, 5);
300 assert_eq!(span.to_string(), "1:5");
301 }
302
303 #[test]
305 fn stack_trace_display() {
306 let trace = StackTrace(vec![
307 CallFrame {
308 name: "foo".into(),
309 file: Some("/a/b.sema".into()),
310 span: Some(Span::point(3, 7)),
311 },
312 CallFrame {
313 name: "bar".into(),
314 file: Some("/c/d.sema".into()),
315 span: None,
316 },
317 CallFrame {
318 name: "baz".into(),
319 file: None,
320 span: Some(Span::point(10, 1)),
321 },
322 CallFrame {
323 name: "qux".into(),
324 file: None,
325 span: None,
326 },
327 ]);
328 let s = trace.to_string();
329 assert!(s.contains("at foo (/a/b.sema:3:7)"));
330 assert!(s.contains("at bar (/c/d.sema)"));
331 assert!(s.contains("at baz (<input>:10:1)"));
332 assert!(s.contains("at qux\n"));
333 }
334
335 #[test]
337 fn eval_error() {
338 let e = SemaError::eval("something broke");
339 assert_eq!(e.to_string(), "Eval error: something broke");
340 }
341
342 #[test]
344 fn type_error() {
345 let e = SemaError::type_error("string", "integer");
346 assert_eq!(e.to_string(), "Type error: expected string, got integer");
347 }
348
349 #[test]
351 fn arity_error() {
352 let e = SemaError::arity("my-fn", "2", 5);
353 assert_eq!(e.to_string(), "Arity error: my-fn expects 2 args, got 5");
354 }
355
356 #[test]
358 fn with_hint() {
359 let e = SemaError::eval("oops").with_hint("try this");
360 assert_eq!(e.hint(), Some("try this"));
361 }
362
363 #[test]
365 fn with_note() {
366 let e = SemaError::eval("oops").with_note("extra info");
367 assert_eq!(e.note(), Some("extra info"));
368 }
369
370 #[test]
372 fn with_hint_preserves_note() {
373 let e = SemaError::eval("oops")
374 .with_note("kept note")
375 .with_hint("new hint");
376 assert_eq!(e.hint(), Some("new hint"));
377 assert_eq!(e.note(), Some("kept note"));
378 }
379
380 #[test]
382 fn with_note_preserves_hint() {
383 let e = SemaError::eval("oops")
384 .with_hint("kept hint")
385 .with_note("new note");
386 assert_eq!(e.hint(), Some("kept hint"));
387 assert_eq!(e.note(), Some("new note"));
388 }
389
390 #[test]
392 fn with_stack_trace() {
393 let trace = StackTrace(vec![CallFrame {
394 name: "f".into(),
395 file: None,
396 span: None,
397 }]);
398 let e = SemaError::eval("err").with_stack_trace(trace);
399 let st = e.stack_trace().expect("should have stack trace");
400 assert_eq!(st.0.len(), 1);
401 assert_eq!(st.0[0].name, "f");
402 }
403
404 #[test]
406 fn with_stack_trace_empty_is_noop() {
407 let e = SemaError::eval("err").with_stack_trace(StackTrace(vec![]));
408 assert!(e.stack_trace().is_none());
409 assert!(matches!(e, SemaError::Eval(_)));
410 }
411
412 #[test]
414 fn with_stack_trace_already_wrapped_is_noop() {
415 let frame = || CallFrame {
416 name: "first".into(),
417 file: None,
418 span: None,
419 };
420 let e = SemaError::eval("err").with_stack_trace(StackTrace(vec![frame()]));
421 let e2 = e.with_stack_trace(StackTrace(vec![CallFrame {
422 name: "second".into(),
423 file: None,
424 span: None,
425 }]));
426 let st = e2.stack_trace().unwrap();
427 assert_eq!(st.0.len(), 1);
428 assert_eq!(st.0[0].name, "first");
429 }
430
431 #[test]
433 fn inner_unwraps() {
434 let e = SemaError::eval("root")
435 .with_hint("h")
436 .with_stack_trace(StackTrace(vec![CallFrame {
437 name: "x".into(),
438 file: None,
439 span: None,
440 }]));
441 let inner = e.inner();
442 assert!(matches!(inner, SemaError::Eval(msg) if msg == "root"));
443 }
444
445 #[test]
447 fn hint_note_none_on_plain() {
448 let e = SemaError::eval("plain");
449 assert!(e.hint().is_none());
450 assert!(e.note().is_none());
451 }
452
453 #[test]
455 fn check_arity_exact() {
456 fn run(args: &[Value]) -> Result<(), SemaError> {
457 check_arity!(args, "test-fn", 2);
458 Ok(())
459 }
460 assert!(run(&[Value::nil(), Value::nil()]).is_ok());
461 let err = run(&[Value::nil()]).unwrap_err();
462 assert!(err.to_string().contains("test-fn"));
463 assert!(err.to_string().contains("2"));
464 }
465
466 #[test]
468 fn check_arity_range() {
469 fn run(args: &[Value]) -> Result<(), SemaError> {
470 check_arity!(args, "range-fn", 1..=3);
471 Ok(())
472 }
473 assert!(run(&[Value::nil()]).is_ok());
474 assert!(run(&[Value::nil(), Value::nil()]).is_ok());
475 assert!(run(&[Value::nil(), Value::nil(), Value::nil()]).is_ok());
476 assert!(run(&[]).is_err());
477 assert!(run(&[Value::nil(), Value::nil(), Value::nil(), Value::nil()]).is_err());
478 }
479
480 #[test]
482 fn check_arity_open_range() {
483 fn run(args: &[Value]) -> Result<(), SemaError> {
484 check_arity!(args, "open-fn", 2..);
485 Ok(())
486 }
487 assert!(run(&[Value::nil(), Value::nil()]).is_ok());
488 assert!(run(&[Value::nil(), Value::nil(), Value::nil()]).is_ok());
489 assert!(run(&[Value::nil()]).is_err());
490 assert!(run(&[]).is_err());
491 }
492}