diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-05-10 00:32:08 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-05-10 00:32:08 +0900 |
| commit | 8cc1ba8a02c0318b65658f1634de378c780392b9 (patch) | |
| tree | fdd5cb61e488018891a486b25991b87c84220bb8 /crates/mozart-core/src/dependency_resolver/rule.rs | |
| parent | 72b2e877c01e67ba7edd37e34ac2eadb7a1c62c4 (diff) | |
| download | php-mozart-8cc1ba8a02c0318b65658f1634de378c780392b9.tar.gz php-mozart-8cc1ba8a02c0318b65658f1634de378c780392b9.tar.zst php-mozart-8cc1ba8a02c0318b65658f1634de378c780392b9.zip | |
refactor(workspace): consolidate crates into mozart-core
Merged mozart-archiver, mozart-autoload, mozart-registry,
mozart-sat-resolver, and mozart-vcs into mozart-core to align
the source layout with Composer's structure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart-core/src/dependency_resolver/rule.rs')
| -rw-r--r-- | crates/mozart-core/src/dependency_resolver/rule.rs | 280 |
1 files changed, 280 insertions, 0 deletions
diff --git a/crates/mozart-core/src/dependency_resolver/rule.rs b/crates/mozart-core/src/dependency_resolver/rule.rs new file mode 100644 index 0000000..546b932 --- /dev/null +++ b/crates/mozart-core/src/dependency_resolver/rule.rs @@ -0,0 +1,280 @@ +use super::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()); + } +} |
