PageRenderTime 38ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Composer/DependencyResolver/Solver.php

https://github.com/skug/composer
PHP | 778 lines | 529 code | 163 blank | 86 comment | 104 complexity | 73d710e8934819d267096e93d26e42fb MD5 | raw file
  1. <?php
  2. /*
  3. * This file is part of Composer.
  4. *
  5. * (c) Nils Adermann <naderman@naderman.de>
  6. * Jordi Boggiano <j.boggiano@seld.be>
  7. *
  8. * For the full copyright and license information, please view the LICENSE
  9. * file that was distributed with this source code.
  10. */
  11. namespace Composer\DependencyResolver;
  12. use Composer\Repository\RepositoryInterface;
  13. /**
  14. * @author Nils Adermann <naderman@naderman.de>
  15. */
  16. class Solver
  17. {
  18. const BRANCH_LITERALS = 0;
  19. const BRANCH_LEVEL = 1;
  20. protected $policy;
  21. protected $pool;
  22. protected $installed;
  23. protected $rules;
  24. protected $ruleSetGenerator;
  25. protected $updateAll;
  26. protected $addedMap = array();
  27. protected $updateMap = array();
  28. protected $watchGraph;
  29. protected $decisions;
  30. protected $installedMap;
  31. protected $propagateIndex;
  32. protected $branches = array();
  33. protected $problems = array();
  34. protected $learnedPool = array();
  35. public function __construct(PolicyInterface $policy, Pool $pool, RepositoryInterface $installed)
  36. {
  37. $this->policy = $policy;
  38. $this->pool = $pool;
  39. $this->installed = $installed;
  40. $this->ruleSetGenerator = new RuleSetGenerator($policy, $pool);
  41. }
  42. // aka solver_makeruledecisions
  43. private function makeAssertionRuleDecisions()
  44. {
  45. $decisionStart = count($this->decisions) - 1;
  46. for ($ruleIndex = 0; $ruleIndex < count($this->rules); $ruleIndex++) {
  47. $rule = $this->rules->ruleById($ruleIndex);
  48. if (!$rule->isAssertion() || $rule->isDisabled()) {
  49. continue;
  50. }
  51. $literals = $rule->getLiterals();
  52. $literal = $literals[0];
  53. if (!$this->decisions->decided(abs($literal))) {
  54. $this->decisions->decide($literal, 1, $rule);
  55. continue;
  56. }
  57. if ($this->decisions->satisfy($literal)) {
  58. continue;
  59. }
  60. // found a conflict
  61. if (RuleSet::TYPE_LEARNED === $rule->getType()) {
  62. $rule->disable();
  63. continue;
  64. }
  65. $conflict = $this->decisions->decisionRule($literal);
  66. if ($conflict && RuleSet::TYPE_PACKAGE === $conflict->getType()) {
  67. $problem = new Problem;
  68. $problem->addRule($rule);
  69. $problem->addRule($conflict);
  70. $this->disableProblem($rule);
  71. $this->problems[] = $problem;
  72. continue;
  73. }
  74. // conflict with another job
  75. $problem = new Problem;
  76. $problem->addRule($rule);
  77. $problem->addRule($conflict);
  78. // push all of our rules (can only be job rules)
  79. // asserting this literal on the problem stack
  80. foreach ($this->rules->getIteratorFor(RuleSet::TYPE_JOB) as $assertRule) {
  81. if ($assertRule->isDisabled() || !$assertRule->isAssertion()) {
  82. continue;
  83. }
  84. $assertRuleLiterals = $assertRule->getLiterals();
  85. $assertRuleLiteral = $assertRuleLiterals[0];
  86. if (abs($literal) !== abs($assertRuleLiteral)) {
  87. continue;
  88. }
  89. $problem->addRule($assertRule);
  90. $this->disableProblem($assertRule);
  91. }
  92. $this->problems[] = $problem;
  93. $this->resetToOffset($decisionStart);
  94. $ruleIndex = -1;
  95. }
  96. }
  97. protected function setupInstalledMap()
  98. {
  99. $this->installedMap = array();
  100. foreach ($this->installed->getPackages() as $package) {
  101. $this->installedMap[$package->getId()] = $package;
  102. }
  103. foreach ($this->jobs as $job) {
  104. switch ($job['cmd']) {
  105. case 'update':
  106. foreach ($job['packages'] as $package) {
  107. if (isset($this->installedMap[$package->getId()])) {
  108. $this->updateMap[$package->getId()] = true;
  109. }
  110. }
  111. break;
  112. case 'update-all':
  113. foreach ($this->installedMap as $package) {
  114. $this->updateMap[$package->getId()] = true;
  115. }
  116. break;
  117. case 'install':
  118. if (!$job['packages']) {
  119. $problem = new Problem();
  120. $problem->addRule(new Rule($this->pool, array(), null, null, $job));
  121. $this->problems[] = $problem;
  122. }
  123. break;
  124. }
  125. }
  126. }
  127. public function solve(Request $request)
  128. {
  129. $this->jobs = $request->getJobs();
  130. $this->setupInstalledMap();
  131. $this->decisions = new Decisions($this->pool);
  132. $this->rules = $this->ruleSetGenerator->getRulesFor($this->jobs, $this->installedMap);
  133. $this->watchGraph = new RuleWatchGraph;
  134. foreach ($this->rules as $rule) {
  135. $this->watchGraph->insert(new RuleWatchNode($rule));
  136. }
  137. /* make decisions based on job/update assertions */
  138. $this->makeAssertionRuleDecisions();
  139. $this->runSat(true);
  140. // decide to remove everything that's installed and undecided
  141. foreach ($this->installedMap as $packageId => $void) {
  142. if ($this->decisions->undecided($packageId)) {
  143. $this->decisions->decide(-$packageId, 1, null);
  144. }
  145. }
  146. if ($this->problems) {
  147. throw new SolverProblemsException($this->problems, $this->installedMap);
  148. }
  149. $transaction = new Transaction($this->policy, $this->pool, $this->installedMap, $this->decisions);
  150. return $transaction->getOperations();
  151. }
  152. protected function literalFromId($id)
  153. {
  154. $package = $this->pool->packageById(abs($id));
  155. return new Literal($package, $id > 0);
  156. }
  157. /**
  158. * Makes a decision and propagates it to all rules.
  159. *
  160. * Evaluates each term affected by the decision (linked through watches)
  161. * If we find unit rules we make new decisions based on them
  162. *
  163. * @return Rule|null A rule on conflict, otherwise null.
  164. */
  165. protected function propagate($level)
  166. {
  167. while ($this->decisions->validOffset($this->propagateIndex)) {
  168. $decision = $this->decisions->atOffset($this->propagateIndex);
  169. $conflict = $this->watchGraph->propagateLiteral(
  170. $decision[Decisions::DECISION_LITERAL],
  171. $level,
  172. $this->decisions
  173. );
  174. $this->propagateIndex++;
  175. if ($conflict) {
  176. return $conflict;
  177. }
  178. }
  179. return null;
  180. }
  181. /**
  182. * Reverts a decision at the given level.
  183. */
  184. private function revert($level)
  185. {
  186. while (!$this->decisions->isEmpty()) {
  187. $literal = $this->decisions->lastLiteral();
  188. if ($this->decisions->undecided($literal)) {
  189. break;
  190. }
  191. $decisionLevel = $this->decisions->decisionLevel($literal);
  192. if ($decisionLevel <= $level) {
  193. break;
  194. }
  195. $this->decisions->revertLast();
  196. $this->propagateIndex = count($this->decisions);
  197. }
  198. while (!empty($this->branches) && $this->branches[count($this->branches) - 1][self::BRANCH_LEVEL] >= $level) {
  199. array_pop($this->branches);
  200. }
  201. }
  202. /**-------------------------------------------------------------------
  203. *
  204. * setpropagatelearn
  205. *
  206. * add free decision (a positive literal) to decision queue
  207. * increase level and propagate decision
  208. * return if no conflict.
  209. *
  210. * in conflict case, analyze conflict rule, add resulting
  211. * rule to learnt rule set, make decision from learnt
  212. * rule (always unit) and re-propagate.
  213. *
  214. * returns the new solver level or 0 if unsolvable
  215. *
  216. */
  217. private function setPropagateLearn($level, $literal, $disableRules, Rule $rule)
  218. {
  219. $level++;
  220. $this->decisions->decide($literal, $level, $rule);
  221. while (true) {
  222. $rule = $this->propagate($level);
  223. if (!$rule) {
  224. break;
  225. }
  226. if ($level == 1) {
  227. return $this->analyzeUnsolvable($rule, $disableRules);
  228. }
  229. // conflict
  230. list($learnLiteral, $newLevel, $newRule, $why) = $this->analyze($level, $rule);
  231. if ($newLevel <= 0 || $newLevel >= $level) {
  232. throw new SolverBugException(
  233. "Trying to revert to invalid level ".(int) $newLevel." from level ".(int) $level."."
  234. );
  235. } elseif (!$newRule) {
  236. throw new SolverBugException(
  237. "No rule was learned from analyzing $rule at level $level."
  238. );
  239. }
  240. $level = $newLevel;
  241. $this->revert($level);
  242. $this->rules->add($newRule, RuleSet::TYPE_LEARNED);
  243. $this->learnedWhy[$newRule->getId()] = $why;
  244. $ruleNode = new RuleWatchNode($newRule);
  245. $ruleNode->watch2OnHighest($this->decisions);
  246. $this->watchGraph->insert($ruleNode);
  247. $this->decisions->decide($learnLiteral, $level, $newRule);
  248. }
  249. return $level;
  250. }
  251. private function selectAndInstall($level, array $decisionQueue, $disableRules, Rule $rule)
  252. {
  253. // choose best package to install from decisionQueue
  254. $literals = $this->policy->selectPreferedPackages($this->pool, $this->installedMap, $decisionQueue);
  255. $selectedLiteral = array_shift($literals);
  256. // if there are multiple candidates, then branch
  257. if (count($literals)) {
  258. $this->branches[] = array($literals, $level);
  259. }
  260. return $this->setPropagateLearn($level, $selectedLiteral, $disableRules, $rule);
  261. }
  262. protected function analyze($level, $rule)
  263. {
  264. $analyzedRule = $rule;
  265. $ruleLevel = 1;
  266. $num = 0;
  267. $l1num = 0;
  268. $seen = array();
  269. $learnedLiterals = array(null);
  270. $decisionId = count($this->decisions);
  271. $this->learnedPool[] = array();
  272. while (true) {
  273. $this->learnedPool[count($this->learnedPool) - 1][] = $rule;
  274. foreach ($rule->getLiterals() as $literal) {
  275. // skip the one true literal
  276. if ($this->decisions->satisfy($literal)) {
  277. continue;
  278. }
  279. if (isset($seen[abs($literal)])) {
  280. continue;
  281. }
  282. $seen[abs($literal)] = true;
  283. $l = $this->decisions->decisionLevel($literal);
  284. if (1 === $l) {
  285. $l1num++;
  286. } elseif ($level === $l) {
  287. $num++;
  288. } else {
  289. // not level1 or conflict level, add to new rule
  290. $learnedLiterals[] = $literal;
  291. if ($l > $ruleLevel) {
  292. $ruleLevel = $l;
  293. }
  294. }
  295. }
  296. $l1retry = true;
  297. while ($l1retry) {
  298. $l1retry = false;
  299. if (!$num && !--$l1num) {
  300. // all level 1 literals done
  301. break 2;
  302. }
  303. while (true) {
  304. if ($decisionId <= 0) {
  305. throw new SolverBugException(
  306. "Reached invalid decision id $decisionId while looking through $rule for a literal present in the analyzed rule $analyzedRule."
  307. );
  308. }
  309. $decisionId--;
  310. $decision = $this->decisions->atOffset($decisionId);
  311. $literal = $decision[Decisions::DECISION_LITERAL];
  312. if (isset($seen[abs($literal)])) {
  313. break;
  314. }
  315. }
  316. unset($seen[abs($literal)]);
  317. if ($num && 0 === --$num) {
  318. $learnedLiterals[0] = -abs($literal);
  319. if (!$l1num) {
  320. break 2;
  321. }
  322. foreach ($learnedLiterals as $i => $learnedLiteral) {
  323. if ($i !== 0) {
  324. unset($seen[abs($learnedLiteral)]);
  325. }
  326. }
  327. // only level 1 marks left
  328. $l1num++;
  329. $l1retry = true;
  330. }
  331. }
  332. $decision = $this->decisions->atOffset($decisionId);
  333. $rule = $decision[Decisions::DECISION_REASON];
  334. }
  335. $why = count($this->learnedPool) - 1;
  336. if (!$learnedLiterals[0]) {
  337. throw new SolverBugException(
  338. "Did not find a learnable literal in analyzed rule $analyzedRule."
  339. );
  340. }
  341. $newRule = new Rule($this->pool, $learnedLiterals, Rule::RULE_LEARNED, $why);
  342. return array($learnedLiterals[0], $ruleLevel, $newRule, $why);
  343. }
  344. private function analyzeUnsolvableRule($problem, $conflictRule)
  345. {
  346. $why = $conflictRule->getId();
  347. if ($conflictRule->getType() == RuleSet::TYPE_LEARNED) {
  348. $learnedWhy = $this->learnedWhy[$why];
  349. $problemRules = $this->learnedPool[$learnedWhy];
  350. foreach ($problemRules as $problemRule) {
  351. $this->analyzeUnsolvableRule($problem, $problemRule);
  352. }
  353. return;
  354. }
  355. if ($conflictRule->getType() == RuleSet::TYPE_PACKAGE) {
  356. // package rules cannot be part of a problem
  357. return;
  358. }
  359. $problem->nextSection();
  360. $problem->addRule($conflictRule);
  361. }
  362. private function analyzeUnsolvable($conflictRule, $disableRules)
  363. {
  364. $problem = new Problem;
  365. $problem->addRule($conflictRule);
  366. $this->analyzeUnsolvableRule($problem, $conflictRule);
  367. $this->problems[] = $problem;
  368. $seen = array();
  369. $literals = $conflictRule->getLiterals();
  370. foreach ($literals as $literal) {
  371. // skip the one true literal
  372. if ($this->decisions->satisfy($literal)) {
  373. continue;
  374. }
  375. $seen[abs($literal)] = true;
  376. }
  377. foreach ($this->decisions as $decision) {
  378. $literal = $decision[Decisions::DECISION_LITERAL];
  379. // skip literals that are not in this rule
  380. if (!isset($seen[abs($literal)])) {
  381. continue;
  382. }
  383. $why = $decision[Decisions::DECISION_REASON];
  384. $problem->addRule($why);
  385. $this->analyzeUnsolvableRule($problem, $why);
  386. $literals = $why->getLiterals();
  387. foreach ($literals as $literal) {
  388. // skip the one true literal
  389. if ($this->decisions->satisfy($literal)) {
  390. continue;
  391. }
  392. $seen[abs($literal)] = true;
  393. }
  394. }
  395. if ($disableRules) {
  396. foreach ($this->problems[count($this->problems) - 1] as $reason) {
  397. $this->disableProblem($reason['rule']);
  398. }
  399. $this->resetSolver();
  400. return 1;
  401. }
  402. return 0;
  403. }
  404. private function disableProblem($why)
  405. {
  406. $job = $why->getJob();
  407. if (!$job) {
  408. $why->disable();
  409. return;
  410. }
  411. // disable all rules of this job
  412. foreach ($this->rules as $rule) {
  413. if ($job === $rule->getJob()) {
  414. $rule->disable();
  415. }
  416. }
  417. }
  418. private function resetSolver()
  419. {
  420. $this->decisions->reset();
  421. $this->propagateIndex = 0;
  422. $this->branches = array();
  423. $this->enableDisableLearnedRules();
  424. $this->makeAssertionRuleDecisions();
  425. }
  426. /*-------------------------------------------------------------------
  427. * enable/disable learnt rules
  428. *
  429. * we have enabled or disabled some of our rules. We now reenable all
  430. * of our learnt rules except the ones that were learnt from rules that
  431. * are now disabled.
  432. */
  433. private function enableDisableLearnedRules()
  434. {
  435. foreach ($this->rules->getIteratorFor(RuleSet::TYPE_LEARNED) as $rule) {
  436. $why = $this->learnedWhy[$rule->getId()];
  437. $problemRules = $this->learnedPool[$why];
  438. $foundDisabled = false;
  439. foreach ($problemRules as $problemRule) {
  440. if ($problemRule->isDisabled()) {
  441. $foundDisabled = true;
  442. break;
  443. }
  444. }
  445. if ($foundDisabled && $rule->isEnabled()) {
  446. $rule->disable();
  447. } elseif (!$foundDisabled && $rule->isDisabled()) {
  448. $rule->enable();
  449. }
  450. }
  451. }
  452. private function runSat($disableRules = true)
  453. {
  454. $this->propagateIndex = 0;
  455. // /*
  456. // * here's the main loop:
  457. // * 1) propagate new decisions (only needed once)
  458. // * 2) fulfill jobs
  459. // * 3) fulfill all unresolved rules
  460. // * 4) minimalize solution if we had choices
  461. // * if we encounter a problem, we rewind to a safe level and restart
  462. // * with step 1
  463. // */
  464. $decisionQueue = array();
  465. $decisionSupplementQueue = array();
  466. $disableRules = array();
  467. $level = 1;
  468. $systemLevel = $level + 1;
  469. $installedPos = 0;
  470. while (true) {
  471. if (1 === $level) {
  472. $conflictRule = $this->propagate($level);
  473. if (null !== $conflictRule) {
  474. if ($this->analyzeUnsolvable($conflictRule, $disableRules)) {
  475. continue;
  476. }
  477. return;
  478. }
  479. }
  480. // handle job rules
  481. if ($level < $systemLevel) {
  482. $iterator = $this->rules->getIteratorFor(RuleSet::TYPE_JOB);
  483. foreach ($iterator as $rule) {
  484. if ($rule->isEnabled()) {
  485. $decisionQueue = array();
  486. $noneSatisfied = true;
  487. foreach ($rule->getLiterals() as $literal) {
  488. if ($this->decisions->satisfy($literal)) {
  489. $noneSatisfied = false;
  490. break;
  491. }
  492. if ($literal > 0 && $this->decisions->undecided($literal)) {
  493. $decisionQueue[] = $literal;
  494. }
  495. }
  496. if ($noneSatisfied && count($decisionQueue)) {
  497. // prune all update packages until installed version
  498. // except for requested updates
  499. if (count($this->installed) != count($this->updateMap)) {
  500. $prunedQueue = array();
  501. foreach ($decisionQueue as $literal) {
  502. if (isset($this->installedMap[abs($literal)])) {
  503. $prunedQueue[] = $literal;
  504. if (isset($this->updateMap[abs($literal)])) {
  505. $prunedQueue = $decisionQueue;
  506. break;
  507. }
  508. }
  509. }
  510. $decisionQueue = $prunedQueue;
  511. }
  512. }
  513. if ($noneSatisfied && count($decisionQueue)) {
  514. $oLevel = $level;
  515. $level = $this->selectAndInstall($level, $decisionQueue, $disableRules, $rule);
  516. if (0 === $level) {
  517. return;
  518. }
  519. if ($level <= $oLevel) {
  520. break;
  521. }
  522. }
  523. }
  524. }
  525. $systemLevel = $level + 1;
  526. // jobs left
  527. $iterator->next();
  528. if ($iterator->valid()) {
  529. continue;
  530. }
  531. }
  532. if ($level < $systemLevel) {
  533. $systemLevel = $level;
  534. }
  535. for ($i = 0, $n = 0; $n < count($this->rules); $i++, $n++) {
  536. if ($i == count($this->rules)) {
  537. $i = 0;
  538. }
  539. $rule = $this->rules->ruleById($i);
  540. $literals = $rule->getLiterals();
  541. if ($rule->isDisabled()) {
  542. continue;
  543. }
  544. $decisionQueue = array();
  545. // make sure that
  546. // * all negative literals are installed
  547. // * no positive literal is installed
  548. // i.e. the rule is not fulfilled and we
  549. // just need to decide on the positive literals
  550. //
  551. foreach ($literals as $literal) {
  552. if ($literal <= 0) {
  553. if (!$this->decisions->decidedInstall(abs($literal))) {
  554. continue 2; // next rule
  555. }
  556. } else {
  557. if ($this->decisions->decidedInstall(abs($literal))) {
  558. continue 2; // next rule
  559. }
  560. if ($this->decisions->undecided(abs($literal))) {
  561. $decisionQueue[] = $literal;
  562. }
  563. }
  564. }
  565. // need to have at least 2 item to pick from
  566. if (count($decisionQueue) < 2) {
  567. continue;
  568. }
  569. $oLevel = $level;
  570. $level = $this->selectAndInstall($level, $decisionQueue, $disableRules, $rule);
  571. if (0 === $level) {
  572. return;
  573. }
  574. // something changed, so look at all rules again
  575. $n = -1;
  576. }
  577. if ($level < $systemLevel) {
  578. continue;
  579. }
  580. // minimization step
  581. if (count($this->branches)) {
  582. $lastLiteral = null;
  583. $lastLevel = null;
  584. $lastBranchIndex = 0;
  585. $lastBranchOffset = 0;
  586. $l = 0;
  587. for ($i = count($this->branches) - 1; $i >= 0; $i--) {
  588. list($literals, $l) = $this->branches[$i];
  589. foreach ($literals as $offset => $literal) {
  590. if ($literal && $literal > 0 && $this->decisions->decisionLevel($literal) > $l + 1) {
  591. $lastLiteral = $literal;
  592. $lastBranchIndex = $i;
  593. $lastBranchOffset = $offset;
  594. $lastLevel = $l;
  595. }
  596. }
  597. }
  598. if ($lastLiteral) {
  599. unset($this->branches[$lastBranchIndex][self::BRANCH_LITERALS][$lastBranchOffset]);
  600. array_values($this->branches[$lastBranchIndex][self::BRANCH_LITERALS]);
  601. $level = $lastLevel;
  602. $this->revert($level);
  603. $why = $this->decisions->lastReason();
  604. $oLevel = $level;
  605. $level = $this->setPropagateLearn($level, $lastLiteral, $disableRules, $why);
  606. if ($level == 0) {
  607. return;
  608. }
  609. continue;
  610. }
  611. }
  612. break;
  613. }
  614. }
  615. }