1use harn_lexer::Span;
2
3use super::VmValue;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ArityExpect {
10 Exact(usize),
12 Range { min: usize, max: usize },
14 AtLeast(usize),
17}
18
19impl std::fmt::Display for ArityExpect {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 match self {
22 ArityExpect::Exact(n) => write!(f, "{n}"),
23 ArityExpect::Range { min, max } => write!(f, "{min}..={max}"),
24 ArityExpect::AtLeast(n) => write!(f, "at least {n}"),
25 }
26 }
27}
28
29#[derive(Debug, Clone)]
30pub struct ArityMismatchError {
31 pub callee: String,
32 pub expected: ArityExpect,
33 pub got: usize,
34 pub span: Option<Span>,
35}
36
37#[derive(Debug, Clone)]
38pub struct ArgTypeMismatchError {
39 pub callee: String,
40 pub param: String,
41 pub expected: String,
42 pub got: &'static str,
43 pub span: Option<Span>,
44}
45
46#[derive(Debug, Clone)]
47pub enum VmError {
48 StackUnderflow,
49 StackOverflow,
50 UndefinedVariable(String),
51 UndefinedBuiltin(String),
52 ImmutableAssignment(String),
53 TypeError(String),
54 Runtime(String),
55 DivisionByZero,
56 Thrown(VmValue),
57 CategorizedError {
59 message: String,
60 category: ErrorCategory,
61 },
62 DaemonQueueFull {
63 daemon_id: String,
64 capacity: usize,
65 },
66 Return(VmValue),
67 InvalidInstruction(u8),
68 ArityMismatch(Box<ArityMismatchError>),
72 ArgTypeMismatch(Box<ArgTypeMismatchError>),
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
82pub enum ErrorCategory {
83 Timeout,
85 Auth,
87 RateLimit,
89 Overloaded,
93 ServerError,
95 TransientNetwork,
98 SchemaValidation,
100 ToolError,
102 ToolRejected,
104 EgressBlocked,
106 Cancelled,
108 NotFound,
110 CircuitOpen,
112 BudgetExceeded,
114 Generic,
116}
117
118impl ErrorCategory {
119 pub fn as_str(&self) -> &'static str {
120 match self {
121 ErrorCategory::Timeout => "timeout",
122 ErrorCategory::Auth => "auth",
123 ErrorCategory::RateLimit => "rate_limit",
124 ErrorCategory::Overloaded => "overloaded",
125 ErrorCategory::ServerError => "server_error",
126 ErrorCategory::TransientNetwork => "transient_network",
127 ErrorCategory::SchemaValidation => "schema_validation",
128 ErrorCategory::ToolError => "tool_error",
129 ErrorCategory::ToolRejected => "tool_rejected",
130 ErrorCategory::EgressBlocked => "egress_blocked",
131 ErrorCategory::Cancelled => "cancelled",
132 ErrorCategory::NotFound => "not_found",
133 ErrorCategory::CircuitOpen => "circuit_open",
134 ErrorCategory::BudgetExceeded => "budget_exceeded",
135 ErrorCategory::Generic => "generic",
136 }
137 }
138
139 pub fn parse(s: &str) -> Self {
140 match s {
141 "timeout" => ErrorCategory::Timeout,
142 "auth" => ErrorCategory::Auth,
143 "rate_limit" => ErrorCategory::RateLimit,
144 "overloaded" => ErrorCategory::Overloaded,
145 "server_error" => ErrorCategory::ServerError,
146 "transient_network" => ErrorCategory::TransientNetwork,
147 "schema_validation" => ErrorCategory::SchemaValidation,
148 "tool_error" => ErrorCategory::ToolError,
149 "tool_rejected" => ErrorCategory::ToolRejected,
150 "egress_blocked" => ErrorCategory::EgressBlocked,
151 "cancelled" => ErrorCategory::Cancelled,
152 "not_found" => ErrorCategory::NotFound,
153 "circuit_open" => ErrorCategory::CircuitOpen,
154 "budget_exceeded" => ErrorCategory::BudgetExceeded,
155 _ => ErrorCategory::Generic,
156 }
157 }
158
159 pub fn is_transient(&self) -> bool {
163 matches!(
164 self,
165 ErrorCategory::Timeout
166 | ErrorCategory::RateLimit
167 | ErrorCategory::Overloaded
168 | ErrorCategory::ServerError
169 | ErrorCategory::TransientNetwork
170 )
171 }
172}
173
174pub fn categorized_error(message: impl Into<String>, category: ErrorCategory) -> VmError {
176 VmError::CategorizedError {
177 message: message.into(),
178 category,
179 }
180}
181
182pub fn error_to_category(err: &VmError) -> ErrorCategory {
191 match err {
192 VmError::CategorizedError { category, .. } => category.clone(),
193 VmError::Thrown(VmValue::Dict(d)) => d
194 .get("category")
195 .map(|v| ErrorCategory::parse(&v.display()))
196 .unwrap_or(ErrorCategory::Generic),
197 VmError::Thrown(VmValue::String(s)) => classify_error_message(s),
198 VmError::Runtime(msg) => classify_error_message(msg),
199 _ => ErrorCategory::Generic,
200 }
201}
202
203pub fn classify_error_message(msg: &str) -> ErrorCategory {
206 if let Some(cat) = classify_by_http_status(msg) {
208 return cat;
209 }
210 if msg.contains("Deadline exceeded") || msg.contains("context deadline exceeded") {
213 return ErrorCategory::Timeout;
214 }
215 if msg.contains("overloaded_error") {
216 return ErrorCategory::Overloaded;
218 }
219 if msg.contains("api_error") {
220 return ErrorCategory::ServerError;
222 }
223 if msg.contains("insufficient_quota") || msg.contains("billing_hard_limit_reached") {
224 return ErrorCategory::RateLimit;
226 }
227 if msg.contains("invalid_api_key") || msg.contains("authentication_error") {
228 return ErrorCategory::Auth;
229 }
230 if msg.contains("not_found_error") || msg.contains("model_not_found") {
231 return ErrorCategory::NotFound;
232 }
233 if msg.contains("circuit_open") {
234 return ErrorCategory::CircuitOpen;
235 }
236 let lower = msg.to_lowercase();
238 if lower.contains("connection reset")
239 || lower.contains("connection refused")
240 || lower.contains("connection closed")
241 || lower.contains("broken pipe")
242 || lower.contains("dns error")
243 || lower.contains("stream error")
244 || lower.contains("unexpected eof")
245 {
246 return ErrorCategory::TransientNetwork;
247 }
248 ErrorCategory::Generic
249}
250
251fn classify_by_http_status(msg: &str) -> Option<ErrorCategory> {
255 for code in extract_http_status_codes(msg) {
258 return Some(match code {
259 401 | 403 => ErrorCategory::Auth,
260 404 | 410 => ErrorCategory::NotFound,
261 408 | 504 | 522 | 524 => ErrorCategory::Timeout,
262 429 => ErrorCategory::RateLimit,
263 503 | 529 => ErrorCategory::Overloaded,
264 500 | 502 => ErrorCategory::ServerError,
265 _ => continue,
266 });
267 }
268 None
269}
270
271fn extract_http_status_codes(msg: &str) -> Vec<u16> {
273 let mut codes = Vec::new();
274 let bytes = msg.as_bytes();
275 for i in 0..bytes.len().saturating_sub(2) {
276 if bytes[i].is_ascii_digit()
278 && bytes[i + 1].is_ascii_digit()
279 && bytes[i + 2].is_ascii_digit()
280 {
281 let before_ok = i == 0 || !bytes[i - 1].is_ascii_digit();
283 let after_ok = i + 3 >= bytes.len() || !bytes[i + 3].is_ascii_digit();
284 if before_ok && after_ok {
285 if let Ok(code) = msg[i..i + 3].parse::<u16>() {
286 if (400..=599).contains(&code) {
287 codes.push(code);
288 }
289 }
290 }
291 }
292 }
293 codes
294}
295
296impl std::fmt::Display for VmError {
297 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
298 match self {
299 VmError::StackUnderflow => write!(f, "Stack underflow"),
300 VmError::StackOverflow => write!(f, "Stack overflow: too many nested calls"),
301 VmError::UndefinedVariable(n) => write!(f, "Undefined variable: {n}"),
302 VmError::UndefinedBuiltin(n) => write!(f, "Undefined builtin: {n}"),
303 VmError::ImmutableAssignment(n) => {
304 write!(f, "Cannot assign to immutable binding: {n}")
305 }
306 VmError::TypeError(msg) => write!(f, "Type error: {msg}"),
307 VmError::Runtime(msg) => write!(f, "Runtime error: {msg}"),
308 VmError::DivisionByZero => write!(f, "Division by zero"),
309 VmError::Thrown(v) => write!(f, "Thrown: {}", v.display()),
310 VmError::CategorizedError { message, category } => {
311 write!(f, "Error [{}]: {}", category.as_str(), message)
312 }
313 VmError::DaemonQueueFull {
314 daemon_id,
315 capacity,
316 } => write!(
317 f,
318 "Daemon queue full: daemon '{daemon_id}' reached its event_queue_capacity of {capacity}"
319 ),
320 VmError::Return(_) => write!(f, "Return from function"),
321 VmError::InvalidInstruction(op) => write!(f, "Invalid instruction: 0x{op:02x}"),
322 VmError::ArityMismatch(err) => {
323 write!(
324 f,
325 "Arity mismatch: '{}' expects {} argument(s), got {}{}",
326 err.callee,
327 err.expected,
328 err.got,
329 fmt_span_suffix(&err.span)
330 )
331 }
332 VmError::ArgTypeMismatch(err) => {
333 write!(
334 f,
335 "Type error: '{}' parameter `{}` expects {}, got {}{}",
336 err.callee,
337 err.param,
338 err.expected,
339 err.got,
340 fmt_span_suffix(&err.span)
341 )
342 }
343 }
344 }
345}
346
347fn fmt_span_suffix(span: &Option<Span>) -> String {
348 match span {
349 Some(s) => format!(" (at byte {}..{})", s.start, s.end),
350 None => String::new(),
351 }
352}
353
354impl std::error::Error for VmError {}