Solver.php 24 KB

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