Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/permaref.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Automatically creates a tag for each commit to `main` so when we rebase
# changes on top of the upstream, we retain permanent references to each
# previous commit so they are not orphaned and eventually deleted.
name: Create permanent reference

on:
push:
branches:
- "main"

jobs:
create-permaref:
runs-on: ubuntu-latest
permissions:
contents: "write"
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Get the permanent ref number
id: get_version
run: |
# Enable pipefail so git command failures do not result in null versions downstream
set -x

echo "LAST_PERMA_NUMBER=$(\
git ls-remote --tags --refs --sort="v:refname" \
https://github.com/astral-sh/pubgrub.git | grep "tags/perma-" | tail -n1 | sed 's/.*\/perma-//' \
)" >> $GITHUB_OUTPUT

- name: Configure Git
run: |
git config user.name "$GITHUB_ACTOR"
git config user.email "[email protected]"

- name: Create and push the new tag
run: |
TAG="perma-$((LAST_PERMA_NUMBER + 1))"
git tag -a "$TAG" -m 'Automatically created on push to `main`'
git push origin "$TAG"
env:
LAST_PERMA_NUMBER: ${{ steps.get_version.outputs.LAST_PERMA_NUMBER }}
16 changes: 11 additions & 5 deletions src/internal/arena.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ type FnvIndexSet<V> = indexmap::IndexSet<V, rustc_hash::FxBuildHasher>;
/// that we actually don't need since it is phantom.
///
/// <https://github.com/rust-lang/rust/issues/26925>
pub(crate) struct Id<T> {
pub struct Id<T> {
raw: u32,
_ty: PhantomData<fn() -> T>,
}
Expand Down Expand Up @@ -73,7 +73,7 @@ impl<T> Id<T> {
/// to have references between those items.
/// They are all dropped at once when the arena is dropped.
#[derive(Clone, PartialEq, Eq)]
pub(crate) struct Arena<T> {
pub struct Arena<T> {
data: Vec<T>,
}

Expand Down Expand Up @@ -150,9 +150,7 @@ impl<T: Hash + Eq + fmt::Debug> fmt::Debug for HashArena<T> {

impl<T: Hash + Eq> HashArena<T> {
pub fn new() -> Self {
HashArena {
data: FnvIndexSet::default(),
}
Self::default()
}

pub fn alloc(&mut self, value: T) -> Id<T> {
Expand All @@ -161,6 +159,14 @@ impl<T: Hash + Eq> HashArena<T> {
}
}

impl<T: Hash + Eq> Default for HashArena<T> {
fn default() -> Self {
Self {
data: FnvIndexSet::default(),
}
}
}

impl<T: Hash + Eq> Index<Id<T>> for HashArena<T> {
type Output = T;
fn index(&self, id: Id<T>) -> &T {
Expand Down
71 changes: 60 additions & 11 deletions src/internal/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ use crate::{DependencyProvider, DerivationTree, Map, NoSolutionError, VersionSet

/// Current state of the PubGrub algorithm.
#[derive(Clone)]
pub(crate) struct State<DP: DependencyProvider> {
pub struct State<DP: DependencyProvider> {
/// The root package and version.
pub root_package: Id<DP::P>,
root_version: DP::V,

/// All incompatibilities indexed by package.
#[allow(clippy::type_complexity)]
incompatibilities: Map<Id<DP::P>, Vec<IncompDpId<DP>>>,
pub incompatibilities: Map<Id<DP::P>, Vec<IncompDpId<DP>>>,

/// As an optimization, store the ids of incompatibilities that are already contradicted.
///
Expand All @@ -33,14 +35,13 @@ pub(crate) struct State<DP: DependencyProvider> {
merged_dependencies: Map<(Id<DP::P>, Id<DP::P>), SmallVec<IncompDpId<DP>>>,

/// Partial solution.
/// TODO: remove pub.
pub(crate) partial_solution: PartialSolution<DP>,
pub partial_solution: PartialSolution<DP>,

/// The store is the reference storage for all incompatibilities.
pub(crate) incompatibility_store: Arena<Incompatibility<DP::P, DP::VS, DP::M>>,
pub incompatibility_store: Arena<Incompatibility<DP::P, DP::VS, DP::M>>,

/// The store is the reference storage for all packages.
pub(crate) package_store: HashArena<DP::P>,
pub package_store: HashArena<DP::P>,

/// This is a stack of work to be done in `unit_propagation`.
/// It can definitely be a local variable to that method, but
Expand All @@ -50,7 +51,7 @@ pub(crate) struct State<DP: DependencyProvider> {

impl<DP: DependencyProvider> State<DP> {
/// Initialization of PubGrub state.
pub(crate) fn init(root_package: DP::P, root_version: DP::V) -> Self {
pub fn init(root_package: DP::P, root_version: DP::V) -> Self {
let mut incompatibility_store = Arena::new();
let mut package_store = HashArena::new();
let root_package = package_store.alloc(root_package);
Expand All @@ -74,7 +75,7 @@ impl<DP: DependencyProvider> State<DP> {
}

/// Add the dependencies for the current version of the current package as incompatibilities.
pub(crate) fn add_package_version_dependencies(
pub fn add_package_version_dependencies(
&mut self,
package: Id<DP::P>,
version: DP::V,
Expand All @@ -91,11 +92,43 @@ impl<DP: DependencyProvider> State<DP> {
}

/// Add an incompatibility to the state.
pub(crate) fn add_incompatibility(&mut self, incompat: Incompatibility<DP::P, DP::VS, DP::M>) {
pub fn add_incompatibility(&mut self, incompat: Incompatibility<DP::P, DP::VS, DP::M>) {
let id = self.incompatibility_store.alloc(incompat);
self.merge_incompatibility(id);
}

/// Add a single custom incompatibility that requires the base package and the proxy package
/// share the same version range.
///
/// This intended for cases where proxy packages (also known as virtual packages) are used.
/// Without this information, pubgrub does not know that these packages have to be at the same
/// version. In cases where the base package is already to an incompatible version, this avoids
/// going through all versions of the proxy package. In cases where there are two incompatible
/// proxy packages, it avoids trying versions for both of them. Both improve performance (we
/// don't need to check all versions when there is a conflict) and error messages (report a
/// conflict of version ranges instead of enumerating the conflicting versions).
///
/// Using this method requires that each version of the proxy package depends on the exact
/// version of the base package.
pub fn add_proxy_package(
&mut self,
proxy_package: Id<DP::P>,
base_package: Id<DP::P>,
versions: DP::VS,
) {
let incompat = Incompatibility::from_dependency(
proxy_package,
versions.clone(),
(base_package, versions),
);
let id = self
.incompatibility_store
.alloc_iter([incompat].into_iter());
for id in IncompDpId::<DP>::range_to_iter(id) {
self.merge_incompatibility(id);
}
}

/// Add an incompatibility to the state.
#[cold]
pub(crate) fn add_incompatibility_from_dependencies(
Expand Down Expand Up @@ -129,7 +162,7 @@ impl<DP: DependencyProvider> State<DP> {
/// incompatibility.
#[cold]
#[allow(clippy::type_complexity)] // Type definitions don't support impl trait.
pub(crate) fn unit_propagation(
pub fn unit_propagation(
&mut self,
package: Id<DP::P>,
) -> Result<SmallVec<(Id<DP::P>, IncompDpId<DP>)>, NoSolutionError<DP>> {
Expand Down Expand Up @@ -288,7 +321,8 @@ impl<DP: DependencyProvider> State<DP> {
}
}

/// Backtracking.
/// After a conflict occurred, backtrack the partial solution to a given decision level, and add
/// the incompatibility if it was new.
fn backtrack(
&mut self,
incompat: IncompDpId<DP>,
Expand All @@ -304,6 +338,21 @@ impl<DP: DependencyProvider> State<DP> {
}
}

/// Manually backtrack before the given package was selected.
///
/// This can be used to switch the order of packages if the previous prioritization was bad.
///
/// Returns the number of the decisions that were backtracked, or `None` if the package was not
/// decided on yet.
pub fn backtrack_package(&mut self, package: Id<DP::P>) -> Option<u32> {
let base_decision_level = self.partial_solution.current_decision_level();
let new_decision_level = self.partial_solution.backtrack_package(package).ok()?;
// Remove contradicted incompatibilities that depend on decisions we just backtracked away.
self.contradicted_incompatibilities
.retain(|_, dl| *dl <= new_decision_level);
Some(base_decision_level.0 - new_decision_level.0)
}

/// Add this incompatibility into the set of all incompatibilities.
///
/// PubGrub collapses identical dependencies from adjacent package versions
Expand Down
21 changes: 12 additions & 9 deletions src/internal/incompatibility.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,24 @@ use crate::{
/// during conflict resolution. More about all this in
/// [PubGrub documentation](https://github.com/dart-lang/pub/blob/master/doc/solver.md#incompatibility).
#[derive(Debug, Clone)]
pub(crate) struct Incompatibility<P: Package, VS: VersionSet, M: Eq + Clone + Debug + Display> {
pub struct Incompatibility<P: Package, VS: VersionSet, M: Eq + Clone + Debug + Display> {
package_terms: SmallMap<Id<P>, Term<VS>>,
kind: Kind<P, VS, M>,
/// The reason for the incompatibility.
pub kind: Kind<P, VS, M>,
}

/// Type alias of unique identifiers for incompatibilities.
pub(crate) type IncompId<P, VS, M> = Id<Incompatibility<P, VS, M>>;
pub type IncompId<P, VS, M> = Id<Incompatibility<P, VS, M>>;

pub(crate) type IncompDpId<DP> = IncompId<
<DP as DependencyProvider>::P,
<DP as DependencyProvider>::VS,
<DP as DependencyProvider>::M,
>;

/// The reason for the incompatibility.
#[derive(Debug, Clone)]
enum Kind<P: Package, VS: VersionSet, M: Eq + Clone + Debug + Display> {
pub enum Kind<P: Package, VS: VersionSet, M: Eq + Clone + Debug + Display> {
/// Initial incompatibility aiming at picking the root package for the first decision.
///
/// This incompatibility drives the resolution, it requires that we pick the (virtual) root
Expand Down Expand Up @@ -104,7 +106,7 @@ impl<P: Package, VS: VersionSet, M: Eq + Clone + Debug + Display> Incompatibilit
}

/// Create an incompatibility to remember that a given set does not contain any version.
pub(crate) fn no_versions(package: Id<P>, term: Term<VS>) -> Self {
pub fn no_versions(package: Id<P>, term: Term<VS>) -> Self {
let set = match &term {
Term::Positive(r) => r.clone(),
Term::Negative(_) => panic!("No version should have a positive term"),
Expand All @@ -117,7 +119,7 @@ impl<P: Package, VS: VersionSet, M: Eq + Clone + Debug + Display> Incompatibilit

/// Create an incompatibility for a reason outside pubgrub.
#[allow(dead_code)] // Used by uv
pub(crate) fn custom_term(package: Id<P>, term: Term<VS>, metadata: M) -> Self {
pub fn custom_term(package: Id<P>, term: Term<VS>, metadata: M) -> Self {
let set = match &term {
Term::Positive(r) => r.clone(),
Term::Negative(_) => panic!("No version should have a positive term"),
Expand All @@ -129,7 +131,7 @@ impl<P: Package, VS: VersionSet, M: Eq + Clone + Debug + Display> Incompatibilit
}

/// Create an incompatibility for a reason outside pubgrub.
pub(crate) fn custom_version(package: Id<P>, version: VS::V, metadata: M) -> Self {
pub fn custom_version(package: Id<P>, version: VS::V, metadata: M) -> Self {
let set = VS::singleton(version);
let term = Term::Positive(set.clone());
Self {
Expand All @@ -139,7 +141,7 @@ impl<P: Package, VS: VersionSet, M: Eq + Clone + Debug + Display> Incompatibilit
}

/// Build an incompatibility from a given dependency.
pub(crate) fn from_dependency(package: Id<P>, versions: VS, dep: (Id<P>, VS)) -> Self {
pub fn from_dependency(package: Id<P>, versions: VS, dep: (Id<P>, VS)) -> Self {
let (p2, set2) = dep;
Self {
package_terms: if set2 == VS::empty() {
Expand Down Expand Up @@ -254,7 +256,7 @@ impl<P: Package, VS: VersionSet, M: Eq + Clone + Debug + Display> Incompatibilit
}

/// Iterate over packages.
pub(crate) fn iter(&self) -> impl Iterator<Item = (Id<P>, &Term<VS>)> {
pub fn iter(&self) -> impl Iterator<Item = (Id<P>, &Term<VS>)> {
self.package_terms
.iter()
.map(|(package, term)| (*package, term))
Expand Down Expand Up @@ -353,6 +355,7 @@ impl<'a, P: Package, VS: VersionSet + 'a, M: Eq + Clone + Debug + Display + 'a>
}

impl<P: Package, VS: VersionSet, M: Eq + Clone + Debug + Display> Incompatibility<P, VS, M> {
/// Display the incompatibility.
pub fn display<'a>(&'a self, package_store: &'a HashArena<P>) -> impl Display + 'a {
match self.iter().collect::<Vec<_>>().as_slice() {
[] => "version solving failed".into(),
Expand Down
10 changes: 7 additions & 3 deletions src/internal/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@ mod partial_solution;
mod small_map;
mod small_vec;

pub(crate) use arena::{Arena, HashArena, Id};
pub(crate) use core::State;
pub(crate) use incompatibility::{IncompDpId, IncompId, Incompatibility, Relation};
pub(crate) use arena::{Arena, HashArena};
pub(crate) use incompatibility::{IncompDpId, Relation};
pub(crate) use partial_solution::{DecisionLevel, PartialSolution, SatisfierSearch};
pub(crate) use small_map::SmallMap;
pub(crate) use small_vec::SmallVec;

// uv-specific additions
pub use arena::Id;
pub use core::State;
pub use incompatibility::{IncompId, Incompatibility, Kind};
Loading