adam_rs/error.rs
1//! The `Error` type returned by all fallible operations in this crate.
2//!
3//! ```
4//! use adam_rs::{CellId, Error, ErrorSite, Sheet};
5//! let mut sheet = Sheet::new();
6//! let value = sheet.add_cell(1_i32);
7//! assert!(matches!(sheet.read::<i32>(CellId::default()), Err(Error::InvalidId)));
8//! let source = sheet.add_source(0_i32);
9//! let error = sheet.add_relationship(vec![
10//! adam_rs::Method::from_fn_1_1(value, source, |v: &i32| Ok(*v))
11//! ]).unwrap_err();
12//! assert!(matches!(error, Error::InvalidCellKind { .. }));
13//! assert!(error.sites().contains(&ErrorSite::Cell(source)));
14//! ```
15
16use crate::cell::CellId;
17use crate::relationship::RelationshipId;
18use std::any::TypeId;
19
20/// Identifies a single sheet component an error implicates, for translating back to a
21/// source location. An `Error`'s `sites()` is an ordered list of these: the primary
22/// culprit first, followed by related context (e.g. the other method(s) and cell(s)
23/// involved in a structural conflict, or the remaining members of a cycle).
24///
25/// # Examples
26/// See the [error example](crate::error).
27#[non_exhaustive]
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
29pub enum ErrorSite {
30 /// Index into the `methods` `Vec` passed to `add_relationship`, before any
31 /// `RelationshipId` exists for it.
32 MethodIndex(usize),
33 /// A method within an already-registered relationship.
34 Method(RelationshipId, usize),
35 /// A relationship as a whole (no single method within it is implicated).
36 Relationship(RelationshipId),
37 /// A single cell, including the cell governed by a failing requirement.
38 Cell(CellId),
39 /// The live conditional whose match expression failed.
40 Conditional(crate::ConditionalId),
41}
42
43/// Errors returned by `Sheet` operations and propagation.
44///
45/// # Examples
46/// See the [error example](crate::error).
47#[derive(Debug)]
48#[non_exhaustive]
49pub enum Error {
50 /// A value's TypeId did not match the cell's registered TypeId.
51 ///
52 /// - `expected`: the TypeId registered when the cell was created.
53 /// - `found`: the TypeId of the value or declaration supplied by the caller.
54 TypeMismatch {
55 /// The TypeId registered when the cell was created.
56 expected: TypeId,
57 /// The TypeId of the value or declaration supplied by the caller.
58 found: TypeId,
59 /// The method this mismatch was detected in, if known. Empty when unknown.
60 sites: Vec<ErrorSite>,
61 },
62
63 /// A `CellId` or `RelationshipId` was not found in the sheet.
64 InvalidId,
65
66 /// No valid method assignment exists, or staged prerequisite provenance conflicts
67 /// with the final selected plan.
68 Conflict {
69 /// Relationships, methods, and cells involved in an impossible assignment or
70 /// an incompatible staged prerequisite reuse.
71 sites: Vec<ErrorSite>,
72 },
73
74 /// The selected methods form a cycle.
75 Cycle {
76 /// The cycle's members in loop-traversal order, alternating `Relationship` and
77 /// `Cell` entries for the length of the loop. Either kind may be `sites[0]`:
78 /// the loop is reported starting at the first node revisited during recovery,
79 /// which is not necessarily a `Relationship`.
80 sites: Vec<ErrorSite>,
81 },
82
83 /// The seed dependency graph contains a non-self cycle.
84 SeedCycle {
85 /// The cycle's members in deterministic traversal order, starting at the first
86 /// revisited `Cell` and alternating through the sibling `Relationship`s that
87 /// force the cycle. The closing relationship appears last; the starting cell is
88 /// not repeated at the end.
89 sites: Vec<ErrorSite>,
90 },
91
92 /// A method's function returned an error during execution.
93 MethodFailed {
94 /// The underlying error the method's function (or a requirement's/conditional's
95 /// expression function) returned.
96 error: anyhow::Error,
97 /// The executing method, conditional, or governed requirement cell, if known.
98 /// Empty when no live execution identity is available.
99 sites: Vec<ErrorSite>,
100 },
101
102 /// A method is structurally invalid (e.g. the outputs list is empty, a
103 /// relationship's methods reference different sets of cells, or two methods in
104 /// a relationship share an identical output set). A method with no inputs is
105 /// not an error: it defines a fixed point (a constant) rather than a derivation.
106 InvalidMethod {
107 /// The method (by index within the `Vec` passed to `add_relationship`) that's
108 /// invalid, if known. Empty when `methods` itself is empty.
109 sites: Vec<ErrorSite>,
110 },
111
112 /// Two methods in the same relationship have `inputs ∪ outputs` sets that don't
113 /// match. Every method in a relationship must reference exactly the same set of
114 /// cells.
115 MismatchedMethodCells {
116 /// `sites[0]` is the first method (by index within the `Vec` passed to
117 /// `add_relationship`) whose cell set diverges from method 0's; `sites[1]` is
118 /// always `MethodIndex(0)`, the baseline every other method's cell set is
119 /// compared against; further entries are the cells in the symmetric
120 /// difference of the two methods' cell sets.
121 sites: Vec<ErrorSite>,
122 },
123
124 /// A method's own `outputs` list names a cell more than once, or two methods in
125 /// the same relationship have identical or nested `outputs` sets. Overlapping
126 /// output sets are allowed when neither set nests the other.
127 InvalidMethodOutputs {
128 /// `sites[0]` is the method (by index within the `Vec` passed to
129 /// `add_relationship`) whose output set collided. For a method's own outputs
130 /// repeating a cell, further entries are the repeated cell(s). For two methods
131 /// with identical or nested output sets, `sites[1]` is the earlier method's
132 /// index and further entries are the duplicate or nested subset cell(s).
133 sites: Vec<ErrorSite>,
134 },
135
136 /// A conditional is structurally invalid: the cell was not found, a referenced
137 /// relationship was not found, a relationship appears in more than one conditional
138 /// branch, a branch key's type does not match the cell's registered type, or a branch
139 /// has no keys.
140 InvalidConditional {
141 /// Empty for the expression-output type-mismatch case; the match-cell
142 /// type-mismatch case names the cell (`sites = [Cell(match_cell)]`). The
143 /// duplicate-relationship case names the offending relationship. Empty for the
144 /// missing-relationship and empty-branch-keys cases.
145 sites: Vec<ErrorSite>,
146 },
147
148 /// An `add_out` call is structurally invalid: the writer method does not have
149 /// exactly one output cell.
150 InvalidOutput,
151
152 /// A relationship or conditional attempted to claim a `Source`-kind cell as a
153 /// method's output, `write()` targeted an `Out`-kind cell, or `add_out` targeted a
154 /// cell that is already `Source`/`Out` kind or already claimed as another method's
155 /// output.
156 InvalidCellKind {
157 /// `sites[0]` is the method (by index within the `Vec` passed to
158 /// `add_relationship`) whose output cell has the wrong kind, if the error
159 /// originated there; `sites[1]` is that offending cell. Empty when the error
160 /// did not originate from `add_relationship`.
161 sites: Vec<ErrorSite>,
162 },
163
164 /// An `add_filter` call is structurally invalid: the cell already has a filter,
165 /// the filter's own value type does not match the cell's registered type, or the
166 /// filter's own argument list names `cell` itself. (An unknown cell or an
167 /// argument-cell type mismatch use the shared `InvalidId`/`TypeMismatch` variants
168 /// instead — `add_filter` has no cell-kind restriction, so it never returns
169 /// `InvalidCellKind`.)
170 InvalidFilter,
171
172 /// An `add_requirement` call has an invalid input signature.
173 InvalidRequirement,
174
175 /// A filter's argument, or a conditional's match subject, depends on a cell that
176 /// filter or conditional governs, in the sheet's static dependency graph: every
177 /// method's `input → output` edges (a self-referencing input adds none), each
178 /// filter's `argument → filtered` guard edges, and each conditional's
179 /// `match cell → output` guard edges for every method output of its branch and
180 /// default relationships. Returned by `Sheet::validate` and by `Sheet::propagate`,
181 /// which validates before mutating propagation state.
182 DependencyCycle {
183 /// The cycle in dependency order, starting at the governed cell `t` and ending at
184 /// the guard cell `g` whose guard edge `g → t` closes it. `Cell` entries are
185 /// separated by the `Relationship` each method hop runs through. Two consecutive
186 /// `Cell` entries are a guard hop.
187 sites: Vec<ErrorSite>,
188 },
189}
190
191impl std::fmt::Display for Error {
192 /// Describes the failed runtime contract and any underlying method failure.
193 ///
194 /// - Complexity: O(L) for L formatted characters, plus underlying error formatting.
195 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196 match self {
197 Error::TypeMismatch {
198 expected, found, ..
199 } => {
200 write!(f, "type mismatch: expected {expected:?}, found {found:?}")
201 }
202 Error::InvalidId => write!(f, "invalid cell or relationship id"),
203 Error::Conflict { .. } => write!(f, "no valid method assignment (overconstrained)"),
204 Error::Cycle { .. } => write!(f, "selected methods form a cycle"),
205 Error::SeedCycle { .. } => write!(f, "seed dependency cycle"),
206 Error::MethodFailed { error, .. } => write!(f, "method execution failed: {error}"),
207 Error::InvalidMethod { .. } => write!(f, "method is structurally invalid"),
208 Error::MismatchedMethodCells { .. } => write!(
209 f,
210 "methods in a relationship must reference the same set of cells"
211 ),
212 Error::InvalidMethodOutputs { .. } => write!(
213 f,
214 "a method's outputs must be duplicate-free, and no two methods in a \
215 relationship may have identical or nested output sets"
216 ),
217 Error::InvalidConditional { .. } => write!(f, "conditional is structurally invalid"),
218 Error::InvalidOutput => write!(f, "output is structurally invalid"),
219 Error::InvalidCellKind { .. } => {
220 write!(f, "cell's kind does not permit this operation")
221 }
222 Error::InvalidFilter => write!(f, "filter is structurally invalid"),
223 Error::InvalidRequirement => write!(f, "requirement is structurally invalid"),
224 Error::DependencyCycle { .. } => write!(
225 f,
226 "a filter or conditional depends on a cell it governs (dependency cycle)"
227 ),
228 }
229 }
230}
231
232impl std::error::Error for Error {
233 /// Returns the underlying `anyhow::Error` source for `MethodFailed`.
234 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
235 if let Error::MethodFailed { error, .. } = self {
236 Some(error.as_ref())
237 } else {
238 None
239 }
240 }
241}
242
243impl Error {
244 /// The source components this error implicates, primary first.
245 ///
246 /// `sites[0]` is the primary culprit; further entries are ordered related context
247 /// (for a cycle, the remaining members in loop order). Empty for variants that never
248 /// track a site, or when none applies to this occurrence.
249 ///
250 /// # Examples
251 /// See the [error example](crate::error).
252 pub fn sites(&self) -> &[ErrorSite] {
253 match self {
254 Error::TypeMismatch { sites, .. }
255 | Error::MethodFailed { sites, .. }
256 | Error::InvalidMethod { sites }
257 | Error::MismatchedMethodCells { sites }
258 | Error::InvalidMethodOutputs { sites }
259 | Error::InvalidCellKind { sites }
260 | Error::Conflict { sites }
261 | Error::Cycle { sites }
262 | Error::SeedCycle { sites }
263 | Error::DependencyCycle { sites }
264 | Error::InvalidConditional { sites } => sites,
265 _ => &[],
266 }
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 /// Checks that mismatch fields distinguish expected and supplied types.
276 fn type_mismatch_fields_convention() {
277 use std::any::TypeId;
278 let expected = TypeId::of::<i32>();
279 let found = TypeId::of::<f64>();
280 let e = Error::TypeMismatch {
281 expected,
282 found,
283 sites: vec![],
284 };
285 match e {
286 Error::TypeMismatch {
287 expected: e,
288 found: f,
289 ..
290 } => {
291 assert_eq!(e, TypeId::of::<i32>());
292 assert_eq!(f, TypeId::of::<f64>());
293 }
294 _ => panic!("wrong variant"),
295 }
296 }
297
298 #[test]
299 /// Checks that mismatch formatting identifies the type failure.
300 fn type_mismatch_display_contains_type_mismatch() {
301 let err = Error::TypeMismatch {
302 expected: TypeId::of::<i32>(),
303 found: TypeId::of::<f64>(),
304 sites: vec![],
305 };
306 assert!(err.to_string().contains("type mismatch"));
307 }
308
309 #[test]
310 /// Checks that unknown-identity formatting identifies invalid input.
311 fn invalid_id_display_contains_invalid() {
312 assert!(Error::InvalidId.to_string().contains("invalid"));
313 }
314
315 #[test]
316 /// Checks that conflict formatting identifies an overconstrained graph.
317 fn conflict_display_contains_overconstrained() {
318 assert!(
319 Error::Conflict { sites: vec![] }
320 .to_string()
321 .contains("overconstrained")
322 );
323 }
324
325 #[test]
326 /// Checks that execution-cycle formatting identifies the cycle.
327 fn cycle_display_contains_cycle() {
328 assert!(Error::Cycle { sites: vec![] }.to_string().contains("cycle"));
329 }
330
331 #[test]
332 /// Checks that method-failure formatting includes its underlying message.
333 fn method_failed_display_contains_source_message() {
334 let err = Error::MethodFailed {
335 error: anyhow::anyhow!("division by zero"),
336 sites: vec![],
337 };
338 assert!(err.to_string().contains("division by zero"));
339 }
340
341 #[test]
342 /// Checks that malformed-method formatting identifies invalid input.
343 fn invalid_method_display_contains_invalid() {
344 assert!(
345 Error::InvalidMethod { sites: vec![] }
346 .to_string()
347 .contains("invalid")
348 );
349 }
350
351 #[test]
352 /// Checks that runtime failures satisfy the standard error interface.
353 fn error_implements_std_error() {
354 /// Accepts a borrowed value through the standard error interface.
355 fn takes_error(_: &dyn std::error::Error) {}
356 takes_error(&Error::InvalidId);
357 takes_error(&Error::Conflict { sites: vec![] });
358 }
359
360 #[test]
361 /// Checks that a failed method exposes its underlying error.
362 fn method_failed_source_returns_some() {
363 let err = Error::MethodFailed {
364 error: anyhow::anyhow!("inner"),
365 sites: vec![],
366 };
367 assert!(std::error::Error::source(&err).is_some());
368 }
369
370 #[test]
371 /// Checks that structural failures expose no underlying callback error.
372 fn non_method_failed_variants_have_no_source() {
373 assert!(std::error::Error::source(&Error::InvalidId).is_none());
374 assert!(std::error::Error::source(&Error::Conflict { sites: vec![] }).is_none());
375 assert!(std::error::Error::source(&Error::Cycle { sites: vec![] }).is_none());
376 assert!(std::error::Error::source(&Error::SeedCycle { sites: vec![] }).is_none());
377 assert!(std::error::Error::source(&Error::InvalidMethod { sites: vec![] }).is_none());
378 assert!(
379 std::error::Error::source(&Error::TypeMismatch {
380 expected: std::any::TypeId::of::<i32>(),
381 found: std::any::TypeId::of::<f64>(),
382 sites: vec![],
383 })
384 .is_none()
385 );
386 assert!(
387 std::error::Error::source(&Error::MismatchedMethodCells { sites: vec![] }).is_none()
388 );
389 assert!(
390 std::error::Error::source(&Error::InvalidMethodOutputs { sites: vec![] }).is_none()
391 );
392 }
393
394 #[test]
395 /// Checks that conditional-validation formatting names the component.
396 fn invalid_conditional_display_contains_conditional() {
397 assert!(
398 Error::InvalidConditional { sites: vec![] }
399 .to_string()
400 .contains("conditional")
401 );
402 }
403
404 #[test]
405 /// Checks that conditional-validation failure has no underlying error.
406 fn invalid_conditional_has_no_source() {
407 assert!(std::error::Error::source(&Error::InvalidConditional { sites: vec![] }).is_none());
408 }
409
410 #[test]
411 /// Checks that incompatible-method formatting identifies the cell mismatch.
412 fn mismatched_method_cells_display_contains_cells() {
413 assert!(
414 Error::MismatchedMethodCells { sites: vec![] }
415 .to_string()
416 .contains("cells")
417 );
418 }
419
420 #[test]
421 /// Checks that malformed-output formatting identifies the output sets.
422 fn invalid_method_outputs_display_contains_outputs() {
423 assert!(
424 Error::InvalidMethodOutputs { sites: vec![] }
425 .to_string()
426 .contains("outputs")
427 );
428 }
429
430 #[test]
431 /// Checks that malformed-Out formatting identifies invalid input.
432 fn invalid_output_display_contains_invalid() {
433 assert!(Error::InvalidOutput.to_string().contains("invalid"));
434 }
435
436 #[test]
437 /// Checks that malformed-Out failure has no underlying error.
438 fn invalid_output_has_no_source() {
439 assert!(std::error::Error::source(&Error::InvalidOutput).is_none());
440 }
441
442 #[test]
443 /// Checks that cell-kind formatting names the rejected kind.
444 fn invalid_cell_kind_display_contains_kind() {
445 assert!(
446 Error::InvalidCellKind { sites: vec![] }
447 .to_string()
448 .contains("kind")
449 );
450 }
451
452 // Regression guard for https://github.com/stlab/cel-rs/issues/166: the message used to
453 // claim the cell "belongs to a terminal output", which stopped being true once `out` cells
454 // became usable as inputs, and never covered the `Source`-kind case at all.
455 #[test]
456 /// Checks that cell-kind formatting avoids the obsolete terminal terminology.
457 fn invalid_cell_kind_display_does_not_mention_terminal() {
458 assert!(
459 !Error::InvalidCellKind { sites: vec![] }
460 .to_string()
461 .contains("terminal")
462 );
463 }
464
465 #[test]
466 /// Checks that a rejected cell kind has no underlying error.
467 fn invalid_cell_kind_has_no_source() {
468 assert!(std::error::Error::source(&Error::InvalidCellKind { sites: vec![] }).is_none());
469 }
470
471 #[test]
472 /// Checks that filter-validation formatting names the filter.
473 fn invalid_filter_display_contains_filter() {
474 assert!(Error::InvalidFilter.to_string().contains("filter"));
475 }
476
477 #[test]
478 /// Checks that filter-validation failure has no underlying error.
479 fn invalid_filter_has_no_source() {
480 assert!(std::error::Error::source(&Error::InvalidFilter).is_none());
481 }
482
483 #[test]
484 /// Checks that requirement-validation formatting names the requirement.
485 fn invalid_requirement_display_contains_requirement() {
486 assert!(
487 Error::InvalidRequirement
488 .to_string()
489 .contains("requirement")
490 );
491 }
492
493 #[test]
494 /// Checks that requirement-validation failure has no underlying error.
495 fn invalid_requirement_has_no_source() {
496 assert!(std::error::Error::source(&Error::InvalidRequirement).is_none());
497 }
498
499 #[test]
500 /// Checks that guard-dependency formatting identifies the cycle.
501 fn dependency_cycle_display_contains_cycle() {
502 assert!(
503 Error::DependencyCycle { sites: vec![] }
504 .to_string()
505 .contains("cycle")
506 );
507 }
508
509 #[test]
510 /// Checks that seed-cycle formatting distinguishes seed dependencies.
511 fn seed_cycle_display_mentions_seed_dependency_cycle() {
512 assert!(
513 Error::SeedCycle { sites: vec![] }
514 .to_string()
515 .contains("seed dependency cycle")
516 );
517 }
518
519 #[test]
520 /// Checks that a guard-dependency cycle has no underlying error.
521 fn dependency_cycle_has_no_source() {
522 assert!(std::error::Error::source(&Error::DependencyCycle { sites: vec![] }).is_none());
523 }
524
525 #[test]
526 /// Checks that a guard-dependency cycle preserves its diagnostic sites.
527 fn dependency_cycle_exposes_its_sites() {
528 let site = ErrorSite::Cell(CellId::default());
529 let e = Error::DependencyCycle { sites: vec![site] };
530 assert_eq!(e.sites(), &[site]);
531 }
532
533 #[test]
534 /// Checks that a seed cycle preserves its diagnostic sites.
535 fn seed_cycle_exposes_its_sites() {
536 let site = ErrorSite::Cell(CellId::default());
537 let e = Error::SeedCycle { sites: vec![site] };
538 assert_eq!(e.sites(), &[site]);
539 }
540
541 #[test]
542 /// Checks that cell, relationship and method sites retain distinct identities.
543 fn error_site_variants_are_distinct() {
544 let a = ErrorSite::MethodIndex(0);
545 let b = ErrorSite::Method(RelationshipId::default(), 0);
546 let c = ErrorSite::Relationship(RelationshipId::default());
547 assert_ne!(a, b);
548 assert_ne!(b, c);
549 assert_eq!(a, ErrorSite::MethodIndex(0));
550 }
551
552 #[test]
553 /// Checks that an error without locations exposes an empty site slice.
554 fn sites_is_empty_for_a_siteless_variant() {
555 assert!(Error::InvalidId.sites().is_empty());
556 }
557
558 #[test]
559 /// Checks that a located error exposes its recorded site.
560 fn sites_returns_the_recorded_site() {
561 let rid = RelationshipId::default();
562 let e = Error::MethodFailed {
563 error: anyhow::anyhow!("x"),
564 sites: vec![ErrorSite::Method(rid, 2)],
565 };
566 assert_eq!(e.sites(), &[ErrorSite::Method(rid, 2)]);
567 }
568}