Skip to main content

devscout_rs/semantic/
uncertainty.rs

1// The eight-state uncertainty vocabulary every compiler fact is classified
2// into, and the one place every producer-side and consumer-owned signal
3// folds into it. Only `Confirmed` ever participates in the resolver's
4// same-context override (`precedence.rs`); every other state falls through
5// to the syntax ladder unchanged, which is what makes "absence of a record
6// is never a negative fact" true by construction rather than by a second
7// check somewhere else.
8
9/// One of eight distinct uncertainty states, each carrying its own reason.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum Uncertainty {
12    /// A same-context compiler fact names exactly one target.
13    Confirmed,
14    /// A resolution string this admission path does not yet recognise --
15    /// forward-compatible with a producer that adds a new outcome, never
16    /// silently promoted to `Confirmed`.
17    Candidate,
18    /// The compiler itself could not choose among more than one symbol.
19    Ambiguous,
20    /// The compiler found no candidate at all for this occurrence.
21    Unresolved,
22    /// The compiler bound the occurrence to something this consumer does not
23    /// treat as a same-context answer -- dynamic dispatch (late binding), for
24    /// example.
25    Unsupported,
26    /// The compiler found a candidate and explicitly ruled it out
27    /// (inaccessible from the call site), rather than merely failing to
28    /// choose one.
29    Excluded,
30    /// The admitted artifact's own freshness check failed: the checkout has
31    /// moved since the artifact was captured.
32    Stale {
33        /// The machine-readable freshness trigger -- owned because an
34        /// import-changed/import-missing trigger names the specific import.
35        reason: String,
36    },
37    /// The occurrence's own compilation is a diagnostically incomplete or
38    /// unsupported unit -- a partial compile result can never be reported as
39    /// a clean, confirmed answer.
40    Incomplete {
41        /// The compilation's own first-error or drop reason.
42        reason: String,
43    },
44}
45
46impl Uncertainty {
47    /// A short, stable, machine-readable reason string -- every state
48    /// carries one, so every fall-through to the syntax ladder is explainable.
49    pub fn reason(&self) -> String {
50        match self {
51            Uncertainty::Confirmed => "confirmed".to_string(),
52            Uncertainty::Candidate => "unrecognised-resolution".to_string(),
53            Uncertainty::Ambiguous => "compiler-ambiguous".to_string(),
54            Uncertainty::Unresolved => "compiler-unresolved".to_string(),
55            Uncertainty::Unsupported => "dynamic-dispatch".to_string(),
56            Uncertainty::Excluded => "inaccessible".to_string(),
57            Uncertainty::Stale { reason } => reason.clone(),
58            Uncertainty::Incomplete { reason } => reason.clone(),
59        }
60    }
61
62    /// Whether this state may participate in the resolver's same-context
63    /// override. Every other state falls through to the syntax ladder
64    /// untouched.
65    pub fn is_confirmed(&self) -> bool {
66        matches!(self, Uncertainty::Confirmed)
67    }
68}
69
70/// Maps one occurrence site's own `resolution` string (the literal the
71/// engine writes) to its `Uncertainty` state. Producer-recognised values
72/// today: `"confirmed"`, `"ambiguous"`, `"unresolved"`, `"inaccessible"`,
73/// `"dynamic"`; anything else maps to `Candidate` rather than being assumed
74/// confirmed.
75pub fn from_resolution(resolution: &str) -> Uncertainty {
76    match resolution {
77        "confirmed" => Uncertainty::Confirmed,
78        "ambiguous" => Uncertainty::Ambiguous,
79        "unresolved" => Uncertainty::Unresolved,
80        "dynamic" => Uncertainty::Unsupported,
81        "inaccessible" => Uncertainty::Excluded,
82        _ => Uncertainty::Candidate,
83    }
84}
85
86/// Maps a compilation's own declared `state` (the context envelope's
87/// per-compilation field -- `"complete"`/`"partial"`/`"unsupported"`) to the
88/// `Incomplete`/`Unsupported` states an occurrence in that compilation
89/// inherits, or `None` for `"complete"`, meaning the occurrence's own
90/// resolution decides instead.
91pub fn from_compilation_state(state: &str, reason: &str) -> Option<Uncertainty> {
92    match state {
93        "complete" => None,
94        "unsupported" => Some(Uncertainty::Unsupported),
95        _ => Some(Uncertainty::Incomplete {
96            reason: reason.to_string(),
97        }),
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn recognised_resolutions_map_to_their_own_state() {
107        assert_eq!(from_resolution("confirmed"), Uncertainty::Confirmed);
108        assert_eq!(from_resolution("ambiguous"), Uncertainty::Ambiguous);
109        assert_eq!(from_resolution("unresolved"), Uncertainty::Unresolved);
110        assert_eq!(from_resolution("dynamic"), Uncertainty::Unsupported);
111        assert_eq!(from_resolution("inaccessible"), Uncertainty::Excluded);
112    }
113
114    #[test]
115    fn an_unrecognised_resolution_is_a_candidate_not_a_confirmation() {
116        assert_eq!(from_resolution("future-outcome"), Uncertainty::Candidate);
117        assert!(!from_resolution("future-outcome").is_confirmed());
118    }
119
120    #[test]
121    fn only_confirmed_participates_in_override() {
122        assert!(Uncertainty::Confirmed.is_confirmed());
123        assert!(!Uncertainty::Ambiguous.is_confirmed());
124        assert!(!Uncertainty::Unresolved.is_confirmed());
125        assert!(!Uncertainty::Unsupported.is_confirmed());
126        assert!(!Uncertainty::Excluded.is_confirmed());
127        assert!(!Uncertainty::Candidate.is_confirmed());
128        assert!(!Uncertainty::Stale {
129            reason: "x".to_string()
130        }
131        .is_confirmed());
132        assert!(!Uncertainty::Incomplete {
133            reason: "x".to_string()
134        }
135        .is_confirmed());
136    }
137
138    #[test]
139    fn a_complete_compilation_defers_to_the_occurrence_resolution() {
140        assert_eq!(from_compilation_state("complete", "complete"), None);
141    }
142
143    #[test]
144    fn a_partial_or_unsupported_compilation_overrides_the_occurrence() {
145        assert_eq!(
146            from_compilation_state("partial", "binding-error"),
147            Some(Uncertainty::Incomplete {
148                reason: "binding-error".to_string()
149            })
150        );
151        assert_eq!(
152            from_compilation_state("unsupported", "no-target"),
153            Some(Uncertainty::Unsupported)
154        );
155    }
156
157    #[test]
158    fn every_state_carries_a_nonempty_reason() {
159        let states = [
160            Uncertainty::Confirmed,
161            Uncertainty::Candidate,
162            Uncertainty::Ambiguous,
163            Uncertainty::Unresolved,
164            Uncertainty::Unsupported,
165            Uncertainty::Excluded,
166            Uncertainty::Stale {
167                reason: "stale-x".to_string(),
168            },
169            Uncertainty::Incomplete {
170                reason: "incomplete-x".to_string(),
171            },
172        ];
173        for state in states {
174            assert!(!state.reason().is_empty());
175        }
176    }
177}