Skip to main content

molframe_core/contract/
outcome.rs

1//! The answer, or the reason there is none.
2//!
3//! A refusal to answer used to be a [`Status`](super::Status) beside a value
4//! that "is not meaningful". A value that must not be read but can be is a
5//! documented hazard, not a guarantee. Here the two states are different
6//! shapes: [`Outcome::Determinate`] holds the value and [`Outcome::Indeterminate`]
7//! holds why there is none, so there is nothing to misread.
8
9use super::analysis::{Coverage, Status};
10use super::policy::MissingPolicy;
11use std::fmt;
12
13/// Why no defensible answer exists under a policy.
14#[derive(Clone, PartialEq, Eq, Debug)]
15#[non_exhaustive]
16pub enum Indeterminacy {
17    /// The policy refuses to compute over atoms that are missing or ambiguous.
18    MissingInputs {
19        /// Intended atoms that were absent.
20        missing: u32,
21        /// Intended atoms that were present but not uniquely resolvable.
22        ambiguous: u32,
23    },
24    /// The alternate-conformation rule could not select a consistent set of atoms.
25    UnresolvedConformations,
26    /// One frame of a series had no defensible answer, so the series has none.
27    Frame {
28        /// Zero-based frame index.
29        frame: usize,
30        /// Why that frame has none.
31        reason: Box<Indeterminacy>,
32    },
33    /// A reason supplied by the analysis itself.
34    Other(Box<str>),
35}
36
37impl fmt::Display for Indeterminacy {
38    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
39        match self {
40            Self::MissingInputs { missing, ambiguous } => write!(
41                formatter,
42                "the policy rejects {missing} missing and {ambiguous} ambiguous inputs"
43            ),
44            Self::UnresolvedConformations => {
45                formatter.write_str("the alternate-conformation rule selected no consistent atoms")
46            }
47            Self::Frame { frame, reason } => write!(formatter, "frame {frame}: {reason}"),
48            Self::Other(reason) => formatter.write_str(reason),
49        }
50    }
51}
52
53impl std::error::Error for Indeterminacy {}
54
55/// A value, or the reason there is none.
56///
57/// There is deliberately no `Deref` and no accessor that returns a value for the
58/// indeterminate case: the way to a number goes through a match, [`Outcome::value`]
59/// or [`Outcome::into_result`], each of which says what happens otherwise.
60#[derive(Clone, PartialEq, Eq, Debug)]
61pub enum Outcome<T> {
62    /// A defensible answer.
63    Determinate(T),
64    /// No defensible answer exists.
65    Indeterminate(Indeterminacy),
66}
67
68impl<T> Outcome<T> {
69    /// Returns true when there is an answer.
70    #[must_use]
71    pub const fn is_determinate(&self) -> bool {
72        matches!(self, Self::Determinate(_))
73    }
74
75    /// The answer, if there is one.
76    #[must_use]
77    pub const fn value(&self) -> Option<&T> {
78        match self {
79            Self::Determinate(value) => Some(value),
80            Self::Indeterminate(_) => None,
81        }
82    }
83
84    /// The reason there is no answer, if there is none.
85    #[must_use]
86    pub const fn indeterminacy(&self) -> Option<&Indeterminacy> {
87        match self {
88            Self::Determinate(_) => None,
89            Self::Indeterminate(reason) => Some(reason),
90        }
91    }
92
93    /// The answer, or the reason it is missing.
94    ///
95    /// # Errors
96    ///
97    /// Returns the [`Indeterminacy`] when no defensible answer exists.
98    pub fn into_result(self) -> Result<T, Indeterminacy> {
99        match self {
100            Self::Determinate(value) => Ok(value),
101            Self::Indeterminate(reason) => Err(reason),
102        }
103    }
104
105    /// Borrows the answer, or the reason it is missing.
106    ///
107    /// # Errors
108    ///
109    /// Returns the [`Indeterminacy`] when no defensible answer exists.
110    pub const fn as_result(&self) -> Result<&T, &Indeterminacy> {
111        match self {
112            Self::Determinate(value) => Ok(value),
113            Self::Indeterminate(reason) => Err(reason),
114        }
115    }
116
117    /// Transforms the answer and leaves a refusal as it is.
118    pub fn map<U>(self, transform: impl FnOnce(T) -> U) -> Outcome<U> {
119        match self {
120            Self::Determinate(value) => Outcome::Determinate(transform(value)),
121            Self::Indeterminate(reason) => Outcome::Indeterminate(reason),
122        }
123    }
124}
125
126/// How complete a determinate answer is.
127///
128/// The qualities of an answer that exists. "No answer" is not one of them: it is
129/// [`Outcome::Indeterminate`].
130#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Default)]
131pub enum Quality {
132    /// Every intended atom was available and unambiguous.
133    #[default]
134    Complete,
135    /// Computed, but some intended atoms were not available.
136    Partial,
137    /// Several defensible answers exist; one was chosen deterministically.
138    Ambiguous,
139}
140
141impl Quality {
142    /// The weaker of two qualities, as a combined result is only as good as its parts.
143    #[must_use]
144    pub fn worst(self, other: Self) -> Self {
145        // Ambiguity is the stronger caveat: a partial answer is still unique.
146        match (self, other) {
147            (Self::Ambiguous, _) | (_, Self::Ambiguous) => Self::Ambiguous,
148            (Self::Partial, _) | (_, Self::Partial) => Self::Partial,
149            (Self::Complete, Self::Complete) => Self::Complete,
150        }
151    }
152}
153
154impl From<Quality> for Status {
155    fn from(quality: Quality) -> Self {
156        match quality {
157            Quality::Complete => Self::Complete,
158            Quality::Partial => Self::Partial,
159            Quality::Ambiguous => Self::Ambiguous,
160        }
161    }
162}
163
164/// Why a missing-input policy could not be applied.
165#[derive(Clone, Copy, PartialEq, Eq, Debug)]
166#[non_exhaustive]
167pub enum MissingPolicyError {
168    /// The policy is to fail when intended inputs are missing or ambiguous.
169    Fail {
170        /// Intended inputs that were absent.
171        missing: u32,
172        /// Intended inputs that were present but not uniquely resolvable.
173        ambiguous: u32,
174    },
175}
176
177impl fmt::Display for MissingPolicyError {
178    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
179        match self {
180            Self::Fail { missing, ambiguous } => write!(
181                formatter,
182                "the policy fails on {missing} missing and {ambiguous} ambiguous inputs"
183            ),
184        }
185    }
186}
187
188impl std::error::Error for MissingPolicyError {}
189
190/// What the policy for missing inputs makes of a value computed over `coverage`.
191///
192/// Complete coverage keeps the value at the quality it came with. Otherwise
193/// `Ignore` keeps it as it is, `Report` marks it partial, `Indeterminate` drops
194/// the value for a refusal that says what was missing, and `Fail` is an error.
195/// This is the one place the rule lives; every analysis that governs its
196/// completeness goes through it.
197///
198/// # Errors
199///
200/// Returns [`MissingPolicyError::Fail`] when the policy is to fail and inputs are
201/// missing or ambiguous.
202pub fn resolve_missing<T>(
203    value: T,
204    quality: Quality,
205    coverage: Coverage,
206    policy: MissingPolicy,
207) -> Result<(Outcome<T>, Quality), MissingPolicyError> {
208    if coverage.missing == 0 && coverage.ambiguous == 0 {
209        return Ok((Outcome::Determinate(value), quality));
210    }
211    match policy {
212        MissingPolicy::Ignore => Ok((Outcome::Determinate(value), quality)),
213        MissingPolicy::Report => Ok((Outcome::Determinate(value), quality.worst(Quality::Partial))),
214        MissingPolicy::Indeterminate => Ok((
215            Outcome::Indeterminate(Indeterminacy::MissingInputs {
216                missing: coverage.missing,
217                ambiguous: coverage.ambiguous,
218            }),
219            Quality::Complete,
220        )),
221        MissingPolicy::Fail => Err(MissingPolicyError::Fail {
222            missing: coverage.missing,
223            ambiguous: coverage.ambiguous,
224        }),
225    }
226}