Skip to main content

sim_kernel/
error.rs

1//! The error and diagnostic contract for the kernel.
2//!
3//! Defines the [`enum@Error`] enum, the [`Result`] alias, and the [`Diagnostic`]
4//! and [`Severity`] types that every kernel contract and library reports
5//! against.
6
7use thiserror::Error;
8
9use crate::{
10    capability::{CapabilityName, TrustLevel},
11    expr::{SourceId, Span},
12    id::{CaseId, ClassId, CodecId, FunctionId, ShapeId, Symbol},
13    library::{ExportKind, Version},
14};
15
16/// The kernel result alias, defaulting the error type to [`enum@Error`].
17pub type Result<T, E = Error> = core::result::Result<T, E>;
18
19/// A structured diagnostic message with optional source location.
20///
21/// The kernel defines the diagnostic record; libraries and contracts attach
22/// diagnostics to errors to explain failures with source context.
23#[derive(Clone, Debug, PartialEq, Eq)]
24pub struct Diagnostic {
25    /// The severity level.
26    pub severity: Severity,
27    /// The human-readable message.
28    pub message: String,
29    /// The optional source the diagnostic refers to.
30    pub source: Option<SourceId>,
31    /// The optional span within the source.
32    pub span: Option<Span>,
33    /// The optional machine-readable diagnostic code.
34    pub code: Option<Symbol>,
35    /// Related sub-diagnostics providing more detail.
36    pub related: Vec<Diagnostic>,
37}
38
39/// The severity level of a [`Diagnostic`].
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41pub enum Severity {
42    /// An error.
43    Error,
44    /// A warning.
45    Warning,
46    /// Informational output.
47    Info,
48    /// A note attached to another diagnostic.
49    Note,
50}
51
52impl Diagnostic {
53    /// Builds an error-severity diagnostic with just a message.
54    pub fn error(message: impl Into<String>) -> Self {
55        Self {
56            severity: Severity::Error,
57            message: message.into(),
58            source: None,
59            span: None,
60            code: None,
61            related: Vec::new(),
62        }
63    }
64
65    /// Builds an info-severity diagnostic with just a message.
66    pub fn info(message: impl Into<String>) -> Self {
67        Self {
68            severity: Severity::Info,
69            message: message.into(),
70            source: None,
71            span: None,
72            code: None,
73            related: Vec::new(),
74        }
75    }
76
77    /// Adds a machine-readable diagnostic code.
78    pub fn with_code(mut self, code: Symbol) -> Self {
79        self.code = Some(code);
80        self
81    }
82
83    /// Adds a related sub-diagnostic.
84    pub fn with_related(mut self, related: Diagnostic) -> Self {
85        self.related.push(related);
86        self
87    }
88}
89
90/// The kernel error type reported by every contract and library.
91///
92/// The kernel defines the shared error vocabulary; libraries raise these
93/// variants (and wrap their own messages in the string-carrying variants)
94/// rather than inventing parallel error types.
95#[derive(Clone, Debug, Error)]
96pub enum Error {
97    /// A symbol could not be resolved.
98    #[error("unknown symbol {symbol}")]
99    UnknownSymbol {
100        /// The unresolved symbol.
101        symbol: Symbol,
102    },
103    /// A class name could not be resolved.
104    #[error("unknown class {class}")]
105    UnknownClass {
106        /// The unresolved class name.
107        class: Symbol,
108    },
109    /// A class exists but does not provide a callable constructor.
110    #[error("class {class} is not constructible")]
111    NonConstructibleClass {
112        /// The class that was called as a constructor.
113        class: Symbol,
114    },
115    /// A function name could not be resolved.
116    #[error("unknown function {function}")]
117    UnknownFunction {
118        /// The unresolved function name.
119        function: Symbol,
120    },
121    /// No class is registered under the given id.
122    #[error("missing class with id {0:?}")]
123    MissingClass(ClassId),
124    /// No shape is registered under the given id.
125    #[error("missing shape with id {0:?}")]
126    MissingShape(ShapeId),
127    /// A shape reference could not be resolved.
128    #[error("unresolved shape ref {reference:?}")]
129    UnresolvedShapeRef {
130        /// The unresolved shape reference.
131        reference: Box<crate::Ref>,
132    },
133    /// A value's class did not match the expected class.
134    #[error("wrong class: expected {expected:?}, found {found:?}")]
135    WrongClass {
136        /// The class that was required.
137        expected: ClassId,
138        /// The class actually found.
139        found: ClassId,
140    },
141    /// A value did not match the expected shape.
142    #[error("wrong shape: expected {expected:?}")]
143    WrongShape {
144        /// The shape that was required.
145        expected: ShapeId,
146        /// Diagnostics explaining the mismatch.
147        diagnostics: Vec<Diagnostic>,
148    },
149    /// More than one overload matched a call.
150    #[error("ambiguous overload for function {function:?}")]
151    AmbiguousOverload {
152        /// The function being dispatched.
153        function: FunctionId,
154        /// The competing candidate cases.
155        candidates: Vec<CaseId>,
156    },
157    /// No overload matched a call.
158    #[error("no matching overload for function {function:?}")]
159    NoMatchingOverload {
160        /// The function being dispatched.
161        function: FunctionId,
162        /// Diagnostics explaining why each case was rejected.
163        diagnostics: Vec<Diagnostic>,
164    },
165    /// More than one numeric domain pairing matched an operator.
166    #[error("ambiguous numeric dispatch for operator {operator}")]
167    AmbiguousNumberDispatch {
168        /// The operator being dispatched.
169        operator: Symbol,
170        /// The competing left/right domain pairings.
171        candidates: Vec<(Symbol, Symbol)>,
172    },
173    /// No promotion path joined two number domains for an operator.
174    #[error("no promotion path for operator {operator} from {left_domain} and {right_domain}")]
175    NoPromotionPath {
176        /// The operator being dispatched.
177        operator: Symbol,
178        /// The left operand's domain.
179        left_domain: Symbol,
180        /// The right operand's domain.
181        right_domain: Symbol,
182    },
183    /// Promotion search exceeded its configured limits.
184    #[error(
185        "number promotion search from {from_domain} to {target_domain} exceeded limits \
186         (max_depth={max_depth}, max_states={max_states})"
187    )]
188    PromotionSearchLimitExceeded {
189        /// The domain the search started from.
190        from_domain: Symbol,
191        /// The domain the search was targeting.
192        target_domain: Symbol,
193        /// The depth limit that was hit.
194        max_depth: usize,
195        /// The state-count limit that was hit.
196        max_states: usize,
197    },
198    /// A required capability was not granted.
199    #[error("capability denied: {capability}")]
200    CapabilityDenied {
201        /// The denied capability.
202        capability: CapabilityName,
203    },
204    /// A capability is not allowed at the caller's trust level.
205    #[error("trust denied: {capability} is not allowed for {trust:?}")]
206    TrustDenied {
207        /// The requested capability.
208        capability: CapabilityName,
209        /// The caller's trust level.
210        trust: TrustLevel,
211    },
212    /// A host grant seat was used with a different context than the one it was
213    /// minted for.
214    #[error("grant seat cannot grant into a foreign Cx")]
215    ForeignGrantSeat,
216    /// A codec failed to read or write a form.
217    #[error("codec error in {codec:?}: {message}")]
218    CodecError {
219        /// The codec that failed.
220        codec: CodecId,
221        /// The failure message.
222        message: String,
223    },
224    /// A number (or other) domain reported a categorized failure.
225    #[error("domain error in {domain} ({category}): {message}")]
226    DomainError {
227        /// The domain that failed.
228        domain: Symbol,
229        /// The error category within the domain.
230        category: Symbol,
231        /// The failure message.
232        message: String,
233    },
234    /// Two exports of the same kind claimed the same symbol.
235    #[error("duplicate export for {kind} {symbol}")]
236    DuplicateExport {
237        /// The export kind label.
238        kind: &'static str,
239        /// The conflicting symbol.
240        symbol: Symbol,
241    },
242    /// Two libraries claimed the same name.
243    #[error("duplicate lib {symbol}")]
244    DuplicateLib {
245        /// The conflicting library name.
246        symbol: Symbol,
247    },
248    /// A catalog write conflicted with an existing key.
249    #[error("catalog conflict in {table} for {key}")]
250    CatalogConflict {
251        /// The catalog table.
252        table: Symbol,
253        /// The conflicting key.
254        key: Symbol,
255    },
256    /// A write targeted a read-only catalog table.
257    #[error("catalog table {table} is read-only")]
258    CatalogReadOnly {
259        /// The read-only table.
260        table: Symbol,
261    },
262    /// A catalog row violated its table schema.
263    #[error("catalog schema error in {table}: {message}")]
264    CatalogSchema {
265        /// The table whose schema was violated.
266        table: Symbol,
267        /// The schema-violation message.
268        message: String,
269    },
270    /// A library declared a dependency that is not loaded.
271    #[error("missing dependency {dependency} for {lib}")]
272    MissingDependency {
273        /// The depending library.
274        lib: Symbol,
275        /// The missing dependency.
276        dependency: Symbol,
277    },
278    /// A dependency is present but older than required.
279    #[error(
280        "dependency {dependency} for {lib} requires at least version {required:?} but loaded {loaded:?}"
281    )]
282    DependencyVersionMismatch {
283        /// The depending library.
284        lib: Symbol,
285        /// The dependency name.
286        dependency: Symbol,
287        /// The minimum required version.
288        required: Version,
289        /// The version actually loaded.
290        loaded: Version,
291    },
292    /// A cycle was found among library dependencies.
293    #[error("cyclic lib dependency involving {symbol}")]
294    CyclicDependency {
295        /// A library on the cycle.
296        symbol: Symbol,
297    },
298    /// A library cannot be unloaded because loaded libraries depend on it.
299    #[error("cannot unload {lib}; loaded dependents remain: {dependents:?}")]
300    LibHasDependents {
301        /// The library requested for unload.
302        lib: Symbol,
303        /// Loaded libraries that depend on it.
304        dependents: Vec<Symbol>,
305    },
306    /// An export record was produced without a matching manifest declaration.
307    #[error("export record for {kind:?} {symbol} was not declared in the manifest")]
308    UndeclaredExportRecord {
309        /// The export kind.
310        kind: ExportKind,
311        /// The undeclared symbol.
312        symbol: Symbol,
313    },
314    /// A value's static type did not match what was expected.
315    #[error("type mismatch: expected {expected}, found {found}")]
316    TypeMismatch {
317        /// The expected type label.
318        expected: &'static str,
319        /// The type label actually found.
320        found: &'static str,
321    },
322    /// Evaluation failed; carries a free-form message.
323    #[error("evaluation error: {0}")]
324    Eval(String),
325    /// A library reported a free-form failure.
326    #[error("lib error: {0}")]
327    Lib(String),
328    /// A lock was poisoned by a panic; carries the lock name.
329    #[error("poisoned lock: {0}")]
330    PoisonedLock(&'static str),
331    /// A thunk was forced while already being forced.
332    #[error("recursive thunk force detected")]
333    RecursiveThunkForce,
334    /// The host environment reported a free-form failure.
335    #[error("host error: {0}")]
336    HostError(String),
337}
338
339impl Error {
340    /// Builds a [`Error::DomainError`] from its parts.
341    pub fn domain_error(domain: Symbol, category: Symbol, message: impl Into<String>) -> Self {
342        Self::DomainError {
343            domain,
344            category,
345            message: message.into(),
346        }
347    }
348
349    /// Wraps a host [`std::io::Error`] into an [`Error::HostError`].
350    ///
351    /// This is a named constructor, deliberately not a blanket `From` impl, so
352    /// that every host-IO-to-kernel conversion stays explicit at the call site.
353    pub fn host_io(err: std::io::Error) -> Self {
354        Self::HostError(err.to_string())
355    }
356}
357
358#[cfg(test)]
359mod tests {
360    use super::Error;
361
362    #[test]
363    fn host_io_wraps_an_io_error_into_host_error() {
364        let io = std::io::Error::new(std::io::ErrorKind::NotFound, "no such file");
365        let error = Error::host_io(io);
366        match error {
367            Error::HostError(message) => assert_eq!(message, "no such file"),
368            other => panic!("expected HostError, got {other:?}"),
369        }
370    }
371}