@@ -125,6 +125,11 @@ impl GraphOptimizer<'_> {
125
125
did_work
126
126
}
127
127
128
+ /// This function merges nodes that can only be reached from a single starting point.
129
+ /// Example:
130
+ /// If we have a graph with edges: A->B, B->C, A->C, B->E, D->E
131
+ /// Then B and C can only be reached from A, so they will be merged into A.
132
+ /// The resulting graph would have edges like: (A,B,C)->E, D->E
128
133
pub ( super ) fn merge_nodes_with_same_starting_point < N > (
129
134
& self ,
130
135
g : & mut Graph < Vec < N > , Dependency > ,
@@ -134,64 +139,115 @@ impl GraphOptimizer<'_> {
134
139
Self : Index < N , Output = ItemId > ,
135
140
{
136
141
let mut did_work = false ;
137
- let mut nodes_to_merge = Vec :: new ( ) ;
142
+ let mut reachability : FxHashMap < _ , FxHashSet < _ > > = FxHashMap :: default ( ) ;
138
143
139
- for node in g. node_indices ( ) {
140
- let items = g. node_weight ( node) . expect ( "Node should exist" ) ;
141
- for item in items {
142
- let item_id = & self [ * item] ;
143
- if matches ! ( item_id, ItemId :: Group ( ..) ) {
144
- let incoming_edges: Vec < _ > =
145
- g. edges_directed ( node, Direction :: Incoming ) . collect ( ) ;
146
- if incoming_edges. len ( ) == 1 && incoming_edges[ 0 ] . source ( ) == node {
147
- nodes_to_merge. push ( node) ;
148
- break ;
149
- }
144
+ // Step 1: Build a reverse reachability map (which starting nodes can reach each node)
145
+ // We consider a "starting node" as one with no incoming edges
146
+ let starting_nodes: Vec < _ > = g
147
+ . node_indices ( )
148
+ . filter ( |& node| g. edges_directed ( node, Direction :: Incoming ) . count ( ) == 0 )
149
+ . collect ( ) ;
150
+
151
+ // For each starting node, find all nodes reachable from it
152
+ for & start in & starting_nodes {
153
+ let mut visited = FxHashSet :: default ( ) ;
154
+ let mut queue = vec ! [ start] ;
155
+
156
+ while let Some ( node) = queue. pop ( ) {
157
+ if !visited. insert ( node) {
158
+ continue ;
159
+ }
160
+
161
+ // For each outgoing edge, add the target to queue
162
+ for edge in g. edges_directed ( node, Direction :: Outgoing ) {
163
+ let target = edge. target ( ) ;
164
+ queue. push ( target) ;
165
+
166
+ // Add this starting node to the set of starting nodes that can reach target
167
+ reachability. entry ( target) . or_default ( ) . insert ( start) ;
150
168
}
151
169
}
152
170
}
153
171
154
- for node in nodes_to_merge {
155
- let mut dependencies = g
156
- . edges_directed ( node, Direction :: Outgoing )
157
- . map ( |e| ( e. target ( ) , * e. weight ( ) ) )
158
- . collect :: < Vec < _ > > ( ) ;
172
+ // Step 2: Find nodes that are reachable from exactly one starting node
173
+ // and group them by that starting node
174
+ let mut merge_groups: FxHashMap < _ , Vec < _ > > = FxHashMap :: default ( ) ;
159
175
160
- // Handle transitive dependencies
161
- let mut visited = FxHashSet :: default ( ) ;
162
- let mut stack = dependencies. clone ( ) ;
163
- while let Some ( ( dependency, _weight) ) = stack. pop ( ) {
164
- if visited. insert ( dependency) {
165
- let transitive_deps = g
166
- . edges_directed ( dependency, Direction :: Outgoing )
167
- . map ( |e| ( e. target ( ) , * e. weight ( ) ) )
168
- . collect :: < Vec < _ > > ( ) ;
169
- stack. extend ( transitive_deps. clone ( ) ) ;
170
- dependencies. extend ( transitive_deps) ;
176
+ for node in g. node_indices ( ) {
177
+ // Skip starting nodes
178
+ if starting_nodes. contains ( & node) {
179
+ continue ;
180
+ }
181
+
182
+ // Skip nodes that should not be merged
183
+ if self . should_not_merge_iter ( g. node_weight ( node) . expect ( "Node should exist" ) ) {
184
+ continue ;
185
+ }
186
+
187
+ // If this node is reachable from exactly one starting node, add it to that group
188
+ if let Some ( reachable_from) = reachability. get ( & node) {
189
+ if reachable_from. len ( ) == 1 {
190
+ let start = * reachable_from. iter ( ) . next ( ) . unwrap ( ) ;
191
+
192
+ // Don't merge if the starting node should not be merged
193
+ if self . should_not_merge_iter ( g. node_weight ( start) . expect ( "Node should exist" ) )
194
+ {
195
+ continue ;
196
+ }
197
+
198
+ merge_groups. entry ( start) . or_default ( ) . push ( node) ;
171
199
}
172
200
}
201
+ }
173
202
174
- for ( dependency, weight) in dependencies {
175
- let edge = g
176
- . find_edge ( node, dependency)
177
- . and_then ( |e| g. edge_weight_mut ( e) ) ;
178
- match edge {
179
- Some ( v) => {
180
- if matches ! ( v, Dependency :: Weak ) {
181
- * v = weight;
203
+ // Step 3: Merge nodes into their starting points
204
+ for ( start, nodes_to_merge) in merge_groups {
205
+ if nodes_to_merge. is_empty ( ) {
206
+ continue ;
207
+ }
208
+
209
+ let mut nodes_to_remove = Vec :: new ( ) ;
210
+
211
+ for node in nodes_to_merge {
212
+ // Move outgoing edges from node to start
213
+ let outgoing_edges: Vec < _ > = g
214
+ . edges_directed ( node, Direction :: Outgoing )
215
+ . map ( |e| ( e. target ( ) , * e. weight ( ) ) )
216
+ . collect ( ) ;
217
+
218
+ for ( target, weight) in outgoing_edges {
219
+ // If there's already an edge from start to target, only update if necessary
220
+ let existing_edge = g. find_edge ( start, target) ;
221
+ match existing_edge {
222
+ Some ( e) => {
223
+ let edge_weight = g. edge_weight_mut ( e) . unwrap ( ) ;
224
+ // Only upgrade from weak to strong dependency
225
+ if matches ! ( edge_weight, Dependency :: Weak )
226
+ && !matches ! ( weight, Dependency :: Weak )
227
+ {
228
+ * edge_weight = weight;
229
+ }
230
+ }
231
+ None => {
232
+ // Add a new edge
233
+ g. add_edge ( start, target, weight) ;
182
234
}
183
- }
184
- None => {
185
- g. add_edge ( node, dependency, weight) ;
186
235
}
187
236
}
188
- }
189
237
190
- let items = g. node_weight ( node) . expect ( "Node should exist" ) . clone ( ) ;
191
- g. node_weight_mut ( node) . unwrap ( ) . extend ( items) ;
238
+ // Move items from this node to the starting node
239
+ let items = g. node_weight ( node) . expect ( "Node should exist" ) . clone ( ) ;
240
+ g. node_weight_mut ( start) . unwrap ( ) . extend ( items) ;
192
241
193
- g. remove_node ( node) . expect ( "Node should exist" ) ;
194
- did_work = true ;
242
+ nodes_to_remove. push ( node) ;
243
+ }
244
+
245
+ // Remove merged nodes (in reverse order to preserve indices)
246
+ nodes_to_remove. sort ( ) ;
247
+ for node in nodes_to_remove. into_iter ( ) . rev ( ) {
248
+ g. remove_node ( node) ;
249
+ did_work = true ;
250
+ }
195
251
}
196
252
197
253
did_work
0 commit comments