diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-10 00:32:08 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-10 00:32:08 +0900 |
| commit | 8cc1ba8a02c0318b65658f1634de378c780392b9 (patch) | |
| tree | fdd5cb61e488018891a486b25991b87c84220bb8 /crates/mozart-core/src/dependency_resolver/decisions.rs | |
| parent | 72b2e877c01e67ba7edd37e34ac2eadb7a1c62c4 (diff) | |
| download | php-mozart-8cc1ba8a02c0318b65658f1634de378c780392b9.tar.gz php-mozart-8cc1ba8a02c0318b65658f1634de378c780392b9.tar.zst php-mozart-8cc1ba8a02c0318b65658f1634de378c780392b9.zip | |
refactor(workspace): consolidate crates into mozart-core
Merged mozart-archiver, mozart-autoload, mozart-registry,
mozart-sat-resolver, and mozart-vcs into mozart-core to align
the source layout with Composer's structure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart-core/src/dependency_resolver/decisions.rs')
| -rw-r--r-- | crates/mozart-core/src/dependency_resolver/decisions.rs | 263 |
1 files changed, 263 insertions, 0 deletions
diff --git a/crates/mozart-core/src/dependency_resolver/decisions.rs b/crates/mozart-core/src/dependency_resolver/decisions.rs new file mode 100644 index 0000000..510092f --- /dev/null +++ b/crates/mozart-core/src/dependency_resolver/decisions.rs @@ -0,0 +1,263 @@ +use super::error::SolverBugError; +use super::pool::{Literal, PackageId, literal_to_package_id}; +use super::rule_set::RuleId; +use indexmap::IndexMap; + +/// A decision entry: which literal was decided and which rule caused it. +#[derive(Debug, Clone)] +pub struct Decision { + pub literal: Literal, + pub rule_id: RuleId, +} + +/// Tracks all decisions (variable assignments) made during solving. +/// +/// Port of Composer's Decisions.php. +pub struct Decisions { + /// Package ID → signed level. Positive = install, negative = uninstall. + /// The absolute value is the decision level. + decision_map: IndexMap<PackageId, i32>, + /// Queue of decisions in order. + decision_queue: Vec<Decision>, +} + +impl Decisions { + pub fn new() -> Self { + Decisions { + decision_map: IndexMap::new(), + decision_queue: Vec::new(), + } + } + + /// Record a decision. + pub fn decide( + &mut self, + literal: Literal, + level: i32, + rule_id: RuleId, + ) -> Result<(), SolverBugError> { + let package_id = literal_to_package_id(literal); + let previous = self.decision_map.get(&package_id).copied().unwrap_or(0); + if previous != 0 { + return Err(SolverBugError { + message: format!( + "Trying to decide literal {literal} on level {level}, \ + even though package {package_id} was previously decided as {previous}." + ), + }); + } + + if literal > 0 { + self.decision_map.insert(package_id, level); + } else { + self.decision_map.insert(package_id, -level); + } + + self.decision_queue.push(Decision { literal, rule_id }); + Ok(()) + } + + /// Check if literal is satisfied (true in current assignment). + pub fn satisfy(&self, literal: Literal) -> bool { + let package_id = literal_to_package_id(literal); + match self.decision_map.get(&package_id) { + Some(&val) => (literal > 0 && val > 0) || (literal < 0 && val < 0), + None => false, + } + } + + /// Check if literal conflicts with current assignment. + pub fn conflict(&self, literal: Literal) -> bool { + let package_id = literal_to_package_id(literal); + match self.decision_map.get(&package_id) { + Some(&val) => (val > 0 && literal < 0) || (val < 0 && literal > 0), + None => false, + } + } + + /// Check if package has been decided. + pub fn decided(&self, literal_or_id: i32) -> bool { + let package_id = literal_or_id.unsigned_abs(); + self.decision_map.get(&package_id).copied().unwrap_or(0) != 0 + } + + /// Check if package is undecided. + pub fn undecided(&self, literal_or_id: i32) -> bool { + !self.decided(literal_or_id) + } + + /// Check if package is decided for installation. + pub fn decided_install(&self, literal_or_id: i32) -> bool { + let package_id = literal_or_id.unsigned_abs(); + self.decision_map.get(&package_id).copied().unwrap_or(0) > 0 + } + + /// Get the decision level for a package (0 if undecided). + pub fn decision_level(&self, literal_or_id: i32) -> i32 { + let package_id = literal_or_id.unsigned_abs(); + self.decision_map + .get(&package_id) + .copied() + .unwrap_or(0) + .abs() + } + + /// Get the rule ID that caused a decision for a package. + pub fn decision_rule(&self, literal_or_id: i32) -> Result<RuleId, SolverBugError> { + let package_id = literal_or_id.unsigned_abs(); + for decision in &self.decision_queue { + if literal_to_package_id(decision.literal) == package_id { + return Ok(decision.rule_id); + } + } + Err(SolverBugError { + message: format!("Did not find a decision rule for {literal_or_id}"), + }) + } + + /// Get decision at a specific offset in the queue. + pub fn at_offset(&self, offset: usize) -> &Decision { + &self.decision_queue[offset] + } + + /// Check if an offset is valid. + pub fn valid_offset(&self, offset: usize) -> bool { + offset < self.decision_queue.len() + } + + /// Get the rule ID of the last decision. + pub fn last_reason(&self) -> RuleId { + self.decision_queue.last().unwrap().rule_id + } + + /// Get the literal of the last decision. + pub fn last_literal(&self) -> Literal { + self.decision_queue.last().unwrap().literal + } + + /// Clear all decisions. + pub fn reset(&mut self) { + while let Some(decision) = self.decision_queue.pop() { + let pkg_id = literal_to_package_id(decision.literal); + self.decision_map.insert(pkg_id, 0); + } + } + + /// Remove decisions after the given offset (keep offset+1 items). + pub fn reset_to_offset(&mut self, offset: usize) { + while self.decision_queue.len() > offset + 1 { + let decision = self.decision_queue.pop().unwrap(); + let pkg_id = literal_to_package_id(decision.literal); + self.decision_map.insert(pkg_id, 0); + } + } + + /// Remove the last decision. + pub fn revert_last(&mut self) { + let decision = self.decision_queue.pop().unwrap(); + let pkg_id = literal_to_package_id(decision.literal); + self.decision_map.insert(pkg_id, 0); + } + + /// Number of decisions. + pub fn len(&self) -> usize { + self.decision_queue.len() + } + + /// Whether there are no decisions. + pub fn is_empty(&self) -> bool { + self.decision_queue.is_empty() + } + + /// Iterate decisions in reverse order (newest first). + /// Used by analyzeUnsolvable in Composer. + pub fn iter_reverse(&self) -> impl Iterator<Item = &Decision> { + self.decision_queue.iter().rev() + } +} + +impl Default for Decisions { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_decide_and_satisfy() { + let mut d = Decisions::new(); + d.decide(1, 1, 0).unwrap(); // install package 1 at level 1 + + assert!(d.satisfy(1)); + assert!(!d.satisfy(-1)); + assert!(d.conflict(-1)); + assert!(!d.conflict(1)); + assert!(d.decided(1)); + assert!(d.decided_install(1)); + } + + #[test] + fn test_decide_negative() { + let mut d = Decisions::new(); + d.decide(-1, 1, 0).unwrap(); // don't install package 1 + + assert!(d.satisfy(-1)); + assert!(!d.satisfy(1)); + assert!(d.conflict(1)); + assert!(d.decided(1)); + assert!(!d.decided_install(1)); + } + + #[test] + fn test_undecided() { + let d = Decisions::new(); + assert!(d.undecided(1)); + assert!(!d.decided(1)); + assert!(!d.satisfy(1)); + assert!(!d.conflict(1)); + } + + #[test] + fn test_revert_last() { + let mut d = Decisions::new(); + d.decide(1, 1, 0).unwrap(); + d.decide(2, 2, 1).unwrap(); + + assert!(d.decided(2)); + d.revert_last(); + assert!(d.undecided(2)); + assert!(d.decided(1)); + } + + #[test] + fn test_reset_to_offset() { + let mut d = Decisions::new(); + d.decide(1, 1, 0).unwrap(); + d.decide(2, 2, 1).unwrap(); + d.decide(3, 3, 2).unwrap(); + + d.reset_to_offset(0); // keep only first decision + assert_eq!(d.len(), 1); + assert!(d.decided(1)); + assert!(d.undecided(2)); + assert!(d.undecided(3)); + } + + #[test] + fn test_double_decide_error() { + let mut d = Decisions::new(); + d.decide(1, 1, 0).unwrap(); + assert!(d.decide(1, 2, 1).is_err()); + } + + #[test] + fn test_decision_level() { + let mut d = Decisions::new(); + d.decide(1, 3, 0).unwrap(); + assert_eq!(d.decision_level(1), 3); + assert_eq!(d.decision_level(2), 0); // undecided + } +} |
