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}
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#[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#[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
85pub 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 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 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 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 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 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}