summaryrefslogtreecommitdiff
path: root/lib/bundler/vendor/pub_grub/lib/pub_grub/partial_solution.rb
blob: 4c4b8ca8444b93b4ad90cf63d47e9557b9d57cb2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
require_relative 'assignment'

module Bundler::PubGrub
  class PartialSolution
    attr_reader :assignments, :decisions
    attr_reader :attempted_solutions

    def initialize
      reset!

      @attempted_solutions = 1
      @backtracking = false
    end

    def decision_level
      @decisions.length
    end

    def relation(term)
      package = term.package
      return :overlap if !@terms.key?(package)

      @relation_cache[package][term] ||=
        @terms[package].relation(term)
    end

    def satisfies?(term)
      relation(term) == :subset
    end

    def derive(term, cause)
      add_assignment(Assignment.new(term, cause, decision_level, assignments.length))
    end

    def satisfier(term)
      assignment =
        @assignments_by[term.package].bsearch do |assignment_by|
          @cumulative_assignments[assignment_by].satisfies?(term)
        end

      assignment || raise("#{term} unsatisfied")
    end

    # A list of unsatisfied terms
    def unsatisfied
      @required.keys.reject do |package|
        @decisions.key?(package)
      end.map do |package|
        @terms[package]
      end
    end

    def decide(package, version)
      @attempted_solutions += 1 if @backtracking
      @backtracking = false;

      decisions[package] = version
      assignment = Assignment.decision(package, version, decision_level, assignments.length)
      add_assignment(assignment)
    end

    def backtrack(previous_level)
      @backtracking = true

      new_assignments = assignments.select do |assignment|
        assignment.decision_level <= previous_level
      end

      new_decisions = Hash[decisions.first(previous_level)]

      reset!

      @decisions = new_decisions

      new_assignments.each do |assignment|
        add_assignment(assignment)
      end
    end

    private

    def reset!
      # { Array<Assignment> }
      @assignments = []

      # { Package => Array<Assignment> }
      @assignments_by = Hash.new { |h,k| h[k] = [] }
      @cumulative_assignments = {}.compare_by_identity

      # { Package => Package::Version }
      @decisions = {}

      # { Package => Term }
      @terms = {}
      @relation_cache = Hash.new { |h,k| h[k] = {} }

      # { Package => Boolean }
      @required = {}
    end

    def add_assignment(assignment)
      term = assignment.term
      package = term.package

      @assignments << assignment
      @assignments_by[package] << assignment

      @required[package] = true if term.positive?

      if @terms.key?(package)
        old_term = @terms[package]
        @terms[package] = old_term.intersect(term)
      else
        @terms[package] = term
      end
      @relation_cache[package].clear

      @cumulative_assignments[assignment] = @terms[package]
    end
  end
end