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}