Skip to main content

sim_incremental_core/projection/
graph.rs

1use std::{
2    collections::{BTreeMap, BTreeSet},
3    error::Error,
4    fmt,
5};
6
7use super::{ConclusionId, Explanation, FactId};
8
9/// One owner's canonical conclusion-to-fact dependency graph.
10#[derive(Clone, Debug, Eq, PartialEq)]
11pub struct OwnerProjectionGraph {
12    owner: String,
13    dependencies: BTreeMap<ConclusionId, BTreeSet<FactId>>,
14}
15
16impl OwnerProjectionGraph {
17    /// Constructs a local owner graph and rejects an empty owner or conclusion.
18    pub fn new(
19        owner: impl Into<String>,
20        dependencies: impl IntoIterator<Item = (ConclusionId, BTreeSet<FactId>)>,
21    ) -> Result<Self, ClosureError> {
22        let owner = owner.into();
23        if owner.trim().is_empty() {
24            return Err(ClosureError::EmptyOwner);
25        }
26        let mut canonical = BTreeMap::new();
27        for (conclusion, facts) in dependencies {
28            if facts.is_empty() {
29                return Err(ClosureError::ConclusionWithoutFacts(conclusion));
30            }
31            if canonical.insert(conclusion.clone(), facts).is_some() {
32                return Err(ClosureError::DuplicateConclusion(conclusion));
33            }
34        }
35        if canonical.is_empty() {
36            return Err(ClosureError::EmptyOwnerGraph(owner));
37        }
38        Ok(Self {
39            owner,
40            dependencies: canonical,
41        })
42    }
43
44    /// Returns the owner identity.
45    #[must_use]
46    pub fn owner(&self) -> &str {
47        &self.owner
48    }
49}
50
51/// Sealed union of local owner graphs with a canonical reverse dependency map.
52#[derive(Clone, Debug, Eq, PartialEq)]
53pub struct FederatedClosure {
54    owners: BTreeMap<ConclusionId, String>,
55    dependencies: BTreeMap<ConclusionId, BTreeSet<FactId>>,
56    consumers: BTreeMap<FactId, BTreeSet<ConclusionId>>,
57}
58
59impl FederatedClosure {
60    /// Joins owner-local graphs and refuses unknown facts or duplicate ownership.
61    pub fn seal(
62        facts: impl IntoIterator<Item = FactId>,
63        graphs: impl IntoIterator<Item = OwnerProjectionGraph>,
64    ) -> Result<Self, ClosureError> {
65        let facts = facts.into_iter().collect::<BTreeSet<_>>();
66        let mut owners = BTreeMap::new();
67        let mut dependencies = BTreeMap::new();
68        let mut consumers = facts
69            .iter()
70            .cloned()
71            .map(|fact| (fact, BTreeSet::new()))
72            .collect::<BTreeMap<_, _>>();
73        for graph in graphs {
74            for (conclusion, required) in graph.dependencies {
75                if let Some(first) = owners.insert(conclusion.clone(), graph.owner.clone()) {
76                    return Err(ClosureError::DuplicateOwner {
77                        conclusion,
78                        first,
79                        second: graph.owner,
80                    });
81                }
82                for fact in &required {
83                    let Some(fact_consumers) = consumers.get_mut(fact) else {
84                        return Err(ClosureError::UnknownFact {
85                            conclusion,
86                            fact: fact.clone(),
87                        });
88                    };
89                    fact_consumers.insert(conclusion.clone());
90                }
91                dependencies.insert(conclusion, required);
92            }
93        }
94        if dependencies.is_empty() {
95            return Err(ClosureError::EmptyClosure);
96        }
97        Ok(Self {
98            owners,
99            dependencies,
100            consumers,
101        })
102    }
103
104    /// Computes the exact conclusion closure affected by changed facts.
105    #[must_use]
106    pub fn affected(&self, changed: impl IntoIterator<Item = FactId>) -> Vec<ConclusionId> {
107        changed
108            .into_iter()
109            .filter_map(|fact| self.consumers.get(&fact))
110            .flatten()
111            .cloned()
112            .collect::<BTreeSet<_>>()
113            .into_iter()
114            .collect()
115    }
116
117    /// Explains one exact conclusion-to-fact dependency.
118    pub fn explain(
119        &self,
120        conclusion: &ConclusionId,
121        fact: &FactId,
122    ) -> Result<Explanation, ClosureError> {
123        let Some(required) = self.dependencies.get(conclusion) else {
124            return Err(ClosureError::UnknownConclusion(conclusion.clone()));
125        };
126        if !required.contains(fact) {
127            return Err(ClosureError::Unrelated {
128                conclusion: conclusion.clone(),
129                fact: fact.clone(),
130            });
131        }
132        Ok(Explanation {
133            conclusion: conclusion.clone(),
134            fact: fact.clone(),
135            path: vec![
136                format!("owner/{}", self.owners[conclusion]),
137                format!("conclusion/{conclusion}"),
138                format!("fact/{fact}"),
139            ],
140        })
141    }
142}
143
144/// Fail-closed federated-closure construction or explanation error.
145#[derive(Clone, Debug, Eq, PartialEq)]
146pub enum ClosureError {
147    /// Owner identity is empty.
148    EmptyOwner,
149    /// Owner graph has no conclusions.
150    EmptyOwnerGraph(String),
151    /// A conclusion has no semantic dependency.
152    ConclusionWithoutFacts(ConclusionId),
153    /// One owner declared a conclusion twice.
154    DuplicateConclusion(ConclusionId),
155    /// Two owners claimed one conclusion.
156    DuplicateOwner {
157        /// Conflicting conclusion.
158        conclusion: ConclusionId,
159        /// First owner.
160        first: String,
161        /// Second owner.
162        second: String,
163    },
164    /// A graph referenced a fact outside the sealed world.
165    UnknownFact {
166        /// Referring conclusion.
167        conclusion: ConclusionId,
168        /// Missing fact.
169        fact: FactId,
170    },
171    /// No owner graph contributed a conclusion.
172    EmptyClosure,
173    /// Requested conclusion is absent.
174    UnknownConclusion(ConclusionId),
175    /// Conclusion does not consume the requested fact.
176    Unrelated {
177        /// Requested conclusion.
178        conclusion: ConclusionId,
179        /// Requested fact.
180        fact: FactId,
181    },
182}
183
184impl fmt::Display for ClosureError {
185    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
186        write!(formatter, "{self:?}")
187    }
188}
189
190impl Error for ClosureError {}