Browse Source

Merge remote-tracking branch 'naderman/ordered-transactions'

Jordi Boggiano 12 years ago
parent
commit
d29a387bcb

+ 215 - 0
src/Composer/DependencyResolver/Decisions.php

@@ -0,0 +1,215 @@
+<?php
+
+/*
+ * This file is part of Composer.
+ *
+ * (c) Nils Adermann <naderman@naderman.de>
+ *     Jordi Boggiano <j.boggiano@seld.be>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Composer\DependencyResolver;
+
+/**
+ * Stores decisions on installing, removing or keeping packages
+ *
+ * @author Nils Adermann <naderman@naderman.de>
+ */
+class Decisions implements \Iterator
+{
+    const DECISION_LITERAL = 0;
+    const DECISION_REASON = 1;
+
+    protected $pool;
+    protected $decisionMap;
+    protected $decisionQueue = array();
+    protected $decisionQueueFree = array();
+
+    public function __construct($pool)
+    {
+        $this->pool = $pool;
+
+        if (version_compare(PHP_VERSION, '5.3.4', '>=')) {
+            $this->decisionMap = new \SplFixedArray($this->pool->getMaxId() + 1);
+        } else {
+            $this->decisionMap = array_fill(0, $this->pool->getMaxId() + 1, 0);
+        }
+    }
+
+    protected function addDecision($literal, $level)
+    {
+        $packageId = abs($literal);
+
+        $previousDecision = $this->decisionMap[$packageId];
+        if ($previousDecision != 0) {
+            $literalString = $this->pool->literalToString($literal);
+            $package = $this->pool->literalToPackage($literal);
+            throw new SolverBugException(
+                "Trying to decide $literalString on level $level, even though $package was previously decided as ".(int) $previousDecision."."
+            );
+        }
+
+        if ($literal > 0) {
+            $this->decisionMap[$packageId] = $level;
+        } else {
+            $this->decisionMap[$packageId] = -$level;
+        }
+    }
+
+    public function decide($literal, $level, $why, $addToFreeQueue = false)
+    {
+        $this->addDecision($literal, $level);
+        $this->decisionQueue[] = array(
+            self::DECISION_LITERAL => $literal,
+            self::DECISION_REASON => $why,
+        );
+
+        if ($addToFreeQueue) {
+            $this->decisionQueueFree[count($this->decisionQueue) - 1] = true;
+        }
+    }
+
+    public function contain($literal)
+    {
+        $packageId = abs($literal);
+
+        return (
+            $this->decisionMap[$packageId] > 0 && $literal > 0 ||
+            $this->decisionMap[$packageId] < 0 && $literal < 0
+        );
+    }
+
+    public function satisfy($literal)
+    {
+        $packageId = abs($literal);
+
+        return (
+            $literal > 0 && $this->decisionMap[$packageId] > 0 ||
+            $literal < 0 && $this->decisionMap[$packageId] < 0
+        );
+    }
+
+    public function conflict($literal)
+    {
+        $packageId = abs($literal);
+
+        return (
+            ($this->decisionMap[$packageId] > 0 && $literal < 0) ||
+            ($this->decisionMap[$packageId] < 0 && $literal > 0)
+        );
+    }
+
+    public function decided($literalOrPackageId)
+    {
+        return $this->decisionMap[abs($literalOrPackageId)] != 0;
+    }
+
+    public function undecided($literalOrPackageId)
+    {
+        return $this->decisionMap[abs($literalOrPackageId)] == 0;
+    }
+
+    public function decidedInstall($literalOrPackageId)
+    {
+        return $this->decisionMap[abs($literalOrPackageId)] > 0;
+    }
+
+    public function decisionLevel($literalOrPackageId)
+    {
+        return abs($this->decisionMap[abs($literalOrPackageId)]);
+    }
+
+    public function decisionRule($literalOrPackageId)
+    {
+        $packageId = abs($literalOrPackageId);
+
+        foreach ($this->decisionQueue as $i => $decision) {
+            if ($packageId === abs($decision[self::DECISION_LITERAL])) {
+                return $decision[self::DECISION_REASON];
+            }
+        }
+
+        return null;
+    }
+
+    public function atOffset($queueOffset)
+    {
+        return $this->decisionQueue[$queueOffset];
+    }
+
+    public function validOffset($queueOffset)
+    {
+        return $queueOffset >= 0 && $queueOffset < count($this->decisionQueue);
+    }
+
+    public function lastReason()
+    {
+        return $this->decisionQueue[count($this->decisionQueue) - 1][self::DECISION_REASON];
+    }
+
+    public function lastLiteral()
+    {
+        return $this->decisionQueue[count($this->decisionQueue) - 1][self::DECISION_LITERAL];
+    }
+
+    public function reset()
+    {
+        while ($decision = array_pop($this->decisionQueue)) {
+            $this->decisionMap[abs($decision[self::DECISION_LITERAL])] = 0;
+        }
+
+        $this->decisionQueueFree = array();
+    }
+
+    public function resetToOffset($offset)
+    {
+        while (count($this->decisionQueue) > $offset + 1) {
+            $decision = array_pop($this->decisionQueue);
+            unset($this->decisionQueueFree[count($this->decisionQueue)]);
+            $this->decisionMap[abs($decision[self::DECISION_LITERAL])] = 0;
+        }
+    }
+
+    public function revertLast()
+    {
+        $this->decisionMap[abs($this->lastLiteral())] = 0;
+        array_pop($this->decisionQueue);
+    }
+
+    public function getMaxOffset()
+    {
+        return count($this->decisionQueue) - 1;
+    }
+
+    public function rewind()
+    {
+        end($this->decisionQueue);
+    }
+
+    public function current()
+    {
+        return current($this->decisionQueue);
+    }
+
+    public function key()
+    {
+        return key($this->decisionQueue);
+    }
+
+    public function next()
+    {
+        return prev($this->decisionQueue);
+    }
+
+    public function valid()
+    {
+        return false !== current($this->decisionQueue);
+    }
+
+    public function isEmpty()
+    {
+        return count($this->decisionQueue) === 0;
+    }
+}

+ 10 - 15
src/Composer/DependencyResolver/RuleSetGenerator.php

@@ -190,23 +190,18 @@ class RuleSetGenerator
                 }
             }
 
-            // check implicit obsoletes
-            // for installed packages we only need to check installed/installed problems,
-            // as the others are picked up when looking at the uninstalled package.
-            if (!$isInstalled) {
-                $obsoleteProviders = $this->pool->whatProvides($package->getName(), null);
+            $obsoleteProviders = $this->pool->whatProvides($package->getName(), null);
 
-                foreach ($obsoleteProviders as $provider) {
-                    if ($provider === $package) {
-                        continue;
-                    }
+            foreach ($obsoleteProviders as $provider) {
+                if ($provider === $package) {
+                    continue;
+                }
 
-                    if (($package instanceof AliasPackage) && $package->getAliasOf() === $provider) {
-                        $this->addRule(RuleSet::TYPE_PACKAGE, $rule = $this->createRequireRule($package, array($provider), Rule::RULE_PACKAGE_ALIAS, (string) $package));
-                    } elseif (!$this->obsoleteImpossibleForAlias($package, $provider)) {
-                        $reason = ($package->getName() == $provider->getName()) ? Rule::RULE_PACKAGE_SAME_NAME : Rule::RULE_PACKAGE_IMPLICIT_OBSOLETES;
-                        $this->addRule(RuleSet::TYPE_PACKAGE, $rule = $this->createConflictRule($package, $provider, $reason, (string) $package));
-                    }
+                if (($package instanceof AliasPackage) && $package->getAliasOf() === $provider) {
+                    $this->addRule(RuleSet::TYPE_PACKAGE, $rule = $this->createRequireRule($package, array($provider), Rule::RULE_PACKAGE_ALIAS, (string) $package));
+                } elseif (!$this->obsoleteImpossibleForAlias($package, $provider)) {
+                    $reason = ($package->getName() == $provider->getName()) ? Rule::RULE_PACKAGE_SAME_NAME : Rule::RULE_PACKAGE_IMPLICIT_OBSOLETES;
+                    $this->addRule(RuleSet::TYPE_PACKAGE, $rule = $this->createConflictRule($package, $provider, $reason, (string) $package));
                 }
             }
         }

+ 8 - 13
src/Composer/DependencyResolver/RuleWatchGraph.php

@@ -72,16 +72,11 @@ class RuleWatchGraph
      * @param int $decidedLiteral The literal which was decided (A in our example)
      * @param int $level          The level at which the decision took place and at which
      *     all resulting decisions should be made.
-     * @param Callable $decisionsSatisfyCallback A callback which checks if a
-     *     literal has already been positively decided and the rule is thus
-     *     already true and can be skipped.
-     * @param Callable $conflictCallback A callback which checks if a literal
-     *     would conflict with previously made decisions on the same package
-     * @param Callable $decideCallback A callback which is responsible for
-     *     registering decided literals resulting from unit clauses
+     * @param Decisions $decisions Used to check previous decisions and to
+     *     register decisions resulting from propagation
      * @return Rule|null If a conflict is found the conflicting rule is returned
      */
-    public function propagateLiteral($decidedLiteral, $level, $decisionsSatisfyCallback, $conflictCallback, $decideCallback)
+    public function propagateLiteral($decidedLiteral, $level, $decisions)
     {
         // we invert the decided literal here, example:
         // A was decided => (-A|B) now requires B to be true, so we look for
@@ -99,13 +94,13 @@ class RuleWatchGraph
             $node = $chain->current();
             $otherWatch = $node->getOtherWatch($literal);
 
-            if (!$node->getRule()->isDisabled() && !call_user_func($decisionsSatisfyCallback, $otherWatch)) {
+            if (!$node->getRule()->isDisabled() && !$decisions->contain($otherWatch)) {
                 $ruleLiterals = $node->getRule()->getLiterals();
 
-                $alternativeLiterals = array_filter($ruleLiterals, function ($ruleLiteral) use ($literal, $otherWatch, $conflictCallback) {
+                $alternativeLiterals = array_filter($ruleLiterals, function ($ruleLiteral) use ($literal, $otherWatch, $decisions) {
                     return $literal !== $ruleLiteral &&
                         $otherWatch !== $ruleLiteral &&
-                        !call_user_func($conflictCallback, $ruleLiteral);
+                        !$decisions->conflict($ruleLiteral);
                 });
 
                 if ($alternativeLiterals) {
@@ -114,11 +109,11 @@ class RuleWatchGraph
                     continue;
                 }
 
-                if (call_user_func($conflictCallback, $otherWatch)) {
+                if ($decisions->conflict($otherWatch)) {
                     return $node->getRule();
                 }
 
-                call_user_func($decideCallback, $otherWatch, $level, $node->getRule());
+                $decisions->decide($otherWatch, $level, $node->getRule());
             }
 
             $chain->next();

+ 3 - 3
src/Composer/DependencyResolver/RuleWatchNode.php

@@ -47,9 +47,9 @@ class RuleWatchNode
      * Useful for learned rules where the literal for the highest rule is most
      * likely to quickly lead to further decisions.
      *
-     * @param SplFixedArray $decisionMap A package to decision lookup table
+     * @param Decisions $decisions The decisions made so far by the solver
      */
-    public function watch2OnHighest($decisionMap)
+    public function watch2OnHighest(Decisions $decisions)
     {
         $literals = $this->rule->getLiterals();
 
@@ -61,7 +61,7 @@ class RuleWatchNode
         $watchLevel = 0;
 
         foreach ($literals as $literal) {
-            $level = abs($decisionMap[abs($literal)]);
+            $level = $decisions->decisionLevel($literal);
 
             if ($level > $watchLevel) {
                 $this->rule->watch2 = $literal;

+ 61 - 150
src/Composer/DependencyResolver/Solver.php

@@ -29,12 +29,9 @@ class Solver
     protected $addedMap = array();
     protected $updateMap = array();
     protected $watchGraph;
-    protected $decisionMap;
+    protected $decisions;
     protected $installedMap;
 
-    protected $decisionQueue = array();
-    protected $decisionQueueWhy = array();
-    protected $decisionQueueFree = array();
     protected $propagateIndex;
     protected $branches = array();
     protected $problems = array();
@@ -48,21 +45,10 @@ class Solver
         $this->ruleSetGenerator = new RuleSetGenerator($policy, $pool);
     }
 
-    private function findDecisionRule($packageId)
-    {
-        foreach ($this->decisionQueue as $i => $literal) {
-            if ($packageId === abs($literal)) {
-                return $this->decisionQueueWhy[$i];
-            }
-        }
-
-        return null;
-    }
-
     // aka solver_makeruledecisions
     private function makeAssertionRuleDecisions()
     {
-        $decisionStart = count($this->decisionQueue);
+        $decisionStart = $this->decisions->getMaxOffset();
 
         for ($ruleIndex = 0; $ruleIndex < count($this->rules); $ruleIndex++) {
             $rule = $this->rules->ruleById($ruleIndex);
@@ -74,12 +60,12 @@ class Solver
             $literals = $rule->getLiterals();
             $literal = $literals[0];
 
-            if (!$this->decided(abs($literal))) {
-                $this->decide($literal, 1, $rule);
+            if (!$this->decisions->decided(abs($literal))) {
+                $this->decisions->decide($literal, 1, $rule);
                 continue;
             }
 
-            if ($this->decisionsSatisfy($literal)) {
+            if ($this->decisions->satisfy($literal)) {
                 continue;
             }
 
@@ -89,7 +75,7 @@ class Solver
                 continue;
             }
 
-            $conflict = $this->findDecisionRule(abs($literal));
+            $conflict = $this->decisions->decisionRule($literal);
 
             if ($conflict && RuleSet::TYPE_PACKAGE === $conflict->getType()) {
 
@@ -126,13 +112,7 @@ class Solver
             }
             $this->problems[] = $problem;
 
-            // start over
-            while (count($this->decisionQueue) > $decisionStart) {
-                $decisionLiteral = array_pop($this->decisionQueue);
-                array_pop($this->decisionQueueWhy);
-                unset($this->decisionQueueFree[count($this->decisionQueue)]);
-                $this->decisionMap[abs($decisionLiteral)] = 0;
-            }
+            $this->resetToOffset($decisionStart);
             $ruleIndex = -1;
         }
     }
@@ -177,11 +157,7 @@ class Solver
 
         $this->setupInstalledMap();
 
-        if (version_compare(PHP_VERSION, '5.3.4', '>=')) {
-            $this->decisionMap = new \SplFixedArray($this->pool->getMaxId() + 1);
-        } else {
-            $this->decisionMap = array_fill(0, $this->pool->getMaxId() + 1, 0);
-        }
+        $this->decisions = new Decisions($this->pool);
 
         $this->rules = $this->ruleSetGenerator->getRulesFor($this->jobs, $this->installedMap);
         $this->watchGraph = new RuleWatchGraph;
@@ -195,11 +171,18 @@ class Solver
 
         $this->runSat(true);
 
+        // decide to remove everything that's installed and undecided
+        foreach ($this->installedMap as $packageId => $void) {
+            if ($this->decisions->undecided($packageId)) {
+                $this->decisions->decide(-$packageId, 1, null);
+            }
+        }
+
         if ($this->problems) {
             throw new SolverProblemsException($this->problems);
         }
 
-        $transaction = new Transaction($this->policy, $this->pool, $this->installedMap, $this->decisionMap, $this->decisionQueue, $this->decisionQueueWhy);
+        $transaction = new Transaction($this->policy, $this->pool, $this->installedMap, $this->decisions);
 
         return $transaction->getOperations();
     }
@@ -211,77 +194,6 @@ class Solver
         return new Literal($package, $id > 0);
     }
 
-    protected function addDecision($literal, $level)
-    {
-        $packageId = abs($literal);
-
-        $previousDecision = $this->decisionMap[$packageId];
-        if ($previousDecision != 0) {
-            $literalString = $this->pool->literalToString($literal);
-            $package = $this->pool->literalToPackage($literal);
-            throw new SolverBugException(
-                "Trying to decide $literalString on level $level, even though $package was previously decided as ".(int) $previousDecision."."
-            );
-        }
-
-        if ($literal > 0) {
-            $this->decisionMap[$packageId] = $level;
-        } else {
-            $this->decisionMap[$packageId] = -$level;
-        }
-    }
-
-    public function decide($literal, $level, $why)
-    {
-        $this->addDecision($literal, $level);
-        $this->decisionQueue[] = $literal;
-        $this->decisionQueueWhy[] = $why;
-    }
-
-    public function decisionsContain($literal)
-    {
-        $packageId = abs($literal);
-
-        return (
-            $this->decisionMap[$packageId] > 0 && $literal > 0 ||
-            $this->decisionMap[$packageId] < 0 && $literal < 0
-        );
-    }
-
-    protected function decisionsSatisfy($literal)
-    {
-        $packageId = abs($literal);
-
-        return (
-            $literal > 0 && $this->decisionMap[$packageId] > 0 ||
-            $literal < 0 && $this->decisionMap[$packageId] < 0
-        );
-    }
-
-    public function decisionsConflict($literal)
-    {
-        $packageId = abs($literal);
-
-        return (
-            ($this->decisionMap[$packageId] > 0 && $literal < 0) ||
-            ($this->decisionMap[$packageId] < 0 && $literal > 0)
-        );
-    }
-    protected function decided($packageId)
-    {
-        return $this->decisionMap[$packageId] != 0;
-    }
-
-    protected function undecided($packageId)
-    {
-        return $this->decisionMap[$packageId] == 0;
-    }
-
-    protected function decidedInstall($packageId)
-    {
-        return $this->decisionMap[$packageId] > 0;
-    }
-
     /**
      * Makes a decision and propagates it to all rules.
      *
@@ -292,15 +204,17 @@ class Solver
      */
     protected function propagate($level)
     {
-        while ($this->propagateIndex < count($this->decisionQueue)) {
+        while ($this->decisions->validOffset($this->propagateIndex)) {
+            $decision = $this->decisions->atOffset($this->propagateIndex);
+
             $conflict = $this->watchGraph->propagateLiteral(
-                $this->decisionQueue[$this->propagateIndex++],
+                $decision[Decisions::DECISION_LITERAL],
                 $level,
-                array($this, 'decisionsContain'),
-                array($this, 'decisionsConflict'),
-                array($this, 'decide')
+                $this->decisions
             );
 
+            $this->propagateIndex++;
+
             if ($conflict) {
                 return $conflict;
             }
@@ -314,30 +228,27 @@ class Solver
      */
     private function revert($level)
     {
-        while (!empty($this->decisionQueue)) {
-            $literal = $this->decisionQueue[count($this->decisionQueue) - 1];
+        while (!$this->decisions->isEmpty()) {
+            $literal = $this->decisions->lastLiteral();
 
-            if (!$this->decisionMap[abs($literal)]) {
+            if ($this->decisions->undecided($literal)) {
                 break;
             }
 
-            $decisionLevel = abs($this->decisionMap[abs($literal)]);
+            $decisionLevel = $this->decisions->decisionLevel($literal);
 
             if ($decisionLevel <= $level) {
                 break;
             }
 
-            $this->decisionMap[abs($literal)] = 0;
-            array_pop($this->decisionQueue);
-            array_pop($this->decisionQueueWhy);
-
-            $this->propagateIndex = count($this->decisionQueue);
+            $this->decisions->revertLast();
+            $this->propagateIndex = $this->decisions->getMaxOffset() + 1;
         }
 
         while (!empty($this->branches)) {
             list($literals, $branchLevel) = $this->branches[count($this->branches) - 1];
 
-            if ($branchLevel >= $level) {
+            if ($branchLevel < $level) {
                 break;
             }
 
@@ -349,7 +260,7 @@ class Solver
      *
      * setpropagatelearn
      *
-     * add free decision (solvable to install) to decisionq
+     * add free decision (a positive literal) to decision queue
      * increase level and propagate decision
      * return if no conflict.
      *
@@ -364,8 +275,7 @@ class Solver
     {
         $level++;
 
-        $this->decide($literal, $level, $rule);
-        $this->decisionQueueFree[count($this->decisionQueueWhy) - 1] = true;
+        $this->decisions->decide($literal, $level, $rule, true);
 
         while (true) {
             $rule = $this->propagate($level);
@@ -400,10 +310,10 @@ class Solver
             $this->learnedWhy[$newRule->getId()] = $why;
 
             $ruleNode = new RuleWatchNode($newRule);
-            $ruleNode->watch2OnHighest($this->decisionMap);
+            $ruleNode->watch2OnHighest($this->decisions);
             $this->watchGraph->insert($ruleNode);
 
-            $this->decide($learnLiteral, $level, $newRule);
+            $this->decisions->decide($learnLiteral, $level, $newRule);
         }
 
         return $level;
@@ -433,7 +343,7 @@ class Solver
         $seen = array();
         $learnedLiterals = array(null);
 
-        $decisionId = count($this->decisionQueue);
+        $decisionId = $this->decisions->getMaxOffset() + 1;
 
         $this->learnedPool[] = array();
 
@@ -442,7 +352,7 @@ class Solver
 
             foreach ($rule->getLiterals() as $literal) {
                 // skip the one true literal
-                if ($this->decisionsSatisfy($literal)) {
+                if ($this->decisions->satisfy($literal)) {
                     continue;
                 }
 
@@ -451,7 +361,7 @@ class Solver
                 }
                 $seen[abs($literal)] = true;
 
-                $l = abs($this->decisionMap[abs($literal)]);
+                $l = $this->decisions->decisionLevel($literal);
 
                 if (1 === $l) {
                     $l1num++;
@@ -485,7 +395,8 @@ class Solver
 
                     $decisionId--;
 
-                    $literal = $this->decisionQueue[$decisionId];
+                    $decision = $this->decisions->atOffset($decisionId);
+                    $literal = $decision[Decisions::DECISION_LITERAL];
 
                     if (isset($seen[abs($literal)])) {
                         break;
@@ -512,7 +423,8 @@ class Solver
                 }
             }
 
-            $rule = $this->decisionQueueWhy[$decisionId];
+            $decision = $this->decisions->atOffset($decisionId);
+            $rule = $decision[Decisions::DECISION_REASON];
         }
 
         $why = count($this->learnedPool) - 1;
@@ -565,34 +477,35 @@ class Solver
 
         foreach ($literals as $literal) {
             // skip the one true literal
-            if ($this->decisionsSatisfy($literal)) {
+            if ($this->decisions->satisfy($literal)) {
                 continue;
             }
             $seen[abs($literal)] = true;
         }
 
-        $decisionId = count($this->decisionQueue);
+        $decisionId = $this->decisions->getMaxOffset() + 1;
 
         while ($decisionId > 0) {
             $decisionId--;
 
-            $literal = $this->decisionQueue[$decisionId];
+            $decision = $this->decisions->atOffset($decisionId);
+            $literal = $decision[Decisions::DECISION_LITERAL];
 
             // skip literals that are not in this rule
             if (!isset($seen[abs($literal)])) {
                 continue;
             }
 
-            $why = $this->decisionQueueWhy[$decisionId];
-            $problem->addRule($why);
+            $why = $decision[Decisions::DECISION_REASON];
 
+            $problem->addRule($why);
             $this->analyzeUnsolvableRule($problem, $why);
 
             $literals = $why->getLiterals();
 
             foreach ($literals as $literal) {
                 // skip the one true literal
-                if ($this->decisionsSatisfy($literal)) {
+                if ($this->decisions->satisfy($literal)) {
                     continue;
                 }
                 $seen[abs($literal)] = true;
@@ -630,12 +543,8 @@ class Solver
 
     private function resetSolver()
     {
-        while ($literal = array_pop($this->decisionQueue)) {
-            $this->decisionMap[abs($literal)] = 0;
-        }
+        $this->decisions->reset();
 
-        $this->decisionQueueWhy = array();
-        $this->decisionQueueFree = array();
         $this->propagateIndex = 0;
         $this->branches = array();
 
@@ -717,11 +626,11 @@ class Solver
                         $noneSatisfied = true;
 
                         foreach ($rule->getLiterals() as $literal) {
-                            if ($this->decisionsSatisfy($literal)) {
+                            if ($this->decisions->satisfy($literal)) {
                                 $noneSatisfied = false;
                                 break;
                             }
-                            if ($literal > 0 && $this->undecided($literal)) {
+                            if ($literal > 0 && $this->decisions->undecided($literal)) {
                                 $decisionQueue[] = $literal;
                             }
                         }
@@ -794,14 +703,14 @@ class Solver
                 //
                 foreach ($literals as $literal) {
                     if ($literal <= 0) {
-                        if (!$this->decidedInstall(abs($literal))) {
+                        if (!$this->decisions->decidedInstall(abs($literal))) {
                             continue 2; // next rule
                         }
                     } else {
-                        if ($this->decidedInstall(abs($literal))) {
+                        if ($this->decisions->decidedInstall(abs($literal))) {
                             continue 2; // next rule
                         }
-                        if ($this->undecided(abs($literal))) {
+                        if ($this->decisions->undecided(abs($literal))) {
                             $decisionQueue[] = $literal;
                         }
                     }
@@ -834,28 +743,30 @@ class Solver
                 $lastLevel = null;
                 $lastBranchIndex = 0;
                 $lastBranchOffset  = 0;
+                $l = 0;
 
                 for ($i = count($this->branches) - 1; $i >= 0; $i--) {
-                    list($literals, $level) = $this->branches[$i];
+                    list($literals, $l) = $this->branches[$i];
 
                     foreach ($literals as $offset => $literal) {
-                        if ($literal && $literal > 0 && $this->decisionMap[abs($literal)] > $level + 1) {
+                        if ($literal && $literal > 0 && $this->decisions->decisionLevel($literal) > $l + 1) {
                             $lastLiteral = $literal;
                             $lastBranchIndex = $i;
                             $lastBranchOffset = $offset;
-                            $lastLevel = $level;
+                            $lastLevel = $l;
                         }
                     }
                 }
 
                 if ($lastLiteral) {
-                    $this->branches[$lastBranchIndex][$lastBranchOffset] = null;
+                    unset($this->branches[$lastBranchIndex][0][$lastBranchOffset]);
+                    $this->branches[$lastBranchIndex][0] = array_values($this->branches[$lastBranchIndex][0]);
                     $minimizationSteps++;
 
                     $level = $lastLevel;
                     $this->revert($level);
 
-                    $why = $this->decisionQueueWhy[count($this->decisionQueueWhy) - 1];
+                    $why = $this->decisions->lastReason();
 
                     $oLevel = $level;
                     $level = $this->setPropagateLearn($level, $lastLiteral, $disableRules, $why);

+ 157 - 85
src/Composer/DependencyResolver/Transaction.php

@@ -23,47 +23,30 @@ class Transaction
     protected $policy;
     protected $pool;
     protected $installedMap;
-    protected $decisionMap;
-    protected $decisionQueue;
-    protected $decisionQueueWhy;
+    protected $decisions;
+    protected $transaction;
 
-    public function __construct($policy, $pool, $installedMap, $decisionMap, array $decisionQueue, $decisionQueueWhy)
+    public function __construct($policy, $pool, $installedMap, $decisions)
     {
         $this->policy = $policy;
         $this->pool = $pool;
         $this->installedMap = $installedMap;
-        $this->decisionMap = $decisionMap;
-        $this->decisionQueue = $decisionQueue;
-        $this->decisionQueueWhy = $decisionQueueWhy;
+        $this->decisions = $decisions;
+        $this->transaction = array();
     }
 
     public function getOperations()
     {
-        $transaction = array();
-        $installMeansUpdateMap = array();
+        $installMeansUpdateMap = $this->findUpdates();
 
-        foreach ($this->decisionQueue as $i => $literal) {
-            $package = $this->pool->literalToPackage($literal);
+        $updateMap = array();
+        $installMap = array();
+        $uninstallMap = array();
 
-            // !wanted & installed
-            if ($literal <= 0 && isset($this->installedMap[$package->getId()])) {
-                $updates = $this->policy->findUpdatePackages($this->pool, $this->installedMap, $package);
-
-                $literals = array($package->getId());
-
-                foreach ($updates as $update) {
-                    $literals[] = $update->getId();
-                }
-
-                foreach ($literals as $updateLiteral) {
-                    if ($updateLiteral !== $literal) {
-                        $installMeansUpdateMap[abs($updateLiteral)] = $package;
-                    }
-                }
-            }
-        }
+        foreach ($this->decisions as $i => $decision) {
+            $literal = $decision[Decisions::DECISION_LITERAL];
+            $reason = $decision[Decisions::DECISION_REASON];
 
-        foreach ($this->decisionQueue as $i => $literal) {
             $package = $this->pool->literalToPackage($literal);
 
             // wanted & installed || !wanted & !installed
@@ -72,96 +55,185 @@ class Transaction
             }
 
             if ($literal > 0) {
-                if ($package instanceof AliasPackage) {
-                    $transaction[] = new Operation\MarkAliasInstalledOperation(
-                        $package, $this->decisionQueueWhy[$i]
-                    );
-                    continue;
-                }
-
-                if (isset($installMeansUpdateMap[abs($literal)])) {
+                if (isset($installMeansUpdateMap[abs($literal)]) && !$package instanceof AliasPackage) {
 
                     $source = $installMeansUpdateMap[abs($literal)];
 
-                    $transaction[] = new Operation\UpdateOperation(
-                        $source, $package, $this->decisionQueueWhy[$i]
+                    $updateMap[$package->getId()] = array(
+                        'package' => $package,
+                        'source' => $source,
+                        'reason' => $reason,
                     );
 
                     // avoid updates to one package from multiple origins
                     unset($installMeansUpdateMap[abs($literal)]);
                     $ignoreRemove[$source->getId()] = true;
                 } else {
-                    $transaction[] = new Operation\InstallOperation(
-                        $package, $this->decisionQueueWhy[$i]
-                    );
-                }
-            } elseif (!isset($ignoreRemove[$package->getId()])) {
-                if ($package instanceof AliasPackage) {
-                    $transaction[] = new Operation\MarkAliasInstalledOperation(
-                        $package, $this->decisionQueueWhy[$i]
-                    );
-                } else {
-                    $transaction[] = new Operation\UninstallOperation(
-                        $package, $this->decisionQueueWhy[$i]
+                    $installMap[$package->getId()] = array(
+                        'package' => $package,
+                        'reason' => $reason,
                     );
                 }
             }
         }
 
-        $allDecidedMap = $this->decisionMap;
-        foreach ($this->decisionMap as $packageId => $decision) {
-            if ($decision != 0) {
-                $package = $this->pool->packageById($packageId);
-                if ($package instanceof AliasPackage) {
-                    $allDecidedMap[$package->getAliasOf()->getId()] = $decision;
-                }
+        foreach ($this->decisions as $i => $decision) {
+            $literal = $decision[Decisions::DECISION_LITERAL];
+            $package = $this->pool->literalToPackage($literal);
+
+            if ($literal <= 0 &&
+                isset($this->installedMap[$package->getId()]) &&
+                !isset($ignoreRemove[$package->getId()])) {
+                $uninstallMap[$package->getId()] = array(
+                    'package' => $package,
+                    'reason' => $reason,
+                );
+
             }
         }
 
-        foreach ($allDecidedMap as $packageId => $decision) {
-            if ($packageId === 0) {
-                continue;
-            }
+        $this->transactionFromMaps($installMap, $updateMap, $uninstallMap);
+
+        return $this->transaction;
+    }
+
+    protected function transactionFromMaps($installMap, $updateMap, $uninstallMap)
+    {
+        $queue = array_map(function ($operation) {
+                return $operation['package'];
+            },
+            $this->findRootPackages($installMap, $updateMap)
+        );
 
-            if (0 == $decision && isset($this->installedMap[$packageId])) {
-                $package = $this->pool->packageById($packageId);
+        $visited = array();
+
+        while (!empty($queue)) {
+            $package = array_pop($queue);
+            $packageId = $package->getId();
+
+            if (!isset($visited[$packageId])) {
+                array_push($queue, $package);
 
                 if ($package instanceof AliasPackage) {
-                    $transaction[] = new Operation\MarkAliasInstalledOperation(
-                        $package, null
-                    );
+                    array_push($queue, $package->getAliasOf());
                 } else {
-                    $transaction[] = new Operation\UninstallOperation(
-                        $package, null
-                    );
+                    foreach ($package->getRequires() as $link) {
+                        $possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint());
+
+                        foreach ($possibleRequires as $require) {
+                            array_push($queue, $require);
+                        }
+                    }
                 }
 
-                $this->decisionMap[$packageId] = -1;
+                $visited[$package->getId()] = true;
+            } else {
+                if (isset($installMap[$packageId])) {
+                    $this->install(
+                        $installMap[$packageId]['package'],
+                        $installMap[$packageId]['reason']
+                    );
+                    unset($installMap[$packageId]);
+                }
+                if (isset($updateMap[$packageId])) {
+                    $this->update(
+                        $updateMap[$packageId]['source'],
+                        $updateMap[$packageId]['package'],
+                        $updateMap[$packageId]['reason']
+                    );
+                    unset($updateMap[$packageId]);
+                }
             }
         }
 
-        foreach ($allDecidedMap as $packageId => $decision) {
-            if ($packageId === 0) {
+        foreach ($uninstallMap as $uninstall) {
+            $this->uninstall($uninstall['package'], $uninstall['reason']);
+        }
+    }
+
+    protected function findRootPackages($installMap, $updateMap)
+    {
+        $packages = $installMap + $updateMap;
+        $roots = $packages;
+
+        foreach ($packages as $packageId => $operation) {
+            $package = $operation['package'];
+
+            if (!isset($roots[$packageId])) {
                 continue;
             }
 
-            if (0 == $decision && isset($this->installedMap[$packageId])) {
-                $package = $this->pool->packageById($packageId);
+            foreach ($package->getRequires() as $link) {
+                $possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint());
 
-                if ($package instanceof AliasPackage) {
-                    $transaction[] = new Operation\MarkAliasInstalledOperation(
-                        $package, null
-                    );
-                } else {
-                    $transaction[] = new Operation\UninstallOperation(
-                        $package, null
-                    );
+                foreach ($possibleRequires as $require) {
+                    unset($roots[$require->getId()]);
                 }
+            }
+        }
+
+        return $roots;
+    }
+
+    protected function findUpdates()
+    {
+        $installMeansUpdateMap = array();
+
+        foreach ($this->decisions as $i => $decision) {
+            $literal = $decision[Decisions::DECISION_LITERAL];
+            $package = $this->pool->literalToPackage($literal);
 
-                $this->decisionMap[$packageId] = -1;
+            // !wanted & installed
+            if ($literal <= 0 && isset($this->installedMap[$package->getId()])) {
+                $updates = $this->policy->findUpdatePackages($this->pool, $this->installedMap, $package);
+
+                $literals = array($package->getId());
+
+                foreach ($updates as $update) {
+                    $literals[] = $update->getId();
+                }
+
+                foreach ($literals as $updateLiteral) {
+                    if ($updateLiteral !== $literal) {
+                        $installMeansUpdateMap[abs($updateLiteral)] = $package;
+                    }
+                }
             }
         }
 
-        return array_reverse($transaction);
+        return $installMeansUpdateMap;
+    }
+
+    protected function install($package, $reason)
+    {
+        if ($package instanceof AliasPackage) {
+            return $this->markAliasInstalled($package, $reason);
+        }
+
+        $this->transaction[] = new Operation\InstallOperation($package, $reason);
+    }
+
+    protected function update($from, $to, $reason)
+    {
+        $this->transaction[] = new Operation\UpdateOperation($from, $to, $reason);
+    }
+
+    protected function uninstall($package, $reason)
+    {
+        if ($package instanceof AliasPackage) {
+            return $this->markAliasUninstalled($package, $reason);
+        }
+
+        $this->transaction[] = new Operation\UninstallOperation($package, $reason);
+    }
+
+    protected function markAliasInstalled($package, $reason)
+    {
+        $this->transaction[] = new Operation\MarkAliasInstalledOperation($package, $reason);
+    }
+
+    protected function markAliasUninstalled($package, $reason)
+    {
+        $this->transaction[] = new Operation\MarkAliasUninstalledOperation($package, $reason);
     }
 }

+ 38 - 10
tests/Composer/Test/DependencyResolver/SolverTest.php

@@ -118,6 +118,33 @@ class SolverTest extends TestCase
         ));
     }
 
+    public function testSolverInstallWithDepsInOrder()
+    {
+        $this->repo->addPackage($packageA = $this->getPackage('A', '1.0'));
+        $this->repo->addPackage($packageB = $this->getPackage('B', '1.0'));
+        $this->repo->addPackage($packageC = $this->getPackage('C', '1.0'));
+
+        $packageB->setRequires(array(
+            new Link('B', 'A', $this->getVersionConstraint('>=', '1.0'), 'requires'),
+            new Link('B', 'C', $this->getVersionConstraint('>=', '1.0'), 'requires'),
+        ));
+        $packageC->setRequires(array(
+            new Link('C', 'A', $this->getVersionConstraint('>=', '1.0'), 'requires'),
+        ));
+
+        $this->reposComplete();
+
+        $this->request->install('A');
+        $this->request->install('B');
+        $this->request->install('C');
+
+        $this->checkSolverResult(array(
+            array('job' => 'install', 'package' => $packageA),
+            array('job' => 'install', 'package' => $packageC),
+            array('job' => 'install', 'package' => $packageB),
+        ));
+    }
+
     public function testSolverInstallInstalled()
     {
         $this->repoInstalled->addPackage($this->getPackage('A', '1.0'));
@@ -291,15 +318,16 @@ class SolverTest extends TestCase
         $this->request->update('A', $this->getVersionConstraint('=', '1.0.0.0'));
 
         $this->checkSolverResult(array(
-            array(
-                'job' => 'remove',
-                'package' => $packageB,
-            ),
             array(
                 'job' => 'update',
                 'from' => $packageA,
                 'to' => $newPackageA,
-        )));
+            ),
+            array(
+                'job' => 'remove',
+                'package' => $packageB,
+            ),
+        ));
     }
 
     public function testSolverAllJobs()
@@ -324,8 +352,8 @@ class SolverTest extends TestCase
         $this->checkSolverResult(array(
             array('job' => 'update',  'from' => $oldPackageC, 'to' => $packageC),
             array('job' => 'install', 'package' => $packageB),
-            array('job' => 'remove',  'package' => $packageD),
             array('job' => 'install', 'package' => $packageA),
+            array('job' => 'remove',  'package' => $packageD),
         ));
     }
 
@@ -477,8 +505,8 @@ class SolverTest extends TestCase
         $this->request->install('X');
 
         $this->checkSolverResult(array(
-            array('job' => 'install', 'package' => $packageA),
             array('job' => 'install', 'package' => $newPackageB),
+            array('job' => 'install', 'package' => $packageA),
             array('job' => 'install', 'package' => $packageX),
         ));
     }
@@ -520,9 +548,9 @@ class SolverTest extends TestCase
         $this->request->install('A');
 
         $this->checkSolverResult(array(
-            array('job' => 'install', 'package' => $packageC),
             array('job' => 'install', 'package' => $packageB),
             array('job' => 'install', 'package' => $packageA),
+            array('job' => 'install', 'package' => $packageC),
         ));
     }
 
@@ -690,8 +718,8 @@ class SolverTest extends TestCase
         $this->request->install('A', $this->getVersionConstraint('==', '1.1.0.0'));
 
         $this->checkSolverResult(array(
-            array('job' => 'install', 'package' => $packageB),
             array('job' => 'install', 'package' => $packageA2),
+            array('job' => 'install', 'package' => $packageB),
             array('job' => 'install', 'package' => $packageA2Alias),
         ));
     }
@@ -713,9 +741,9 @@ class SolverTest extends TestCase
         $this->request->install('B');
 
         $this->checkSolverResult(array(
+            array('job' => 'install', 'package' => $packageA),
             array('job' => 'install', 'package' => $packageAAlias),
             array('job' => 'install', 'package' => $packageB),
-            array('job' => 'install', 'package' => $packageA),
         ));
     }
 

+ 2 - 2
tests/Composer/Test/Fixtures/installer/aliased-priority-conflicting.test

@@ -45,7 +45,7 @@ Aliases take precedence over default package even if default is selected
 --RUN--
 install
 --EXPECT--
-Marking a/req (dev-master feat.f) as installed, alias of a/req (dev-feature-foo feat.f)
 Installing a/req (dev-feature-foo feat.f)
-Installing a/b (dev-master)
+Marking a/req (dev-master feat.f) as installed, alias of a/req (dev-feature-foo feat.f)
 Installing a/a (dev-master)
+Installing a/b (dev-master)

+ 3 - 3
tests/Composer/Test/Fixtures/installer/aliased-priority.test

@@ -47,9 +47,9 @@ Aliases take precedence over default package
 --RUN--
 install
 --EXPECT--
-Marking a/b (1.0.x-dev forked) as installed, alias of a/b (dev-master forked)
-Installing a/b (dev-master forked)
+Installing a/c (dev-feature-foo feat.f)
 Marking a/c (dev-master feat.f) as installed, alias of a/c (dev-feature-foo feat.f)
+Installing a/b (dev-master forked)
 Installing a/a (dev-master master)
-Installing a/c (dev-feature-foo feat.f)
 Marking a/a (1.0.x-dev master) as installed, alias of a/a (dev-master master)
+Marking a/b (1.0.x-dev forked) as installed, alias of a/b (dev-master forked)