guppy/graph/feature/weak.rs
1// Copyright (c) The cargo-guppy Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Support for weak features.
5//!
6//! A weak feature such as `a = ["foo?/std"]` is a single edge in the feature
7//! graph for each package `foo` resolves to (usually one), but Cargo applies
8//! it to each declaration of `foo` separately. guppy models that by splitting
9//! each edge's link into two halves, and running the traversal through the
10//! buffered edge filter in [`crate::petgraph_support::dfs`]:
11//!
12//! * The half covering `foo`'s required declarations is offered to the visitor
13//! as soon as the edge is reached. It is absent if `foo` has no required
14//! declaration.
15//! * The half covering `foo`'s optional declarations is held in a buffer in a
16//! forward query until `dep:foo` is activated, and offered then. If
17//! `dep:foo` is never reached, that half is never offered. In a reverse
18//! query, it is offered as soon as the edge is reached.
19//!
20//! The edge is followed if the visitor accepts either half.
21
22use crate::graph::{
23 DependencyDirection, FeatureIx, PackageIx,
24 feature::{ConditionalLink, EdgeLinks, FeatureEdgeReference},
25};
26use ahash::AHashMap;
27use indexmap::IndexSet;
28use petgraph::graph::{EdgeIndex, NodeIndex};
29use smallvec::SmallVec;
30
31/// Data structure that tracks pairs of package indexes that form weak dependencies.
32#[derive(Clone, Debug)]
33pub(super) struct WeakDependencies {
34 ixs: IndexSet<EdgeIndex<PackageIx>>,
35 // A map of dep:foo node to the weak indexes it releases when reached.
36 by_optional_dependency: AHashMap<NodeIndex<FeatureIx>, SmallVec<[WeakIndex; 1]>>,
37}
38
39impl WeakDependencies {
40 pub(super) fn new() -> Self {
41 Self {
42 ixs: IndexSet::new(),
43 by_optional_dependency: AHashMap::new(),
44 }
45 }
46
47 pub(super) fn insert(
48 &mut self,
49 edge_ix: EdgeIndex<PackageIx>,
50 optional_dependency_ix: NodeIndex<FeatureIx>,
51 ) -> WeakIndex {
52 let (index, inserted) = self.ixs.insert_full(edge_ix);
53 let index = WeakIndex(index);
54 if inserted {
55 self.by_optional_dependency
56 .entry(optional_dependency_ix)
57 .or_default()
58 .push(index);
59 }
60 index
61 }
62}
63
64// Not part of the public API -- exposed for testing.
65#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
66#[doc(hidden)]
67pub struct WeakIndex(pub(super) usize);
68
69/// Buffer states for weak indexes, to be used during a feature resolver traversal.
70pub(super) struct WeakBufferStates<'g, 'a, F> {
71 buffers: WeakBuffers<'g, 'a>,
72 accept_fn: F,
73}
74
75/// The buffers a traversal keeps for the optional halves of weak edges.
76enum WeakBuffers<'g, 'a> {
77 /// Buffering is enabled. Used by forward queries.
78 ///
79 /// A buffer is released when the traversal discovers `dep:foo`, which is
80 /// when the dependent activates `foo`'s optional declarations.
81 PerPackageEdge {
82 /// The weak dependencies that map a `dep:foo` node back to the indexes
83 /// it releases.
84 deps: &'a WeakDependencies,
85
86 /// A buffer for each weak index.
87 states: Vec<SingleBufferState<'g>>,
88 },
89
90 /// No weak buffers: optional links are offered as soon as they are reached.
91 /// Used by reverse queries.
92 Unbuffered,
93}
94
95impl<'g, 'a, F> WeakBufferStates<'g, 'a, F>
96where
97 F: FnMut(ConditionalLink<'g>) -> bool,
98{
99 /// Returns buffer states for a traversal in `direction`.
100 #[inline]
101 pub(super) fn new(
102 deps: &'a WeakDependencies,
103 direction: DependencyDirection,
104 accept_fn: F,
105 ) -> Self {
106 let buffers = match direction {
107 DependencyDirection::Forward => {
108 let len = deps.ixs.len();
109 let mut states = Vec::with_capacity(len);
110 states.resize_with(len, || SingleBufferState::Buffered(SingleBufferVec::new()));
111 WeakBuffers::PerPackageEdge { deps, states }
112 }
113 DependencyDirection::Reverse => WeakBuffers::Unbuffered,
114 };
115 Self { buffers, accept_fn }
116 }
117
118 pub(super) fn track(
119 &mut self,
120 edge_ref: FeatureEdgeReference<'g>,
121 links: EdgeLinks<'g>,
122 ) -> Option<FeatureEdgeReference<'g>> {
123 let accepted = match links {
124 EdgeLinks::Weak {
125 required,
126 optional,
127 index,
128 } => {
129 // Handle both halves, required first.
130 //
131 // Both halves must be dealt with before they are combined, so
132 // don't fold these into a single `a || b` expression. If the
133 // required half is accepted, a short-circuiting `||` would skip
134 // the optional half, and the visitor would never see it.
135 //
136 // A buffered optional half is held along with `edge_ref` even
137 // when the required half was just accepted, so releasing the
138 // buffer can hand the same `edge_ref` to the traversal a second
139 // time. That is harmless: the DFS checks its discovered set
140 // before pushing a target.
141 let required_accepted = required.is_some_and(|required| (self.accept_fn)(required));
142 let optional_accepted = match &mut self.buffers {
143 WeakBuffers::PerPackageEdge { deps: _, states } => match &mut states[index.0] {
144 SingleBufferState::Buffered(buffer) => {
145 buffer.push((optional, edge_ref));
146 false
147 }
148 SingleBufferState::Released => (self.accept_fn)(optional),
149 },
150 WeakBuffers::Unbuffered => (self.accept_fn)(optional),
151 };
152 required_accepted || optional_accepted
153 }
154 EdgeLinks::NonWeak(link) => (self.accept_fn)(link),
155 };
156 accepted.then_some(edge_ref)
157 }
158
159 // Called when the DFS reaches a feature node.
160 pub(super) fn discover(
161 &mut self,
162 feature_ix: NodeIndex<FeatureIx>,
163 ) -> Vec<FeatureEdgeReference<'g>> {
164 let (deps, states) = match &mut self.buffers {
165 WeakBuffers::PerPackageEdge { deps, states } => (*deps, states),
166 WeakBuffers::Unbuffered => return Vec::new(),
167 };
168 let Some(weak_indexes) = deps.by_optional_dependency.get(&feature_ix) else {
169 return Vec::new();
170 };
171
172 release_buffers(states, weak_indexes, &mut self.accept_fn)
173 }
174}
175
176fn release_buffers<'g, F>(
177 states: &mut [SingleBufferState<'g>],
178 weak_indexes: &[WeakIndex],
179 accept_fn: &mut F,
180) -> Vec<FeatureEdgeReference<'g>>
181where
182 F: FnMut(ConditionalLink<'g>) -> bool,
183{
184 let mut released = Vec::new();
185 for weak_index in weak_indexes {
186 match std::mem::replace(&mut states[weak_index.0], SingleBufferState::Released) {
187 SingleBufferState::Buffered(buffer) => {
188 // Transition from buffered to released.
189 released.extend(buffer.into_iter().filter_map(|(link, edge_ref)| {
190 // Filter buffered links.
191 accept_fn(link).then_some(edge_ref)
192 }));
193 }
194 SingleBufferState::Released => {
195 // This should never happen, since nodes are discovered at
196 // most once.
197 debug_assert!(false, "weak index {weak_index:?} is released once");
198 }
199 }
200 }
201 released
202}
203
204/// Buffer state for a single weak index in an in-progress resolver.
205enum SingleBufferState<'g> {
206 /// The optional halves seen so far, held until the buffer is released.
207 Buffered(SingleBufferVec<'g>),
208
209 /// The buffer has been released: optional halves are offered to the
210 /// visitor as they are reached.
211 Released,
212}
213
214type SingleBufferVec<'g> = Vec<(ConditionalLink<'g>, FeatureEdgeReference<'g>)>;