Skip to content

Commit 6ff4fcf

Browse files
authored
fix: incremental solving (rip issue prefix-dev#75) (prefix-dev#15)
The original bug was caused by adding a conflicting `requires` clause, yet failing to detect and handle the conflict. This commit introduces a clearer separation between: adding new clauses, detecting conflicts, and handling said conflicts. Closes prefix-dev#13 Closes prefix-dev/rip#75
1 parent e9cb47f commit 6ff4fcf

File tree

3 files changed

+285
-151
lines changed

3 files changed

+285
-151
lines changed

src/solver/clause.rs

+192-26
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use crate::{
99
};
1010

1111
use crate::internal::id::StringId;
12+
use crate::solver::decision_tracker::DecisionTracker;
1213
use elsa::FrozenMap;
1314
use std::fmt::{Debug, Display, Formatter};
1415
use std::hash::Hash;
@@ -83,31 +84,74 @@ pub(crate) enum Clause {
8384
}
8485

8586
impl Clause {
86-
/// Returns the ids of the solvables that will be watched as well as the clause itself.
87+
/// Returns the building blocks needed for a new [ClauseState] of the [Clause::Requires] kind.
88+
///
89+
/// These building blocks are:
90+
///
91+
/// - The [Clause] itself;
92+
/// - The ids of the solvables that will be watched (unless there are no candidates, i.e. the
93+
/// clause is actually an assertion);
94+
/// - A boolean indicating whether the clause conflicts with existing decisions. This should
95+
/// always be false when adding clauses before starting the solving process, but can be true
96+
/// for clauses that are added dynamically.
8797
fn requires(
88-
candidate: SolvableId,
98+
parent: SolvableId,
8999
requirement: VersionSetId,
90100
candidates: &[SolvableId],
91-
) -> (Self, Option<[SolvableId; 2]>) {
92-
(
93-
Clause::Requires(candidate, requirement),
94-
if candidates.is_empty() {
95-
None
96-
} else {
97-
Some([candidate, candidates[0]])
98-
},
99-
)
101+
decision_tracker: &DecisionTracker,
102+
) -> (Self, Option<[SolvableId; 2]>, bool) {
103+
// It only makes sense to introduce a requires clause when the parent solvable is undecided
104+
// or going to be installed
105+
assert_ne!(decision_tracker.assigned_value(parent), Some(false));
106+
107+
let kind = Clause::Requires(parent, requirement);
108+
if candidates.is_empty() {
109+
(kind, None, false)
110+
} else {
111+
match candidates
112+
.iter()
113+
.find(|&&c| decision_tracker.assigned_value(c) != Some(false))
114+
{
115+
// Watch any candidate that is not assigned to false
116+
Some(watched_candidate) => (kind, Some([parent, *watched_candidate]), false),
117+
118+
// All candidates are assigned to false! Therefore the clause conflicts with the
119+
// current decisions. There are no valid watches for it at the moment, but we will
120+
// assign default ones nevertheless, because they will become valid after the solver
121+
// restarts.
122+
None => (kind, Some([parent, candidates[0]]), true),
123+
}
124+
}
100125
}
101126

102-
/// Returns the ids of the solvables that will be watched as well as the clause itself.
127+
/// Returns the building blocks needed for a new [ClauseState] of the [Clause::Constrains] kind.
128+
///
129+
/// These building blocks are:
130+
///
131+
/// - The [Clause] itself;
132+
/// - The ids of the solvables that will be watched;
133+
/// - A boolean indicating whether the clause conflicts with existing decisions. This should
134+
/// always be false when adding clauses before starting the solving process, but can be true
135+
/// for clauses that are added dynamically.
103136
fn constrains(
104-
candidate: SolvableId,
105-
constrained_candidate: SolvableId,
137+
parent: SolvableId,
138+
forbidden_solvable: SolvableId,
106139
via: VersionSetId,
107-
) -> (Self, Option<[SolvableId; 2]>) {
140+
decision_tracker: &DecisionTracker,
141+
) -> (Self, Option<[SolvableId; 2]>, bool) {
142+
// It only makes sense to introduce a constrains clause when the parent solvable is
143+
// undecided or going to be installed
144+
assert_ne!(decision_tracker.assigned_value(parent), Some(false));
145+
146+
// If the forbidden solvable is already assigned to true, that means that there is a
147+
// conflict with current decisions, because it implies the parent solvable would be false
148+
// (and we just asserted that it is not)
149+
let conflict = decision_tracker.assigned_value(forbidden_solvable) == Some(true);
150+
108151
(
109-
Clause::Constrains(candidate, constrained_candidate, via),
110-
Some([candidate, constrained_candidate]),
152+
Clause::Constrains(parent, forbidden_solvable, via),
153+
Some([parent, forbidden_solvable]),
154+
conflict,
111155
)
112156
}
113157

@@ -243,25 +287,48 @@ impl ClauseState {
243287
Self::from_kind_and_initial_watches(kind, watched_literals)
244288
}
245289

246-
/// Shorthand method to construct a Clause::Requires without requiring complicated arguments.
290+
/// Shorthand method to construct a [Clause::Requires] without requiring complicated arguments.
291+
///
292+
/// The returned boolean value is true when adding the clause resulted in a conflict.
247293
pub fn requires(
248294
candidate: SolvableId,
249295
requirement: VersionSetId,
250296
matching_candidates: &[SolvableId],
251-
) -> Self {
252-
let (kind, watched_literals) =
253-
Clause::requires(candidate, requirement, matching_candidates);
254-
Self::from_kind_and_initial_watches(kind, watched_literals)
297+
decision_tracker: &DecisionTracker,
298+
) -> (Self, bool) {
299+
let (kind, watched_literals, conflict) = Clause::requires(
300+
candidate,
301+
requirement,
302+
matching_candidates,
303+
decision_tracker,
304+
);
305+
306+
(
307+
Self::from_kind_and_initial_watches(kind, watched_literals),
308+
conflict,
309+
)
255310
}
256311

312+
/// Shorthand method to construct a [Clause::Constrains] without requiring complicated arguments.
313+
///
314+
/// The returned boolean value is true when adding the clause resulted in a conflict.
257315
pub fn constrains(
258316
candidate: SolvableId,
259317
constrained_package: SolvableId,
260318
requirement: VersionSetId,
261-
) -> Self {
262-
let (kind, watched_literals) =
263-
Clause::constrains(candidate, constrained_package, requirement);
264-
Self::from_kind_and_initial_watches(kind, watched_literals)
319+
decision_tracker: &DecisionTracker,
320+
) -> (Self, bool) {
321+
let (kind, watched_literals, conflict) = Clause::constrains(
322+
candidate,
323+
constrained_package,
324+
requirement,
325+
decision_tracker,
326+
);
327+
328+
(
329+
Self::from_kind_and_initial_watches(kind, watched_literals),
330+
conflict,
331+
)
265332
}
266333

267334
pub fn lock(locked_candidate: SolvableId, other_candidate: SolvableId) -> Self {
@@ -556,6 +623,7 @@ impl<VS: VersionSet, N: PackageName + Display> Debug for ClauseDebug<'_, VS, N>
556623
mod test {
557624
use super::*;
558625
use crate::internal::arena::ArenaId;
626+
use crate::solver::decision::Decision;
559627

560628
fn clause(next_clauses: [ClauseId; 2], watched_solvables: [SolvableId; 2]) -> ClauseState {
561629
ClauseState {
@@ -694,6 +762,104 @@ mod test {
694762
}
695763
}
696764

765+
#[test]
766+
fn test_requires_with_and_without_conflict() {
767+
let mut decisions = DecisionTracker::new();
768+
769+
let parent = SolvableId::from_usize(1);
770+
let candidate1 = SolvableId::from_usize(2);
771+
let candidate2 = SolvableId::from_usize(3);
772+
773+
// No conflict, all candidates available
774+
let (clause, conflict) = ClauseState::requires(
775+
parent,
776+
VersionSetId::from_usize(0),
777+
&[candidate1, candidate2],
778+
&decisions,
779+
);
780+
assert!(!conflict);
781+
assert_eq!(clause.watched_literals[0], parent);
782+
assert_eq!(clause.watched_literals[1], candidate1);
783+
784+
// No conflict, still one candidate available
785+
decisions
786+
.try_add_decision(Decision::new(candidate1, false, ClauseId::null()), 1)
787+
.unwrap();
788+
let (clause, conflict) = ClauseState::requires(
789+
parent,
790+
VersionSetId::from_usize(0),
791+
&[candidate1, candidate2],
792+
&decisions,
793+
);
794+
assert!(!conflict);
795+
assert_eq!(clause.watched_literals[0], parent);
796+
assert_eq!(clause.watched_literals[1], candidate2);
797+
798+
// Conflict, no candidates available
799+
decisions
800+
.try_add_decision(Decision::new(candidate2, false, ClauseId::null()), 1)
801+
.unwrap();
802+
let (clause, conflict) = ClauseState::requires(
803+
parent,
804+
VersionSetId::from_usize(0),
805+
&[candidate1, candidate2],
806+
&decisions,
807+
);
808+
assert!(conflict);
809+
assert_eq!(clause.watched_literals[0], parent);
810+
assert_eq!(clause.watched_literals[1], candidate1);
811+
812+
// Panic
813+
decisions
814+
.try_add_decision(Decision::new(parent, false, ClauseId::null()), 1)
815+
.unwrap();
816+
let panicked = std::panic::catch_unwind(|| {
817+
ClauseState::requires(
818+
parent,
819+
VersionSetId::from_usize(0),
820+
&[candidate1, candidate2],
821+
&decisions,
822+
)
823+
})
824+
.is_err();
825+
assert!(panicked);
826+
}
827+
828+
#[test]
829+
fn test_constrains_with_and_without_conflict() {
830+
let mut decisions = DecisionTracker::new();
831+
832+
let parent = SolvableId::from_usize(1);
833+
let forbidden = SolvableId::from_usize(2);
834+
835+
// No conflict, forbidden package not installed
836+
let (clause, conflict) =
837+
ClauseState::constrains(parent, forbidden, VersionSetId::from_usize(0), &decisions);
838+
assert!(!conflict);
839+
assert_eq!(clause.watched_literals[0], parent);
840+
assert_eq!(clause.watched_literals[1], forbidden);
841+
842+
// Conflict, forbidden package installed
843+
decisions
844+
.try_add_decision(Decision::new(forbidden, true, ClauseId::null()), 1)
845+
.unwrap();
846+
let (clause, conflict) =
847+
ClauseState::constrains(parent, forbidden, VersionSetId::from_usize(0), &decisions);
848+
assert!(conflict);
849+
assert_eq!(clause.watched_literals[0], parent);
850+
assert_eq!(clause.watched_literals[1], forbidden);
851+
852+
// Panic
853+
decisions
854+
.try_add_decision(Decision::new(parent, false, ClauseId::null()), 1)
855+
.unwrap();
856+
let panicked = std::panic::catch_unwind(|| {
857+
ClauseState::constrains(parent, forbidden, VersionSetId::from_usize(0), &decisions)
858+
})
859+
.is_err();
860+
assert!(panicked);
861+
}
862+
697863
#[test]
698864
fn test_clause_size() {
699865
// This test is here to ensure we don't increase the size of `ClauseState` by accident, as

0 commit comments

Comments
 (0)