1use thiserror::Error;
9use torvyn_types::ComponentId;
10
11#[derive(Debug, Error)]
33pub enum EngineError {
34 #[error(
39 "[E0800] Compilation failed{}: {reason}. \
40 Verify the .wasm file is a valid WebAssembly Component \
41 (not a core module). Re-compile with a supported toolchain.",
42 match source_hint {
43 Some(s) => format!(" for '{s}'"),
44 None => String::new(),
45 }
46 )]
47 CompilationFailed {
48 reason: String,
50 source_hint: Option<String>,
52 },
53
54 #[error(
59 "[E0801] Deserialization failed: {reason}. \
60 The compilation cache may be stale. \
61 Delete the cache directory and retry."
62 )]
63 DeserializationFailed {
64 reason: String,
66 },
67
68 #[error(
73 "[E0802] Instantiation failed for component {component_id}: {reason}. \
74 Check that all imports are satisfied and resource limits are sufficient."
75 )]
76 InstantiationFailed {
77 component_id: ComponentId,
79 reason: String,
81 },
82
83 #[error(
85 "[E0803] Unresolved import '{import_name}' for component {component_id}. \
86 Ensure the pipeline topology provides all required interfaces."
87 )]
88 UnresolvedImport {
89 component_id: ComponentId,
91 import_name: String,
93 },
94
95 #[error(
101 "[E0804] Component {component_id} trapped: {trap_code}. \
102 This indicates a bug in the component. \
103 Check component logs and consider filing a bug report."
104 )]
105 Trap {
106 component_id: ComponentId,
108 trap_code: String,
110 },
111
112 #[error(
118 "[E0805] Fuel exhausted for component {component_id}. \
119 The component exceeded its CPU budget of {fuel_limit} fuel units. \
120 Consider increasing the fuel budget in the pipeline configuration \
121 or optimizing the component."
122 )]
123 FuelExhausted {
124 component_id: ComponentId,
126 fuel_limit: u64,
128 },
129
130 #[error(
132 "[E0806] Memory limit exceeded for component {component_id}: \
133 attempted to grow to {attempted_bytes} bytes, limit is {limit_bytes} bytes. \
134 Increase the component's memory limit in the pipeline configuration."
135 )]
136 MemoryLimitExceeded {
137 component_id: ComponentId,
139 attempted_bytes: usize,
141 limit_bytes: usize,
143 },
144
145 #[error(
150 "[E0807] Type mismatch during invocation of '{function_name}' on \
151 component {component_id}: {detail}. \
152 Verify the component was compiled against the correct WIT contract."
153 )]
154 TypeMismatch {
155 component_id: ComponentId,
157 function_name: String,
159 detail: String,
161 },
162
163 #[error(
165 "[E0808] Export '{function_name}' not found on component {component_id}. \
166 Verify the component exports the required Torvyn interface \
167 (e.g., `torvyn:streaming/processor`)."
168 )]
169 ExportNotFound {
170 component_id: ComponentId,
172 function_name: String,
174 },
175
176 #[error(
178 "[E0809] Internal engine error: {reason}. \
179 This may indicate a Torvyn bug. Please report this issue."
180 )]
181 Internal {
182 reason: String,
184 },
185
186 #[error(
188 "[E0810] WASI configuration failed for component {component_id}: {reason}. \
189 Check the component's capability grants and sandbox configuration."
190 )]
191 WasiConfigError {
192 component_id: ComponentId,
194 reason: String,
196 },
197
198 #[error(
200 "[E0811] Invocation of '{function_name}' on component {component_id} \
201 timed out after {timeout_ms}ms. \
202 Consider increasing the invocation timeout."
203 )]
204 InvocationTimeout {
205 component_id: ComponentId,
207 function_name: String,
209 timeout_ms: u64,
211 },
212}
213
214impl EngineError {
215 #[inline]
220 pub fn is_fatal(&self) -> bool {
221 matches!(
222 self,
223 EngineError::Trap { .. }
224 | EngineError::FuelExhausted { .. }
225 | EngineError::MemoryLimitExceeded { .. }
226 )
227 }
228
229 #[inline]
234 pub fn is_retryable(&self) -> bool {
235 matches!(self, EngineError::InvocationTimeout { .. })
236 }
237
238 #[inline]
242 pub fn code(&self) -> &'static str {
243 match self {
244 EngineError::CompilationFailed { .. } => "E0800",
245 EngineError::DeserializationFailed { .. } => "E0801",
246 EngineError::InstantiationFailed { .. } => "E0802",
247 EngineError::UnresolvedImport { .. } => "E0803",
248 EngineError::Trap { .. } => "E0804",
249 EngineError::FuelExhausted { .. } => "E0805",
250 EngineError::MemoryLimitExceeded { .. } => "E0806",
251 EngineError::TypeMismatch { .. } => "E0807",
252 EngineError::ExportNotFound { .. } => "E0808",
253 EngineError::Internal { .. } => "E0809",
254 EngineError::WasiConfigError { .. } => "E0810",
255 EngineError::InvocationTimeout { .. } => "E0811",
256 }
257 }
258}
259
260impl From<EngineError> for torvyn_types::TorvynError {
268 fn from(e: EngineError) -> Self {
269 let types_err = match &e {
270 EngineError::CompilationFailed {
271 source_hint,
272 reason,
273 } => torvyn_types::EngineError::CompilationFailed {
274 module: source_hint.clone().unwrap_or_default(),
275 reason: reason.clone(),
276 },
277 EngineError::InstantiationFailed {
278 component_id,
279 reason,
280 } => torvyn_types::EngineError::InstantiationFailed {
281 module: component_id.to_string(),
282 reason: reason.clone(),
283 },
284 other => torvyn_types::EngineError::CompilationFailed {
285 module: String::new(),
286 reason: other.to_string(),
287 },
288 };
289 torvyn_types::TorvynError::Engine(types_err)
290 }
291}
292
293#[cfg(test)]
298mod tests {
299 use super::*;
300 use torvyn_types::ComponentId;
301
302 #[test]
303 fn test_compilation_failed_display_actionable() {
304 let err = EngineError::CompilationFailed {
305 reason: "invalid magic number".into(),
306 source_hint: Some("my-component.wasm".into()),
307 };
308 let msg = format!("{err}");
309 assert!(msg.contains("E0800"), "should contain error code");
310 assert!(
311 msg.contains("my-component.wasm"),
312 "should contain source hint"
313 );
314 assert!(
315 msg.contains("valid WebAssembly Component"),
316 "should contain remediation"
317 );
318 }
319
320 #[test]
321 fn test_compilation_failed_no_source_hint() {
322 let err = EngineError::CompilationFailed {
323 reason: "bad binary".into(),
324 source_hint: None,
325 };
326 let msg = format!("{err}");
327 assert!(msg.contains("E0800"));
328 assert!(!msg.contains("for ''"), "should not have empty for clause");
329 }
330
331 #[test]
332 fn test_trap_display_actionable() {
333 let err = EngineError::Trap {
334 component_id: ComponentId::new(42),
335 trap_code: "unreachable instruction".into(),
336 };
337 let msg = format!("{err}");
338 assert!(msg.contains("E0804"));
339 assert!(msg.contains("component-42"));
340 assert!(msg.contains("bug in the component"));
341 }
342
343 #[test]
344 fn test_fuel_exhausted_display_actionable() {
345 let err = EngineError::FuelExhausted {
346 component_id: ComponentId::new(7),
347 fuel_limit: 1_000_000,
348 };
349 let msg = format!("{err}");
350 assert!(msg.contains("E0805"));
351 assert!(msg.contains("1000000"));
352 assert!(msg.contains("fuel budget"));
353 }
354
355 #[test]
356 fn test_memory_limit_exceeded_display() {
357 let err = EngineError::MemoryLimitExceeded {
358 component_id: ComponentId::new(1),
359 attempted_bytes: 32 * 1024 * 1024,
360 limit_bytes: 16 * 1024 * 1024,
361 };
362 let msg = format!("{err}");
363 assert!(msg.contains("E0806"));
364 }
365
366 #[test]
367 fn test_export_not_found_display() {
368 let err = EngineError::ExportNotFound {
369 component_id: ComponentId::new(3),
370 function_name: "process".into(),
371 };
372 let msg = format!("{err}");
373 assert!(msg.contains("E0808"));
374 assert!(msg.contains("process"));
375 }
376
377 #[test]
378 fn test_is_fatal() {
379 assert!(EngineError::Trap {
380 component_id: ComponentId::new(1),
381 trap_code: "x".into(),
382 }
383 .is_fatal());
384 assert!(EngineError::FuelExhausted {
385 component_id: ComponentId::new(1),
386 fuel_limit: 0,
387 }
388 .is_fatal());
389 assert!(EngineError::MemoryLimitExceeded {
390 component_id: ComponentId::new(1),
391 attempted_bytes: 0,
392 limit_bytes: 0,
393 }
394 .is_fatal());
395 assert!(!EngineError::CompilationFailed {
396 reason: "x".into(),
397 source_hint: None,
398 }
399 .is_fatal());
400 }
401
402 #[test]
403 fn test_is_retryable() {
404 assert!(EngineError::InvocationTimeout {
405 component_id: ComponentId::new(1),
406 function_name: "process".into(),
407 timeout_ms: 5000,
408 }
409 .is_retryable());
410 assert!(!EngineError::Trap {
411 component_id: ComponentId::new(1),
412 trap_code: "x".into(),
413 }
414 .is_retryable());
415 }
416
417 #[test]
418 fn test_error_codes_unique() {
419 let codes = vec![
420 EngineError::CompilationFailed {
421 reason: String::new(),
422 source_hint: None,
423 }
424 .code(),
425 EngineError::DeserializationFailed {
426 reason: String::new(),
427 }
428 .code(),
429 EngineError::InstantiationFailed {
430 component_id: ComponentId::new(0),
431 reason: String::new(),
432 }
433 .code(),
434 EngineError::UnresolvedImport {
435 component_id: ComponentId::new(0),
436 import_name: String::new(),
437 }
438 .code(),
439 EngineError::Trap {
440 component_id: ComponentId::new(0),
441 trap_code: String::new(),
442 }
443 .code(),
444 EngineError::FuelExhausted {
445 component_id: ComponentId::new(0),
446 fuel_limit: 0,
447 }
448 .code(),
449 EngineError::MemoryLimitExceeded {
450 component_id: ComponentId::new(0),
451 attempted_bytes: 0,
452 limit_bytes: 0,
453 }
454 .code(),
455 EngineError::TypeMismatch {
456 component_id: ComponentId::new(0),
457 function_name: String::new(),
458 detail: String::new(),
459 }
460 .code(),
461 EngineError::ExportNotFound {
462 component_id: ComponentId::new(0),
463 function_name: String::new(),
464 }
465 .code(),
466 EngineError::Internal {
467 reason: String::new(),
468 }
469 .code(),
470 EngineError::WasiConfigError {
471 component_id: ComponentId::new(0),
472 reason: String::new(),
473 }
474 .code(),
475 EngineError::InvocationTimeout {
476 component_id: ComponentId::new(0),
477 function_name: String::new(),
478 timeout_ms: 0,
479 }
480 .code(),
481 ];
482 let unique: std::collections::HashSet<_> = codes.iter().collect();
483 assert_eq!(unique.len(), codes.len(), "all error codes must be unique");
484 }
485
486 #[test]
487 fn test_conversion_to_torvyn_error() {
488 let err = EngineError::CompilationFailed {
489 reason: "bad binary".into(),
490 source_hint: Some("test.wasm".into()),
491 };
492 let torvyn_err: torvyn_types::TorvynError = err.into();
493 let msg = format!("{torvyn_err}");
494 assert!(msg.contains("bad binary"));
495 }
496}