OCaml implementation of PubGrub

vibe

+1
.gitignore
···
+
_build
+13
dune
···
+
(library
+
(name pubgrub)
+
(modules pubgrub)
+
(public_name pubgrub)
+
(flags (:standard -w -27-32-34))
+
)
+
+
(executable
+
(name pubgrub_test)
+
(modules pubgrub_test)
+
(libraries pubgrub)
+
(modes byte exe)
+
)
+1
dune-project
···
+
(lang dune 3.17)
+492
pubgrub.ml
···
+
(* Implementation of the PubGrub algorithm in OCaml *)
+
+
(* Core Types *)
+
module type PACKAGE_ID = sig
+
type t
+
val compare : t -> t -> int
+
val to_string : t -> string
+
end
+
+
module type VERSION = sig
+
type t
+
val compare : t -> t -> int
+
val to_string : t -> string
+
end
+
+
module type VERSION_SET = sig
+
type t
+
type version
+
val empty : t
+
val any : t
+
val singleton : version -> t
+
val union : t -> t -> t
+
val intersection : t -> t -> t
+
val complement : t -> t
+
val is_empty : t -> bool
+
val contains : version -> t -> bool
+
val subset_of : t -> t -> bool
+
val is_disjoint : t -> t -> bool
+
val to_string : t -> string
+
end
+
+
(* Terms and Constraints *)
+
type ('v, 'vs) term =
+
| Positive of 'vs (* The package version must be in this set *)
+
| Negative of 'vs (* The package version must not be in this set *)
+
+
module Term = struct
+
type ('v, 'vs) t = ('v, 'vs) term
+
+
let any (vs : 'vs) : ('v, 'vs) t = Positive vs
+
let empty (vs : 'vs) : ('v, 'vs) t = Negative vs
+
let exact (v : 'v) (mk_singleton : 'v -> 'vs) : ('v, 'vs) t =
+
Positive (mk_singleton v)
+
+
let negate = function
+
| Positive vs -> Negative vs
+
| Negative vs -> Positive vs
+
+
let contains (v : 'v) (vs_contains : 'v -> 'vs -> bool) term =
+
match term with
+
| Positive vs -> vs_contains v vs
+
| Negative vs -> not (vs_contains v vs)
+
+
let intersect_positive_positive vs_intersect vs1 vs2 =
+
Positive (vs_intersect vs1 vs2)
+
+
let intersect_negative_negative vs_intersect vs_complement vs1 vs2 =
+
Negative (vs_intersect (vs_complement vs1) (vs_complement vs2))
+
+
let intersect_positive_negative vs_intersect vs_complement vs1 vs2 =
+
let intersection = vs_intersect vs1 (vs_complement vs2) in
+
Positive intersection
+
+
let intersect_negative_positive vs_intersect vs_complement vs1 vs2 =
+
let intersection = vs_intersect (vs_complement vs1) vs2 in
+
Positive intersection
+
+
let intersection vs_intersect vs_complement t1 t2 =
+
match t1, t2 with
+
| Positive vs1, Positive vs2 ->
+
intersect_positive_positive vs_intersect vs1 vs2
+
| Negative vs1, Negative vs2 ->
+
intersect_negative_negative vs_intersect vs_complement vs1 vs2
+
| Positive vs1, Negative vs2 ->
+
intersect_positive_negative vs_intersect vs_complement vs1 vs2
+
| Negative vs1, Positive vs2 ->
+
intersect_negative_positive vs_intersect vs_complement vs1 vs2
+
+
let union_positive_positive vs_union vs1 vs2 =
+
Positive (vs_union vs1 vs2)
+
+
let union_negative_negative vs_intersect vs1 vs2 =
+
Negative (vs_intersect vs1 vs2)
+
+
let union_positive_negative vs_intersect vs_complement vs_is_empty vs1 vs2 =
+
let diff = vs_intersect vs1 (vs_complement vs2) in
+
if vs_is_empty diff then Negative vs2 else Positive diff
+
+
let union_negative_positive vs_intersect vs_complement vs_is_empty vs1 vs2 =
+
let diff = vs_intersect (vs_complement vs1) vs2 in
+
if vs_is_empty diff then Negative vs1 else Positive diff
+
+
let union vs_union vs_intersect vs_complement vs_is_empty t1 t2 =
+
match t1, t2 with
+
| Positive vs1, Positive vs2 ->
+
union_positive_positive vs_union vs1 vs2
+
| Negative vs1, Negative vs2 ->
+
union_negative_negative vs_intersect vs1 vs2
+
| Positive vs1, Negative vs2 ->
+
union_positive_negative vs_intersect vs_complement vs_is_empty vs1 vs2
+
| Negative vs1, Positive vs2 ->
+
union_negative_positive vs_intersect vs_complement vs_is_empty vs1 vs2
+
+
let is_positive = function
+
| Positive _ -> true
+
| Negative _ -> false
+
+
let to_string (_ : 'v -> string) (vs_to_str : 'vs -> string) term =
+
match term with
+
| Positive vs -> vs_to_str vs
+
| Negative vs -> "not " ^ vs_to_str vs
+
end
+
+
(* Incompatibilities *)
+
type ('p, 'v, 'vs, 'meta) incompatibility = {
+
terms : (('p * ('v, 'vs) term) list);
+
cause : ('p, 'v, 'vs, 'meta) incompatibility_cause;
+
}
+
and ('p, 'v, 'vs, 'meta) incompatibility_cause =
+
| Root of 'p * 'v
+
| NoVersions of 'p * 'vs
+
| Dependency of 'p * 'vs * 'p * 'vs
+
| Derived of int * int
+
| External of 'p * 'vs * 'meta
+
+
module Incompatibility = struct
+
type ('p, 'v, 'vs, 'meta) t = ('p, 'v, 'vs, 'meta) incompatibility
+
+
let not_root pkg ver vs_singleton =
+
{ terms = [(pkg, Negative (vs_singleton ver))];
+
cause = Root (pkg, ver) }
+
+
let no_versions pkg term =
+
match term with
+
| Positive vs ->
+
{ terms = [(pkg, term)];
+
cause = NoVersions (pkg, vs) }
+
| _ ->
+
failwith "Expected positive term"
+
+
let from_dependency pkg_from vs_from pkg_to vs_to =
+
{ terms = [(pkg_from, Positive vs_from); (pkg_to, Negative vs_to)];
+
cause = Dependency (pkg_from, vs_from, pkg_to, vs_to) }
+
+
let from_derivation incomp1 incomp2 =
+
{ terms = []; (* This will be populated during conflict resolution *)
+
cause = Derived (incomp1, incomp2) }
+
end
+
+
(* Partial Solutions *)
+
type ('p, 'v, 'vs) partial_solution = {
+
decision_level : int;
+
decisions : ('p * 'v) list;
+
assignments : ('p * ('v, 'vs) term * int * bool) list;
+
}
+
+
module PartialSolution = struct
+
type ('p, 'v, 'vs) t = ('p, 'v, 'vs) partial_solution
+
+
let empty = {
+
decision_level = 0;
+
decisions = [];
+
assignments = [];
+
}
+
+
let current_decision_level solution = solution.decision_level
+
+
let add_decision solution package version vs_singleton =
+
let decision_level = solution.decision_level + 1 in
+
let new_assignment = (package, Positive (vs_singleton version), decision_level, true) in
+
{
+
decision_level;
+
decisions = (package, version) :: solution.decisions;
+
assignments = new_assignment :: solution.assignments;
+
}
+
+
let add_derivation solution package term cause_incompat decision_level =
+
let new_assignment = (package, term, decision_level, false) in
+
{ solution with
+
assignments = new_assignment :: solution.assignments;
+
}
+
+
let check_assignment p_compare pkg term (assign_pkg, assign_term, _, _) =
+
if p_compare assign_pkg pkg = 0 then
+
match assign_term, term with
+
| Positive _, Positive _ -> true
+
| Negative _, Negative _ -> true
+
| _ -> false
+
else
+
false
+
+
let relation solution p_compare incompatibility =
+
let rec check_terms satisfied unsatisfied_term = function
+
| [] ->
+
if satisfied then `Satisfied else `AlmostSatisfied unsatisfied_term
+
| (pkg, term) :: rest ->
+
let pkg_satisfies =
+
List.exists (fun a -> check_assignment p_compare pkg term a) solution.assignments in
+
if pkg_satisfies then
+
check_terms satisfied unsatisfied_term rest
+
else
+
check_terms false (pkg, term) rest
+
in
+
check_terms true (List.hd incompatibility.terms) incompatibility.terms
+
+
let extract_solution solution =
+
solution.decisions |> List.rev
+
+
let backtrack solution level =
+
let new_assignments =
+
List.filter (fun (_, _, assignment_level, _) -> assignment_level <= level) solution.assignments in
+
let new_decisions =
+
List.filter (fun (_, _) -> level <= solution.decision_level) solution.decisions in
+
{ decision_level = level; assignments = new_assignments; decisions = new_decisions }
+
end
+
+
(* Dependency Provider *)
+
module type DEPENDENCY_PROVIDER = sig
+
type package_id
+
type version
+
type version_set
+
type error
+
+
val get_root_package : unit -> package_id
+
val get_root_version : unit -> version
+
val available_versions : package_id -> version list
+
val get_dependencies : package_id -> version -> ((package_id * version_set) list, error) result
+
val choose_version : package_id -> version_set -> version option
+
end
+
+
(* Solver Configuration *)
+
type 'a config = {
+
max_iterations : int option;
+
}
+
+
let default_config = {
+
max_iterations = None;
+
}
+
+
(* Main Solver Interface *)
+
module type SOLVER = sig
+
type package_id
+
type version
+
type version_set
+
type metadata
+
+
type error =
+
| Unsatisfiable of {
+
explanation : string;
+
}
+
| DependencyProviderError of {
+
package : package_id;
+
version : version;
+
message : string;
+
}
+
| MaxIterationsExceeded
+
+
type solution = (package_id * version) list
+
+
val solve :
+
(module DEPENDENCY_PROVIDER with
+
type package_id = package_id and
+
type version = version and
+
type version_set = version_set and
+
type error = string) ->
+
'a config ->
+
(solution, error) result
+
+
val explain_error : error -> string
+
end
+
+
(* Functor Implementation *)
+
module Make
+
(P : PACKAGE_ID)
+
(V : VERSION)
+
(VS : VERSION_SET with type version = V.t) = struct
+
+
type package_id = P.t
+
type version = V.t
+
type version_set = VS.t
+
type metadata = string
+
+
type error =
+
| Unsatisfiable of {
+
explanation : string;
+
}
+
| DependencyProviderError of {
+
package : package_id;
+
version : version;
+
message : string;
+
}
+
| MaxIterationsExceeded
+
+
type solution = (package_id * version) list
+
+
(* Helper functions for the PubGrub algorithm *)
+
+
let add_incompatibility_from_dependencies package version deps =
+
List.map (fun (dep_pkg, dep_vs) ->
+
Incompatibility.from_dependency package (VS.singleton version) dep_pkg dep_vs
+
) deps
+
+
let check_max_iterations config iterations =
+
match config.max_iterations with
+
| Some max when iterations >= max -> Error MaxIterationsExceeded
+
| _ -> Ok ()
+
+
let process_unit_propagation solution incompatibility =
+
match PartialSolution.relation solution P.compare incompatibility with
+
| `Satisfied ->
+
Error (`Conflict incompatibility)
+
| `AlmostSatisfied (pkg, term) ->
+
(* Add derived assignment *)
+
let new_solution =
+
PartialSolution.add_derivation
+
solution pkg (Term.negate term) incompatibility solution.decision_level in
+
Ok new_solution
+
| `Unsatisfied ->
+
Ok solution
+
+
let unit_propagation solution incompatibilities package =
+
let relevant_incompats =
+
List.filter (fun incomp ->
+
List.exists (fun (pkg, _) -> P.compare pkg package = 0) incomp.terms
+
) incompatibilities in
+
+
let rec process_incompats solution = function
+
| [] -> Ok solution
+
| incomp :: rest ->
+
match process_unit_propagation solution incomp with
+
| Ok new_solution -> process_incompats new_solution rest
+
| Error e -> Error e
+
in
+
process_incompats solution relevant_incompats
+
+
let propagate_all solution incompatibilities pending_packages =
+
let rec propagate solution = function
+
| [] -> Ok solution
+
| pkg :: rest ->
+
match unit_propagation solution incompatibilities pkg with
+
| Ok new_solution -> propagate new_solution rest
+
| Error (`Conflict incomp) ->
+
(* Simple conflict handling for now - just fails with explanation *)
+
let explanation =
+
Printf.sprintf "Conflict found for package %s" (P.to_string pkg) in
+
Error (Unsatisfiable { explanation })
+
in
+
propagate solution pending_packages
+
+
let get_packages_with_constraints solution =
+
List.fold_left (fun acc (package, _, _, _) ->
+
if List.exists (fun (p, _) -> P.compare p package = 0) solution.decisions then
+
acc
+
else
+
package :: acc
+
) [] solution.assignments
+
|> List.sort_uniq P.compare
+
+
let get_package_constraints solution package =
+
List.fold_left (fun acc (pkg, term, _, _) ->
+
if P.compare pkg package = 0 then
+
term :: acc
+
else
+
acc
+
) [] solution.assignments
+
+
let compute_effective_range constraints =
+
let process_constraint range term =
+
match term, range with
+
| Positive vs1, Positive vs2 ->
+
Positive (VS.intersection vs1 vs2)
+
| Negative vs1, Positive vs2 ->
+
let complement = VS.complement vs1 in
+
Positive (VS.intersection vs2 complement)
+
| Positive vs1, Negative vs2 ->
+
let complement = VS.complement vs2 in
+
Positive (VS.intersection vs1 complement)
+
| Negative vs1, Negative vs2 ->
+
Negative (VS.union vs1 vs2)
+
in
+
+
match constraints with
+
| [] -> Positive VS.any
+
| first :: rest ->
+
List.fold_left process_constraint first rest
+
+
let extract_version_set = function
+
| Positive vs -> vs
+
| Negative vs -> VS.complement vs
+
+
(* Main solver implementation *)
+
let solve
+
(module Provider : DEPENDENCY_PROVIDER with
+
type package_id = package_id and
+
type version = version and
+
type version_set = version_set and
+
type error = string)
+
(config : 'a config) =
+
+
let root_pkg = Provider.get_root_package () in
+
let root_version = Provider.get_root_version () in
+
+
let initial_solution = PartialSolution.empty in
+
let initial_incompats = [
+
Incompatibility.not_root root_pkg root_version VS.singleton
+
] in
+
+
let rec solve_loop solution incompatibilities iterations =
+
match check_max_iterations config iterations with
+
| Error e -> Error e
+
| Ok () ->
+
(* Get all packages that need propagation *)
+
let pending_packages =
+
List.map (fun (package, _, _, _) -> package) solution.assignments in
+
+
(* Run unit propagation on all packages *)
+
match propagate_all solution incompatibilities pending_packages with
+
| Error e -> Error e
+
| Ok propagated_solution ->
+
(* Check if we're done *)
+
if List.length propagated_solution.decisions = 0 then
+
Ok []
+
else if List.exists (fun (pkg, _) -> P.compare pkg root_pkg = 0) propagated_solution.decisions then
+
(* We have a solution *)
+
Ok (PartialSolution.extract_solution propagated_solution)
+
else
+
(* Decision making - pick next package to decide on *)
+
let packages_with_constraints =
+
get_packages_with_constraints propagated_solution in
+
+
match packages_with_constraints with
+
| [] ->
+
(* No more packages to decide on, we're done *)
+
Ok (PartialSolution.extract_solution propagated_solution)
+
| next_pkg :: _ ->
+
(* Find constraints for this package *)
+
let constraints =
+
get_package_constraints propagated_solution next_pkg in
+
+
(* Compute effective range *)
+
let effective_range =
+
compute_effective_range constraints in
+
+
(* Choose version *)
+
let effective_vs = extract_version_set effective_range in
+
+
match Provider.choose_version next_pkg effective_vs with
+
| None ->
+
(* No valid version - add incompatibility and backtrack *)
+
let incomp = Incompatibility.no_versions next_pkg effective_range in
+
solve_loop propagated_solution (incomp :: incompatibilities) (iterations + 1)
+
| Some version ->
+
(* Get dependencies for this version *)
+
match Provider.get_dependencies next_pkg version with
+
| Error msg ->
+
Error (DependencyProviderError {
+
package = next_pkg;
+
version;
+
message = msg;
+
})
+
| Ok deps ->
+
(* Add decision and dependencies as incompatibilities *)
+
let new_solution =
+
PartialSolution.add_decision
+
propagated_solution next_pkg version VS.singleton in
+
+
let new_incompats =
+
add_incompatibility_from_dependencies next_pkg version deps in
+
+
solve_loop
+
new_solution
+
(new_incompats @ incompatibilities)
+
(iterations + 1)
+
in
+
+
match solve_loop initial_solution initial_incompats 0 with
+
| Ok [] ->
+
(* Empty solution - just return the root package *)
+
Ok [(root_pkg, root_version)]
+
| Ok solution -> Ok solution
+
| Error e -> Error e
+
+
(* Format error messages *)
+
let explain_error = function
+
| Unsatisfiable { explanation } ->
+
"Dependency resolution failed: " ^ explanation
+
| DependencyProviderError { package; version; message } ->
+
Printf.sprintf "Error retrieving dependencies for %s %s: %s"
+
(P.to_string package) (V.to_string version) message
+
| MaxIterationsExceeded ->
+
"Dependency resolution exceeded maximum iterations"
+
end
+268
pubgrub.mli
···
+
(** PubGrub: A Dependency Resolution Algorithm
+
+
This library implements the PubGrub algorithm, a next-generation version solving algorithm
+
designed for package managers. It efficiently finds a set of package versions that satisfy
+
all dependencies, or provides a clear explanation of why no solution exists.
+
*)
+
+
(** {1 Core Types} *)
+
+
(** Package identifier. Must be comparable and displayable. *)
+
module type PACKAGE_ID = sig
+
type t
+
(** The package identifier type. *)
+
+
val compare : t -> t -> int
+
(** Compare two package identifiers. *)
+
+
val to_string : t -> string
+
(** Convert a package identifier to a string. *)
+
end
+
+
(** Version type. Must be orderable and displayable. *)
+
module type VERSION = sig
+
type t
+
(** The version type. *)
+
+
val compare : t -> t -> int
+
(** Compare two versions. Newer versions should be greater. *)
+
+
val to_string : t -> string
+
(** Convert a version to a string. *)
+
end
+
+
(** {1 Version Sets and Ranges} *)
+
+
(** A set or range of versions. *)
+
module type VERSION_SET = sig
+
type t
+
(** The version set type. *)
+
+
type version
+
(** The version type. *)
+
+
val empty : t
+
(** The empty set. *)
+
+
val any : t
+
(** The set of all versions. *)
+
+
val singleton : version -> t
+
(** A set containing exactly one version. *)
+
+
val union : t -> t -> t
+
(** Union of two version sets. *)
+
+
val intersection : t -> t -> t
+
(** Intersection of two version sets. *)
+
+
val complement : t -> t
+
(** Complement of a version set. *)
+
+
val is_empty : t -> bool
+
(** Whether the set is empty. *)
+
+
val contains : version -> t -> bool
+
(** Whether a version is in the set. *)
+
+
val subset_of : t -> t -> bool
+
(** Whether the first set is a subset of the second. *)
+
+
val is_disjoint : t -> t -> bool
+
(** Whether two sets are disjoint. *)
+
+
val to_string : t -> string
+
(** Convert a version set to a string. *)
+
end
+
+
(** {1 Terms and Constraints} *)
+
+
(** A term is either a positive or negative constraint on a package version. *)
+
type ('v, 'vs) term =
+
| Positive of 'vs (** The package version must be in this set. *)
+
| Negative of 'vs (** The package version must not be in this set. *)
+
+
(** Term operations. *)
+
module Term : sig
+
type ('v, 'vs) t = ('v, 'vs) term
+
+
val any : 'vs -> ('v, 'vs) t
+
(** A term that matches any version in the given set. *)
+
+
val empty : 'vs -> ('v, 'vs) t
+
(** A term that matches no version in the given set. *)
+
+
val exact : 'v -> ('v -> 'vs) -> ('v, 'vs) t
+
(** A term requiring exactly this version. *)
+
+
val negate : ('v, 'vs) t -> ('v, 'vs) t
+
(** Negate a term. *)
+
+
val contains : 'v -> ('v -> 'vs -> bool) -> ('v, 'vs) t -> bool
+
(** Whether a version satisfies a term. *)
+
+
val intersection : ('vs -> 'vs -> 'vs) -> ('vs -> 'vs) -> ('v, 'vs) t -> ('v, 'vs) t -> ('v, 'vs) t
+
(** Intersection of two terms. *)
+
+
val union : ('vs -> 'vs -> 'vs) -> ('vs -> 'vs -> 'vs) -> ('vs -> 'vs) -> ('vs -> bool) -> ('v, 'vs) t -> ('v, 'vs) t -> ('v, 'vs) t
+
(** Union of two terms. *)
+
+
val is_positive : ('v, 'vs) t -> bool
+
(** Whether a term is positive. *)
+
+
val to_string : ('v -> string) -> ('vs -> string) -> ('v, 'vs) t -> string
+
(** Convert a term to a string. *)
+
end
+
+
(** {1 Incompatibilities} *)
+
+
(** An incompatibility is a set of terms that cannot be satisfied together. *)
+
type ('p, 'v, 'vs, 'meta) incompatibility = {
+
terms : (('p * ('v, 'vs) term) list);
+
(** The terms that cannot be satisfied together. *)
+
+
cause : ('p, 'v, 'vs, 'meta) incompatibility_cause;
+
(** The reason this incompatibility exists. *)
+
}
+
+
(** The cause of an incompatibility. *)
+
and ('p, 'v, 'vs, 'meta) incompatibility_cause =
+
| Root of 'p * 'v
+
(** Incompatibility derived from root package requirement. *)
+
+
| NoVersions of 'p * 'vs
+
(** No versions satisfy the constraint. *)
+
+
| Dependency of 'p * 'vs * 'p * 'vs
+
(** Package depends on another package. *)
+
+
| Derived of int * int
+
(** Derived from two other incompatibilities. *)
+
+
| External of 'p * 'vs * 'meta
+
(** External incompatibility with custom metadata. *)
+
+
(** {1 Partial Solutions} *)
+
+
(** A partial solution represents the current state of the solver. *)
+
type ('p, 'v, 'vs) partial_solution = {
+
decision_level : int;
+
(** Current decision level. *)
+
+
decisions : ('p * 'v) list;
+
(** Package versions that have been selected. *)
+
+
assignments : ('p * ('v, 'vs) term * int * bool) list;
+
(** Terms derived from decisions. Each tuple contains:
+
- The package
+
- The term
+
- The decision level
+
- Whether this is a decision (true) or derived (false) *)
+
}
+
+
(** {1 Dependency Provider} *)
+
+
(** Interface for a dependency provider. *)
+
module type DEPENDENCY_PROVIDER = sig
+
type package_id
+
(** The package identifier type. *)
+
+
type version
+
(** The package version type. *)
+
+
type version_set
+
(** The version set type. *)
+
+
type error
+
(** The type of errors that can occur. *)
+
+
val get_root_package : unit -> package_id
+
(** Get the root package identifier. *)
+
+
val get_root_version : unit -> version
+
(** Get the version of the root package. *)
+
+
val available_versions : package_id -> version list
+
(** List all available versions of a package in descending order (newest first). *)
+
+
val get_dependencies :
+
package_id -> version -> ((package_id * version_set) list, error) result
+
(** Get the dependencies of a package at a specific version. *)
+
+
val choose_version :
+
package_id -> version_set -> version option
+
(** Choose a version for a package given a constraint. *)
+
end
+
+
(** {1 Solver Configuration} *)
+
+
(** Configuration options for the solver. *)
+
type 'a config = {
+
max_iterations : int option;
+
(** Maximum number of iterations before giving up. None means no limit. *)
+
}
+
+
(** Default configuration with no iteration limit. *)
+
val default_config : 'a config
+
+
(** {1 Main Solver Interface} *)
+
+
(** Types and operations for the PubGrub solver. *)
+
module type SOLVER = sig
+
type package_id
+
(** Package identifier type. *)
+
+
type version
+
(** Version type. *)
+
+
type version_set
+
(** Version set type. *)
+
+
type metadata
+
(** Custom metadata type for external incompatibilities. *)
+
+
(** Error types. *)
+
type error =
+
| Unsatisfiable of {
+
explanation : string;
+
(** A human-readable explanation of why no solution exists. *)
+
}
+
| DependencyProviderError of {
+
package : package_id;
+
version : version;
+
message : string;
+
(** An error from the dependency provider. *)
+
}
+
| MaxIterationsExceeded
+
(** The solver exceeded the maximum number of iterations. *)
+
+
(** The solution type: a list of packages and their selected versions. *)
+
type solution = (package_id * version) list
+
+
(** Solve dependencies for a package.
+
Returns either a solution or an error explaining why no solution exists. *)
+
val solve :
+
(module DEPENDENCY_PROVIDER with
+
type package_id = package_id and
+
type version = version and
+
type version_set = version_set and
+
type error = string) ->
+
'a config ->
+
(solution, error) result
+
+
(** Generate a human-readable explanation of an error. *)
+
val explain_error : error -> string
+
end
+
+
(** {1 Functor Interface} *)
+
+
(** Create a solver with the given types. *)
+
module Make
+
(P : PACKAGE_ID)
+
(V : VERSION)
+
(VS : VERSION_SET with type version = V.t) :
+
SOLVER
+
with type package_id = P.t
+
and type version = V.t
+
and type version_set = VS.t
+
and type metadata = string
pubgrub.opam

This is a binary file and will not be displayed.

+320
pubgrub_test.ml
···
+
(* Enhanced test for the PubGrub OCaml implementation with more complex examples *)
+
+
(* Create a simple string-based package ID module *)
+
module StringPackage = struct
+
type t = string
+
let compare = String.compare
+
let to_string s = s
+
end
+
+
(* Create a simple integer-based version module *)
+
module IntVersion = struct
+
type t = int
+
let compare = Int.compare
+
let to_string = string_of_int
+
end
+
+
(* Create a simple range-based version set module *)
+
module IntVersionRange = struct
+
type t = { min : int option; max : int option }
+
type version = int
+
+
let empty = { min = None; max = Some 0 }
+
let any = { min = None; max = None }
+
let singleton v = { min = Some v; max = Some v }
+
+
let union r1 r2 =
+
let min = match r1.min, r2.min with
+
| None, _ -> None
+
| _, None -> None
+
| Some v1, Some v2 -> Some (Int.min v1 v2)
+
in
+
let max = match r1.max, r2.max with
+
| None, _ -> None
+
| _, None -> None
+
| Some v1, Some v2 -> Some (Int.max v1 v2)
+
in
+
{ min; max }
+
+
let intersection r1 r2 =
+
let min = match r1.min, r2.min with
+
| None, Some v -> Some v
+
| Some v, None -> Some v
+
| None, None -> None
+
| Some v1, Some v2 -> Some (Int.max v1 v2)
+
in
+
let max = match r1.max, r2.max with
+
| None, Some v -> Some v
+
| Some v, None -> Some v
+
| None, None -> None
+
| Some v1, Some v2 -> Some (Int.min v1 v2)
+
in
+
{ min; max }
+
+
let complement r =
+
match r.min, r.max with
+
| None, None -> empty
+
| Some v, None -> { min = None; max = Some (v - 1) }
+
| None, Some v -> { min = Some (v + 1); max = None }
+
| Some v1, Some v2 ->
+
if v1 = v2 then
+
{ min = None; max = Some (v1 - 1) }
+
else
+
{ min = Some (v2 + 1); max = None }
+
+
let is_empty r =
+
match r.min, r.max with
+
| Some min, Some max -> min > max
+
| _ -> false
+
+
let contains v r =
+
match r.min, r.max with
+
| None, None -> true
+
| Some min, None -> v >= min
+
| None, Some max -> v <= max
+
| Some min, Some max -> v >= min && v <= max
+
+
let subset_of r1 r2 =
+
match r1.min, r1.max, r2.min, r2.max with
+
| _, _, None, None -> true (* Any set is a subset of the universal set *)
+
| Some min1, Some max1, Some min2, Some max2 -> min1 >= min2 && max1 <= max2
+
| Some min1, None, Some min2, _ -> min1 >= min2
+
| None, Some max1, _, Some max2 -> max1 <= max2
+
| None, None, _, _ -> false (* The universal set is not a subset of anything except itself *)
+
| _, _, Some _, None -> false
+
| _, _, None, Some _ -> false
+
+
let is_disjoint r1 r2 =
+
match r1.min, r1.max, r2.min, r2.max with
+
| Some min1, _, _, Some max2 -> min1 > max2
+
| _, Some max1, Some min2, _ -> max1 < min2
+
| _ -> false
+
+
let to_string r =
+
match r.min, r.max with
+
| None, None -> "any version"
+
| Some v, None -> Printf.sprintf ">= %d" v
+
| None, Some v -> Printf.sprintf "<= %d" v
+
| Some v1, Some v2 ->
+
if v1 = v2 then string_of_int v1
+
else Printf.sprintf "%d to %d" v1 v2
+
end
+
+
(* Create the solver *)
+
module Solver = Pubgrub.Make(StringPackage)(IntVersion)(IntVersionRange)
+
+
(* Create a simple dependency provider for our first example *)
+
module SimpleProvider = struct
+
type package_id = StringPackage.t
+
type version = IntVersion.t
+
type version_set = IntVersionRange.t
+
type error = string
+
+
(* In-memory "database" of packages and dependencies *)
+
let dependencies = [
+
("root", 1, [("a", { IntVersionRange.min = Some 1; max = Some 2 })]);
+
("a", 1, [("b", { IntVersionRange.min = Some 1; max = Some 1 })]);
+
("a", 2, [("b", { IntVersionRange.min = Some 2; max = Some 2 })]);
+
("b", 1, []);
+
("b", 2, []);
+
]
+
+
let get_root_package () = "root"
+
let get_root_version () = 1
+
+
let available_versions pkg =
+
dependencies
+
|> List.filter (fun (p, _, _) -> p = pkg)
+
|> List.map (fun (_, v, _) -> v)
+
|> List.sort (fun a b -> - (IntVersion.compare a b)) (* Newest first *)
+
+
let get_dependencies pkg ver =
+
match List.find_opt (fun (p, v, _) -> p = pkg && v = ver) dependencies with
+
| Some (_, _, deps) -> Ok deps
+
| None ->
+
Error (Printf.sprintf "No dependencies found for %s %d" pkg ver)
+
+
let choose_version pkg range =
+
let versions = available_versions pkg in
+
List.find_opt (fun v -> IntVersionRange.contains v range) versions
+
end
+
+
(* Create a complex dependency provider based on Rust's doc_interface.rs example *)
+
(* root depends on menu and icons, menu depends on dropdown, dropdown depends on icons *)
+
module ComplexProvider = struct
+
type package_id = StringPackage.t
+
type version = IntVersion.t
+
type version_set = IntVersionRange.t
+
type error = string
+
+
(* In-memory "database" of packages and dependencies *)
+
let dependencies = [
+
("root", 1, [
+
("menu", IntVersionRange.any);
+
("icons", IntVersionRange.any);
+
]);
+
("menu", 1, [
+
("dropdown", IntVersionRange.any);
+
]);
+
("dropdown", 1, [
+
("icons", IntVersionRange.any);
+
]);
+
("icons", 1, []);
+
]
+
+
let get_root_package () = "root"
+
let get_root_version () = 1
+
+
let available_versions pkg =
+
dependencies
+
|> List.filter (fun (p, _, _) -> p = pkg)
+
|> List.map (fun (_, v, _) -> v)
+
|> List.sort (fun a b -> - (IntVersion.compare a b)) (* Newest first *)
+
+
let get_dependencies pkg ver =
+
match List.find_opt (fun (p, v, _) -> p = pkg && v = ver) dependencies with
+
| Some (_, _, deps) -> Ok deps
+
| None ->
+
Error (Printf.sprintf "No dependencies found for %s %d" pkg ver)
+
+
let choose_version pkg range =
+
let versions = available_versions pkg in
+
List.find_opt (fun v -> IntVersionRange.contains v range) versions
+
end
+
+
(* Create a conflict provider to test conflict resolution *)
+
(* root depends on foo>=2 and bar>=2, foo@2 depends on bar=1, which creates a conflict *)
+
module ConflictProvider = struct
+
type package_id = StringPackage.t
+
type version = IntVersion.t
+
type version_set = IntVersionRange.t
+
type error = string
+
+
(* In-memory "database" of packages and dependencies *)
+
let dependencies = [
+
("root", 1, [
+
("foo", { IntVersionRange.min = Some 2; max = None });
+
("bar", { IntVersionRange.min = Some 2; max = None });
+
]);
+
("foo", 1, []);
+
("foo", 2, [
+
("bar", { IntVersionRange.min = Some 1; max = Some 1 });
+
]);
+
("bar", 1, []);
+
("bar", 2, []);
+
]
+
+
let get_root_package () = "root"
+
let get_root_version () = 1
+
+
let available_versions pkg =
+
dependencies
+
|> List.filter (fun (p, _, _) -> p = pkg)
+
|> List.map (fun (_, v, _) -> v)
+
|> List.sort (fun a b -> - (IntVersion.compare a b)) (* Newest first *)
+
+
let get_dependencies pkg ver =
+
match List.find_opt (fun (p, v, _) -> p = pkg && v = ver) dependencies with
+
| Some (_, _, deps) -> Ok deps
+
| None ->
+
Error (Printf.sprintf "No dependencies found for %s %d" pkg ver)
+
+
let choose_version pkg range =
+
let versions = available_versions pkg in
+
List.find_opt (fun v -> IntVersionRange.contains v range) versions
+
end
+
+
(* Create more complex provider inspired by another Rust example *)
+
(* More complex diamond dependency example *)
+
module DiamondProvider = struct
+
type package_id = StringPackage.t
+
type version = IntVersion.t
+
type version_set = IntVersionRange.t
+
type error = string
+
+
(* In-memory "database" of packages and dependencies *)
+
let dependencies = [
+
("root", 1, [
+
("a", IntVersionRange.any);
+
("c", IntVersionRange.any);
+
]);
+
("a", 1, [
+
("b", { IntVersionRange.min = Some 1; max = Some 1 });
+
]);
+
("a", 2, [
+
("b", { IntVersionRange.min = Some 2; max = Some 2 });
+
]);
+
("b", 1, []);
+
("b", 2, []);
+
("c", 1, [
+
("b", { IntVersionRange.min = Some 1; max = Some 1 });
+
]);
+
("c", 2, [
+
("b", { IntVersionRange.min = Some 2; max = Some 2 });
+
]);
+
]
+
+
let get_root_package () = "root"
+
let get_root_version () = 1
+
+
let available_versions pkg =
+
dependencies
+
|> List.filter (fun (p, _, _) -> p = pkg)
+
|> List.map (fun (_, v, _) -> v)
+
|> List.sort (fun a b -> - (IntVersion.compare a b)) (* Newest first *)
+
+
let get_dependencies pkg ver =
+
match List.find_opt (fun (p, v, _) -> p = pkg && v = ver) dependencies with
+
| Some (_, _, deps) -> Ok deps
+
| None ->
+
Error (Printf.sprintf "No dependencies found for %s %d" pkg ver)
+
+
let choose_version pkg range =
+
let versions = available_versions pkg in
+
List.find_opt (fun v -> IntVersionRange.contains v range) versions
+
end
+
+
(* Helper function to print a solution *)
+
let print_solution = function
+
| Ok solution ->
+
Printf.printf "Solution found with %d packages:\n" (List.length solution);
+
List.iter (fun (pkg, ver) ->
+
Printf.printf " %s @ %d\n" pkg ver
+
) solution;
+
true
+
| Error err ->
+
Printf.printf "Error: %s\n" (Solver.explain_error err);
+
false
+
+
(* Run the tests *)
+
let run_tests () =
+
Printf.printf "\n=== Simple Test ===\n";
+
let simple_result = Solver.solve (module SimpleProvider) Pubgrub.default_config in
+
let simple_passed = print_solution simple_result in
+
+
Printf.printf "\n=== Complex Test ===\n";
+
let complex_result = Solver.solve (module ComplexProvider) Pubgrub.default_config in
+
let complex_passed = print_solution complex_result in
+
+
Printf.printf "\n=== Conflict Test ===\n";
+
let conflict_result = Solver.solve (module ConflictProvider) Pubgrub.default_config in
+
let conflict_passed = not (print_solution conflict_result) in
+
+
Printf.printf "\n=== Diamond Dependency Test ===\n";
+
let diamond_result = Solver.solve (module DiamondProvider) Pubgrub.default_config in
+
let diamond_passed = print_solution diamond_result in
+
+
Printf.printf "\n=== Test Results ===\n";
+
Printf.printf "Simple test: %s\n" (if simple_passed then "PASSED" else "FAILED");
+
Printf.printf "Complex test: %s\n" (if complex_passed then "PASSED" else "FAILED");
+
Printf.printf "Conflict test: %s\n" (if conflict_passed then "PASSED" else "FAILED");
+
Printf.printf "Diamond test: %s\n" (if diamond_passed then "PASSED" else "FAILED");
+
+
let all_passed = simple_passed && complex_passed && conflict_passed && diamond_passed in
+
if all_passed then
+
Printf.printf "\nAll tests passed!\n"
+
else
+
Printf.printf "\nSome tests failed.\n"
+
+
(* Run all the tests *)
+
let () = run_tests ()