Skip to content

Commit 73dbf78

Browse files
committed
Minor simplifications+optimizations for core dedup solver, added a few more comments
1 parent 3d28fda commit 73dbf78

File tree

2 files changed

+112
-74
lines changed
  • compiler/rustc_trait_selection/src/solve/eval_ctxt/canonical/dedup_solver

2 files changed

+112
-74
lines changed

compiler/rustc_trait_selection/src/solve/eval_ctxt/canonical/dedup_solver/solver.rs

Lines changed: 109 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use rustc_data_structures::fx::{FxIndexMap, FxIndexSet};
22
use rustc_index::{Idx, IndexVec};
33
use std::cell::RefCell;
4-
use std::collections::BTreeMap;
4+
use std::collections::{BTreeMap, BTreeSet};
55

66
#[cfg(test)]
77
mod tests;
@@ -47,8 +47,18 @@ pub struct DedupResult {
4747
struct Mapping(BTreeMap<VarIndex, VarIndex>);
4848
#[derive(Debug, Clone, PartialEq, Eq)]
4949
struct MappingInfo {
50-
dependencies: FxIndexMap<ConstraintIndex, FxIndexSet<MappingIndex>>,
50+
/// The constraints that a mapping will eliminate. For example, if we have the constraints
51+
/// [1, 2] (with ConstraintIndex of 0) and [3, 4], the mapping 1:3,2:4 will eliminate constraint 0
5152
eliminated_constraints: FxIndexSet<ConstraintIndex>,
53+
/// A mapping has dependencies if it only maps a subset of the variables in a constraint, and therefore
54+
/// depends on another mapping to complete the full mapping. For example, if we have the constraints
55+
/// [1, 2] (index 1), [11, 12] (index 2), [2, 3] (index 3), and [12, 13] (index 4), the mapping
56+
/// that merges the 1st constraint into the 2nd (mapping vars 1 to 11, 2 to 12) will also "partially"
57+
/// map the 3rd constraint (because the mapping maps var 2, which the 3rd constraint contains).
58+
/// This will partially map the 3rd constraint into [12, 3], which isn't a pre-existing constraint - HOWEVER,
59+
/// if we also apply the mapping var2->var12,var3->var13, then it maps the constraint to [12, 13] which *is*
60+
/// a preexisting constraint. Therefore, the two constraints depend on each other
61+
dependencies: FxIndexMap<ConstraintIndex, BTreeSet<MappingIndex>>,
5262
}
5363
#[derive(Debug, PartialEq, Eq)]
5464
enum MapEvalErr {
@@ -69,16 +79,23 @@ impl DedupSolver {
6979

7080
mappings: FxIndexMap::default(),
7181
removed_constraints: RefCell::new(FxIndexSet::default()),
72-
applied_mappings: RefCell::new(Mapping::map_constraints(&[], &[])),
82+
applied_mappings: RefCell::new(Mapping::from(&[], &[])),
7383
};
7484
deduper.refine_cliques();
7585
deduper.compute_mappings();
7686
deduper.resolve_dependencies();
7787

7888
DedupResult { removed_constraints: deduper.removed_constraints.into_inner() }
7989
}
90+
/// The input cliques are provided just on a basis of the structure of the constraints, i.e.
91+
/// "are they the same if we ignore variables unnameable from the caller". However, just because
92+
/// this is the case doesn't mean that two constraints can be merged - for example, the constraint
93+
/// involving vars [1, 3] can't be merged with a constraint involving vars [2, 2].
94+
/// This function refines the cliques such that if we create a graph with constraints as vertices
95+
/// and edges if they can be merged, constraint_cliques now represents the **true** cliques of the
96+
/// graph, i.e. any two constrains in the same clique can now create a valid mapping
8097
fn refine_cliques(&mut self) {
81-
// Refine categories based on shape
98+
// Refine categories based on shape - see canonicalize_constraint_shape for more info
8299
for clique_indx in (0..self.constraint_cliques.len()).map(CliqueIndex::new) {
83100
let mut shape_cliques: FxIndexMap<Vec<usize>, CliqueIndex> = FxIndexMap::default();
84101
let mut constraint_indx = 0;
@@ -103,7 +120,17 @@ impl DedupSolver {
103120
self.constraint_cliques[new_clique].push(constraint);
104121
}
105122
}
106-
// Refine categories based on indices of variables
123+
// Refine categories based on indices of variables. This is based on the observation that
124+
// if a variable V is present in a constraint C1 at some set of indices I, then a constraint
125+
// C2 can be merged with C1 only if one of the following cases are satisfied:
126+
// a. V is present in constraint C2 at the **same** set of indices I, where in that case
127+
// the variable mapping that merges these two constraints would just map V onto V
128+
// b. V is not present in constraint C2 at all, in which case some other variable would
129+
// be mapped onto V
130+
// If none of these above cases are true, that means we have a situation where we map V
131+
// to another variable U, and a variable W would be mapped onto V - in this case, we're just
132+
// shuffling variables around without actually eliminating any, which is unproductive and
133+
// hence an "invalid mapping"
107134
for clique_indx in (0..self.constraint_cliques.len()).map(CliqueIndex::new) {
108135
// First element of tuple (the FxIndexMap) maps a variable to
109136
// the index it occurs in
@@ -178,7 +205,6 @@ impl DedupSolver {
178205
/// Deduplication can be done greedily because if two constraints can be merged, then they're
179206
/// equivalent in every way, including in relations to other constraints
180207
fn compute_mappings(&mut self) {
181-
let mut invalid_maps: FxIndexSet<Mapping> = FxIndexSet::default();
182208
for clique in self.constraint_cliques.iter() {
183209
for (n, constraint_1) in clique
184210
.iter()
@@ -191,19 +217,17 @@ impl DedupSolver {
191217
.filter(|x| !self.removed_constraints.borrow().contains(*x))
192218
{
193219
// Maps constraint_1 to constraint_2
194-
let forward = Mapping::map_constraints(
220+
let forward = Mapping::from(
195221
&self.constraint_vars[*constraint_1],
196222
&self.constraint_vars[*constraint_2],
197223
);
198-
if invalid_maps.contains(&forward) || self.mappings.contains_key(&forward) {
199-
continue;
200-
}
201224
// Maps constraint_2 to constraint_1
202-
let reverse = Mapping::map_constraints(
225+
let reverse = Mapping::from(
203226
&self.constraint_vars[*constraint_2],
204227
&self.constraint_vars[*constraint_1],
205228
);
206-
if invalid_maps.contains(&reverse) || self.mappings.contains_key(&reverse) {
229+
if self.mappings.contains_key(&forward) || self.mappings.contains_key(&reverse)
230+
{
207231
continue;
208232
}
209233

@@ -219,17 +243,15 @@ impl DedupSolver {
219243
if eval_forward == Err(MapEvalErr::Conflicts)
220244
|| eval_reverse == Err(MapEvalErr::Conflicts)
221245
{
222-
invalid_maps.insert(forward);
223-
invalid_maps.insert(reverse);
224246
continue;
225247
}
226248
if let Ok(eval_forward) = eval_forward {
227-
if !self.try_apply_mapping(&forward, &eval_forward, false) {
249+
if self.try_apply_mapping(&forward, &eval_forward, false) == Err(true) {
228250
self.mappings.insert(forward, eval_forward);
229251
}
230252
}
231253
if let Ok(eval_reverse) = eval_reverse {
232-
if !self.try_apply_mapping(&reverse, &eval_reverse, false) {
254+
if self.try_apply_mapping(&reverse, &eval_reverse, false) == Err(true) {
233255
self.mappings.insert(reverse, eval_reverse);
234256
}
235257
}
@@ -258,7 +280,7 @@ impl DedupSolver {
258280
let mut found_non_conflicting = false;
259281
for constraint_2 in clique.iter() {
260282
let vars_2 = &self.constraint_vars[*constraint_2];
261-
let trial_mapping = Mapping::map_constraints(vars_1, vars_2);
283+
let trial_mapping = Mapping::from(vars_1, vars_2);
262284
if mapping.conflicts_with(&trial_mapping) {
263285
continue;
264286
}
@@ -267,7 +289,7 @@ impl DedupSolver {
267289
if !mapping.contains_fully(&trial_mapping) {
268290
// The input mapping can be applied only if there's another mapping that
269291
// maps every variable in constraint_1 (and doesn't conflict with the input mapping)
270-
info.dependencies.insert(*constraint_1, FxIndexSet::default());
292+
info.dependencies.insert(*constraint_1, BTreeSet::default());
271293
continue;
272294
}
273295
if *constraint_1 != *constraint_2 {
@@ -302,46 +324,41 @@ impl DedupSolver {
302324
true
303325
});
304326
// A map from a constraint to the mappings that will eliminate it (i.e. map it fully)
305-
let mut constraint_mappings: FxIndexMap<ConstraintIndex, FxIndexSet<MappingIndex>> =
327+
let mut constraint_mappings: FxIndexMap<ConstraintIndex, BTreeSet<MappingIndex>> =
306328
FxIndexMap::default();
307329
for (indx, (_, mapping_info)) in self.mappings.iter().enumerate() {
308330
for eliminated_constraint in mapping_info.eliminated_constraints.iter() {
309331
constraint_mappings
310332
.entry(*eliminated_constraint)
311-
.or_insert_with(FxIndexSet::default)
333+
.or_insert_with(BTreeSet::default)
312334
.insert(MappingIndex::new(indx));
313335
}
314336
}
315337
for indx in (0..self.mappings.len()).map(MappingIndex::new) {
316338
let mapping = self.get_mapping(indx);
317339
let input_dependencies = &self.get_mapping_info(indx).dependencies;
318-
let mut dependencies = IndexVec::new();
340+
let mut dependencies = FxIndexSet::default();
319341
for (dependency, _) in input_dependencies.iter() {
320342
// The set of mappings that can resolve this dependency
321-
let mut resolve_options = FxIndexSet::default();
322-
if let Some(resolve_mappings) = constraint_mappings.get(dependency) {
323-
resolve_options.extend(
324-
resolve_mappings
325-
.iter()
326-
.filter(|x| !mapping.conflicts_with(&self.get_mapping(**x)))
327-
.cloned(),
328-
)
329-
}
330-
// Don't duplicate dependency groups
331-
if dependencies.iter().any(|x| x == &resolve_options) {
332-
continue;
333-
}
334-
dependencies.push(resolve_options);
343+
let mut resolve_options =
344+
constraint_mappings.get(dependency).cloned().unwrap_or_else(BTreeSet::new);
345+
resolve_options.retain(|x| !mapping.conflicts_with(&self.get_mapping(*x)));
346+
dependencies.insert(resolve_options);
335347
}
336348
// After this point, the actual constraints that a dependency maps
337349
// stops mattering - all that matters is that the dependency *exists*
338-
self.mappings.get_index_mut(indx.index()).unwrap().1.dependencies =
339-
dependencies.into_iter_enumerated().collect();
350+
let old_dependencies =
351+
&mut self.mappings.get_index_mut(indx.index()).unwrap().1.dependencies;
352+
*old_dependencies = dependencies
353+
.into_iter()
354+
.enumerate()
355+
.map(|(indx, x)| (ConstraintIndex::from(indx), x))
356+
.collect();
340357
}
341358
}
342359

343-
/// Resolves dependencies on mappings - i.e. we find a set of mappings that mutually satisfy each other's
344-
/// dependencies, and don't conflict
360+
/// Resolves dependencies on mappings - i.e. we find sets of mappings that mutually satisfy each other's
361+
/// dependencies, and don't conflict, then apply these mappings
345362
fn resolve_dependencies(&mut self) {
346363
let mut used_mappings = FxIndexSet::default();
347364
for indx in (0..self.mappings.len()).map(MappingIndex::new) {
@@ -366,66 +383,81 @@ impl DedupSolver {
366383
self.get_mapping_info(mapping),
367384
true,
368385
);
369-
assert!(application_result);
386+
assert!(application_result.is_ok());
370387
used_mappings.insert(mapping);
371388
}
372389
}
373390
}
374391
}
375-
/// Performs a depth-first search on the dependencies graph of mappings. Each call to this functino
376-
/// takes in 3 arguments - the mappings that are already presumed to be part of the set of mappings
377-
/// that should be applied together, these mappings aggregated into a single mapping, and a `from` set,
378-
/// i.e. a set of mappings that we just added, and thus might have unresolved dependencies
379-
/// There's quite a few heuristics that will probably yield *significant* speedups
380-
/// I'll look into that later, if the rest of this approach is sound
392+
/// Finds a set of mappings that mutually satisfy each other's dependencies without conflicting with each
393+
/// other, or the mappings that have already been applied. It does this through depth first search -
394+
/// it takes a FxIndexSet of the mappings that have already been presumed to be part of the mapping set as well
395+
/// as a FxIndexSet of mappings that we are trying to add to this set (`from`). These mappings still may have
396+
/// dependencies that might be unresolved, so dfs_search attempts to resolve these dependencies, recursively calling
397+
/// itself if necessary. If `from` has no unresolved dependencies, then a set of mappings is found, and we return
381398
fn dfs_search(
382399
&self,
383400
mut used_mappings: FxIndexSet<MappingIndex>,
384401
mut applied_mappings: Mapping,
385-
from: FxIndexSet<MappingIndex>,
402+
mut from: FxIndexSet<MappingIndex>,
386403
) -> Option<FxIndexSet<MappingIndex>> {
404+
// Apply the mappings that we're trying to apply (in `from`), aborting if there's any conflicts
387405
for mapping_indx in from.iter() {
388406
let (mapping, _) = self.mappings.get_index(mapping_indx.index()).unwrap();
389407
if applied_mappings.conflicts_with(mapping) {
390408
return None;
391409
}
392410
applied_mappings.0.extend(&mapping.0);
393411
}
394-
if from.iter().all(|x| used_mappings.contains(x)) {
412+
// If we already applied a mapping, we now remove it from `from`, as its dependencies have
413+
// been resolved and therefore we don't need to worry about it
414+
from.retain(|x| !used_mappings.contains(x));
415+
if from.is_empty() {
395416
return Some(used_mappings);
396417
}
397418
used_mappings.extend(from.iter());
398419

399-
// For each unresolved dependency, we have a set of Mappings that can resolve it
400-
let unresolved_dependencies: Vec<&FxIndexSet<MappingIndex>> =
401-
from.iter().flat_map(|x| self.get_mapping_info(*x).dependencies.values()).collect();
402-
if unresolved_dependencies.is_empty() {
403-
return Some(used_mappings);
420+
// For each unresolved dependency, we have a list of Mappings that can resolve it
421+
let mut unresolved_dependencies: FxIndexSet<Vec<MappingIndex>> = FxIndexSet::default();
422+
for from_mapping in from.iter() {
423+
let resolve_options = self.get_mapping_info(*from_mapping).dependencies.values();
424+
let resolve_options = resolve_options.map(|x| {
425+
Vec::from_iter(
426+
x.iter()
427+
.cloned()
428+
// Throw out mappings that conflict with the current `used_mappings` we're
429+
// trying to satisfy the dependencies of
430+
.filter(|x| !self.get_mapping(*x).conflicts_with(&applied_mappings)),
431+
)
432+
});
433+
unresolved_dependencies.extend(resolve_options);
434+
}
435+
if unresolved_dependencies.iter().any(|x| x.is_empty()) {
436+
return None;
404437
}
405-
// For each unresolved dependency, we have an index denoting the index in the FxIndexSet that
406-
// we will try applying next
407-
// Essentially, we just sweep through all the combinations exhaustively, e.g. if we have 3
408-
// unresolved dependencies, each with 3 options, we would go "[0, 0, 0]", "[1, 0, 0]", "[2, 0, 0]",
409-
// "[0, 1, 0]", "[1, 1, 0]", "[2, 1, 0]", so on and so forth until we find one that succeeds
438+
// For each unresolved dependency, we have a list of Mappings that can resolve it. The idea is that
439+
// we need to pick one mapping from each list to create the set of mappings we're going to try adding
440+
// to the set of mappings, and right now, it's done incredibly naively - for each list of mappings,
441+
// we store the value that denotes the index of the mapping in that list that we're going to try
442+
// adding next. We start out with a vec of all zeroes, i.e. we try taking the first element of each
443+
// list, and we sweep the indices in order, e.g. going [0, 0, 0], [1, 0, 0], [2, 0, 0], [3, 0, 0],
444+
// [0, 1, 0], [1, 1, 0], [2, 1, 0], [3, 1, 0], [0, 2, 0], so on and so forth
410445
let mut trial_indices = vec![0; unresolved_dependencies.len()];
411446
while *trial_indices.last().unwrap() < unresolved_dependencies.last().unwrap().len() {
412-
let choice: FxIndexSet<MappingIndex> = trial_indices
413-
.iter()
414-
.zip(&unresolved_dependencies)
415-
.map(|(x, y)| *y.get_index(*x).unwrap())
416-
.collect();
447+
// The set of mappings that were chosen to be added next
448+
let choice: FxIndexSet<MappingIndex> =
449+
trial_indices.iter().zip(&unresolved_dependencies).map(|(x, y)| y[*x]).collect();
417450
let search_result =
418451
self.dfs_search(used_mappings.clone(), applied_mappings.clone(), choice);
419452
if search_result.is_some() {
420453
return search_result;
421454
}
422455

423-
for (val, limit) in
424-
trial_indices.iter_mut().zip(unresolved_dependencies.iter().map(|x| x.len()))
425-
{
426-
*val += 1;
427-
if *val >= limit {
428-
*val = 0;
456+
// Advance the indices to the next possibility
457+
for (indx, options) in trial_indices.iter_mut().zip(&unresolved_dependencies) {
458+
*indx += 1;
459+
if *indx >= options.len() {
460+
*indx = 0;
429461
continue;
430462
}
431463
break;
@@ -434,21 +466,24 @@ impl DedupSolver {
434466
None
435467
}
436468

469+
/// Tries to apply a mapping, returning Ok(()) if the application was a success, Err(true) if the
470+
/// application failed but only because of unresolved dependencies, and Err(false) if the application
471+
/// fails because of conflicts
437472
fn try_apply_mapping(
438473
&self,
439474
mapping: &Mapping,
440475
info: &MappingInfo,
441476
allow_dependencies: bool,
442-
) -> bool {
477+
) -> Result<(), bool> {
443478
if !allow_dependencies && !info.dependencies.is_empty() {
444-
return false;
479+
return Err(true);
445480
}
446481
if self.applied_mappings.borrow().conflicts_with(mapping) {
447-
return false;
482+
return Err(false);
448483
}
449484
self.removed_constraints.borrow_mut().extend(info.eliminated_constraints.iter());
450485
self.applied_mappings.borrow_mut().0.extend(&mapping.0);
451-
true
486+
Ok(())
452487
}
453488
fn get_mapping(&self, index: MappingIndex) -> &Mapping {
454489
&self.mappings.get_index(index.index()).unwrap().0
@@ -459,7 +494,7 @@ impl DedupSolver {
459494
}
460495

461496
impl Mapping {
462-
fn map_constraints(from: &[VarIndex], to: &[VarIndex]) -> Self {
497+
fn from(from: &[VarIndex], to: &[VarIndex]) -> Self {
463498
Self(from.iter().zip(to).map(|(x, y)| (*x, *y)).collect())
464499
}
465500
fn maps_var(&self, constraint: VarIndex) -> Option<VarIndex> {

0 commit comments

Comments
 (0)