Skip to main content

torvyn_engine/
error.rs

1//! Engine error types for the Torvyn runtime.
2//!
3//! Error code range: E0800–E0899.
4//!
5//! These errors cover Wasm compilation, instantiation, invocation,
6//! fuel exhaustion, memory limits, and type mismatches.
7
8use thiserror::Error;
9use torvyn_types::ComponentId;
10
11/// Errors originating from the Wasm engine layer.
12///
13/// This is the primary error type returned by [`WasmEngine`](crate::WasmEngine)
14/// and [`ComponentInvoker`](crate::ComponentInvoker) operations.
15///
16/// All variants include actionable error messages that tell the user
17/// what went wrong, where, and how to fix it.
18///
19/// # Error Code Range
20/// E0800–E0899
21///
22/// # Examples
23/// ```
24/// use torvyn_engine::EngineError;
25///
26/// let err = EngineError::CompilationFailed {
27///     reason: "invalid magic number".into(),
28///     source_hint: Some("my-component.wasm".into()),
29/// };
30/// assert!(format!("{}", err).contains("E0800"));
31/// ```
32#[derive(Debug, Error)]
33pub enum EngineError {
34    /// The Wasm component binary could not be compiled.
35    ///
36    /// Causes: invalid binary format, unsupported Wasm features,
37    /// compiler internal error.
38    #[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        /// The reason compilation failed.
49        reason: String,
50        /// Optional hint about the source file.
51        source_hint: Option<String>,
52    },
53
54    /// A previously serialized (cached) compiled component could not
55    /// be deserialized.
56    ///
57    /// Causes: cache corruption, engine version mismatch, config mismatch.
58    #[error(
59        "[E0801] Deserialization failed: {reason}. \
60         The compilation cache may be stale. \
61         Delete the cache directory and retry."
62    )]
63    DeserializationFailed {
64        /// The reason deserialization failed.
65        reason: String,
66    },
67
68    /// Component instantiation failed.
69    ///
70    /// Causes: unresolved imports, resource limit exceeded during
71    /// instantiation, initialization trap.
72    #[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        /// The component that failed to instantiate.
78        component_id: ComponentId,
79        /// The reason instantiation failed.
80        reason: String,
81    },
82
83    /// A component import could not be resolved during linking.
84    #[error(
85        "[E0803] Unresolved import '{import_name}' for component {component_id}. \
86         Ensure the pipeline topology provides all required interfaces."
87    )]
88    UnresolvedImport {
89        /// The component with the unresolved import.
90        component_id: ComponentId,
91        /// The name of the unresolved import.
92        import_name: String,
93    },
94
95    /// The component trapped during execution.
96    ///
97    /// A trap is an unrecoverable error within the Wasm execution
98    /// (e.g., unreachable instruction, division by zero, out-of-bounds
99    /// memory access).
100    #[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        /// The component that trapped.
107        component_id: ComponentId,
108        /// Description of the trap.
109        trap_code: String,
110    },
111
112    /// The component exhausted its fuel budget.
113    ///
114    /// The component consumed more CPU than its allocated fuel allows.
115    /// This is a safety mechanism to prevent infinite loops and
116    /// CPU-intensive components from starving others.
117    #[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        /// The component that ran out of fuel.
125        component_id: ComponentId,
126        /// The fuel budget that was exceeded.
127        fuel_limit: u64,
128    },
129
130    /// The component exceeded its memory limit.
131    #[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        /// The component that exceeded the limit.
138        component_id: ComponentId,
139        /// How many bytes the component tried to use.
140        attempted_bytes: usize,
141        /// The configured limit.
142        limit_bytes: usize,
143    },
144
145    /// A type mismatch occurred during invocation.
146    ///
147    /// The arguments or return values did not match the expected
148    /// Component Model types.
149    #[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        /// The component with the type mismatch.
156        component_id: ComponentId,
157        /// The function being invoked.
158        function_name: String,
159        /// Details about the mismatch.
160        detail: String,
161    },
162
163    /// The requested export function was not found on the component.
164    #[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        /// The component missing the export.
171        component_id: ComponentId,
172        /// The function that was not found.
173        function_name: String,
174    },
175
176    /// An internal engine error that should not occur under normal operation.
177    #[error(
178        "[E0809] Internal engine error: {reason}. \
179         This may indicate a Torvyn bug. Please report this issue."
180    )]
181    Internal {
182        /// Description of the internal error.
183        reason: String,
184    },
185
186    /// WASI configuration failed.
187    #[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        /// The component with the WASI config problem.
193        component_id: ComponentId,
194        /// The reason WASI config failed.
195        reason: String,
196    },
197
198    /// Timeout waiting for a component invocation to complete.
199    #[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        /// The component that timed out.
206        component_id: ComponentId,
207        /// The function that timed out.
208        function_name: String,
209        /// How long we waited (ms).
210        timeout_ms: u64,
211    },
212}
213
214impl EngineError {
215    /// Returns `true` if this error represents a fatal, unrecoverable
216    /// condition for the component (trap, fuel exhaustion).
217    ///
218    /// # WARM PATH — called per error to determine component fate.
219    #[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    /// Returns `true` if this error is transient and the operation
230    /// might succeed on retry.
231    ///
232    /// # WARM PATH
233    #[inline]
234    pub fn is_retryable(&self) -> bool {
235        matches!(self, EngineError::InvocationTimeout { .. })
236    }
237
238    /// Returns the error code as a static string for metrics labels.
239    ///
240    /// # WARM PATH
241    #[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
260// ---------------------------------------------------------------------------
261// Conversions
262// ---------------------------------------------------------------------------
263
264// LLI DEVIATION: The LLI doc maps EngineError to LinkError::CompilationFailed,
265// but torvyn-types now has a proper TorvynError::Engine(EngineError) variant.
266// We map to that via the torvyn_types::EngineError intermediate type.
267impl 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// ---------------------------------------------------------------------------
294// Tests
295// ---------------------------------------------------------------------------
296
297#[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}