Skip to main content

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}