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//! <!-- @@ end lint list maintained by maint/add_warning @@ -->
48
49// @@ begin test lint list maintained by maint/add_warning @@
50#![allow(clippy::bool_assert_comparison)]
51#![allow(clippy::clone_on_copy)]
52#![allow(clippy::dbg_macro)]
53#![allow(clippy::mixed_attributes_style)]
54#![allow(clippy::print_stderr)]
55#![allow(clippy::print_stdout)]
56#![allow(clippy::single_char_pattern)]
57#![allow(clippy::unwrap_used)]
58#![allow(clippy::unchecked_duration_subtraction)]
59#![allow(clippy::useless_vec)]
60#![allow(clippy::needless_pass_by_value)]
61//! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
62
63#![allow(clippy::missing_docs_in_private_items)]
64#![allow(unreachable_pub)]
65
66use anyhow::{anyhow, Result};
67use std::collections::{HashMap, HashSet};
68use std::hash::Hash;
69
70/// Given a hashmap representing a binary relationship, compute its transitive closure.
71fn transitive_closure<K: Hash + Eq + PartialEq + Clone>(
72    inp: &HashMap<K, HashSet<K>>,
73) -> HashMap<K, HashSet<K>> {
74    let mut out = inp.clone();
75    let max_iters = inp.len();
76    for _ in 0..max_iters {
77        let mut new_out = out.clone();
78        for (k, vs) in out.iter() {
79            let new_vs = new_out
80                .get_mut(k)
81                .expect("That key should have been there when I cloned it...");
82            for v in vs.iter() {
83                if let Some(vv) = out.get(v) {
84                    new_vs.extend(vv.iter().cloned());
85                }
86            }
87        }
88        if out == new_out {
89            break;
90        } else {
91            out = new_out;
92        }
93    }
94    out
95}
96
97/// Given a hashmap representing a binary relationship, invert it.
98fn invert<K: Hash + Eq + PartialEq + Clone>(
99    inp: &HashMap<K, HashSet<K>>,
100) -> HashMap<K, HashSet<K>> {
101    let mut out: HashMap<K, HashSet<K>> = HashMap::new();
102    for (k, vs) in inp.iter() {
103        for v in vs {
104            out.entry(v.clone()).or_default().insert(k.clone());
105        }
106    }
107    out
108}
109
110/// A representation of which features depend on what.
111#[derive(Clone, Debug, Default)]
112pub struct FeatureGraph {
113    /// List of all features.
114    all_features: HashSet<String>,
115    /// Adjacency map: for each feature F, `depends_on[F]` includes G if F
116    /// directly depends on G.
117    depends_on: HashMap<String, HashSet<String>>,
118    /// Inverse adjacency map.
119    depended_on_by: HashMap<String, HashSet<String>>,
120    /// Transitive closure of the adjacency map.
121    reachable_from: HashMap<String, HashSet<String>>,
122}
123
124impl FeatureGraph {
125    /// Convert a toml `[features]` section into a [`FeatureGraph`].
126    pub fn from_features_table(features: &toml_edit::Table) -> Result<Self> {
127        let mut depends_on = HashMap::new();
128        for (k, vs) in features.iter() {
129            let mut d = HashSet::new();
130            for v in vs
131                .as_array()
132                .ok_or_else(|| anyhow!("features.{} was not an array", k))?
133            {
134                let v = v
135                    .as_str()
136                    .ok_or_else(|| anyhow!("features.{} contained a non-string", k))?;
137                d.insert(v.to_string());
138            }
139            depends_on.insert(k.to_string(), d);
140        }
141        let all_features = depends_on.keys().cloned().collect();
142        let reachable_from = transitive_closure(&depends_on);
143        let depended_on_by = invert(&depends_on);
144        Ok(Self {
145            all_features,
146            depends_on,
147            depended_on_by,
148            reachable_from,
149        })
150    }
151
152    pub fn all_features(&self) -> impl Iterator<Item = String> + '_ {
153        self.depends_on.keys().cloned()
154    }
155
156    pub fn contains_feature(&self, feature: &str) -> bool {
157        self.all_features.contains(feature)
158    }
159
160    pub fn contains_edge(&self, from: &str, to: &str) -> bool {
161        match self.depends_on.get(from) {
162            Some(fs) => fs.contains(to),
163            None => false,
164        }
165    }
166
167    pub fn all_reachable_from(&self, feature: &str) -> impl Iterator<Item = String> + '_ {
168        match self.reachable_from.get(feature) {
169            Some(set) => itertools::Either::Left(set.iter().cloned()),
170            None => itertools::Either::Right(std::iter::empty()),
171        }
172    }
173
174    /// Return all the features that `feature` depends on.  Return an empty iterator if
175    /// it has no dependencies, or is not in this map.
176    pub fn edges_from(&self, feature: &str) -> impl Iterator<Item = String> + '_ {
177        match self.depends_on.get(feature) {
178            Some(set) => itertools::Either::Left(set.iter().cloned()),
179            None => itertools::Either::Right(std::iter::empty()),
180        }
181    }
182
183    /// Return all the features that depend on `feature` directly.  Return an empty iterator if
184    /// it has no dependencies, or is not in this map.
185    pub fn edges_to(&self, feature: &str) -> impl Iterator<Item = String> + '_ {
186        match self.depended_on_by.get(feature) {
187            Some(set) => itertools::Either::Left(set.iter().cloned()),
188            None => itertools::Either::Right(std::iter::empty()),
189        }
190    }
191}