Skip to main content

adam_rs/
error.rs

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