aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-sat-resolver
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-05-10 00:32:08 +0900
committernsfisis <nsfisis@gmail.com>2026-05-10 00:32:08 +0900
commit8cc1ba8a02c0318b65658f1634de378c780392b9 (patch)
treefdd5cb61e488018891a486b25991b87c84220bb8 /crates/mozart-sat-resolver
parent72b2e877c01e67ba7edd37e34ac2eadb7a1c62c4 (diff)
downloadphp-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-sat-resolver')
-rw-r--r--crates/mozart-sat-resolver/Cargo.toml11
-rw-r--r--crates/mozart-sat-resolver/src/decisions.rs263
-rw-r--r--crates/mozart-sat-resolver/src/error.rs50
-rw-r--r--crates/mozart-sat-resolver/src/lib.rs25
-rw-r--r--crates/mozart-sat-resolver/src/policy.rs264
-rw-r--r--crates/mozart-sat-resolver/src/pool.rs427
-rw-r--r--crates/mozart-sat-resolver/src/pool_builder.rs222
-rw-r--r--crates/mozart-sat-resolver/src/problem.rs499
-rw-r--r--crates/mozart-sat-resolver/src/request.rs65
-rw-r--r--crates/mozart-sat-resolver/src/rule.rs280
-rw-r--r--crates/mozart-sat-resolver/src/rule_set.rs211
-rw-r--r--crates/mozart-sat-resolver/src/rule_set_generator.rs464
-rw-r--r--crates/mozart-sat-resolver/src/rule_watch_graph.rs288
-rw-r--r--crates/mozart-sat-resolver/src/solver.rs1008
-rw-r--r--crates/mozart-sat-resolver/src/transaction.rs568
15 files changed, 0 insertions, 4645 deletions
diff --git a/crates/mozart-sat-resolver/Cargo.toml b/crates/mozart-sat-resolver/Cargo.toml
deleted file mode 100644
index 5b8a46c..0000000
--- a/crates/mozart-sat-resolver/Cargo.toml
+++ /dev/null
@@ -1,11 +0,0 @@
-[package]
-name = "mozart-sat-resolver"
-version.workspace = true
-edition.workspace = true
-
-[dependencies]
-mozart-semver.workspace = true
-mozart-core.workspace = true
-indexmap.workspace = true
-
-[dev-dependencies]
diff --git a/crates/mozart-sat-resolver/src/decisions.rs b/crates/mozart-sat-resolver/src/decisions.rs
deleted file mode 100644
index e9cc935..0000000
--- a/crates/mozart-sat-resolver/src/decisions.rs
+++ /dev/null
@@ -1,263 +0,0 @@
-use crate::error::SolverBugError;
-use crate::pool::{Literal, PackageId, literal_to_package_id};
-use crate::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
- }
-}
diff --git a/crates/mozart-sat-resolver/src/error.rs b/crates/mozart-sat-resolver/src/error.rs
deleted file mode 100644
index e4b9841..0000000
--- a/crates/mozart-sat-resolver/src/error.rs
+++ /dev/null
@@ -1,50 +0,0 @@
-use std::fmt;
-
-/// A bug in the solver itself (should never happen in normal operation).
-/// Equivalent to Composer's SolverBugException.
-#[derive(Debug, Clone)]
-pub struct SolverBugError {
- pub message: String,
-}
-
-impl fmt::Display for SolverBugError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "Solver bug: {}", self.message)
- }
-}
-
-impl std::error::Error for SolverBugError {}
-
-/// Errors produced by the SAT solver.
-#[derive(Debug)]
-pub enum SolverError {
- /// Internal solver bug (should never happen).
- Bug(SolverBugError),
- /// The dependency set is unsolvable. Contains problem descriptions.
- Unsolvable(Vec<String>),
-}
-
-impl fmt::Display for SolverError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- SolverError::Bug(e) => write!(f, "{e}"),
- SolverError::Unsolvable(problems) => {
- for (i, problem) in problems.iter().enumerate() {
- if i > 0 {
- writeln!(f)?;
- }
- write!(f, " Problem {}: {problem}", i + 1)?;
- }
- Ok(())
- }
- }
- }
-}
-
-impl std::error::Error for SolverError {}
-
-impl From<SolverBugError> for SolverError {
- fn from(e: SolverBugError) -> Self {
- SolverError::Bug(e)
- }
-}
diff --git a/crates/mozart-sat-resolver/src/lib.rs b/crates/mozart-sat-resolver/src/lib.rs
deleted file mode 100644
index 2e3fefb..0000000
--- a/crates/mozart-sat-resolver/src/lib.rs
+++ /dev/null
@@ -1,25 +0,0 @@
-pub mod decisions;
-pub mod error;
-pub mod policy;
-pub mod pool;
-pub mod pool_builder;
-pub mod problem;
-pub mod request;
-pub mod rule;
-pub mod rule_set;
-pub mod rule_set_generator;
-pub mod rule_watch_graph;
-pub mod solver;
-pub mod transaction;
-
-// Re-export key types for public API
-pub use error::SolverError;
-pub use policy::DefaultPolicy;
-pub use pool::{Literal, PackageId, Pool, PoolLink, PoolPackage, PoolPackageInput};
-pub use pool_builder::{PoolBuilder, make_pool_links};
-pub use request::Request;
-pub use rule::{ReasonData, Rule, RuleReason};
-pub use rule_set::RuleSet;
-pub use rule_set_generator::RuleSetGenerator;
-pub use solver::{Solver, SolverResult};
-pub use transaction::{LockTransaction, Operation, Transaction};
diff --git a/crates/mozart-sat-resolver/src/policy.rs b/crates/mozart-sat-resolver/src/policy.rs
deleted file mode 100644
index f45c4f5..0000000
--- a/crates/mozart-sat-resolver/src/policy.rs
+++ /dev/null
@@ -1,264 +0,0 @@
-use crate::pool::{Literal, Pool};
-use indexmap::IndexMap;
-
-/// Version selection policy: decides which version to prefer when multiple
-/// candidates satisfy a requirement.
-///
-/// Port of Composer's DefaultPolicy.php.
-pub struct DefaultPolicy {
- /// Whether to prefer stable versions.
- pub prefer_stable: bool,
- /// Whether to prefer lowest versions.
- pub prefer_lowest: bool,
- /// `name → normalized version` overrides used when more than one
- /// candidate could satisfy a requirement: a literal pinned at the
- /// preferred version wins outright over the usual highest/lowest pick.
- /// Mirrors Composer's `DefaultPolicy::pruneToBestVersion` behavior under
- /// `--minimal-changes`, where the lock's previously-installed versions
- /// are passed in so the solver only moves a package when a constraint
- /// actually forces a different version.
- pub preferred_versions: Option<IndexMap<String, String>>,
-}
-
-impl DefaultPolicy {
- pub fn new(prefer_stable: bool, prefer_lowest: bool) -> Self {
- DefaultPolicy {
- prefer_stable,
- prefer_lowest,
- preferred_versions: None,
- }
- }
-
- pub fn with_preferred(
- prefer_stable: bool,
- prefer_lowest: bool,
- preferred_versions: IndexMap<String, String>,
- ) -> Self {
- DefaultPolicy {
- prefer_stable,
- prefer_lowest,
- preferred_versions: Some(preferred_versions),
- }
- }
-
- /// Select preferred packages from a list of candidate literals.
- /// Returns the literals sorted by preference (most preferred first).
- ///
- /// Port of Composer's DefaultPolicy::selectPreferredPackages.
- pub fn select_preferred_packages(
- &self,
- pool: &Pool,
- literals: &[Literal],
- _required_package: Option<&str>,
- ) -> Vec<Literal> {
- if literals.is_empty() {
- return vec![];
- }
-
- // Group literals by package name
- let mut groups: IndexMap<&str, Vec<Literal>> = IndexMap::new();
- for &lit in literals {
- let pkg = pool.literal_to_package(lit);
- groups.entry(pkg.name.as_str()).or_default().push(lit);
- }
-
- // Sort each group by version preference
- for lits in groups.values_mut() {
- lits.sort_by(|&a, &b| self.compare_by_priority(pool, a, b));
- }
-
- // Prune to best version within each group
- for lits in groups.values_mut() {
- *lits = self.prune_to_best_version(pool, lits);
- }
-
- // Merge and sort across all packages
- let mut selected: Vec<Literal> = groups.into_values().flatten().collect();
- selected.sort_by(|&a, &b| self.compare_by_priority(pool, a, b));
-
- selected
- }
-
- /// Compare two package literals by priority.
- /// Returns Ordering: negative means a is preferred.
- fn compare_by_priority(&self, pool: &Pool, a: Literal, b: Literal) -> std::cmp::Ordering {
- let pkg_a = pool.literal_to_package(a);
- let pkg_b = pool.literal_to_package(b);
-
- // If same name, apply Composer's policy ordering. Mirrors
- // `DefaultPolicy::versionCompare`: when `prefer_stable` is on and
- // the two candidates have different stabilities, the more-stable
- // one wins outright — `prefer_lowest` only kicks in within the same
- // stability tier. Otherwise sort by version (asc for prefer_lowest,
- // desc otherwise).
- if pkg_a.name == pkg_b.name {
- if self.prefer_stable {
- let stab_a = stability_priority(&pkg_a.version);
- let stab_b = stability_priority(&pkg_b.version);
- if stab_a != stab_b {
- return stab_a.cmp(&stab_b);
- }
- }
- let cmp = self.compare_versions(&pkg_a.version, &pkg_b.version);
- return if self.prefer_lowest {
- cmp
- } else {
- cmp.reverse()
- };
- }
-
- // Different names: when one package replaces the other, prefer the
- // *replaced* original. Mirrors the `replaces()` shortcut in
- // Composer's `DefaultPolicy::compareByPriority` (the cross-package
- // `ignoreReplace=false` pass). Without this, a request like
- // `update a/installed` where the pool also contains an
- // `a/replacer` declaring `replace: { "a/installed": "dev-master" }`
- // could fall through to package-id tie-break and land on the
- // replacer instead of the package the user actually asked for.
- if pkg_a.replaces.iter().any(|link| link.target == pkg_b.name) {
- return std::cmp::Ordering::Greater;
- }
- if pkg_b.replaces.iter().any(|link| link.target == pkg_a.name) {
- return std::cmp::Ordering::Less;
- }
-
- // Different names, no replace relationship: sort by package ID
- // for reproducibility.
- pkg_a.id.cmp(&pkg_b.id)
- }
-
- /// Compare two normalized version strings.
- fn compare_versions(&self, a: &str, b: &str) -> std::cmp::Ordering {
- match (
- mozart_semver::Version::parse(a),
- mozart_semver::Version::parse(b),
- ) {
- (Ok(va), Ok(vb)) => va.cmp(&vb),
- _ => a.cmp(b),
- }
- }
-
- /// Prune to the best version among a sorted list of literals for the same package.
- fn prune_to_best_version(&self, pool: &Pool, literals: &[Literal]) -> Vec<Literal> {
- if literals.is_empty() {
- return vec![];
- }
-
- // Mirror Composer's `DefaultPolicy::pruneToBestVersion` short-circuit:
- // when a preferred version is set for this package and one of the
- // candidates matches it exactly, that wins over the regular
- // highest/lowest pick. Falls through otherwise (e.g. the locked
- // version no longer satisfies the constraint and was filtered out
- // before reaching this method).
- if let Some(ref preferred) = self.preferred_versions {
- let name = pool.literal_to_package(literals[0]).name.clone();
- if let Some(preferred_ver) = preferred.get(&name) {
- let preferred_lits: Vec<Literal> = literals
- .iter()
- .filter(|&&lit| pool.literal_to_package(lit).version == *preferred_ver)
- .copied()
- .collect();
- if !preferred_lits.is_empty() {
- return preferred_lits;
- }
- }
- }
-
- // The first literal is the best after sorting
- let best_version = &pool.literal_to_package(literals[0]).version;
- literals
- .iter()
- .filter(|&&lit| pool.literal_to_package(lit).version == *best_version)
- .copied()
- .collect()
- }
-}
-
-impl Default for DefaultPolicy {
- fn default() -> Self {
- DefaultPolicy::new(false, false)
- }
-}
-
-/// Map a normalized version string to Composer's stability priority
-/// (`BasePackage::STABILITIES`). Lower = more stable. Stable=0, RC=5, beta=10,
-/// alpha=15, dev=20. Mirrors `DefaultPolicy::versionCompare`'s comparison
-/// when `prefer_stable` is set.
-fn stability_priority(version: &str) -> u8 {
- let Ok(v) = mozart_semver::Version::parse(version) else {
- return 0;
- };
- if v.is_dev_branch {
- return 20;
- }
- match v.pre_release.as_deref() {
- None => 0,
- Some(pre) => {
- let lower = pre.to_lowercase();
- if lower.starts_with("dev") {
- 20
- } else if lower.starts_with("alpha") || lower == "a" {
- 15
- } else if lower.starts_with("beta") || lower == "b" {
- 10
- } else if lower.starts_with("rc") {
- 5
- } else {
- // patch/pl/p / unknown → stable
- 0
- }
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::pool::PoolPackageInput;
-
- fn make_input(name: &str, version: &str) -> PoolPackageInput {
- PoolPackageInput {
- name: name.to_string(),
- version: version.to_string(),
- pretty_version: version.to_string(),
- requires: vec![],
- replaces: vec![],
- provides: vec![],
- conflicts: vec![],
- is_fixed: false,
- is_alias_of: None,
- }
- }
-
- #[test]
- fn test_prefer_highest() {
- let pool = Pool::new(
- vec![
- make_input("a/a", "1.0.0.0"),
- make_input("a/a", "2.0.0.0"),
- make_input("a/a", "3.0.0.0"),
- ],
- vec![],
- );
- let policy = DefaultPolicy::new(false, false);
- let result = policy.select_preferred_packages(&pool, &[1, 2, 3], None);
- // Should prefer highest version (3.0.0.0 = id 3)
- assert_eq!(result[0], 3);
- }
-
- #[test]
- fn test_prefer_lowest() {
- let pool = Pool::new(
- vec![
- make_input("a/a", "1.0.0.0"),
- make_input("a/a", "2.0.0.0"),
- make_input("a/a", "3.0.0.0"),
- ],
- vec![],
- );
- let policy = DefaultPolicy::new(false, true);
- let result = policy.select_preferred_packages(&pool, &[1, 2, 3], None);
- // Should prefer lowest version (1.0.0.0 = id 1)
- assert_eq!(result[0], 1);
- }
-}
diff --git a/crates/mozart-sat-resolver/src/pool.rs b/crates/mozart-sat-resolver/src/pool.rs
deleted file mode 100644
index 8a63c05..0000000
--- a/crates/mozart-sat-resolver/src/pool.rs
+++ /dev/null
@@ -1,427 +0,0 @@
-use indexmap::IndexMap;
-use mozart_semver::VersionConstraint;
-use std::fmt;
-
-/// Unique identifier for a package in the pool. 1-based.
-pub type PackageId = u32;
-
-/// A SAT literal. Positive = install package, negative = don't install.
-/// The absolute value is the PackageId.
-pub type Literal = i32;
-
-/// Returns the PackageId from a literal.
-#[inline]
-pub fn literal_to_package_id(literal: Literal) -> PackageId {
- literal.unsigned_abs()
-}
-
-/// A link from a package to another package name with a version constraint.
-#[derive(Debug, Clone)]
-pub struct PoolLink {
- /// The target package name.
- pub target: String,
- /// The version constraint string (e.g. "^1.0").
- pub constraint: String,
- /// The source package name (the one declaring this link).
- pub source: String,
-}
-
-impl fmt::Display for PoolLink {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "{} {} {}", self.source, self.target, self.constraint)
- }
-}
-
-/// A package entry in the pool. This is the SAT solver's view of a package.
-#[derive(Debug, Clone)]
-pub struct PoolPackage {
- /// 1-based package ID assigned by the pool.
- pub id: PackageId,
- /// Normalized package name (e.g. "monolog/monolog").
- pub name: String,
- /// Normalized version string (e.g. "1.0.0.0").
- pub version: String,
- /// Pretty version string (e.g. "1.0.0").
- pub pretty_version: String,
- /// Package requirements.
- pub requires: Vec<PoolLink>,
- /// Packages this replaces.
- pub replaces: Vec<PoolLink>,
- /// Packages this provides.
- pub provides: Vec<PoolLink>,
- /// Packages this conflicts with.
- pub conflicts: Vec<PoolLink>,
- /// Whether this is a fixed/locked package.
- pub is_fixed: bool,
- /// If `Some`, this package is an `AliasPackage` whose target is the
- /// other pool entry with the given ID. Composer creates these for
- /// `extra.branch-alias` entries (dev branch → numeric alias). When set,
- /// the rule generator emits `PackageAlias`/`PackageInverseAlias` rules
- /// instead of regular requires; same-name conflict rules also skip
- /// alias packages.
- pub is_alias_of: Option<PackageId>,
-}
-
-impl PoolPackage {
- /// Returns all names this package is known by (own name + provides + replaces targets).
- pub fn names(&self) -> Vec<&str> {
- let mut names = vec![self.name.as_str()];
- for link in &self.provides {
- names.push(link.target.as_str());
- }
- for link in &self.replaces {
- names.push(link.target.as_str());
- }
- names
- }
-
- /// Names that drive same-name conflict resolution — own name plus
- /// `replace` targets. `provide` targets are excluded because two packages
- /// providing different versions of the same virtual name may legitimately
- /// coexist; `replace` declares the replacing package fully supplants the
- /// replaced one. Mirrors Composer's `BasePackage::getNames(false)`.
- pub fn conflict_names(&self) -> Vec<&str> {
- let mut names = vec![self.name.as_str()];
- for link in &self.replaces {
- names.push(link.target.as_str());
- }
- names
- }
-}
-
-impl fmt::Display for PoolPackage {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "{} {}", self.name, self.pretty_version)
- }
-}
-
-/// Input for building a Pool. Users of the crate provide these.
-#[derive(Debug, Clone)]
-pub struct PoolPackageInput {
- pub name: String,
- pub version: String,
- pub pretty_version: String,
- pub requires: Vec<PoolLink>,
- pub replaces: Vec<PoolLink>,
- pub provides: Vec<PoolLink>,
- pub conflicts: Vec<PoolLink>,
- pub is_fixed: bool,
- /// When `Some`, the value is the **normalized** version of another input
- /// in this build batch with the same `name`; the pool will resolve it to
- /// that input's [`PackageId`] in [`PoolPackage::is_alias_of`]. Used by
- /// the registry layer to materialize Composer's `AliasPackage` for
- /// `extra.branch-alias` entries.
- pub is_alias_of: Option<String>,
-}
-
-/// The package pool: contains all candidate packages for dependency resolution.
-/// Packages are assigned sequential 1-based IDs.
-///
-/// Port of Composer's Pool.php.
-pub struct Pool {
- /// All packages, indexed by (id - 1).
- packages: Vec<PoolPackage>,
- /// Index: package name → list of package IDs providing that name.
- package_by_name: IndexMap<String, Vec<PackageId>>,
- /// Cache for what_provides results.
- provider_cache: IndexMap<(String, String), Vec<PackageId>>,
- /// Packages that are fixed/locked but unacceptable (e.g. failed stability).
- unacceptable_fixed_packages: Vec<PackageId>,
-}
-
-impl Pool {
- /// Create a new pool from a list of package inputs.
- pub fn new(inputs: Vec<PoolPackageInput>, unacceptable_fixed_ids: Vec<PackageId>) -> Self {
- let mut packages: Vec<PoolPackage> = Vec::with_capacity(inputs.len());
- let mut package_by_name: IndexMap<String, Vec<PackageId>> = IndexMap::new();
- // Collect alias links (alias_idx, target_name, target_normalized) for
- // a second pass once every input has a stable ID.
- let mut pending_aliases: Vec<(usize, String, String)> = Vec::new();
-
- for (idx, input) in inputs.into_iter().enumerate() {
- let id = (idx as PackageId) + 1;
- if let Some(target) = input.is_alias_of.clone() {
- pending_aliases.push((idx, input.name.clone(), target));
- }
- let pkg = PoolPackage {
- id,
- name: input.name,
- version: input.version,
- pretty_version: input.pretty_version,
- requires: input.requires,
- replaces: input.replaces,
- provides: input.provides,
- conflicts: input.conflicts,
- is_fixed: input.is_fixed,
- is_alias_of: None,
- };
-
- // Index by all names this package provides
- for name in pkg.names() {
- package_by_name
- .entry(name.to_string())
- .or_default()
- .push(id);
- }
-
- packages.push(pkg);
- }
-
- // Resolve alias targets: for each alias input, find the matching
- // (name, normalized version) entry and store its ID. Mirrors the
- // post-construction wiring Composer does in
- // `RepositorySet::createAliasPackage` / `addPackage`.
- for (alias_idx, name, target_normalized) in pending_aliases {
- if let Some(ids) = package_by_name.get(&name) {
- let target_id = ids.iter().copied().find(|&id| {
- let candidate = &packages[(id - 1) as usize];
- !candidate.name.is_empty()
- && candidate.name == name
- && candidate.version == target_normalized
- && candidate.is_alias_of.is_none()
- });
- if let Some(tid) = target_id {
- packages[alias_idx].is_alias_of = Some(tid);
- }
- }
- }
-
- Pool {
- packages,
- package_by_name,
- provider_cache: IndexMap::new(),
- unacceptable_fixed_packages: unacceptable_fixed_ids,
- }
- }
-
- /// Returns the number of packages in the pool.
- pub fn len(&self) -> usize {
- self.packages.len()
- }
-
- /// Returns true if the pool has no packages.
- pub fn is_empty(&self) -> bool {
- self.packages.is_empty()
- }
-
- /// Look up a package by its 1-based ID.
- pub fn package_by_id(&self, id: PackageId) -> &PoolPackage {
- &self.packages[(id - 1) as usize]
- }
-
- /// All packages in the pool.
- pub fn packages(&self) -> &[PoolPackage] {
- &self.packages
- }
-
- /// Convert a literal to its package reference.
- pub fn literal_to_package(&self, literal: Literal) -> &PoolPackage {
- self.package_by_id(literal_to_package_id(literal))
- }
-
- /// Format a literal as a human-readable string.
- pub fn literal_to_pretty_string(&self, literal: Literal) -> String {
- let pkg = self.literal_to_package(literal);
- let prefix = if literal > 0 {
- "install"
- } else {
- "don't install"
- };
- format!("{prefix} {} {}", pkg.name, pkg.pretty_version)
- }
-
- /// Find all packages matching a name and optional constraint.
- /// Results are cached.
- pub fn what_provides(&mut self, name: &str, constraint: Option<&str>) -> Vec<PackageId> {
- let key = (name.to_string(), constraint.unwrap_or("").to_string());
- if let Some(cached) = self.provider_cache.get(&key) {
- return cached.clone();
- }
-
- let result = self.compute_what_provides(name, constraint);
- self.provider_cache.insert(key, result.clone());
- result
- }
-
- fn compute_what_provides(&self, name: &str, constraint: Option<&str>) -> Vec<PackageId> {
- let Some(candidate_ids) = self.package_by_name.get(name) else {
- return vec![];
- };
-
- let parsed_constraint = constraint.and_then(|c| VersionConstraint::parse(c).ok());
-
- let mut matches = Vec::new();
- for &id in candidate_ids {
- let pkg = self.package_by_id(id);
- if self.matches_package(pkg, name, parsed_constraint.as_ref()) {
- matches.push(id);
- }
- }
- matches
- }
-
- /// Check if a candidate package matches a name and optional constraint.
- /// Handles provides and replaces.
- fn matches_package(
- &self,
- candidate: &PoolPackage,
- name: &str,
- constraint: Option<&VersionConstraint>,
- ) -> bool {
- if candidate.name == name {
- return match constraint {
- None => true,
- Some(vc) => {
- // Try the normalized version first; fall back to the
- // pretty version. Composer normalizes both sides of a
- // constraint match to a single string form (e.g.
- // `dev-master` → `9999999-dev`), so a query for
- // `dev-master` matches a package whose pretty version
- // is `dev-master` even when the pool stores its
- // version field in a different normalized shape (e.g.
- // the four-segment `9999999.9999999.9999999.9999999-dev`
- // expansion Mozart uses internally for default-branch
- // and root-alias entries). The pretty fallback bridges
- // that gap without forcing the pool to commit to a
- // single normalization.
- if let Ok(v) = mozart_semver::Version::parse(&candidate.version)
- && vc.matches(&v)
- {
- return true;
- }
- if let Ok(pv) = mozart_semver::Version::parse(&candidate.pretty_version)
- && vc.matches(&pv)
- {
- return true;
- }
- false
- }
- };
- }
-
- // Check provides. A package may declare more than one provide link
- // for the same target (e.g. an `AliasPackage` carries the base's link
- // and an extra link tagged at the alias's own version), so keep
- // iterating once a target name matches but the constraint doesn't —
- // a later link may still satisfy.
- for link in &candidate.provides {
- if link.target != name {
- continue;
- }
- match constraint {
- None => return true,
- Some(vc) => {
- if let Ok(provide_vc) = VersionConstraint::parse(&link.constraint)
- && constraints_intersect(vc, &provide_vc)
- {
- return true;
- }
- }
- }
- }
-
- for link in &candidate.replaces {
- if link.target != name {
- continue;
- }
- match constraint {
- None => return true,
- Some(vc) => {
- if let Ok(replace_vc) = VersionConstraint::parse(&link.constraint)
- && constraints_intersect(vc, &replace_vc)
- {
- return true;
- }
- }
- }
- }
-
- false
- }
-
- /// Check if a package is in the unacceptable fixed list.
- pub fn is_unacceptable_fixed_package(&self, id: PackageId) -> bool {
- self.unacceptable_fixed_packages.contains(&id)
- }
-}
-
-impl fmt::Display for Pool {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- writeln!(f, "Pool:")?;
- for pkg in &self.packages {
- writeln!(f, " {:>6}: {} {}", pkg.id, pkg.name, pkg.pretty_version)?;
- }
- Ok(())
- }
-}
-
-/// Whether the request constraint and the provide/replace link constraint
-/// share at least one satisfying version. Mirrors Composer's
-/// `ConstraintInterface::matches` semantics: a provide/replace link only
-/// makes the candidate a viable provider for those versions of the target
-/// that fall in the link's constraint.
-fn constraints_intersect(a: &VersionConstraint, b: &VersionConstraint) -> bool {
- a.intersects(b)
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- fn make_input(name: &str, version: &str) -> PoolPackageInput {
- PoolPackageInput {
- name: name.to_string(),
- version: version.to_string(),
- pretty_version: version.to_string(),
- requires: vec![],
- replaces: vec![],
- provides: vec![],
- conflicts: vec![],
- is_fixed: false,
- is_alias_of: None,
- }
- }
-
- #[test]
- fn test_pool_basic() {
- let mut pool = Pool::new(
- vec![
- make_input("a/a", "1.0.0.0"),
- make_input("a/a", "2.0.0.0"),
- make_input("b/b", "1.0.0.0"),
- ],
- vec![],
- );
-
- assert_eq!(pool.len(), 3);
- assert_eq!(pool.package_by_id(1).name, "a/a");
- assert_eq!(pool.package_by_id(2).name, "a/a");
- assert_eq!(pool.package_by_id(3).name, "b/b");
-
- let providers = pool.what_provides("a/a", None);
- assert_eq!(providers, vec![1, 2]);
- }
-
- #[test]
- fn test_literal_to_package() {
- let pool = Pool::new(
- vec![make_input("a/a", "1.0.0.0"), make_input("b/b", "1.0.0.0")],
- vec![],
- );
-
- assert_eq!(pool.literal_to_package(1).name, "a/a");
- assert_eq!(pool.literal_to_package(-1).name, "a/a");
- assert_eq!(pool.literal_to_package(2).name, "b/b");
- assert_eq!(pool.literal_to_package(-2).name, "b/b");
- }
-
- #[test]
- fn test_literal_pretty_string() {
- let pool = Pool::new(vec![make_input("a/a", "1.0.0.0")], vec![]);
- assert_eq!(pool.literal_to_pretty_string(1), "install a/a 1.0.0.0");
- assert_eq!(
- pool.literal_to_pretty_string(-1),
- "don't install a/a 1.0.0.0"
- );
- }
-}
diff --git a/crates/mozart-sat-resolver/src/pool_builder.rs b/crates/mozart-sat-resolver/src/pool_builder.rs
deleted file mode 100644
index 6088e7d..0000000
--- a/crates/mozart-sat-resolver/src/pool_builder.rs
+++ /dev/null
@@ -1,222 +0,0 @@
-use crate::pool::{Pool, PoolLink, PoolPackageInput};
-use indexmap::IndexSet;
-use std::collections::VecDeque;
-
-/// Builder for constructing a Pool from package metadata.
-///
-/// The builder accepts package inputs and recursively discovers
-/// transitive dependencies. This is done by the registry layer
-/// before solving.
-pub struct PoolBuilder {
- /// Packages to add to the pool.
- inputs: Vec<PoolPackageInput>,
- /// Names already added (to avoid duplicates).
- added: IndexSet<String>,
- /// Queue of package names that need to be explored.
- pending_names: VecDeque<String>,
- /// Package names that have already been explored (returned by next_pending).
- explored_names: IndexSet<String>,
- /// Specific platform packages to ignore (from `--ignore-platform-req=name`).
- ignore_platform_reqs: IndexSet<String>,
- /// When true, ignore every platform package (php, ext-*, lib-*, composer-*).
- /// Mirrors `--ignore-platform-reqs` (no value).
- ignore_all_platform_reqs: bool,
-}
-
-impl PoolBuilder {
- pub fn new() -> Self {
- PoolBuilder {
- inputs: Vec::new(),
- added: IndexSet::new(),
- pending_names: VecDeque::new(),
- explored_names: IndexSet::new(),
- ignore_platform_reqs: IndexSet::new(),
- ignore_all_platform_reqs: false,
- }
- }
-
- /// Set platform requirements to ignore during exploration.
- pub fn set_ignore_platform_reqs(&mut self, names: IndexSet<String>) {
- self.ignore_platform_reqs = names;
- }
-
- /// When set, every platform package is skipped during exploration.
- pub fn set_ignore_all_platform_reqs(&mut self, ignore_all: bool) {
- self.ignore_all_platform_reqs = ignore_all;
- }
-
- fn is_ignored_platform_dep(&self, name: &str) -> bool {
- if self
- .ignore_platform_reqs
- .iter()
- .any(|p| mozart_core::matches_wildcard(name, p))
- {
- return true;
- }
- self.ignore_all_platform_reqs && mozart_core::platform::is_platform_package(name)
- }
-
- /// Add a package version to the builder. Returns true if it's new.
- pub fn add_package(&mut self, input: PoolPackageInput) -> bool {
- let key = format!("{}@{}", input.name, input.version);
- if self.added.contains(&key) {
- return false;
- }
- self.added.insert(key);
-
- // Queue dependency names for exploration
- for link in &input.requires {
- if !self.is_ignored_platform_dep(&link.target) {
- self.pending_names.push_back(link.target.clone());
- }
- }
-
- self.inputs.push(input);
- true
- }
-
- /// Get the next package name that needs to be explored.
- /// The caller should fetch available versions for this package
- /// and add them via `add_package`.
- pub fn next_pending(&mut self) -> Option<String> {
- while let Some(name) = self.pending_names.pop_front() {
- // Skip if already explored or already has versions in inputs
- if self.explored_names.contains(&name) {
- continue;
- }
- if self.inputs.iter().any(|p| p.name == name) {
- continue;
- }
- self.explored_names.insert(name.clone());
- return Some(name);
- }
- None
- }
-
- /// Check if there are more names to explore.
- pub fn has_pending(&self) -> bool {
- !self.pending_names.is_empty()
- }
-
- /// Build the final Pool.
- pub fn build(self) -> Pool {
- Pool::new(self.inputs, vec![])
- }
-
- /// Get the number of packages added so far.
- pub fn len(&self) -> usize {
- self.inputs.len()
- }
-
- /// Read-only access to package inputs collected so far. Used by the
- /// registry layer to materialize root aliases (`require: "X as Y"`) once
- /// every base + branch-alias entry is in place: a second pass scans for
- /// matching `(name, version)` and pushes the alias entry on top.
- pub fn inputs(&self) -> &[PoolPackageInput] {
- &self.inputs
- }
-
- /// Whether the builder has no packages.
- pub fn is_empty(&self) -> bool {
- self.inputs.is_empty()
- }
-}
-
-impl Default for PoolBuilder {
- fn default() -> Self {
- Self::new()
- }
-}
-
-/// Helper to convert (name, constraint) pairs from Packagist into PoolLinks.
-///
-/// `source_version` is the normalized version of the package declaring these
-/// links; it replaces any `"self.version"` constraint, mirroring Composer's
-/// `ArrayLoader::createLink` (and `AliasPackage::replaceSelfVersionDependencies`,
-/// which feeds the alias's own version in for the same purpose).
-pub fn make_pool_links(
- source: &str,
- source_version: &str,
- deps: &[(String, String)],
-) -> Vec<PoolLink> {
- deps.iter()
- .map(|(target, constraint)| PoolLink {
- target: target.clone(),
- constraint: if constraint.trim() == "self.version" {
- source_version.to_string()
- } else {
- constraint.clone()
- },
- source: source.to_string(),
- })
- .collect()
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_pool_builder_basic() {
- let mut builder = PoolBuilder::new();
-
- builder.add_package(PoolPackageInput {
- name: "a/a".to_string(),
- version: "1.0.0.0".to_string(),
- pretty_version: "1.0.0".to_string(),
- requires: vec![PoolLink {
- target: "b/b".to_string(),
- constraint: "^1.0".to_string(),
- source: "a/a".to_string(),
- }],
- replaces: vec![],
- provides: vec![],
- conflicts: vec![],
- is_fixed: false,
- is_alias_of: None,
- });
-
- // Should have b/b pending
- let pending = builder.next_pending();
- assert_eq!(pending, Some("b/b".to_string()));
-
- builder.add_package(PoolPackageInput {
- name: "b/b".to_string(),
- version: "1.0.0.0".to_string(),
- pretty_version: "1.0.0".to_string(),
- requires: vec![],
- replaces: vec![],
- provides: vec![],
- conflicts: vec![],
- is_fixed: false,
- is_alias_of: None,
- });
-
- // No more pending
- assert!(builder.next_pending().is_none());
-
- let pool = builder.build();
- assert_eq!(pool.len(), 2);
- }
-
- #[test]
- fn test_deduplication() {
- let mut builder = PoolBuilder::new();
-
- let input = PoolPackageInput {
- name: "a/a".to_string(),
- version: "1.0.0.0".to_string(),
- pretty_version: "1.0.0".to_string(),
- requires: vec![],
- replaces: vec![],
- provides: vec![],
- conflicts: vec![],
- is_fixed: false,
- is_alias_of: None,
- };
-
- assert!(builder.add_package(input.clone()));
- assert!(!builder.add_package(input));
- assert_eq!(builder.len(), 1);
- }
-}
diff --git a/crates/mozart-sat-resolver/src/problem.rs b/crates/mozart-sat-resolver/src/problem.rs
deleted file mode 100644
index a1692fd..0000000
--- a/crates/mozart-sat-resolver/src/problem.rs
+++ /dev/null
@@ -1,499 +0,0 @@
-use crate::pool::{Literal, Pool, literal_to_package_id};
-use crate::rule::{ReasonData, Rule, RuleReason};
-use crate::rule_set::{RuleId, RuleSet};
-
-/// Represents a conflict found during resolution.
-/// Collects the rules involved in the problem.
-///
-/// Port of Composer's Problem.php.
-#[derive(Debug, Clone)]
-pub struct Problem {
- /// Sections of rules that form this problem.
- /// Each section is a group of related rules.
- sections: Vec<Vec<RuleId>>,
-}
-
-impl Problem {
- pub fn new() -> Self {
- Problem {
- sections: vec![vec![]],
- }
- }
-
- /// Add a rule to the current section.
- pub fn add_rule(&mut self, rule_id: RuleId) {
- if let Some(section) = self.sections.last_mut()
- && !section.contains(&rule_id)
- {
- section.push(rule_id);
- }
- }
-
- /// Start a new section.
- pub fn next_section(&mut self) {
- if self.sections.last().is_some_and(|s| !s.is_empty()) {
- self.sections.push(vec![]);
- }
- }
-
- /// Get all rule IDs in this problem.
- pub fn rule_ids(&self) -> Vec<RuleId> {
- self.sections.iter().flatten().copied().collect()
- }
-
- /// Format the problem as a human-readable string using Pool data.
- ///
- /// Port of Composer's Problem::getPrettyString().
- pub fn pretty_string(&self, pool: &Pool, rules: &RuleSet) -> String {
- // Flatten all sections (reversed) like Composer does
- let mut all_rules: Vec<RuleId> = self.sections.iter().rev().flatten().copied().collect();
-
- if all_rules.is_empty() {
- return "Unknown problem".to_string();
- }
-
- // Sort by priority, then by sortable string
- all_rules.sort_by(|&a, &b| {
- let rule_a = rules.rule_by_id(a);
- let rule_b = rules.rule_by_id(b);
- let prio_a = rule_priority(rule_a);
- let prio_b = rule_priority(rule_b);
- if prio_a != prio_b {
- return prio_b.cmp(&prio_a);
- }
- sortable_string(pool, rule_a).cmp(&sortable_string(pool, rule_b))
- });
-
- // Format each rule
- let mut messages: Vec<String> = Vec::new();
- for &rule_id in &all_rules {
- let rule = rules.rule_by_id(rule_id);
- let msg = rule_pretty_string(pool, rule);
- if !msg.is_empty() {
- messages.push(msg);
- }
- }
-
- // Deduplicate
- let mut seen = indexmap::IndexSet::new();
- let mut unique = Vec::new();
- for msg in messages {
- if seen.insert(msg.clone()) {
- unique.push(msg);
- }
- }
-
- if unique.is_empty() {
- return "Unknown problem".to_string();
- }
-
- unique
- .iter()
- .map(|m| format!(" - {m}"))
- .collect::<Vec<_>>()
- .join("\n")
- }
-
- /// Basic format for backward compatibility (uses rule Display).
- pub fn format(&self, rules: &RuleSet) -> String {
- let mut parts = Vec::new();
- for section in &self.sections {
- for &rule_id in section {
- let rule = rules.rule_by_id(rule_id);
- parts.push(format!(" - {rule}"));
- }
- }
- if parts.is_empty() {
- "Unknown problem".to_string()
- } else {
- parts.join("\n")
- }
- }
-}
-
-impl Default for Problem {
- fn default() -> Self {
- Self::new()
- }
-}
-
-/// Get the sort priority for a rule (higher = more important).
-/// Port of Problem::getRulePriority().
-fn rule_priority(rule: &Rule) -> u8 {
- match rule.reason {
- RuleReason::Fixed => 3,
- RuleReason::RootRequire => 2,
- RuleReason::PackageConflict | RuleReason::PackageRequires => 1,
- RuleReason::PackageSameName
- | RuleReason::Learned
- | RuleReason::PackageAlias
- | RuleReason::PackageInverseAlias => 0,
- }
-}
-
-/// Get a sortable string for a rule.
-/// Port of Problem::getSortableString().
-fn sortable_string(pool: &Pool, rule: &Rule) -> String {
- match (&rule.reason, &rule.reason_data) {
- (RuleReason::RootRequire, ReasonData::RootRequire { package_name, .. }) => {
- package_name.clone()
- }
- (RuleReason::Fixed, ReasonData::Fixed { package_id }) => {
- pool.package_by_id(*package_id).to_string()
- }
- (RuleReason::PackageConflict | RuleReason::PackageRequires, ReasonData::Link(link)) => {
- if let Some(source_lit) = rule.literals().first() {
- let source_pkg = pool.literal_to_package(*source_lit);
- format!("{}//{}", source_pkg, link)
- } else {
- link.to_string()
- }
- }
- (RuleReason::PackageSameName, ReasonData::PackageName(name)) => name.clone(),
- (RuleReason::Learned, _) => rule
- .literals()
- .iter()
- .map(|l: &Literal| l.to_string())
- .collect::<Vec<_>>()
- .join("-"),
- _ => String::new(),
- }
-}
-
-/// Format a rule as a human-readable string.
-/// Port of Composer's Rule::getPrettyString().
-fn rule_pretty_string(pool: &Pool, rule: &Rule) -> String {
- match (&rule.reason, &rule.reason_data) {
- (
- RuleReason::RootRequire,
- ReasonData::RootRequire {
- package_name,
- constraint,
- },
- ) => {
- let providers = format_providers(pool, rule.literals());
- if providers.is_empty() {
- format!(
- "No package found to satisfy root composer.json require {package_name} {constraint}"
- )
- } else {
- format!(
- "Root composer.json requires {package_name} {constraint} -> satisfiable by {providers}."
- )
- }
- }
-
- (RuleReason::Fixed, ReasonData::Fixed { package_id }) => {
- let pkg = pool.package_by_id(*package_id);
- if pkg.is_fixed {
- format!(
- "{} {} is locked to version {} and an update of this package was not requested.",
- pkg.name, pkg.pretty_version, pkg.pretty_version
- )
- } else {
- format!(
- "{} {} is present at version {} and cannot be modified by Mozart",
- pkg.name, pkg.pretty_version, pkg.pretty_version
- )
- }
- }
-
- (RuleReason::PackageConflict, ReasonData::Link(link)) => {
- let literals = rule.literals();
- if literals.len() >= 2 {
- let pkg1 = pool.literal_to_package(literals[0]);
- let pkg2 = pool.literal_to_package(literals[1]);
- // Determine which is the source of the conflict
- if link.source == pkg1.name {
- format!("{pkg2} conflicts with {pkg1}.")
- } else {
- format!("{pkg1} conflicts with {pkg2}.")
- }
- } else {
- format!("Conflict: {link}")
- }
- }
-
- (RuleReason::PackageRequires, ReasonData::Link(link)) => {
- let literals = rule.literals();
- if literals.is_empty() {
- return format!("Requirement: {link}");
- }
-
- let source_pkg = pool.literal_to_package(literals[0]);
- let base_text = format!(
- "{} {} requires {} {}",
- source_pkg.name, source_pkg.pretty_version, link.target, link.constraint
- );
-
- // Remaining literals are the satisfying packages
- let provider_lits: Vec<Literal> = literals[1..].to_vec();
- if provider_lits.is_empty() {
- format!("{base_text} -> no matching package found.")
- } else {
- let providers = format_providers(pool, &provider_lits);
- format!("{base_text} -> satisfiable by {providers}.")
- }
- }
-
- (RuleReason::PackageSameName, ReasonData::PackageName(name)) => {
- let literals = rule.literals();
- // Collect unique package names in this rule
- let mut pkg_names: Vec<String> = Vec::new();
- for &lit in literals {
- let pkg = pool.literal_to_package(lit);
- if !pkg_names.contains(&pkg.name) {
- pkg_names.push(pkg.name.clone());
- }
- }
-
- if pkg_names.len() > 1 {
- // Different packages that replace/provide the same name
- let replacers: Vec<&str> = pkg_names
- .iter()
- .filter(|n| n.as_str() != name)
- .map(|n| n.as_str())
- .collect();
-
- let reason = if replacers.is_empty() {
- format!("They all replace {name} and thus cannot coexist.")
- } else if !pkg_names.contains(name) {
- format!(
- "They {} replace {name} and thus cannot coexist.",
- if literals.len() == 2 { "both" } else { "all" }
- )
- } else if replacers.len() == 1 {
- format!(
- "{} replaces {name} and thus cannot coexist with it.",
- replacers[0]
- )
- } else {
- format!(
- "[{}] replace {name} and thus cannot coexist with it.",
- replacers.join(", ")
- )
- };
-
- let pkgs_str = format_providers(pool, literals);
- format!("Only one of these can be installed: {pkgs_str}. {reason}")
- } else {
- // Same package, different versions
- let pkgs_str = format_providers(pool, literals);
- format!(
- "You can only install one version of a package, so only one of these can be installed: {pkgs_str}."
- )
- }
- }
-
- (RuleReason::Learned, _) => {
- let literals = rule.literals();
- if literals.len() == 1 {
- let pretty = pool.literal_to_pretty_string(literals[0]);
- format!("Conclusion: {pretty} (conflict analysis result)")
- } else {
- // Group literals by install/don't install
- let mut install = Vec::new();
- let mut dont_install = Vec::new();
- for &lit in literals {
- if lit > 0 {
- install.push(lit);
- } else {
- dont_install.push(lit);
- }
- }
-
- let mut parts = Vec::new();
- if !install.is_empty() {
- let pkgs = format_providers(pool, &install);
- if install.len() > 1 {
- parts.push(format!("install one of {pkgs}"));
- } else {
- parts.push(format!("install {pkgs}"));
- }
- }
- if !dont_install.is_empty() {
- let pkgs = format_providers_abs(pool, &dont_install);
- if dont_install.len() > 1 {
- parts.push(format!("don't install one of {pkgs}"));
- } else {
- parts.push(format!("don't install {pkgs}"));
- }
- }
-
- format!(
- "Conclusion: {} (conflict analysis result)",
- parts.join(" | ")
- )
- }
- }
-
- (RuleReason::PackageAlias, _) => {
- let literals = rule.literals();
- if literals.len() >= 2 {
- let alias_pkg = pool.literal_to_package(literals[0]);
- let target_pkg = pool.literal_to_package(literals[1]);
- format!(
- "{alias_pkg} is an alias of {target_pkg} and thus requires it to be installed too."
- )
- } else {
- String::new()
- }
- }
-
- (RuleReason::PackageInverseAlias, _) => {
- let literals = rule.literals();
- if literals.len() >= 2 {
- let target_pkg = pool.literal_to_package(literals[0]);
- let alias_pkg = pool.literal_to_package(literals[1]);
- format!("{alias_pkg} is an alias of {target_pkg} and must be installed with it.")
- } else {
- String::new()
- }
- }
-
- _ => {
- // Fallback: display raw literals
- let literal_strs: Vec<String> = rule
- .literals()
- .iter()
- .map(|&l| pool.literal_to_pretty_string(l))
- .collect();
- literal_strs.join(" | ")
- }
- }
-}
-
-/// Format a list of literals as a list of package names grouped by name.
-/// Similar to Composer's formatPackagesUnique.
-fn format_providers(pool: &Pool, literals: &[Literal]) -> String {
- // Group by package name
- let mut groups: indexmap::IndexMap<&str, Vec<&str>> = indexmap::IndexMap::new();
- for &lit in literals {
- let pkg = pool.literal_to_package(lit);
- groups
- .entry(&pkg.name)
- .or_default()
- .push(&pkg.pretty_version);
- }
-
- let mut parts: Vec<String> = Vec::new();
- for (name, versions) in &groups {
- if versions.len() == 1 {
- parts.push(format!("{name} {}", versions[0]));
- } else {
- let v_str = versions.join(", ");
- parts.push(format!("{name}[{v_str}]"));
- }
- }
-
- parts.sort();
- parts.join(", ")
-}
-
-/// Same as format_providers but uses absolute value of literals.
-fn format_providers_abs(pool: &Pool, literals: &[Literal]) -> String {
- let abs_lits: Vec<Literal> = literals
- .iter()
- .map(|&l| literal_to_package_id(l) as Literal)
- .collect();
- format_providers(pool, &abs_lits)
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::pool::PoolPackageInput;
- use crate::rule::{ReasonData, Rule, RuleReason, RuleType};
-
- fn make_input(name: &str, version: &str, pretty: &str) -> PoolPackageInput {
- PoolPackageInput {
- name: name.to_string(),
- version: version.to_string(),
- pretty_version: pretty.to_string(),
- requires: vec![],
- replaces: vec![],
- provides: vec![],
- conflicts: vec![],
- is_fixed: false,
- is_alias_of: None,
- }
- }
-
- #[test]
- fn test_root_require_pretty_string() {
- let pool = Pool::new(vec![make_input("foo/bar", "1.0.0.0", "1.0.0")], vec![]);
-
- let mut rule_set = RuleSet::new();
- let rule = Rule::new(
- vec![1],
- RuleReason::RootRequire,
- ReasonData::RootRequire {
- package_name: "foo/bar".to_string(),
- constraint: "^1.0".to_string(),
- },
- );
- rule_set.add(rule, RuleType::Request);
-
- let mut problem = Problem::new();
- problem.add_rule(0);
-
- let output = problem.pretty_string(&pool, &rule_set);
- assert!(output.contains("Root composer.json requires foo/bar ^1.0"));
- assert!(output.contains("satisfiable by foo/bar 1.0.0"));
- }
-
- #[test]
- fn test_same_name_pretty_string() {
- let pool = Pool::new(
- vec![
- make_input("foo/bar", "1.0.0.0", "1.0.0"),
- make_input("foo/bar", "2.0.0.0", "2.0.0"),
- ],
- vec![],
- );
-
- let mut rule_set = RuleSet::new();
- let rule = Rule::new(
- vec![-1, -2],
- RuleReason::PackageSameName,
- ReasonData::PackageName("foo/bar".to_string()),
- );
- rule_set.add(rule, RuleType::Package);
-
- let mut problem = Problem::new();
- problem.add_rule(0);
-
- let output = problem.pretty_string(&pool, &rule_set);
- assert!(output.contains("You can only install one version"));
- }
-
- #[test]
- fn test_package_requires_pretty_string() {
- let pool = Pool::new(
- vec![
- make_input("foo/bar", "1.0.0.0", "1.0.0"),
- make_input("baz/qux", "2.0.0.0", "2.0.0"),
- ],
- vec![],
- );
-
- let mut rule_set = RuleSet::new();
- let rule = Rule::new(
- vec![-1, 2],
- RuleReason::PackageRequires,
- ReasonData::Link(crate::pool::PoolLink {
- source: "foo/bar".to_string(),
- target: "baz/qux".to_string(),
- constraint: "^2.0".to_string(),
- }),
- );
- rule_set.add(rule, RuleType::Package);
-
- let mut problem = Problem::new();
- problem.add_rule(0);
-
- let output = problem.pretty_string(&pool, &rule_set);
- assert!(output.contains("foo/bar 1.0.0 requires baz/qux ^2.0"));
- assert!(output.contains("satisfiable by baz/qux 2.0.0"));
- }
-}
diff --git a/crates/mozart-sat-resolver/src/request.rs b/crates/mozart-sat-resolver/src/request.rs
deleted file mode 100644
index 26c17ba..0000000
--- a/crates/mozart-sat-resolver/src/request.rs
+++ /dev/null
@@ -1,65 +0,0 @@
-use crate::pool::PackageId;
-use indexmap::IndexMap;
-
-/// A requirement: package name + version constraint string.
-#[derive(Debug, Clone)]
-pub struct Require {
- pub package_name: String,
- pub constraint: Option<String>,
-}
-
-/// A request for the solver: what to install/fix/lock.
-///
-/// Port of Composer's Request.php.
-#[derive(Debug, Clone)]
-pub struct Request {
- /// Root requirements: package name → constraint string.
- pub requires: IndexMap<String, Option<String>>,
- /// Fixed packages (must be installed, cannot be modified).
- pub fixed_packages: Vec<PackageId>,
- /// Locked packages (installed but can be removed if nothing requires them).
- pub locked_packages: Vec<PackageId>,
-}
-
-impl Request {
- pub fn new() -> Self {
- Request {
- requires: IndexMap::new(),
- fixed_packages: Vec::new(),
- locked_packages: Vec::new(),
- }
- }
-
- /// Add a root requirement.
- pub fn require_name(&mut self, package_name: &str, constraint: Option<&str>) {
- self.requires.insert(
- package_name.to_lowercase(),
- constraint.map(|s| s.to_string()),
- );
- }
-
- /// Mark a package as fixed (must remain installed).
- pub fn fix_package(&mut self, package_id: PackageId) {
- if !self.fixed_packages.contains(&package_id) {
- self.fixed_packages.push(package_id);
- }
- }
-
- /// Mark a package as locked.
- pub fn lock_package(&mut self, package_id: PackageId) {
- if !self.locked_packages.contains(&package_id) {
- self.locked_packages.push(package_id);
- }
- }
-
- /// Check if a package is fixed.
- pub fn is_fixed(&self, package_id: PackageId) -> bool {
- self.fixed_packages.contains(&package_id)
- }
-}
-
-impl Default for Request {
- fn default() -> Self {
- Self::new()
- }
-}
diff --git a/crates/mozart-sat-resolver/src/rule.rs b/crates/mozart-sat-resolver/src/rule.rs
deleted file mode 100644
index 860ae79..0000000
--- a/crates/mozart-sat-resolver/src/rule.rs
+++ /dev/null
@@ -1,280 +0,0 @@
-use crate::pool::{Literal, PoolLink};
-use std::fmt;
-
-/// Why a rule was created.
-/// Port of Composer Rule::RULE_* constants.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum RuleReason {
- /// Root composer.json requirement.
- RootRequire,
- /// Fixed/locked package.
- Fixed,
- /// Two packages conflict.
- PackageConflict,
- /// Package dependency (requires).
- PackageRequires,
- /// Only one version of a package can be installed.
- PackageSameName,
- /// Learned from conflict analysis.
- Learned,
- /// Alias requires its target.
- PackageAlias,
- /// Target requires its alias.
- PackageInverseAlias,
-}
-
-/// Data explaining why a rule was created.
-#[derive(Debug, Clone)]
-pub enum ReasonData {
- /// For RootRequire: package name + constraint string.
- RootRequire {
- package_name: String,
- constraint: String,
- },
- /// For Fixed: the fixed package ID.
- Fixed { package_id: u32 },
- /// For PackageConflict, PackageRequires: a link.
- Link(PoolLink),
- /// For PackageSameName: the package name.
- PackageName(String),
- /// For Learned: index into the learned pool.
- Learned(usize),
- /// For PackageAlias/InverseAlias: the alias package ID.
- AliasPackage(u32),
- /// No data.
- None,
-}
-
-/// The type assigned by RuleSet (which collection this rule belongs to).
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
-pub enum RuleType {
- Package = 0,
- Request = 1,
- Learned = 4,
-}
-
-/// A SAT rule (clause). A disjunction of literals: (L1 | L2 | ... | Ln).
-///
-/// Port of Composer's Rule hierarchy (GenericRule, Rule2Literals, MultiConflictRule).
-/// In Rust we use a single enum instead of class inheritance.
-#[derive(Debug, Clone)]
-pub struct Rule {
- /// The literals in this rule (sorted for deduplication).
- literals: Vec<Literal>,
- /// Whether this is a multi-conflict rule.
- pub is_multi_conflict: bool,
- /// Why this rule was created.
- pub reason: RuleReason,
- /// Additional data about why this rule was created.
- pub reason_data: ReasonData,
- /// Which RuleSet type this rule belongs to.
- pub rule_type: RuleType,
- /// Whether this rule is disabled.
- pub disabled: bool,
-}
-
-impl Rule {
- /// Create a generic rule (arbitrary number of literals).
- /// Equivalent to Composer's GenericRule.
- pub fn new(mut literals: Vec<Literal>, reason: RuleReason, reason_data: ReasonData) -> Self {
- literals.sort();
- Rule {
- literals,
- is_multi_conflict: false,
- reason,
- reason_data,
- rule_type: RuleType::Package, // default, set by RuleSet
- disabled: false,
- }
- }
-
- /// Create a 2-literal rule (optimized common case).
- /// Equivalent to Composer's Rule2Literals.
- pub fn two_literals(
- lit1: Literal,
- lit2: Literal,
- reason: RuleReason,
- reason_data: ReasonData,
- ) -> Self {
- let (a, b) = if lit1 <= lit2 {
- (lit1, lit2)
- } else {
- (lit2, lit1)
- };
- Rule {
- literals: vec![a, b],
- is_multi_conflict: false,
- reason,
- reason_data,
- rule_type: RuleType::Package,
- disabled: false,
- }
- }
-
- /// Create a multi-conflict rule (3+ literals, all negative).
- /// Equivalent to Composer's MultiConflictRule.
- /// Acts as if it were multiple binary conflict rules.
- pub fn multi_conflict(
- mut literals: Vec<Literal>,
- reason: RuleReason,
- reason_data: ReasonData,
- ) -> Self {
- assert!(
- literals.len() >= 3,
- "MultiConflictRule requires at least 3 literals"
- );
- literals.sort();
- Rule {
- literals,
- is_multi_conflict: true,
- reason,
- reason_data,
- rule_type: RuleType::Package,
- disabled: false,
- }
- }
-
- /// Get the sorted literals.
- pub fn literals(&self) -> &[Literal] {
- &self.literals
- }
-
- /// Whether this rule has exactly one literal (unit clause / assertion).
- pub fn is_assertion(&self) -> bool {
- self.literals.len() == 1
- }
-
- /// Compute a hash for deduplication.
- pub fn hash_key(&self) -> String {
- if self.is_multi_conflict {
- let parts: Vec<String> = self.literals.iter().map(|l| l.to_string()).collect();
- format!("c:{}", parts.join(","))
- } else {
- let parts: Vec<String> = self.literals.iter().map(|l| l.to_string()).collect();
- parts.join(",")
- }
- }
-
- /// Structural equality check (same literals).
- pub fn equals(&self, other: &Rule) -> bool {
- self.is_multi_conflict == other.is_multi_conflict && self.literals == other.literals
- }
-
- /// Get the required package name, if applicable.
- pub fn required_package(&self) -> Option<&str> {
- match &self.reason_data {
- ReasonData::RootRequire { package_name, .. } => Some(package_name),
- ReasonData::Link(link) => Some(&link.target),
- ReasonData::Fixed { .. } => None, // would need pool access
- _ => None,
- }
- }
-
- /// Disable this rule.
- pub fn disable(&mut self) {
- if self.is_multi_conflict {
- panic!("Cannot disable a MultiConflictRule");
- }
- self.disabled = true;
- }
-
- /// Enable this rule.
- pub fn enable(&mut self) {
- self.disabled = false;
- }
-
- /// Whether this rule is disabled.
- pub fn is_disabled(&self) -> bool {
- self.disabled
- }
-
- /// Whether this rule is enabled.
- pub fn is_enabled(&self) -> bool {
- !self.disabled
- }
-}
-
-impl fmt::Display for Rule {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- if self.disabled {
- write!(f, "disabled(")?;
- }
- if self.is_multi_conflict {
- write!(f, "(multi(")?;
- for (i, lit) in self.literals.iter().enumerate() {
- if i > 0 {
- write!(f, "|")?;
- }
- write!(f, "{lit}")?;
- }
- write!(f, "))")?;
- } else {
- write!(f, "(")?;
- for (i, lit) in self.literals.iter().enumerate() {
- if i > 0 {
- write!(f, "|")?;
- }
- write!(f, "{lit}")?;
- }
- write!(f, ")")?;
- }
- if self.disabled {
- write!(f, ")")?;
- }
- Ok(())
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_generic_rule() {
- let rule = Rule::new(vec![3, 1, 2], RuleReason::PackageRequires, ReasonData::None);
- assert_eq!(rule.literals(), &[1, 2, 3]);
- assert!(!rule.is_assertion());
- assert_eq!(rule.to_string(), "(1|2|3)");
- }
-
- #[test]
- fn test_two_literal_rule() {
- let rule = Rule::two_literals(-2, -1, RuleReason::PackageConflict, ReasonData::None);
- assert_eq!(rule.literals(), &[-2, -1]);
- assert!(!rule.is_assertion());
- }
-
- #[test]
- fn test_assertion_rule() {
- let rule = Rule::new(vec![1], RuleReason::Fixed, ReasonData::None);
- assert!(rule.is_assertion());
- }
-
- #[test]
- fn test_multi_conflict_rule() {
- let rule = Rule::multi_conflict(
- vec![-3, -1, -2],
- RuleReason::PackageSameName,
- ReasonData::None,
- );
- assert!(rule.is_multi_conflict);
- assert_eq!(rule.literals(), &[-3, -2, -1]);
- }
-
- #[test]
- fn test_hash_key() {
- let r1 = Rule::new(vec![2, 1], RuleReason::PackageRequires, ReasonData::None);
- let r2 = Rule::new(vec![1, 2], RuleReason::PackageConflict, ReasonData::None);
- assert_eq!(r1.hash_key(), r2.hash_key());
- }
-
- #[test]
- fn test_disable_enable() {
- let mut rule = Rule::new(vec![1, 2], RuleReason::PackageRequires, ReasonData::None);
- assert!(rule.is_enabled());
- rule.disable();
- assert!(rule.is_disabled());
- rule.enable();
- assert!(rule.is_enabled());
- }
-}
diff --git a/crates/mozart-sat-resolver/src/rule_set.rs b/crates/mozart-sat-resolver/src/rule_set.rs
deleted file mode 100644
index 918bdae..0000000
--- a/crates/mozart-sat-resolver/src/rule_set.rs
+++ /dev/null
@@ -1,211 +0,0 @@
-use crate::rule::{Rule, RuleType};
-use indexmap::IndexMap;
-
-/// A unique identifier for a rule within the RuleSet.
-pub type RuleId = usize;
-
-/// Container for all rules, organized by type.
-///
-/// Port of Composer's RuleSet.php.
-pub struct RuleSet {
- /// Lookup: rule ID → index into the appropriate type vector.
- /// This is the primary read-only access path used by the solver.
- rules_by_id: Vec<usize>,
- /// Rules grouped by type.
- package_rules: Vec<Rule>,
- request_rules: Vec<Rule>,
- learned_rules: Vec<Rule>,
- /// Total rule count.
- next_rule_id: usize,
- /// Deduplication index.
- rules_by_hash: IndexMap<String, Vec<usize>>,
- /// Maps rule ID → (type, index within type's vec).
- rule_type_index: Vec<(RuleType, usize)>,
-}
-
-impl RuleSet {
- pub fn new() -> Self {
- RuleSet {
- rules_by_id: Vec::new(),
- package_rules: Vec::new(),
- request_rules: Vec::new(),
- learned_rules: Vec::new(),
- next_rule_id: 0,
- rules_by_hash: IndexMap::new(),
- rule_type_index: Vec::new(),
- }
- }
-
- /// Add a rule to the set. Duplicates (by hash + equals) are skipped.
- pub fn add(&mut self, mut rule: Rule, rule_type: RuleType) {
- let hash = rule.hash_key();
-
- // Check for duplicates
- if let Some(existing_ids) = self.rules_by_hash.get(&hash) {
- for &existing_id in existing_ids {
- if rule.equals(self.rule_by_id(existing_id)) {
- return;
- }
- }
- }
-
- rule.rule_type = rule_type;
-
- let rules_vec = match rule_type {
- RuleType::Package => &mut self.package_rules,
- RuleType::Request => &mut self.request_rules,
- RuleType::Learned => &mut self.learned_rules,
- };
- let idx = rules_vec.len();
- rules_vec.push(rule);
-
- let rule_id = self.next_rule_id;
- self.rules_by_id.push(idx);
- self.rule_type_index.push((rule_type, idx));
- self.next_rule_id += 1;
-
- self.rules_by_hash.entry(hash).or_default().push(rule_id);
- }
-
- /// Total number of rules.
- pub fn len(&self) -> usize {
- self.next_rule_id
- }
-
- /// Whether the rule set is empty.
- pub fn is_empty(&self) -> bool {
- self.next_rule_id == 0
- }
-
- /// Look up a rule by its global ID.
- pub fn rule_by_id(&self, id: RuleId) -> &Rule {
- let (rule_type, idx) = self.rule_type_index[id];
- match rule_type {
- RuleType::Package => &self.package_rules[idx],
- RuleType::Request => &self.request_rules[idx],
- RuleType::Learned => &self.learned_rules[idx],
- }
- }
-
- /// Get a mutable reference to a rule by its global ID.
- pub fn rule_by_id_mut(&mut self, id: RuleId) -> &mut Rule {
- let (rule_type, idx) = self.rule_type_index[id];
- match rule_type {
- RuleType::Package => &mut self.package_rules[idx],
- RuleType::Request => &mut self.request_rules[idx],
- RuleType::Learned => &mut self.learned_rules[idx],
- }
- }
-
- /// Iterate over all rules in order (Package, then Request, then Learned).
- pub fn iter(&self) -> impl Iterator<Item = (RuleId, &Rule)> {
- (0..self.next_rule_id).map(move |id| (id, self.rule_by_id(id)))
- }
-
- /// Iterate over rules of a specific type, returning (global_rule_id, &Rule).
- pub fn iter_type(&self, rule_type: RuleType) -> RuleTypeIterator<'_> {
- RuleTypeIterator {
- rule_set: self,
- rule_type,
- current: 0,
- total: self.next_rule_id,
- }
- }
-
- /// Get the request rules slice.
- pub fn request_rules(&self) -> &[Rule] {
- &self.request_rules
- }
-}
-
-impl Default for RuleSet {
- fn default() -> Self {
- Self::new()
- }
-}
-
-/// Iterator over rules of a specific type.
-pub struct RuleTypeIterator<'a> {
- rule_set: &'a RuleSet,
- rule_type: RuleType,
- current: RuleId,
- total: usize,
-}
-
-impl<'a> Iterator for RuleTypeIterator<'a> {
- type Item = (RuleId, &'a Rule);
-
- fn next(&mut self) -> Option<Self::Item> {
- while self.current < self.total {
- let id = self.current;
- self.current += 1;
- let rule = self.rule_set.rule_by_id(id);
- if rule.rule_type == self.rule_type {
- return Some((id, rule));
- }
- }
- None
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::rule::{ReasonData, RuleReason};
-
- #[test]
- fn test_add_and_lookup() {
- let mut rs = RuleSet::new();
- rs.add(
- Rule::new(vec![1, 2], RuleReason::PackageRequires, ReasonData::None),
- RuleType::Package,
- );
- rs.add(
- Rule::new(vec![3], RuleReason::RootRequire, ReasonData::None),
- RuleType::Request,
- );
-
- assert_eq!(rs.len(), 2);
- assert_eq!(rs.rule_by_id(0).literals(), &[1, 2]);
- assert_eq!(rs.rule_by_id(1).literals(), &[3]);
- }
-
- #[test]
- fn test_deduplication() {
- let mut rs = RuleSet::new();
- rs.add(
- Rule::new(vec![1, 2], RuleReason::PackageRequires, ReasonData::None),
- RuleType::Package,
- );
- rs.add(
- Rule::new(vec![2, 1], RuleReason::PackageConflict, ReasonData::None),
- RuleType::Package,
- );
- // Duplicate should be skipped
- assert_eq!(rs.len(), 1);
- }
-
- #[test]
- fn test_iter_type() {
- let mut rs = RuleSet::new();
- rs.add(
- Rule::new(vec![1, 2], RuleReason::PackageRequires, ReasonData::None),
- RuleType::Package,
- );
- rs.add(
- Rule::new(vec![3], RuleReason::RootRequire, ReasonData::None),
- RuleType::Request,
- );
- rs.add(
- Rule::new(vec![4, 5], RuleReason::PackageConflict, ReasonData::None),
- RuleType::Package,
- );
-
- let request_rules: Vec<_> = rs.iter_type(RuleType::Request).collect();
- assert_eq!(request_rules.len(), 1);
- assert_eq!(request_rules[0].1.literals(), &[3]);
-
- let package_rules: Vec<_> = rs.iter_type(RuleType::Package).collect();
- assert_eq!(package_rules.len(), 2);
- }
-}
diff --git a/crates/mozart-sat-resolver/src/rule_set_generator.rs b/crates/mozart-sat-resolver/src/rule_set_generator.rs
deleted file mode 100644
index 2ab9f86..0000000
--- a/crates/mozart-sat-resolver/src/rule_set_generator.rs
+++ /dev/null
@@ -1,464 +0,0 @@
-use crate::pool::{Literal, PackageId, Pool, PoolLink};
-use crate::rule::{ReasonData, Rule, RuleReason, RuleType};
-use crate::rule_set::RuleSet;
-use indexmap::IndexMap;
-use indexmap::IndexSet;
-use mozart_semver::VersionConstraint;
-use std::collections::VecDeque;
-
-/// Generates SAT rules from the pool and request.
-///
-/// Port of Composer's RuleSetGenerator.php.
-pub struct RuleSetGenerator<'a> {
- pool: &'a mut Pool,
- rules: RuleSet,
- /// Packages already processed.
- added_map: IndexSet<PackageId>,
- /// Package names → list of package IDs with that name (non-alias).
- added_packages_by_name: IndexMap<String, Vec<PackageId>>,
- /// Specific platform packages to ignore (from `--ignore-platform-req=name`).
- ignore_platform_reqs: IndexSet<String>,
- /// When true, every platform package is treated as ignored.
- /// Mirrors `--ignore-platform-reqs` (no value).
- ignore_all_platform_reqs: bool,
-}
-
-impl<'a> RuleSetGenerator<'a> {
- pub fn new(pool: &'a mut Pool) -> Self {
- RuleSetGenerator {
- pool,
- rules: RuleSet::new(),
- added_map: IndexSet::new(),
- added_packages_by_name: IndexMap::new(),
- ignore_platform_reqs: IndexSet::new(),
- ignore_all_platform_reqs: false,
- }
- }
-
- /// Set platform requirements to ignore.
- pub fn set_ignore_platform_reqs(&mut self, names: IndexSet<String>) {
- self.ignore_platform_reqs = names;
- }
-
- /// When set, every platform package is treated as ignored.
- pub fn set_ignore_all_platform_reqs(&mut self, ignore_all: bool) {
- self.ignore_all_platform_reqs = ignore_all;
- }
-
- fn is_ignored_platform_dep(&self, name: &str) -> bool {
- if self
- .ignore_platform_reqs
- .iter()
- .any(|p| mozart_core::matches_wildcard(name, p))
- {
- return true;
- }
- self.ignore_all_platform_reqs && mozart_core::platform::is_platform_package(name)
- }
-
- /// Generate rules for a set of requirements and fixed packages.
- ///
- /// Port of Composer's RuleSetGenerator::getRulesFor.
- ///
- /// `root_provides` / `root_replaces` map a target package name to the
- /// constraint declared in the root composer.json's `provide` / `replace`
- /// section. They mirror the "self-fulfilling rule" check in Composer's
- /// `RuleSetGenerator::createRequireRule`: when the root package itself
- /// provides or replaces a name it requires, no install-one-of rule is
- /// emitted for that root require — root is implicitly already installed,
- /// so the requirement is trivially satisfied without forcing a real
- /// provider. Without this, Mozart picks up an inline `provided/pkg` from
- /// the repository even though the root claims to fulfill it itself.
- ///
- /// Returns the generated rule set together with the list of root requires
- /// that have no matching providers in the pool. Mirrors Composer's
- /// `Solver::checkForRootRequireProblems`: a root require with zero
- /// providers does not produce a SAT rule (so the solver would otherwise
- /// succeed with an empty plan), but it must still be reported as an
- /// unresolvable problem.
- pub fn generate(
- mut self,
- requires: &IndexMap<String, Option<String>>,
- fixed_packages: &[PackageId],
- root_provides: &IndexMap<String, String>,
- root_replaces: &IndexMap<String, String>,
- ) -> (RuleSet, Vec<(String, Option<String>)>) {
- let mut missing_root_requires: Vec<(String, Option<String>)> = Vec::new();
- // Process fixed packages
- for &pkg_id in fixed_packages {
- if self.pool.is_unacceptable_fixed_package(pkg_id) {
- continue;
- }
-
- self.add_rules_for_package(pkg_id);
-
- // Create assertion rule: this package must be installed
- let rule = Rule::new(
- vec![pkg_id as Literal],
- RuleReason::Fixed,
- ReasonData::Fixed { package_id: pkg_id },
- );
- self.rules.add(rule, RuleType::Request);
- }
-
- // Process root requirements
- for (name, constraint) in requires {
- if self.is_ignored_platform_dep(name.as_str()) {
- continue;
- }
-
- // Self-fulfilling root require: if the root composer.json declares
- // `provide` / `replace` for this name and the link constraint
- // intersects the require constraint, drop the install-one-of rule
- // entirely. Mirrors Composer's `createRequireRule` returning null
- // when a provider IS the package itself: there, the root is in the
- // pool as a fixed package and `whatProvides` includes it, so the
- // resulting rule is trivially satisfied. Mozart does not yet add
- // the root to the pool, so we make the same decision here based
- // on the explicit root provide/replace tables.
- if root_self_fulfills(name, constraint.as_deref(), root_provides)
- || root_self_fulfills(name, constraint.as_deref(), root_replaces)
- {
- continue;
- }
-
- let providers = self.pool.what_provides(name, constraint.as_deref());
-
- if !providers.is_empty() {
- for &pkg_id in &providers {
- self.add_rules_for_package(pkg_id);
- }
-
- // Create "install one of" rule
- let literals: Vec<Literal> = providers.iter().map(|&id| id as Literal).collect();
- let rule = Rule::new(
- literals,
- RuleReason::RootRequire,
- ReasonData::RootRequire {
- package_name: name.clone(),
- constraint: constraint.clone().unwrap_or_default(),
- },
- );
- self.rules.add(rule, RuleType::Request);
- } else {
- missing_root_requires.push((name.clone(), constraint.clone()));
- }
- }
-
- // Mirror Composer's `RuleSetGenerator::addRulesForRootAliases`:
- // ensure every alias whose target was already added gets its own
- // alias↔target rules, even when the alias itself didn't appear in
- // any root require's `whatProvides` (e.g. the synthetic
- // `9999999-dev` alias from a `default-branch: true` package, which
- // only matches a literal `9999999-dev` constraint).
- let alias_pairs: Vec<(PackageId, PackageId)> = self
- .pool
- .packages()
- .iter()
- .filter_map(|p| p.is_alias_of.map(|t| (p.id, t)))
- .collect();
- for (alias_id, target_id) in alias_pairs {
- if self.added_map.contains(&target_id) && !self.added_map.contains(&alias_id) {
- self.add_rules_for_package(alias_id);
- }
- }
-
- // Add conflict rules
- self.add_conflict_rules();
-
- (self.rules, missing_root_requires)
- }
-
- /// Add rules for a package and its transitive dependencies.
- ///
- /// Port of Composer's RuleSetGenerator::addRulesForPackage.
- fn add_rules_for_package(&mut self, pkg_id: PackageId) {
- let mut work_queue: VecDeque<PackageId> = VecDeque::new();
- work_queue.push_back(pkg_id);
-
- while let Some(current_id) = work_queue.pop_front() {
- if self.added_map.contains(&current_id) {
- continue;
- }
- self.added_map.insert(current_id);
-
- let pkg = self.pool.package_by_id(current_id);
- let conflict_names: Vec<String> =
- pkg.conflict_names().into_iter().map(String::from).collect();
- let requires = pkg.requires.clone();
- let alias_target = pkg.is_alias_of;
-
- if let Some(target_id) = alias_target {
- // Mirror Composer's RuleSetGenerator::addRulesForPackage alias
- // branch: enqueue the target, emit `(-alias | target)` so the
- // alias forces the target, and `(-target | alias)` so the
- // target forces the alias (they install together). The alias
- // is NOT indexed under its name for same-name conflicts —
- // Composer skips that for aliases too.
- work_queue.push_back(target_id);
-
- let alias_rule = Rule::two_literals(
- -(current_id as Literal),
- target_id as Literal,
- RuleReason::PackageAlias,
- ReasonData::AliasPackage(current_id),
- );
- self.rules.add(alias_rule, RuleType::Package);
-
- let inverse_rule = Rule::two_literals(
- -(target_id as Literal),
- current_id as Literal,
- RuleReason::PackageInverseAlias,
- ReasonData::AliasPackage(current_id),
- );
- self.rules.add(inverse_rule, RuleType::Package);
-
- // The aliased target carries the actual requires; skip
- // alias's own (link-rewritten copy) to avoid duplicates.
- continue;
- }
-
- // Index by every name this package fully claims (own name +
- // `replace` targets). Same-name conflict rules (below) then
- // prevent two packages from coexisting under the same logical
- // identity. Mirrors `BasePackage::getNames(false)` indexing in
- // Composer's RuleSetGenerator::addRulesForPackage — `provide`
- // targets are intentionally omitted so that providers can
- // coexist with the package they provide. Alias packages are
- // skipped because the target package's name already covers them.
- for name in conflict_names {
- self.added_packages_by_name
- .entry(name)
- .or_default()
- .push(current_id);
- }
-
- // Process each requirement
- for link in requires {
- if self.is_ignored_platform_dep(&link.target) {
- continue;
- }
-
- let possible_requires = self
- .pool
- .what_provides(&link.target, Some(&link.constraint));
-
- // Create require rule: (-current | provider1 | provider2 | ...)
- let mut literals: Vec<Literal> = vec![-(current_id as Literal)];
- let mut self_fulfilling = false;
-
- for &provider_id in &possible_requires {
- if provider_id == current_id {
- self_fulfilling = true;
- break;
- }
- literals.push(provider_id as Literal);
- }
-
- if !self_fulfilling {
- let rule = Rule::new(
- literals,
- RuleReason::PackageRequires,
- ReasonData::Link(PoolLink {
- target: link.target.clone(),
- constraint: link.constraint.clone(),
- source: self.pool.package_by_id(current_id).name.clone(),
- }),
- );
- self.rules.add(rule, RuleType::Package);
- }
-
- // Enqueue providers for further processing
- for &provider_id in &possible_requires {
- work_queue.push_back(provider_id);
- }
- }
- }
- }
-
- /// Add conflict rules: explicit conflicts and same-name rules.
- ///
- /// Port of Composer's RuleSetGenerator::addConflictRules.
- fn add_conflict_rules(&mut self) {
- // Explicit conflicts
- let added_ids: Vec<PackageId> = self.added_map.iter().copied().collect();
- for &pkg_id in &added_ids {
- let pkg = self.pool.package_by_id(pkg_id);
- let conflicts = pkg.conflicts.clone();
-
- for link in conflicts {
- if self.is_ignored_platform_dep(&link.target) {
- continue;
- }
-
- if !self.added_packages_by_name.contains_key(&link.target) {
- continue;
- }
-
- let conflicting = self
- .pool
- .what_provides(&link.target, Some(&link.constraint));
-
- for &conflict_id in &conflicting {
- if conflict_id == pkg_id {
- continue; // ignore self-conflict
- }
- let rule = Rule::two_literals(
- -(pkg_id as Literal),
- -(conflict_id as Literal),
- RuleReason::PackageConflict,
- ReasonData::Link(link.clone()),
- );
- self.rules.add(rule, RuleType::Package);
- }
- }
- }
-
- // Same-name rules: only one version of a package can be installed
- let names_to_process: Vec<(String, Vec<PackageId>)> = self
- .added_packages_by_name
- .iter()
- .filter(|(_, ids)| ids.len() > 1)
- .map(|(name, ids)| (name.clone(), ids.clone()))
- .collect();
-
- for (name, pkg_ids) in names_to_process {
- let literals: Vec<Literal> = pkg_ids.iter().map(|&id| -(id as Literal)).collect();
-
- if literals.len() == 2 {
- let rule = Rule::two_literals(
- literals[0],
- literals[1],
- RuleReason::PackageSameName,
- ReasonData::PackageName(name),
- );
- self.rules.add(rule, RuleType::Package);
- } else if literals.len() >= 3 {
- let rule = Rule::multi_conflict(
- literals,
- RuleReason::PackageSameName,
- ReasonData::PackageName(name),
- );
- self.rules.add(rule, RuleType::Package);
- }
- }
- }
-}
-
-/// True when the root composer.json's `provide` / `replace` map declares
-/// `target` with a constraint that intersects the require's constraint. A
-/// missing require constraint is treated as `*` (matches anything), and a
-/// missing/unparsable link constraint conservatively does NOT match — the
-/// fixture fails closed back to the regular install-one-of path.
-fn root_self_fulfills(
- target: &str,
- require_constraint: Option<&str>,
- root_links: &IndexMap<String, String>,
-) -> bool {
- let Some(link_constraint_str) = root_links.get(target) else {
- return false;
- };
- let Ok(link_vc) = VersionConstraint::parse(link_constraint_str) else {
- return false;
- };
- match require_constraint {
- None => true,
- Some(req) => match VersionConstraint::parse(req) {
- Ok(req_vc) => req_vc.intersects(&link_vc),
- Err(_) => false,
- },
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::pool::{Pool, PoolLink, PoolPackageInput};
-
- fn make_input(name: &str, version: &str) -> PoolPackageInput {
- PoolPackageInput {
- name: name.to_string(),
- version: version.to_string(),
- pretty_version: version.to_string(),
- requires: vec![],
- replaces: vec![],
- provides: vec![],
- conflicts: vec![],
- is_fixed: false,
- is_alias_of: None,
- }
- }
-
- #[test]
- fn test_root_require_generates_rule() {
- let mut pool = Pool::new(
- vec![make_input("a/a", "1.0.0.0"), make_input("a/a", "2.0.0.0")],
- vec![],
- );
-
- let mut requires = IndexMap::new();
- requires.insert("a/a".to_string(), None);
-
- let generator = RuleSetGenerator::new(&mut pool);
- let (rules, _) = generator.generate(&requires, &[], &IndexMap::new(), &IndexMap::new());
-
- // Should have a request rule: (1 | 2)
- let request_count = rules.iter_type(RuleType::Request).count();
- assert_eq!(request_count, 1);
-
- // Should have a same-name rule: (-1 | -2)
- let package_count = rules.iter_type(RuleType::Package).count();
- assert!(package_count >= 1);
- }
-
- #[test]
- fn test_dependency_chain_rules() {
- // a/a 1.0 requires b/b
- let mut pool = Pool::new(
- vec![
- PoolPackageInput {
- name: "a/a".to_string(),
- version: "1.0.0.0".to_string(),
- pretty_version: "1.0.0".to_string(),
- requires: vec![PoolLink {
- target: "b/b".to_string(),
- constraint: "*".to_string(),
- source: "a/a".to_string(),
- }],
- replaces: vec![],
- provides: vec![],
- conflicts: vec![],
- is_fixed: false,
- is_alias_of: None,
- },
- make_input("b/b", "1.0.0.0"),
- ],
- vec![],
- );
-
- let mut requires = IndexMap::new();
- requires.insert("a/a".to_string(), None);
-
- let generator = RuleSetGenerator::new(&mut pool);
- let (rules, _) = generator.generate(&requires, &[], &IndexMap::new(), &IndexMap::new());
-
- // Should have:
- // 1. Request rule: (1) — root requires a/a
- // 2. Package rule: (-1 | 2) — a/a requires b/b
- assert!(rules.len() >= 2);
- }
-
- #[test]
- fn test_fixed_package_rule() {
- let mut pool = Pool::new(vec![make_input("php", "8.2.0.0")], vec![]);
-
- let generator = RuleSetGenerator::new(&mut pool);
- let (rules, _) =
- generator.generate(&IndexMap::new(), &[1], &IndexMap::new(), &IndexMap::new());
-
- // Should have an assertion rule: (1)
- let request_rules: Vec<_> = rules.iter_type(RuleType::Request).collect();
- assert_eq!(request_rules.len(), 1);
- assert!(request_rules[0].1.is_assertion());
- }
-}
diff --git a/crates/mozart-sat-resolver/src/rule_watch_graph.rs b/crates/mozart-sat-resolver/src/rule_watch_graph.rs
deleted file mode 100644
index 202dcca..0000000
--- a/crates/mozart-sat-resolver/src/rule_watch_graph.rs
+++ /dev/null
@@ -1,288 +0,0 @@
-use crate::decisions::Decisions;
-use crate::pool::Literal;
-use crate::rule::Rule;
-use crate::rule_set::RuleId;
-use indexmap::IndexMap;
-
-/// A watch node: tracks which 2 literals a rule watches.
-///
-/// Port of Composer's RuleWatchNode.php.
-#[derive(Debug, Clone)]
-struct WatchNode {
- /// First watched literal.
- watch1: Literal,
- /// Second watched literal.
- watch2: Literal,
- /// The rule ID this node refers to.
- rule_id: RuleId,
- /// Whether the rule is a multi-conflict rule.
- is_multi_conflict: bool,
-}
-
-/// Efficient unit propagation using 2-watched literals optimization.
-///
-/// Port of Composer's RuleWatchGraph.php.
-pub struct RuleWatchGraph {
- /// Literal → list of watch node indices watching that literal.
- watch_chains: IndexMap<Literal, Vec<usize>>,
- /// All watch nodes.
- nodes: Vec<WatchNode>,
-}
-
-impl RuleWatchGraph {
- pub fn new() -> Self {
- RuleWatchGraph {
- watch_chains: IndexMap::new(),
- nodes: Vec::new(),
- }
- }
-
- /// Insert a rule into the watch graph.
- /// Assertions (single literal) are skipped.
- pub fn insert(&mut self, rule_id: RuleId, rule: &Rule) {
- if rule.is_assertion() {
- return;
- }
-
- let literals = rule.literals();
- let node_idx = self.nodes.len();
-
- let watch1 = literals[0];
- let watch2 = if literals.len() > 1 { literals[1] } else { 0 };
-
- self.nodes.push(WatchNode {
- watch1,
- watch2,
- rule_id,
- is_multi_conflict: rule.is_multi_conflict,
- });
-
- if rule.is_multi_conflict {
- // Multi-conflict rules watch ALL their literals
- for &lit in literals {
- self.watch_chains.entry(lit).or_default().push(node_idx);
- }
- } else {
- // Normal rules watch first 2 literals
- self.watch_chains.entry(watch1).or_default().push(node_idx);
- self.watch_chains.entry(watch2).or_default().push(node_idx);
- }
- }
-
- /// Adjust watch2 to the literal decided at the highest level.
- /// Used for learned rules.
- pub fn watch2_on_highest(&mut self, node_idx: usize, rule: &Rule, decisions: &Decisions) {
- let literals = rule.literals();
- if literals.len() < 3 || rule.is_multi_conflict {
- return;
- }
-
- let mut watch_level = 0i32;
- let mut best_literal = self.nodes[node_idx].watch2;
-
- for &lit in literals {
- let level = decisions.decision_level(lit);
- if level > watch_level {
- best_literal = lit;
- watch_level = level;
- }
- }
-
- let old_watch2 = self.nodes[node_idx].watch2;
- if old_watch2 != best_literal {
- // Remove from old chain, add to new chain
- self.remove_from_chain(old_watch2, node_idx);
- self.nodes[node_idx].watch2 = best_literal;
- self.watch_chains
- .entry(best_literal)
- .or_default()
- .push(node_idx);
- }
- }
-
- /// Propagate a decision literal through the watch graph.
- /// Returns the rule ID of a conflicting rule, if found.
- ///
- /// Port of Composer's RuleWatchGraph::propagateLiteral.
- pub fn propagate_literal(
- &mut self,
- decided_literal: Literal,
- level: i32,
- decisions: &mut Decisions,
- rules: &crate::rule_set::RuleSet,
- ) -> Result<Option<RuleId>, crate::error::SolverBugError> {
- // We look for rules watching the negation of the decided literal
- let literal = -decided_literal;
-
- if !self.watch_chains.contains_key(&literal) {
- return Ok(None);
- }
-
- // Iterate the live chain. When a node is moved away (move_watch removes
- // it from this chain), we stay at the same index since the Vec shrinks.
- // When a node stays, we advance past it.
- let mut i = 0;
- loop {
- let chain = match self.watch_chains.get(&literal) {
- Some(c) if i < c.len() => c,
- _ => break,
- };
-
- let node_idx = chain[i];
- let node = &self.nodes[node_idx];
- let rule_id = node.rule_id;
- let is_multi_conflict = node.is_multi_conflict;
- let rule = rules.rule_by_id(rule_id);
-
- if !is_multi_conflict {
- let other_watch = if node.watch1 == literal {
- node.watch2
- } else {
- node.watch1
- };
-
- if !rule.is_disabled() && !decisions.satisfy(other_watch) {
- let rule_literals = rule.literals();
-
- // Find an alternative literal to watch
- let alternative = rule_literals
- .iter()
- .find(|&&rl| rl != literal && rl != other_watch && !decisions.conflict(rl));
-
- if let Some(&alt_literal) = alternative {
- // Move watch from `literal` to `alt_literal`.
- // This removes node_idx from this chain, so don't increment i.
- self.move_watch(literal, alt_literal, node_idx);
- continue;
- }
-
- if decisions.conflict(other_watch) {
- return Ok(Some(rule_id));
- }
-
- decisions.decide(other_watch, level, rule_id)?;
- }
- } else {
- // Multi-conflict rule: all literals are watched
- let rule_literals = rule.literals().to_vec();
- for &other_literal in &rule_literals {
- if other_literal != literal && !decisions.satisfy(other_literal) {
- if decisions.conflict(other_literal) {
- return Ok(Some(rule_id));
- }
- decisions.decide(other_literal, level, rule_id)?;
- }
- }
- }
-
- i += 1;
- }
-
- Ok(None)
- }
-
- /// Move a watch node from one literal's chain to another's.
- fn move_watch(&mut self, from_literal: Literal, to_literal: Literal, node_idx: usize) {
- // Update the node's watch
- let node = &mut self.nodes[node_idx];
- if node.watch1 == from_literal {
- node.watch1 = to_literal;
- } else {
- node.watch2 = to_literal;
- }
-
- // Remove from old chain
- self.remove_from_chain(from_literal, node_idx);
-
- // Add to new chain
- self.watch_chains
- .entry(to_literal)
- .or_default()
- .push(node_idx);
- }
-
- /// Remove a node from a literal's watch chain.
- fn remove_from_chain(&mut self, literal: Literal, node_idx: usize) {
- if let Some(chain) = self.watch_chains.get_mut(&literal) {
- chain.retain(|&idx| idx != node_idx);
- }
- }
-
- /// Get the last inserted node index (for watch2_on_highest after insert).
- pub fn last_node_idx(&self) -> usize {
- self.nodes.len() - 1
- }
-}
-
-impl Default for RuleWatchGraph {
- fn default() -> Self {
- Self::new()
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::rule::{ReasonData, Rule, RuleReason};
- use crate::rule_set::RuleSet;
-
- #[test]
- fn test_insert_assertion_skipped() {
- let mut graph = RuleWatchGraph::new();
- let rule = Rule::new(vec![1], RuleReason::Fixed, ReasonData::None);
- graph.insert(0, &rule);
- assert_eq!(graph.nodes.len(), 0);
- }
-
- #[test]
- fn test_insert_normal_rule() {
- let mut graph = RuleWatchGraph::new();
- let rule = Rule::new(vec![1, 2, 3], RuleReason::PackageRequires, ReasonData::None);
- graph.insert(0, &rule);
- assert_eq!(graph.nodes.len(), 1);
- // Watches literals 1 and 2
- assert!(graph.watch_chains.contains_key(&1));
- assert!(graph.watch_chains.contains_key(&2));
- }
-
- #[test]
- fn test_propagate_unit_clause() {
- // Rule: (1 | 2). Decide -1, should force 2.
- let mut rs = RuleSet::new();
- rs.add(
- Rule::new(vec![1, 2], RuleReason::PackageRequires, ReasonData::None),
- crate::rule::RuleType::Package,
- );
-
- let mut graph = RuleWatchGraph::new();
- graph.insert(0, rs.rule_by_id(0));
-
- let mut decisions = Decisions::new();
- decisions.decide(-1, 1, 99).unwrap(); // don't install package 1
-
- let conflict = graph.propagate_literal(-1, 1, &mut decisions, &rs).unwrap();
- assert!(conflict.is_none());
- // Package 2 should now be decided install
- assert!(decisions.decided_install(2));
- }
-
- #[test]
- fn test_propagate_conflict() {
- // Rule: (1 | 2). Decide -1, then -2 should conflict.
- let mut rs = RuleSet::new();
- rs.add(
- Rule::new(vec![1, 2], RuleReason::PackageRequires, ReasonData::None),
- crate::rule::RuleType::Package,
- );
-
- let mut graph = RuleWatchGraph::new();
- graph.insert(0, rs.rule_by_id(0));
-
- let mut decisions = Decisions::new();
- decisions.decide(-1, 1, 99).unwrap();
- decisions.decide(-2, 1, 99).unwrap();
-
- let conflict = graph.propagate_literal(-1, 1, &mut decisions, &rs).unwrap();
- assert!(conflict.is_some());
- }
-}
diff --git a/crates/mozart-sat-resolver/src/solver.rs b/crates/mozart-sat-resolver/src/solver.rs
deleted file mode 100644
index 8739381..0000000
--- a/crates/mozart-sat-resolver/src/solver.rs
+++ /dev/null
@@ -1,1008 +0,0 @@
-use crate::decisions::Decisions;
-use crate::error::{SolverBugError, SolverError};
-use crate::policy::DefaultPolicy;
-use crate::pool::{Literal, PackageId, Pool, literal_to_package_id};
-use crate::problem::Problem;
-use crate::rule::{ReasonData, Rule, RuleReason, RuleType};
-use crate::rule_set::{RuleId, RuleSet};
-use crate::rule_watch_graph::RuleWatchGraph;
-use indexmap::{IndexMap, IndexSet};
-
-/// Result of solving: the list of package IDs to install.
-#[derive(Debug)]
-pub struct SolverResult {
- /// Package IDs decided for installation.
- pub installed: Vec<PackageId>,
-}
-
-/// Main SAT solver implementing CDCL (Conflict-Driven Clause Learning).
-///
-/// Port of Composer's Solver.php.
-pub struct Solver<'a> {
- pool: &'a Pool,
- policy: DefaultPolicy,
- rules: RuleSet,
- watch_graph: RuleWatchGraph,
- decisions: Decisions,
- /// Fixed packages by ID.
- fixed_map: IndexSet<PackageId>,
- /// Current propagation index in decision queue.
- propagate_index: usize,
- /// Branch points: (alternative literals, decision level).
- branches: Vec<(Vec<Literal>, i32)>,
- /// Problems found during solving.
- problems: Vec<Problem>,
- /// Learned rule pool: for each learned rule, the chain of rules that led to it.
- learned_pool: Vec<Vec<RuleId>>,
- /// Map from rule ID → learned pool index.
- learned_why: IndexMap<RuleId, usize>,
-}
-
-impl<'a> Solver<'a> {
- /// Create a new solver with the given rules, pool, policy, and fixed package set.
- pub fn new(
- rules: RuleSet,
- pool: &'a Pool,
- policy: DefaultPolicy,
- fixed_packages: IndexSet<PackageId>,
- ) -> Self {
- Solver {
- pool,
- policy,
- rules,
- watch_graph: RuleWatchGraph::new(),
- decisions: Decisions::new(),
- fixed_map: fixed_packages,
- propagate_index: 0,
- branches: Vec::new(),
- problems: Vec::new(),
- learned_pool: Vec::new(),
- learned_why: IndexMap::new(),
- }
- }
-
- /// Solve the dependency resolution problem.
- /// Returns the set of packages to install, or an error.
- pub fn solve(mut self) -> Result<SolverResult, SolverError> {
- // Insert all rules into watch graph
- let rule_count = self.rules.len();
- for id in 0..rule_count {
- let rule = self.rules.rule_by_id(id);
- self.watch_graph.insert(id, rule);
- }
-
- // Make decisions based on assertion rules (unit clauses)
- self.make_assertion_rule_decisions()?;
-
- // Run the main SAT loop
- self.run_sat()?;
-
- if !self.problems.is_empty() {
- let messages: Vec<String> = self
- .problems
- .iter()
- .map(|p| p.pretty_string(self.pool, &self.rules))
- .collect();
- return Err(SolverError::Unsolvable(messages));
- }
-
- // Collect installed packages
- let mut installed = Vec::new();
- for i in 0..self.decisions.len() {
- let decision = self.decisions.at_offset(i);
- if decision.literal > 0 {
- installed.push(literal_to_package_id(decision.literal));
- }
- }
-
- Ok(SolverResult { installed })
- }
-
- /// Process assertion rules (unit clauses) — make immediate decisions.
- ///
- /// Port of Composer's Solver::makeAssertionRuleDecisions.
- fn make_assertion_rule_decisions(&mut self) -> Result<(), SolverError> {
- let decision_start = if self.decisions.is_empty() {
- 0
- } else {
- self.decisions.len() - 1
- };
-
- let mut rule_index: usize = 0;
- while rule_index < self.rules.len() {
- let rule = self.rules.rule_by_id(rule_index);
-
- if !rule.is_assertion() || rule.is_disabled() {
- rule_index += 1;
- continue;
- }
-
- let literal = rule.literals()[0];
-
- if !self.decisions.decided(literal) {
- self.decisions.decide(literal, 1, rule_index)?;
- rule_index += 1;
- continue;
- }
-
- if self.decisions.satisfy(literal) {
- rule_index += 1;
- continue;
- }
-
- // Found a conflict
- let rule_type = self.rules.rule_by_id(rule_index).rule_type;
-
- if rule_type == RuleType::Learned {
- self.rules.rule_by_id_mut(rule_index).disable();
- rule_index += 1;
- continue;
- }
-
- let conflict_rule_id = self.decisions.decision_rule(literal)?;
- let conflict_type = self.rules.rule_by_id(conflict_rule_id).rule_type;
-
- if conflict_type == RuleType::Package {
- let mut problem = Problem::new();
- problem.add_rule(rule_index);
- problem.add_rule(conflict_rule_id);
- self.rules.rule_by_id_mut(rule_index).disable();
- self.problems.push(problem);
- rule_index += 1;
- continue;
- }
-
- // Conflict with another root require/fixed package
- let mut problem = Problem::new();
- problem.add_rule(rule_index);
- problem.add_rule(conflict_rule_id);
-
- // Push all request assertion rules asserting this literal
- let pkg_id = literal_to_package_id(literal);
- let request_rule_ids: Vec<RuleId> = self
- .rules
- .iter_type(RuleType::Request)
- .filter(|(_, r)| {
- !r.is_disabled()
- && r.is_assertion()
- && literal_to_package_id(r.literals()[0]) == pkg_id
- })
- .map(|(id, _)| id)
- .collect();
-
- for rid in &request_rule_ids {
- problem.add_rule(*rid);
- }
- self.problems.push(problem);
-
- for rid in request_rule_ids {
- self.rules.rule_by_id_mut(rid).disable();
- }
-
- self.decisions.reset_to_offset(decision_start);
- rule_index = 0; // restart
- }
-
- Ok(())
- }
-
- /// Unit propagation: propagate decisions through the watch graph.
- ///
- /// Port of Composer's Solver::propagate.
- fn propagate(&mut self, level: i32) -> Result<Option<RuleId>, SolverBugError> {
- while self.decisions.valid_offset(self.propagate_index) {
- let decision = self.decisions.at_offset(self.propagate_index).clone();
- self.propagate_index += 1;
-
- let conflict = self.watch_graph.propagate_literal(
- decision.literal,
- level,
- &mut self.decisions,
- &self.rules,
- )?;
-
- if conflict.is_some() {
- return Ok(conflict);
- }
- }
-
- Ok(None)
- }
-
- /// Revert decisions to a given level.
- ///
- /// Port of Composer's Solver::revert.
- fn revert(&mut self, level: i32) {
- while !self.decisions.is_empty() {
- let literal = self.decisions.last_literal();
- if self.decisions.undecided(literal) {
- break;
- }
- let decision_level = self.decisions.decision_level(literal);
- if decision_level <= level {
- break;
- }
- self.decisions.revert_last();
- self.propagate_index = self.decisions.len();
- }
-
- while !self.branches.is_empty() && self.branches.last().unwrap().1 >= level {
- self.branches.pop();
- }
- }
-
- /// Make a decision, propagate, and learn from conflicts.
- ///
- /// Port of Composer's Solver::setPropagateLearn.
- fn set_propagate_learn(
- &mut self,
- mut level: i32,
- literal: Literal,
- rule_id: RuleId,
- ) -> Result<i32, SolverError> {
- level += 1;
- self.decisions.decide(literal, level, rule_id)?;
-
- loop {
- let conflict = self.propagate(level)?;
-
- let Some(conflict_rule_id) = conflict else {
- break;
- };
-
- if level == 1 {
- self.analyze_unsolvable(conflict_rule_id);
- return Ok(0);
- }
-
- // Conflict analysis
- let (learn_literal, new_level, new_rule, why) =
- self.analyze(level, conflict_rule_id)?;
-
- if new_level <= 0 || new_level >= level {
- return Err(SolverBugError {
- message: format!(
- "Trying to revert to invalid level {new_level} from level {level}."
- ),
- }
- .into());
- }
-
- level = new_level;
- self.revert(level);
-
- // Add learned rule
- self.rules.add(new_rule, RuleType::Learned);
- let new_rule_id = self.rules.len() - 1;
-
- self.learned_why.insert(new_rule_id, why);
-
- let rule_ref = self.rules.rule_by_id(new_rule_id);
- self.watch_graph.insert(new_rule_id, rule_ref);
-
- // Adjust watch2 to highest level literal
- let last_node = self.watch_graph.last_node_idx();
- let rule_for_watch = self.rules.rule_by_id(new_rule_id);
- self.watch_graph
- .watch2_on_highest(last_node, rule_for_watch, &self.decisions);
-
- self.decisions.decide(learn_literal, level, new_rule_id)?;
- }
-
- Ok(level)
- }
-
- /// Choose best package from candidates and install.
- ///
- /// Port of Composer's Solver::selectAndInstall.
- fn select_and_install(
- &mut self,
- level: i32,
- decision_queue: Vec<Literal>,
- rule_id: RuleId,
- ) -> Result<i32, SolverError> {
- let required_package = self
- .rules
- .rule_by_id(rule_id)
- .required_package()
- .map(|s| s.to_string());
- let mut literals = self.policy.select_preferred_packages(
- self.pool,
- &decision_queue,
- required_package.as_deref(),
- );
-
- let selected = literals.remove(0);
-
- // If there are remaining alternatives, save as branch point
- if !literals.is_empty() {
- self.branches.push((literals, level));
- }
-
- self.set_propagate_learn(level, selected, rule_id)
- }
-
- /// First UIP conflict analysis.
- ///
- /// Port of Composer's Solver::analyze.
- fn analyze(
- &mut self,
- level: i32,
- conflict_rule_id: RuleId,
- ) -> Result<(Literal, i32, Rule, usize), SolverError> {
- let mut rule_level: i32 = 1;
- let mut num: i32 = 0;
- let mut l1num: i32 = 0;
- let mut seen: IndexSet<PackageId> = IndexSet::new();
- let mut learned_literal: Option<Literal> = None;
- let mut other_learned_literals: Vec<Literal> = Vec::new();
-
- let mut decision_id = self.decisions.len();
-
- self.learned_pool.push(Vec::new());
- let pool_idx = self.learned_pool.len() - 1;
-
- let mut current_rule_id = conflict_rule_id;
-
- loop {
- self.learned_pool[pool_idx].push(current_rule_id);
-
- let rule = self.rules.rule_by_id(current_rule_id);
- let rule_literals = rule.literals().to_vec();
- let is_multi_conflict = rule.is_multi_conflict;
-
- for &literal in &rule_literals {
- // MultiConflictRule: skip undecided literals
- if is_multi_conflict && !self.decisions.decided(literal) {
- continue;
- }
-
- // Skip the one true literal
- if self.decisions.satisfy(literal) {
- continue;
- }
-
- let pkg_id = literal_to_package_id(literal);
- if seen.contains(&pkg_id) {
- continue;
- }
- seen.insert(pkg_id);
-
- let l = self.decisions.decision_level(literal);
-
- if l == 1 {
- l1num += 1;
- } else if l == level {
- num += 1;
- } else {
- other_learned_literals.push(literal);
- if l > rule_level {
- rule_level = l;
- }
- }
- }
-
- // l1 retry loop
- let mut l1retry = true;
- while l1retry {
- l1retry = false;
-
- if num == 0 {
- l1num -= 1;
- if l1num == 0 {
- // All level 1 literals done
- let why = pool_idx;
- let ll = learned_literal.ok_or_else(|| SolverBugError {
- message: format!(
- "Did not find a learnable literal in analyzed rule {conflict_rule_id}."
- ),
- })?;
-
- let mut all_literals = vec![ll];
- all_literals.extend_from_slice(&other_learned_literals);
-
- let new_rule =
- Rule::new(all_literals, RuleReason::Learned, ReasonData::Learned(why));
-
- return Ok((ll, rule_level, new_rule, why));
- }
- }
-
- loop {
- if decision_id == 0 {
- return Err(SolverBugError {
- message: format!(
- "Reached invalid decision id 0 while analyzing rule {conflict_rule_id}."
- ),
- }
- .into());
- }
-
- decision_id -= 1;
- let decision = self.decisions.at_offset(decision_id);
- let literal = decision.literal;
-
- if seen.contains(&literal_to_package_id(literal)) {
- break;
- }
- }
-
- let decision = self.decisions.at_offset(decision_id);
- let literal = decision.literal;
-
- seen.shift_remove(&literal_to_package_id(literal));
-
- if num != 0 {
- num -= 1;
- if num == 0 {
- learned_literal = Some(-literal);
-
- if l1num == 0 {
- // Done
- let why = pool_idx;
- let ll = learned_literal.unwrap();
-
- let mut all_literals = vec![ll];
- all_literals.extend_from_slice(&other_learned_literals);
-
- let new_rule = Rule::new(
- all_literals,
- RuleReason::Learned,
- ReasonData::Learned(why),
- );
-
- return Ok((ll, rule_level, new_rule, why));
- }
-
- // Only level 1 marks left
- for other in &other_learned_literals {
- seen.shift_remove(&literal_to_package_id(*other));
- }
- l1num += 1;
- l1retry = true;
- } else {
- let decision = self.decisions.at_offset(decision_id);
- let next_rule_id = decision.rule_id;
- let next_rule = self.rules.rule_by_id(next_rule_id);
-
- if next_rule.is_multi_conflict {
- // Handle multi-conflict rule
- let mcr_literals = next_rule.literals().to_vec();
- for &rule_literal in &mcr_literals {
- let pkg_id = literal_to_package_id(rule_literal);
- if !seen.contains(&pkg_id) && self.decisions.satisfy(-rule_literal)
- {
- self.learned_pool[pool_idx].push(next_rule_id);
- let l = self.decisions.decision_level(rule_literal);
- if l == 1 {
- l1num += 1;
- } else if l == level {
- num += 1;
- } else {
- other_learned_literals.push(rule_literal);
- if l > rule_level {
- rule_level = l;
- }
- }
- seen.insert(pkg_id);
- break;
- }
- }
- l1retry = true;
- }
- }
- }
- }
-
- let decision = self.decisions.at_offset(decision_id);
- current_rule_id = decision.rule_id;
- }
- }
-
- /// Recursively collect rules involved in an unsolvable conflict.
- fn analyze_unsolvable_rule(
- &self,
- problem: &mut Problem,
- conflict_rule_id: RuleId,
- rule_seen: &mut IndexSet<RuleId>,
- ) {
- if rule_seen.contains(&conflict_rule_id) {
- return;
- }
- rule_seen.insert(conflict_rule_id);
-
- let rule = self.rules.rule_by_id(conflict_rule_id);
-
- if rule.rule_type == RuleType::Learned {
- if let Some(&why) = self.learned_why.get(&conflict_rule_id)
- && let Some(problem_rules) = self.learned_pool.get(why)
- {
- for &pr_id in problem_rules {
- if !rule_seen.contains(&pr_id) {
- self.analyze_unsolvable_rule(problem, pr_id, rule_seen);
- }
- }
- }
- return;
- }
-
- if rule.rule_type == RuleType::Package {
- // Package rules cannot be part of a problem
- return;
- }
-
- problem.next_section();
- problem.add_rule(conflict_rule_id);
- }
-
- /// Analyze an unsolvable conflict at level 1.
- ///
- /// Port of Composer's Solver::analyzeUnsolvable.
- fn analyze_unsolvable(&mut self, conflict_rule_id: RuleId) {
- let mut problem = Problem::new();
- problem.add_rule(conflict_rule_id);
-
- let mut rule_seen = IndexSet::new();
- self.analyze_unsolvable_rule(&mut problem, conflict_rule_id, &mut rule_seen);
-
- // Collect related decisions
- let mut seen: IndexSet<PackageId> = IndexSet::new();
- let conflict_literals = self.rules.rule_by_id(conflict_rule_id).literals().to_vec();
- for &lit in &conflict_literals {
- if self.decisions.satisfy(lit) {
- continue;
- }
- seen.insert(literal_to_package_id(lit));
- }
-
- // Walk decisions in reverse
- for i in (0..self.decisions.len()).rev() {
- let decision = self.decisions.at_offset(i);
- let dec_literal = decision.literal;
- let pkg_id = literal_to_package_id(dec_literal);
-
- if !seen.contains(&pkg_id) {
- continue;
- }
-
- let why = decision.rule_id;
- problem.add_rule(why);
- self.analyze_unsolvable_rule(&mut problem, why, &mut rule_seen);
-
- let why_literals = self.rules.rule_by_id(why).literals().to_vec();
- for &lit in &why_literals {
- if self.decisions.satisfy(lit) {
- continue;
- }
- seen.insert(literal_to_package_id(lit));
- }
- }
-
- self.problems.push(problem);
- }
-
- /// Main SAT loop.
- ///
- /// Port of Composer's Solver::runSat.
- fn run_sat(&mut self) -> Result<(), SolverError> {
- self.propagate_index = 0;
-
- let mut level: i32 = 1;
- let mut system_level: i32 = level + 1;
-
- loop {
- // Step 1: propagate at level 1
- if level == 1 {
- let conflict = self.propagate(level)?;
- if let Some(conflict_rule_id) = conflict {
- self.analyze_unsolvable(conflict_rule_id);
- return Ok(());
- }
- }
-
- // Step 2: handle root require/fixed package rules
- if level < system_level {
- let mut made_decision = false;
-
- // Collect request rule IDs first to avoid borrow issues
- let request_rule_ids: Vec<RuleId> = self
- .rules
- .iter_type(RuleType::Request)
- .map(|(id, _)| id)
- .collect();
-
- let mut all_satisfied = true;
-
- for &rule_id in &request_rule_ids {
- let rule = self.rules.rule_by_id(rule_id);
- if !rule.is_enabled() {
- continue;
- }
-
- let mut decision_queue: Vec<Literal> = Vec::new();
- let mut none_satisfied = true;
-
- for &lit in rule.literals() {
- if self.decisions.satisfy(lit) {
- none_satisfied = false;
- break;
- }
- if lit > 0 && self.decisions.undecided(lit) {
- decision_queue.push(lit);
- }
- }
-
- if none_satisfied && !decision_queue.is_empty() {
- // Prune: prefer fixed packages
- let pruned: Vec<Literal> = decision_queue
- .iter()
- .filter(|&&lit| self.fixed_map.contains(&literal_to_package_id(lit)))
- .copied()
- .collect();
-
- if !pruned.is_empty() {
- decision_queue = pruned;
- }
- }
-
- if none_satisfied && !decision_queue.is_empty() {
- let old_level = level;
- level = self.select_and_install(level, decision_queue, rule_id)?;
-
- if level == 0 {
- return Ok(());
- }
- if level <= old_level {
- made_decision = true;
- break;
- }
- }
-
- // Check if there are more rules to process
- all_satisfied = false;
- }
-
- system_level = level + 1;
-
- if made_decision || !all_satisfied {
- // Check if we still have unsatisfied request rules
- let has_unsatisfied = request_rule_ids.iter().any(|&rule_id| {
- let rule = self.rules.rule_by_id(rule_id);
- if !rule.is_enabled() {
- return false;
- }
- let mut none_satisfied = true;
- for &lit in rule.literals() {
- if self.decisions.satisfy(lit) {
- none_satisfied = false;
- break;
- }
- }
- if !none_satisfied {
- return false;
- }
- rule.literals()
- .iter()
- .any(|&lit| lit > 0 && self.decisions.undecided(lit))
- });
-
- if has_unsatisfied {
- continue;
- }
- }
- }
-
- if level < system_level {
- system_level = level;
- }
-
- // Step 3: fulfill all unresolved rules
- let mut rules_count = self.rules.len();
- let mut i: usize = 0;
- let mut n: usize = 0;
- let mut made_decision = false;
-
- while n < rules_count {
- if i == rules_count {
- i = 0;
- }
-
- let rule = self.rules.rule_by_id(i);
- let literals = rule.literals().to_vec();
-
- i += 1;
- n += 1;
-
- if rule.is_disabled() {
- continue;
- }
-
- let mut decision_queue: Vec<Literal> = Vec::new();
- let mut skip = false;
-
- for &lit in &literals {
- if lit <= 0 {
- if !self.decisions.decided_install(lit) {
- skip = true;
- break;
- }
- } else {
- if self.decisions.decided_install(lit) {
- skip = true;
- break;
- }
- if self.decisions.undecided(lit) {
- decision_queue.push(lit);
- }
- }
- }
-
- if skip {
- continue;
- }
-
- // Need at least 2 undecided positive literals
- if decision_queue.len() < 2 {
- continue;
- }
-
- let rule_id = i - 1;
- level = self.select_and_install(level, decision_queue, rule_id)?;
-
- if level == 0 {
- return Ok(());
- }
-
- // Something changed, restart scan
- rules_count = self.rules.len();
- n = 0;
- i = 0;
- made_decision = true;
- }
-
- if level < system_level && made_decision {
- continue;
- }
-
- // Step 4: minimization (backjumping)
- if !self.branches.is_empty() {
- let mut last_literal: Option<Literal> = None;
- let mut last_level: Option<i32> = None;
- let mut last_branch_index: usize = 0;
- let mut last_branch_offset: usize = 0;
-
- for i in (0..self.branches.len()).rev() {
- let (ref literals, l) = self.branches[i];
- for (offset, &literal) in literals.iter().enumerate() {
- if literal > 0 && self.decisions.decision_level(literal) > l + 1 {
- last_literal = Some(literal);
- last_branch_index = i;
- last_branch_offset = offset;
- last_level = Some(l);
- }
- }
- }
-
- if let Some(literal) = last_literal {
- let last_l = last_level.unwrap();
- self.branches[last_branch_index]
- .0
- .remove(last_branch_offset);
-
- level = last_l;
- self.revert(level);
-
- let why = self.decisions.last_reason();
-
- level = self.set_propagate_learn(level, literal, why)?;
-
- if level == 0 {
- return Ok(());
- }
-
- continue;
- }
- }
-
- break;
- }
-
- Ok(())
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::pool::PoolPackageInput;
- use crate::rule::{ReasonData, Rule, RuleReason, RuleType};
-
- fn make_input(name: &str, version: &str) -> PoolPackageInput {
- PoolPackageInput {
- name: name.to_string(),
- version: version.to_string(),
- pretty_version: version.to_string(),
- requires: vec![],
- replaces: vec![],
- provides: vec![],
- conflicts: vec![],
- is_fixed: false,
- is_alias_of: None,
- }
- }
-
- /// Helper: create a simple problem and solve it.
- /// Creates a pool with N dummy packages (1..=max_id).
- fn make_rules_and_solve(
- rules: Vec<(Rule, RuleType)>,
- fixed: IndexSet<PackageId>,
- max_id: u32,
- ) -> Result<SolverResult, SolverError> {
- let mut rs = RuleSet::new();
- for (rule, rt) in rules {
- rs.add(rule, rt);
- }
- let inputs: Vec<_> = (1..=max_id)
- .map(|i| make_input(&format!("pkg/{i}"), &format!("{i}.0.0.0")))
- .collect();
- let pool = Pool::new(inputs, vec![]);
- let policy = DefaultPolicy::default();
- let solver = Solver::new(rs, &pool, policy, fixed);
- solver.solve()
- }
-
- #[test]
- fn test_single_package_required() {
- // Root requires package 1
- let result = make_rules_and_solve(
- vec![(
- Rule::new(vec![1], RuleReason::RootRequire, ReasonData::None),
- RuleType::Request,
- )],
- IndexSet::new(),
- 3,
- )
- .unwrap();
-
- assert_eq!(result.installed, vec![1]);
- }
-
- #[test]
- fn test_two_packages_required() {
- // Root requires either package 1 or 2, and also requires 3
- let result = make_rules_and_solve(
- vec![
- (
- Rule::new(vec![1, 2], RuleReason::RootRequire, ReasonData::None),
- RuleType::Request,
- ),
- (
- Rule::new(vec![3], RuleReason::RootRequire, ReasonData::None),
- RuleType::Request,
- ),
- ],
- IndexSet::new(),
- 3,
- )
- .unwrap();
-
- assert!(result.installed.contains(&3));
- // Should install one of 1 or 2
- assert!(result.installed.contains(&1) || result.installed.contains(&2));
- }
-
- #[test]
- fn test_dependency_chain() {
- // Root requires 1. Package 1 requires 2.
- // Rule for root: (1)
- // Rule for dep: (-1 | 2)
- let result = make_rules_and_solve(
- vec![
- (
- Rule::new(vec![1], RuleReason::RootRequire, ReasonData::None),
- RuleType::Request,
- ),
- (
- Rule::new(vec![-1, 2], RuleReason::PackageRequires, ReasonData::None),
- RuleType::Package,
- ),
- ],
- IndexSet::new(),
- 3,
- )
- .unwrap();
-
- assert!(result.installed.contains(&1));
- assert!(result.installed.contains(&2));
- }
-
- #[test]
- fn test_conflict_resolution() {
- // Root requires 1 or 2. Package 1 conflicts with 3.
- // Package 2 requires 3.
- // Rules:
- // Request: (1 | 2)
- // Package: (-1 | -3) -- conflict
- // Package: (-2 | 3) -- dep
- // Request: (3) -- root also requires 3
- let result = make_rules_and_solve(
- vec![
- (
- Rule::new(vec![1, 2], RuleReason::RootRequire, ReasonData::None),
- RuleType::Request,
- ),
- (
- Rule::two_literals(-1, -3, RuleReason::PackageConflict, ReasonData::None),
- RuleType::Package,
- ),
- (
- Rule::new(vec![-2, 3], RuleReason::PackageRequires, ReasonData::None),
- RuleType::Package,
- ),
- (
- Rule::new(vec![3], RuleReason::RootRequire, ReasonData::None),
- RuleType::Request,
- ),
- ],
- IndexSet::new(),
- 3,
- )
- .unwrap();
-
- // Package 3 is required, so 1 conflicts, must choose 2
- assert!(result.installed.contains(&2));
- assert!(result.installed.contains(&3));
- assert!(!result.installed.contains(&1));
- }
-
- #[test]
- fn test_same_name_conflict() {
- // Two versions of same package: 1 and 2. Root requires either.
- // Same-name rule: (-1 | -2)
- let result = make_rules_and_solve(
- vec![
- (
- Rule::new(vec![1, 2], RuleReason::RootRequire, ReasonData::None),
- RuleType::Request,
- ),
- (
- Rule::two_literals(-1, -2, RuleReason::PackageSameName, ReasonData::None),
- RuleType::Package,
- ),
- ],
- IndexSet::new(),
- 3,
- )
- .unwrap();
-
- // Should install exactly one
- let has_1 = result.installed.contains(&1);
- let has_2 = result.installed.contains(&2);
- assert!(has_1 ^ has_2, "Should install exactly one of 1 or 2");
- }
-
- #[test]
- fn test_unsolvable() {
- // Root requires 1. Root requires 2. But 1 and 2 conflict.
- let result = make_rules_and_solve(
- vec![
- (
- Rule::new(vec![1], RuleReason::RootRequire, ReasonData::None),
- RuleType::Request,
- ),
- (
- Rule::new(vec![2], RuleReason::RootRequire, ReasonData::None),
- RuleType::Request,
- ),
- (
- Rule::two_literals(-1, -2, RuleReason::PackageConflict, ReasonData::None),
- RuleType::Package,
- ),
- ],
- IndexSet::new(),
- 3,
- );
-
- assert!(result.is_err());
- }
-}
diff --git a/crates/mozart-sat-resolver/src/transaction.rs b/crates/mozart-sat-resolver/src/transaction.rs
deleted file mode 100644
index bf7befc..0000000
--- a/crates/mozart-sat-resolver/src/transaction.rs
+++ /dev/null
@@ -1,568 +0,0 @@
-use crate::decisions::Decisions;
-use crate::pool::{PackageId, Pool, literal_to_package_id};
-use indexmap::{IndexMap, IndexSet};
-
-/// An operation to perform on a package.
-///
-/// Port of Composer's SolverOperation hierarchy.
-#[derive(Debug, Clone)]
-pub enum Operation {
- /// Install a new package.
- Install { package_id: PackageId },
- /// Update a package from one version to another.
- Update {
- initial_id: PackageId,
- target_id: PackageId,
- },
- /// Remove a package.
- Uninstall { package_id: PackageId },
-}
-
-impl Operation {
- /// Get the operation type as a string.
- pub fn operation_type(&self) -> &'static str {
- match self {
- Operation::Install { .. } => "install",
- Operation::Update { .. } => "update",
- Operation::Uninstall { .. } => "uninstall",
- }
- }
-
- /// Format the operation as a human-readable string using pool data.
- pub fn pretty_string(&self, pool: &Pool) -> String {
- match self {
- Operation::Install { package_id } => {
- let pkg = pool.package_by_id(*package_id);
- format!("Installing {} ({})", pkg.name, pkg.pretty_version)
- }
- Operation::Update {
- initial_id,
- target_id,
- } => {
- let initial = pool.package_by_id(*initial_id);
- let target = pool.package_by_id(*target_id);
- format!(
- "Updating {} ({} => {})",
- target.name, initial.pretty_version, target.pretty_version
- )
- }
- Operation::Uninstall { package_id } => {
- let pkg = pool.package_by_id(*package_id);
- format!("Removing {} ({})", pkg.name, pkg.pretty_version)
- }
- }
- }
-}
-
-/// Computes install/update/remove operations from solver results.
-///
-/// Port of Composer's Transaction.php.
-pub struct Transaction<'a> {
- pool: &'a Pool,
- /// Currently installed package IDs.
- present_ids: Vec<PackageId>,
- /// Result package IDs from the solver.
- result_ids: Vec<PackageId>,
- /// Computed operations.
- operations: Vec<Operation>,
-}
-
-impl<'a> Transaction<'a> {
- /// Create a new transaction from present and result package sets.
- pub fn new(pool: &'a Pool, present_ids: Vec<PackageId>, result_ids: Vec<PackageId>) -> Self {
- let mut tx = Transaction {
- pool,
- present_ids,
- result_ids,
- operations: Vec::new(),
- };
- tx.calculate_operations();
- tx
- }
-
- /// Create a transaction from solver decisions.
- pub fn from_decisions(
- pool: &'a Pool,
- present_ids: Vec<PackageId>,
- decisions: &Decisions,
- ) -> Self {
- let mut result_ids = Vec::new();
- for i in 0..decisions.len() {
- let decision = decisions.at_offset(i);
- if decision.literal > 0 {
- result_ids.push(literal_to_package_id(decision.literal));
- }
- }
- Self::new(pool, present_ids, result_ids)
- }
-
- /// Get the computed operations.
- pub fn operations(&self) -> &[Operation] {
- &self.operations
- }
-
- /// Calculate the delta between present and result packages.
- fn calculate_operations(&mut self) {
- // Build maps: name -> package_id for present packages
- let mut present_by_name: IndexMap<&str, PackageId> = IndexMap::new();
- for &id in &self.present_ids {
- let pkg = self.pool.package_by_id(id);
- present_by_name.insert(&pkg.name, id);
- }
-
- // Track which present packages have been matched
- let mut matched_present: IndexSet<PackageId> = IndexSet::new();
-
- // Build topologically sorted result packages via DFS
- let sorted_results = self.topological_sort();
-
- // Process result packages in topological order
- for &result_id in &sorted_results {
- let result_pkg = self.pool.package_by_id(result_id);
-
- if let Some(&present_id) = present_by_name.get(result_pkg.name.as_str()) {
- matched_present.insert(present_id);
- let present_pkg = self.pool.package_by_id(present_id);
-
- // Check if update is needed (version changed)
- if present_pkg.version != result_pkg.version || present_id != result_id {
- self.operations.push(Operation::Update {
- initial_id: present_id,
- target_id: result_id,
- });
- }
- // Otherwise: no change needed, skip
- } else {
- // New package: install
- self.operations.push(Operation::Install {
- package_id: result_id,
- });
- }
- }
-
- // Remove packages that are present but not in result
- let mut uninstalls = Vec::new();
- for &present_id in &self.present_ids {
- if !matched_present.contains(&present_id) {
- uninstalls.push(Operation::Uninstall {
- package_id: present_id,
- });
- }
- }
-
- // Prepend uninstalls (remove before install/update)
- uninstalls.append(&mut self.operations);
- self.operations = uninstalls;
- }
-
- /// Topologically sort result packages by their dependency order.
- /// Uses DFS: dependencies are processed before dependents.
- fn topological_sort(&self) -> Vec<PackageId> {
- // Index every result package by every name it answers to (own name +
- // `replaces` targets + `provides` targets). Mirrors Composer's
- // `resultPackagesByName` map, which `getProvidersInResult` queries
- // when walking a package's requires — so a replace/provide target
- // resolves to the package that satisfies it. Without this expansion
- // the DFS treats replace/provide-only requires as unsatisfied and
- // misses the transitive ordering edge.
- let mut result_by_target: IndexMap<&str, Vec<PackageId>> = IndexMap::new();
- for &id in &self.result_ids {
- let pkg = self.pool.package_by_id(id);
- result_by_target.entry(&pkg.name).or_default().push(id);
- for link in &pkg.replaces {
- result_by_target.entry(&link.target).or_default().push(id);
- }
- for link in &pkg.provides {
- result_by_target.entry(&link.target).or_default().push(id);
- }
- }
-
- let mut visited: IndexSet<PackageId> = IndexSet::new();
- let mut order: Vec<PackageId> = Vec::new();
-
- // Find root packages (not required by any other result package)
- let roots = self.get_root_packages(&result_by_target);
-
- // DFS from roots
- let mut stack: Vec<(PackageId, bool)> = Vec::new();
- for &root_id in roots.iter().rev() {
- stack.push((root_id, false));
- }
-
- while let Some((pkg_id, processed)) = stack.pop() {
- if processed {
- if visited.insert(pkg_id) {
- order.push(pkg_id);
- }
- continue;
- }
-
- if visited.contains(&pkg_id) {
- continue;
- }
-
- // Push self as "processed" marker
- stack.push((pkg_id, true));
-
- // Push dependencies
- let pkg = self.pool.package_by_id(pkg_id);
- for req in &pkg.requires {
- if let Some(provider_ids) = result_by_target.get(req.target.as_str()) {
- for &dep_id in provider_ids {
- if !visited.contains(&dep_id) {
- stack.push((dep_id, false));
- }
- }
- }
- }
- }
-
- // Add any remaining unvisited packages
- for &id in &self.result_ids {
- if !visited.contains(&id) {
- order.push(id);
- }
- }
-
- order
- }
-
- /// Find root packages: result packages not required by any other result
- /// package. A package whose own name (or any `replaces`/`provides`
- /// target) appears in another result package's `requires` is excluded.
- /// Mirrors Composer's `Transaction::getRootPackages`, which uses
- /// `getProvidersInResult` to do the same expansion.
- fn get_root_packages(
- &self,
- result_by_target: &IndexMap<&str, Vec<PackageId>>,
- ) -> Vec<PackageId> {
- let mut required: IndexSet<PackageId> = IndexSet::new();
- for &id in &self.result_ids {
- let pkg = self.pool.package_by_id(id);
- for req in &pkg.requires {
- if let Some(provider_ids) = result_by_target.get(req.target.as_str()) {
- for &dep_id in provider_ids {
- if dep_id != id {
- required.insert(dep_id);
- }
- }
- }
- }
- }
-
- let mut roots: Vec<PackageId> = Vec::new();
- for &id in &self.result_ids {
- if !required.contains(&id) {
- roots.push(id);
- }
- }
-
- // If no roots found (circular), use all
- if roots.is_empty() {
- return self.result_ids.clone();
- }
-
- roots
- }
-}
-
-/// Lock transaction: specialization for computing lock file operations.
-///
-/// Port of Composer's LockTransaction.php.
-pub struct LockTransaction<'a> {
- /// The base transaction.
- transaction: Transaction<'a>,
- /// All result package IDs.
- all_result_ids: Vec<PackageId>,
- /// Non-dev result package IDs.
- non_dev_ids: Vec<PackageId>,
- /// Dev result package IDs.
- dev_ids: Vec<PackageId>,
-}
-
-impl<'a> LockTransaction<'a> {
- /// Create a lock transaction from solver decisions.
- pub fn new(
- pool: &'a Pool,
- present_ids: Vec<PackageId>,
- unlockable_ids: IndexSet<PackageId>,
- decisions: &Decisions,
- ) -> Self {
- // Extract result packages from decisions
- let mut all_result_ids = Vec::new();
- let mut non_dev_ids = Vec::new();
- for i in 0..decisions.len() {
- let decision = decisions.at_offset(i);
- if decision.literal > 0 {
- let pkg_id = literal_to_package_id(decision.literal);
- all_result_ids.push(pkg_id);
- if !unlockable_ids.contains(&pkg_id) {
- non_dev_ids.push(pkg_id);
- }
- }
- }
-
- let transaction = Transaction::new(pool, present_ids, all_result_ids.clone());
-
- LockTransaction {
- transaction,
- all_result_ids,
- non_dev_ids,
- dev_ids: Vec::new(),
- }
- }
-
- /// Set the non-dev packages from an extraction-only solve result.
- /// `extraction_ids` are the package IDs that were resolved without dev deps.
- pub fn set_non_dev_packages(&mut self, extraction_ids: &[PackageId]) {
- let extraction_names: IndexSet<String> = extraction_ids
- .iter()
- .map(|&id| self.transaction.pool.package_by_id(id).name.clone())
- .collect();
-
- self.non_dev_ids.clear();
- self.dev_ids.clear();
-
- for &id in &self.all_result_ids {
- let pkg = self.transaction.pool.package_by_id(id);
- if extraction_names.contains(&pkg.name) {
- self.non_dev_ids.push(id);
- } else {
- self.dev_ids.push(id);
- }
- }
- }
-
- /// Get the computed operations.
- pub fn operations(&self) -> &[Operation] {
- self.transaction.operations()
- }
-
- /// Get all result package IDs.
- pub fn all_result_ids(&self) -> &[PackageId] {
- &self.all_result_ids
- }
-
- /// Get non-dev result package IDs.
- pub fn non_dev_ids(&self) -> &[PackageId] {
- &self.non_dev_ids
- }
-
- /// Get dev result package IDs.
- pub fn dev_ids(&self) -> &[PackageId] {
- &self.dev_ids
- }
-
- /// Get new lock packages for writing to the lock file.
- /// If `dev_mode` is true, returns dev packages; otherwise non-dev.
- pub fn new_lock_package_ids(&self, dev_mode: bool) -> &[PackageId] {
- if dev_mode {
- &self.dev_ids
- } else {
- &self.non_dev_ids
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::pool::{PoolLink, PoolPackageInput};
-
- fn make_input(name: &str, version: &str, pretty: &str) -> PoolPackageInput {
- PoolPackageInput {
- name: name.to_string(),
- version: version.to_string(),
- pretty_version: pretty.to_string(),
- requires: vec![],
- replaces: vec![],
- provides: vec![],
- conflicts: vec![],
- is_fixed: false,
- is_alias_of: None,
- }
- }
-
- fn make_input_with_deps(
- name: &str,
- version: &str,
- pretty: &str,
- deps: Vec<(&str, &str)>,
- ) -> PoolPackageInput {
- let requires = deps
- .into_iter()
- .map(|(target, constraint)| PoolLink {
- target: target.to_string(),
- constraint: constraint.to_string(),
- source: name.to_string(),
- })
- .collect();
-
- PoolPackageInput {
- name: name.to_string(),
- version: version.to_string(),
- pretty_version: pretty.to_string(),
- requires,
- replaces: vec![],
- provides: vec![],
- conflicts: vec![],
- is_fixed: false,
- is_alias_of: None,
- }
- }
-
- #[test]
- fn test_fresh_install() {
- let pool = Pool::new(
- vec![
- make_input("a/a", "1.0.0.0", "1.0.0"),
- make_input("b/b", "2.0.0.0", "2.0.0"),
- ],
- vec![],
- );
-
- let tx = Transaction::new(&pool, vec![], vec![1, 2]);
- let ops = tx.operations();
-
- assert_eq!(ops.len(), 2);
- assert!(matches!(ops[0], Operation::Install { package_id: _ }));
- assert!(matches!(ops[1], Operation::Install { package_id: _ }));
- }
-
- #[test]
- fn test_update_package() {
- let pool = Pool::new(
- vec![
- make_input("a/a", "1.0.0.0", "1.0.0"),
- make_input("a/a", "2.0.0.0", "2.0.0"),
- ],
- vec![],
- );
-
- // Present: a/a 1.0.0 (id=1), Result: a/a 2.0.0 (id=2)
- let tx = Transaction::new(&pool, vec![1], vec![2]);
- let ops = tx.operations();
-
- assert_eq!(ops.len(), 1);
- match &ops[0] {
- Operation::Update {
- initial_id,
- target_id,
- } => {
- assert_eq!(*initial_id, 1);
- assert_eq!(*target_id, 2);
- }
- _ => panic!("Expected update operation"),
- }
- }
-
- #[test]
- fn test_uninstall_package() {
- let pool = Pool::new(
- vec![
- make_input("a/a", "1.0.0.0", "1.0.0"),
- make_input("b/b", "1.0.0.0", "1.0.0"),
- ],
- vec![],
- );
-
- // Present: a/a and b/b, Result: only a/a
- let tx = Transaction::new(&pool, vec![1, 2], vec![1]);
- let ops = tx.operations();
-
- assert_eq!(ops.len(), 1);
- match &ops[0] {
- Operation::Uninstall { package_id } => {
- assert_eq!(*package_id, 2);
- }
- _ => panic!("Expected uninstall operation"),
- }
- }
-
- #[test]
- fn test_uninstalls_before_installs() {
- let pool = Pool::new(
- vec![
- make_input("a/a", "1.0.0.0", "1.0.0"),
- make_input("b/b", "1.0.0.0", "1.0.0"),
- ],
- vec![],
- );
-
- // Present: a/a, Result: b/b (uninstall a, install b)
- let tx = Transaction::new(&pool, vec![1], vec![2]);
- let ops = tx.operations();
-
- assert_eq!(ops.len(), 2);
- assert!(
- matches!(ops[0], Operation::Uninstall { .. }),
- "Uninstalls should come first"
- );
- assert!(
- matches!(ops[1], Operation::Install { .. }),
- "Installs should come after"
- );
- }
-
- #[test]
- fn test_dependency_ordering() {
- // a/a requires b/b — b/b should be installed before a/a
- let pool = Pool::new(
- vec![
- make_input_with_deps("a/a", "1.0.0.0", "1.0.0", vec![("b/b", "^1.0")]),
- make_input("b/b", "1.0.0.0", "1.0.0"),
- ],
- vec![],
- );
-
- let tx = Transaction::new(&pool, vec![], vec![1, 2]);
- let ops = tx.operations();
-
- assert_eq!(ops.len(), 2);
- // b/b (dependency) should be installed before a/a
- match (&ops[0], &ops[1]) {
- (
- Operation::Install { package_id: first },
- Operation::Install { package_id: second },
- ) => {
- assert_eq!(*first, 2, "b/b should be installed first");
- assert_eq!(*second, 1, "a/a should be installed second");
- }
- _ => panic!("Expected two install operations"),
- }
- }
-
- #[test]
- fn test_no_change() {
- let pool = Pool::new(vec![make_input("a/a", "1.0.0.0", "1.0.0")], vec![]);
-
- // Same package present and in result
- let tx = Transaction::new(&pool, vec![1], vec![1]);
- let ops = tx.operations();
-
- assert!(ops.is_empty(), "No operations when nothing changed");
- }
-
- #[test]
- fn test_operation_pretty_string() {
- let pool = Pool::new(
- vec![
- make_input("a/a", "1.0.0.0", "1.0.0"),
- make_input("a/a", "2.0.0.0", "2.0.0"),
- ],
- vec![],
- );
-
- let install = Operation::Install { package_id: 1 };
- assert_eq!(install.pretty_string(&pool), "Installing a/a (1.0.0)");
-
- let update = Operation::Update {
- initial_id: 1,
- target_id: 2,
- };
- assert_eq!(update.pretty_string(&pool), "Updating a/a (1.0.0 => 2.0.0)");
-
- let uninstall = Operation::Uninstall { package_id: 1 };
- assert_eq!(uninstall.pretty_string(&pool), "Removing a/a (1.0.0)");
- }
-}