Skip to main content

sema_core/
error.rs

1use std::collections::HashMap;
2use std::fmt;
3
4use crate::value::Value;
5
6/// Check arity of a native function's arguments, returning `SemaError::Arity` on mismatch.
7///
8/// # Forms
9///
10/// ```ignore
11/// check_arity!(args, "fn-name", 2);        // exactly 2
12/// check_arity!(args, "fn-name", 1..=3);    // 1 to 3 inclusive
13/// check_arity!(args, "fn-name", 2..);      // 2 or more
14/// ```
15#[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}
51
52impl fmt::Display for Span {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        write!(f, "{}:{}", self.line, self.col)
55    }
56}
57
58/// A single frame in a call stack trace.
59#[derive(Debug, Clone)]
60pub struct CallFrame {
61    pub name: String,
62    pub file: Option<std::path::PathBuf>,
63    pub span: Option<Span>,
64}
65
66/// A captured stack trace (list of call frames, innermost first).
67#[derive(Debug, Clone)]
68pub struct StackTrace(pub Vec<CallFrame>);
69
70impl fmt::Display for StackTrace {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        for frame in &self.0 {
73            write!(f, "  at {}", frame.name)?;
74            match (&frame.file, &frame.span) {
75                (Some(file), Some(span)) => writeln!(f, " ({}:{span})", file.display())?,
76                (Some(file), None) => writeln!(f, " ({})", file.display())?,
77                (None, Some(span)) => writeln!(f, " (<input>:{span})")?,
78                (None, None) => writeln!(f)?,
79            }
80        }
81        Ok(())
82    }
83}
84
85/// Maps Rc pointer addresses to source spans for expression tracking.
86pub type SpanMap = HashMap<usize, Span>;
87
88#[derive(Debug, Clone, thiserror::Error)]
89pub enum SemaError {
90    #[error("Reader error at {span}: {message}")]
91    Reader { message: String, span: Span },
92
93    #[error("Eval error: {0}")]
94    Eval(String),
95
96    #[error("Type error: expected {expected}, got {got}")]
97    Type { expected: String, got: String },
98
99    #[error("Arity error: {name} expects {expected} args, got {got}")]
100    Arity {
101        name: String,
102        expected: String,
103        got: usize,
104    },
105
106    #[error("Unbound variable: {0}")]
107    Unbound(String),
108
109    #[error("LLM error: {0}")]
110    Llm(String),
111
112    #[error("IO error: {0}")]
113    Io(String),
114
115    #[error("Permission denied: {function} requires '{capability}' capability")]
116    PermissionDenied {
117        function: String,
118        capability: String,
119    },
120
121    #[error("User exception: {0}")]
122    UserException(Value),
123
124    #[error("{inner}")]
125    WithTrace {
126        inner: Box<SemaError>,
127        trace: StackTrace,
128    },
129
130    #[error("{inner}")]
131    WithContext {
132        inner: Box<SemaError>,
133        hint: Option<String>,
134        note: Option<String>,
135    },
136}
137
138impl SemaError {
139    pub fn eval(msg: impl Into<String>) -> Self {
140        SemaError::Eval(msg.into())
141    }
142
143    pub fn type_error(expected: impl Into<String>, got: impl Into<String>) -> Self {
144        SemaError::Type {
145            expected: expected.into(),
146            got: got.into(),
147        }
148    }
149
150    pub fn arity(name: impl Into<String>, expected: impl Into<String>, got: usize) -> Self {
151        SemaError::Arity {
152            name: name.into(),
153            expected: expected.into(),
154            got,
155        }
156    }
157
158    /// Attach a hint (actionable suggestion) to this error.
159    pub fn with_hint(self, hint: impl Into<String>) -> Self {
160        match self {
161            SemaError::WithContext { inner, note, .. } => SemaError::WithContext {
162                inner,
163                hint: Some(hint.into()),
164                note,
165            },
166            other => SemaError::WithContext {
167                inner: Box::new(other),
168                hint: Some(hint.into()),
169                note: None,
170            },
171        }
172    }
173
174    /// Attach a note (extra context) to this error.
175    pub fn with_note(self, note: impl Into<String>) -> Self {
176        match self {
177            SemaError::WithContext { inner, hint, .. } => SemaError::WithContext {
178                inner,
179                hint,
180                note: Some(note.into()),
181            },
182            other => SemaError::WithContext {
183                inner: Box::new(other),
184                hint: None,
185                note: Some(note.into()),
186            },
187        }
188    }
189
190    /// Get the hint from this error, if any.
191    pub fn hint(&self) -> Option<&str> {
192        match self {
193            SemaError::WithContext { hint, .. } => hint.as_deref(),
194            SemaError::WithTrace { inner, .. } => inner.hint(),
195            _ => None,
196        }
197    }
198
199    /// Get the note from this error, if any.
200    pub fn note(&self) -> Option<&str> {
201        match self {
202            SemaError::WithContext { note, .. } => note.as_deref(),
203            SemaError::WithTrace { inner, .. } => inner.note(),
204            _ => None,
205        }
206    }
207
208    /// Wrap this error with a stack trace (no-op if already wrapped).
209    pub fn with_stack_trace(self, trace: StackTrace) -> Self {
210        if trace.0.is_empty() {
211            return self;
212        }
213        match self {
214            SemaError::WithTrace { .. } => self,
215            SemaError::WithContext { inner, hint, note } => SemaError::WithContext {
216                inner: Box::new(inner.with_stack_trace(trace)),
217                hint,
218                note,
219            },
220            other => SemaError::WithTrace {
221                inner: Box::new(other),
222                trace,
223            },
224        }
225    }
226
227    pub fn stack_trace(&self) -> Option<&StackTrace> {
228        match self {
229            SemaError::WithTrace { trace, .. } => Some(trace),
230            SemaError::WithContext { inner, .. } => inner.stack_trace(),
231            _ => None,
232        }
233    }
234
235    pub fn inner(&self) -> &SemaError {
236        match self {
237            SemaError::WithTrace { inner, .. } => inner.inner(),
238            SemaError::WithContext { inner, .. } => inner.inner(),
239            other => other,
240        }
241    }
242}