fixup_features/
graph.rs

1//! Graph manipulation code
2//!
3//! I had hoped to use petgraph, but it is optimized for efficiency over
4//! usability, and I got lost in a maze of indices.
5
6// @@ begin lint list maintained by maint/add_warning @@
7#![allow(renamed_and_removed_lints)] // @@REMOVE_WHEN(ci_arti_stable)
8#![allow(unknown_lints)] // @@REMOVE_WHEN(ci_arti_nightly)
9#![warn(missing_docs)]
10#![warn(noop_method_call)]
11#![warn(unreachable_pub)]
12#![warn(clippy::all)]
13#![deny(clippy::await_holding_lock)]
14#![deny(clippy::cargo_common_metadata)]
15#![deny(clippy::cast_lossless)]
16#![deny(clippy::checked_conversions)]
17#![warn(clippy::cognitive_complexity)]
18#![deny(clippy::debug_assert_with_mut_call)]
19#![deny(clippy::exhaustive_enums)]
20#![deny(clippy::exhaustive_structs)]
21#![deny(clippy::expl_impl_clone_on_copy)]
22#![deny(clippy::fallible_impl_from)]
23#![deny(clippy::implicit_clone)]
24#![deny(clippy::large_stack_arrays)]
25#![warn(clippy::manual_ok_or)]
26#![deny(clippy::missing_docs_in_private_items)]
27#![warn(clippy::needless_borrow)]
28#![warn(clippy::needless_pass_by_value)]
29#![warn(clippy::option_option)]
30#![deny(clippy::print_stderr)]
31#![deny(clippy::print_stdout)]
32#![warn(clippy::rc_buffer)]
33#![deny(clippy::ref_option_ref)]
34#![warn(clippy::semicolon_if_nothing_returned)]
35#![warn(clippy::trait_duplication_in_bounds)]
36#![deny(clippy::unchecked_duration_subtraction)]
37#![deny(clippy::unnecessary_wraps)]
38#![warn(clippy::unseparated_literal_suffix)]
39#![deny(clippy::unwrap_used)]
40#![deny(clippy::mod_module_files)]
41#![allow(clippy::let_unit_value)] // This can reasonably be done for explicitness
42#![allow(clippy::uninlined_format_args)]
43#![allow(clippy::significant_drop_in_scrutinee)] // arti/-/merge_requests/588/#note_2812945
44#![allow(clippy::result_large_err)] // temporary workaround for arti#587
45#![allow(clippy::needless_raw_string_hashes)] // complained-about code is fine, often best
46#![allow(clippy::needless_lifetimes)] // See arti#1765
47#![allow(mismatched_lifetime_syntaxes)] // temporary workaround for arti#2060
48//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
49
50// @@ begin test lint list maintained by maint/add_warning @@
51#![allow(clippy::bool_assert_comparison)]
52#![allow(clippy::clone_on_copy)]
53#![allow(clippy::dbg_macro)]
54#![allow(clippy::mixed_attributes_style)]
55#![allow(clippy::print_stderr)]
56#![allow(clippy::print_stdout)]
57#![allow(clippy::single_char_pattern)]
58#![allow(clippy::unwrap_used)]
59#![allow(clippy::unchecked_duration_subtraction)]
60#![allow(clippy::useless_vec)]
61#![allow(clippy::needless_pass_by_value)]
62//! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
63
64#![allow(clippy::missing_docs_in_private_items)]
65#![allow(unreachable_pub)]
66
67use anyhow::{anyhow, Result};
68use std::collections::{HashMap, HashSet};
69use std::hash::Hash;
70
71/// Given a hashmap representing a binary relationship, compute its transitive closure.
72fn transitive_closure<K: Hash + Eq + PartialEq + Clone>(
73    inp: &HashMap<K, HashSet<K>>,
74) -> HashMap<K, HashSet<K>> {
75    let mut out = inp.clone();
76    let max_iters = inp.len();
77    for _ in 0..max_iters {
78        let mut new_out = out.clone();
79        for (k, vs) in out.iter() {
80            let new_vs = new_out
81                .get_mut(k)
82                .expect("That key should have been there when I cloned it...");
83            for v in vs.iter() {
84                if let Some(vv) = out.get(v) {
85                    new_vs.extend(vv.iter().cloned());
86                }
87            }
88        }
89        if out == new_out {
90            break;
91        } else {
92            out = new_out;
93        }
94    }
95    out
96}
97
98/// Given a hashmap representing a binary relationship, invert it.
99fn invert<K: Hash + Eq + PartialEq + Clone>(
100    inp: &HashMap<K, HashSet<K>>,
101) -> HashMap<K, HashSet<K>> {
102    let mut out: HashMap<K, HashSet<K>> = HashMap::new();
103    for (k, vs) in inp.iter() {
104        for v in vs {
105            out.entry(v.clone()).or_default().insert(k.clone());
106        }
107    }
108    out
109}
110
111/// A representation of which features depend on what.
112#[derive(Clone, Debug, Default)]
113pub struct FeatureGraph {
114    /// List of all features.
115    all_features: HashSet<String>,
116    /// Adjacency map: for each feature F, `depends_on[F]` includes G if F
117    /// directly depends on G.
118    depends_on: HashMap<String, HashSet<String>>,
119    /// Inverse adjacency map.
120    depended_on_by: HashMap<String, HashSet<String>>,
121    /// Transitive closure of the adjacency map.
122    reachable_from: HashMap<String, HashSet<String>>,
123}
124
125impl FeatureGraph {
126    /// Convert a toml `[features]` section into a [`FeatureGraph`].
127    pub fn from_features_table(features: &toml_edit::Table) -> Result<Self> {
128        let mut depends_on = HashMap::new();
129        for (k, vs) in features.iter() {
130            let mut d = HashSet::new();
131            for v in vs
132                .as_array()
133                .ok_or_else(|| anyhow!("features.{} was not an array", k))?
134            {
135                let v = v
136                    .as_str()
137                    .ok_or_else(|| anyhow!("features.{} contained a non-string", k))?;
138                d.insert(v.to_string());
139            }
140            depends_on.insert(k.to_string(), d);
141        }
142        let all_features = depends_on.keys().cloned().collect();
143        let reachable_from = transitive_closure(&depends_on);
144        let depended_on_by = invert(&depends_on);
145        Ok(Self {
146            all_features,
147            depends_on,
148            depended_on_by,
149            reachable_from,
150        })
151    }
152
153    pub fn all_features(&self) -> impl Iterator<Item = String> + '_ {
154        self.depends_on.keys().cloned()
155    }
156
157    pub fn contains_feature(&self, feature: &str) -> bool {
158        self.all_features.contains(feature)
159    }
160
161    pub fn contains_edge(&self, from: &str, to: &str) -> bool {
162        match self.depends_on.get(from) {
163            Some(fs) => fs.contains(to),
164            None => false,
165        }
166    }
167
168    pub fn all_reachable_from(&self, feature: &str) -> impl Iterator<Item = String> + '_ {
169        match self.reachable_from.get(feature) {
170            Some(set) => itertools::Either::Left(set.iter().cloned()),
171            None => itertools::Either::Right(std::iter::empty()),
172        }
173    }
174
175    /// Return all the features that `feature` depends on.  Return an empty iterator if
176    /// it has no dependencies, or is not in this map.
177    pub fn edges_from(&self, feature: &str) -> impl Iterator<Item = String> + '_ {
178        match self.depends_on.get(feature) {
179            Some(set) => itertools::Either::Left(set.iter().cloned()),
180            None => itertools::Either::Right(std::iter::empty()),
181        }
182    }
183
184    /// Return all the features that depend on `feature` directly.  Return an empty iterator if
185    /// it has no dependencies, or is not in this map.
186    pub fn edges_to(&self, feature: &str) -> impl Iterator<Item = String> + '_ {
187        match self.depended_on_by.get(feature) {
188            Some(set) => itertools::Either::Left(set.iter().cloned()),
189            None => itertools::Either::Right(std::iter::empty()),
190        }
191    }
192}