From ff4398fe1c16048ef2c82a574eda68e85ba87a03 Mon Sep 17 00:00:00 2001 From: David Greenspan Date: Fri, 30 Jan 2015 16:45:28 -0800 Subject: [PATCH] New constraint solver works! It even explains conflicts. It just doesn't spit out the list of other constraints on conflicted packages yet (easy to do). Left to do: - Remove old solver code - Call nudge() - More nuanced cost function - Clean up solver.js a little - Proper handling of pre-release versions - Lots more tests of different scenarios! --- packages/constraint-solver/catalog-cache.js | 2 +- .../constraint-solver-input.js | 59 +- .../constraint-solver/constraint-solver.js | 215 +------ packages/constraint-solver/package.js | 2 +- packages/constraint-solver/solver.js | 584 ++++++++++++++++++ packages/logic-solver/logic.js | 10 +- packages/logic-solver/optimize.js | 4 + 7 files changed, 651 insertions(+), 225 deletions(-) create mode 100644 packages/constraint-solver/solver.js diff --git a/packages/constraint-solver/catalog-cache.js b/packages/constraint-solver/catalog-cache.js index 5d89a7e83f..db53d102e8 100644 --- a/packages/constraint-solver/catalog-cache.js +++ b/packages/constraint-solver/catalog-cache.js @@ -13,7 +13,7 @@ CS.CatalogCache = function () { // A map derived from the keys of _dependencies, for ease of iteration. // "package" -> ["versions", ...] // Versions in the array are unique but not sorted, unless the `.sorted` - // property is set on the array. + // property is set on the array. The array is never empty. this._versions = {}; }; diff --git a/packages/constraint-solver/constraint-solver-input.js b/packages/constraint-solver/constraint-solver-input.js index bffb0c4ac3..50eb68e5c8 100644 --- a/packages/constraint-solver/constraint-solver-input.js +++ b/packages/constraint-solver/constraint-solver-input.js @@ -5,22 +5,48 @@ var CS = ConstraintSolver; // and it holds the data loaded from the Catalog as well. It can be // serialized to JSON and read back in for testing purposes. CS.Input = function (dependencies, constraints, catalogCache, options) { + var self = this; options = options || {}; - this.dependencies = dependencies; - this.constraints = constraints; - this.upgrade = options.upgrade || []; - this.anticipatedPrereleases = options.anticipatedPrereleases || {}; - this.previousSolution = options.previousSolution || null; + self.dependencies = dependencies; + self.constraints = constraints; + self.upgrade = options.upgrade || []; + self.anticipatedPrereleases = options.anticipatedPrereleases || {}; + self.previousSolution = options.previousSolution || null; - check(this.dependencies, [String]); - check(this.constraints, [PackageConstraintType]); - check(this.upgrade, [String]); - check(this.anticipatedPrereleases, + check(self.dependencies, [String]); + check(self.constraints, [PackageConstraintType]); + check(self.upgrade, [String]); + check(self.anticipatedPrereleases, Match.ObjectWithValues(Match.ObjectWithValues(Boolean))); - check(this.previousSolution, Match.OneOf(Object, null)); + check(self.previousSolution, Match.OneOf(Object, null)); - this.catalogCache = catalogCache; + self.catalogCache = catalogCache; + + self._dependencySet = {}; // package name -> true + _.each(self.dependencies, function (d) { + self._dependencySet[d] = true; + }); + self._upgradeSet = {}; + _.each(self.upgrade, function (u) { + self._upgradeSet[u] = true; + }); +}; + +CS.Input.prototype.isKnownPackage = function (p) { + return this.catalogCache.hasPackage(p); +}; + +CS.Input.prototype.isRootDependency = function (p) { + return _.has(this._dependencySet, p); +}; + +CS.Input.prototype.isUpgrading = function (p) { + return _.has(this._upgradeSet, p); +}; + +CS.Input.prototype.isInPreviousSolution = function (p) { + return !! (this.previousSolution && _.has(this.previousSolution, p)); }; CS.Input.prototype.loadFromCatalog = function (catalogLoader) { @@ -34,9 +60,11 @@ CS.Input.prototype.loadFromCatalog = function (catalogLoader) { _.each(self.constraints, function (constraint) { packagesToLoad[constraint.name] = true; }); - _.each(self.previousSolution, function (version, package) { - packagesToLoad[package] = true; - }); + if (self.previousSolution) { + _.each(self.previousSolution, function (version, package) { + packagesToLoad[package] = true; + }); + } // Load packages into the cache (if they aren't loaded already). catalogLoader.loadAllVersionsRecursive(_.keys(packagesToLoad)); @@ -114,3 +142,6 @@ var PackageConstraintType = Match.OneOf( check(c.vConstraint, VersionConstraintType); return c.constructor !== Object; })); + +CS.Input.VersionConstraintType = VersionConstraintType; +CS.Input.PackageConstraintType = PackageConstraintType; diff --git a/packages/constraint-solver/constraint-solver.js b/packages/constraint-solver/constraint-solver.js index 784d7bfb37..eeaaef07dc 100644 --- a/packages/constraint-solver/constraint-solver.js +++ b/packages/constraint-solver/constraint-solver.js @@ -38,214 +38,9 @@ CS.PackagesResolver.prototype.resolve = function (dependencies, constraints, }; var newResolveWithInput = function (input, _nudge) { - if (input.previousSolution || input.upgrade.length) { - // XXX Bail out to the old solver for now. - console.log("Bailing to old solver..."); - return CS.PackagesResolver._resolveWithInput(input, _nudge); - } - console.log("Using new solver..."); + var solver = new CS.Solver(input); - var cache = input.catalogCache; - - // Packages that are mentioned but aren't found in the CatalogCache - var unknownPackages = {}; // package name -> true - var packageVersionsRequiringPackage = {}; // package -> [package-and-version] - var rootDeps = {}; // package name -> true - _.each(input.dependencies, function (p) { - if (! cache.hasPackage(p)) { - unknownPackages[p] = true; - } - rootDeps[p] = true; - }); - - var solver = new Logic.Solver; - - var resolverOptions = { - anticipatedPrereleases: input.anticipatedPrereleases - }; - - var allConstraints = []; - - var addConstraint = function (pv, p2, vConstraint) { - var p2Versions = cache.getPackageVersions(p2); - var okVersions = _.filter(p2Versions, function (v2) { - return CS.isConstraintSatisfied(p2, vConstraint, - v2, resolverOptions); - }); - var okPVersions = _.map(okVersions, function (v2) { - return p2 + ' ' + v2; - }); - // If we select this version of `p` and we select some version - // of `p2`, we must select an "ok" version. - var constraintName = "constraint#" + allConstraints.length; - allConstraints.push([pv, p2, vConstraint]); - if (pv !== null) { - solver.require(Logic.implies(constraintName, - Logic.or(Logic.not(pv), - Logic.not(p2), - okPVersions))); - } else { - solver.require(Logic.implies(constraintName, - Logic.or(Logic.not(p2), - okPVersions))); - } - }; - - cache.eachPackage(function (p, versions) { - // ["foo 1.0.0", "foo 1.0.1", ...] for a given "foo" - var packageAndVersions = _.map(versions, function (v) { - return p + ' ' + v; - }); - // At most one of ["foo 1.0.0", "foo 1.0.1", ...] is true. - solver.require(Logic.atMostOne(packageAndVersions)); - // The variable "foo" is true if and only if at least one of the - // variables ["foo 1.0.0", "foo 1.0.1", ...] is true. - solver.require(Logic.equiv(p, Logic.or(packageAndVersions))); - - _.each(versions, function (v) { - var pv = p + ' ' + v; - _.each(cache.getDependencyMap(p, v), function (dep) { - // `dep` is a CS.Dependency - var p2 = dep.pConstraint.name; - if (! cache.hasPackage(p2)) { - unknownPackages[p2] = true; - } - var constr = dep.pConstraint.constraintString; - if (! dep.isWeak) { - packageVersionsRequiringPackage[p2] = - (packageVersionsRequiringPackage[p2] || []); - packageVersionsRequiringPackage[p2].push(pv); - } - if (constr) { - addConstraint(pv, p2, dep.pConstraint.vConstraint); - } - }); - }); - }); - - _.each(packageVersionsRequiringPackage, function (pvs, p) { - // pvs are all the package-and-versions that require p. - // We want to select p if-and-only-if we select one of the pvs - // (except for top-level dependencies). - if (! _.has(rootDeps, p)) { - solver.require(Logic.equiv(p, Logic.or(pvs))); - } - }); - - // For good measure, disallow any packages that were mentioned in - // dependencies or constraints but aren't available in the catalog. - solver.forbid(_.keys(unknownPackages)); - - solver.require(input.dependencies); - - _.each(input.constraints, function (c) { - addConstraint(null, c.name, c.vConstraint); - }); - - var allConstraintVars = _.map(allConstraints, function (c, i) { - return "constraint#" + i; - }); - var allConstraintsOn = Logic.and(allConstraintVars); - - var solution = solver.solveAssuming(allConstraintsOn); - - if (! solution) { - var errorMessage; - var looseSolution = solver.solve(); - if (! looseSolution) { - errorMessage = 'unknown package'; - } else { - // try to use as many constraints as possible - looseSolution = solver.maximize(looseSolution, allConstraintVars, 1); - var numConstraintsOn = looseSolution.getWeightedSum(allConstraintVars, 1); - console.log(">>> Needed to remove " + (allConstraints.length - - numConstraintsOn) + " constraints" + - " to get a solution."); - for (var i = 0; i < allConstraints.length; i++) { - if (! looseSolution.evaluate("constraint#" + i)) { - console.log("Skipped: " + JSON.stringify(allConstraints[i])); - } - } - errorMessage = 'conflict'; - } - var e = new Error(errorMessage); - e.constraintSolverError = true; - throw e; - } - - solver.require(allConstraintsOn); - - // optimize - _.each(solution.getTrueVars(), function (x) { - if (x.indexOf(' ') >= 0) { - var pv = CS.PackageAndVersion.fromString(x); - var package = pv.package; - var version = pv.version; - var otherVersions = cache.getPackageVersions(package); // sorted - - if (_.has(rootDeps, package)) { - // try to make newer - _.find(otherVersions, function (v) { - var trialPV = package + ' ' + v; - if (PV.lessThan(v, version)) { - solver.forbid(trialPV); - } else { - var newSolution = solver.solveAssuming(Logic.not(trialPV)); - if (newSolution) { - solution = newSolution; - solver.forbid(trialPV); - } else { - return true; - } - } - return false; - }); - } - } - }); - _.each(solution.getTrueVars(), function (x) { - if (x.indexOf(' ') >= 0) { - var pv = CS.PackageAndVersion.fromString(x); - var package = pv.package; - var version = pv.version; - var otherVersions = cache.getPackageVersions(package); // sorted - - if (! _.has(rootDeps, package)) { - // try to make older - otherVersions = _.clone(otherVersions); - otherVersions.reverse(); - _.find(otherVersions, function (v) { - var trialPV = package + ' ' + v; - if (PV.lessThan(version, v)) { - solver.forbid(trialPV); - } else { - var newSolution = solver.solveAssuming(Logic.not(trialPV)); - if (newSolution) { - solution = newSolution; - solver.forbid(trialPV); - } else { - return true; - } - } - return false; - }); - } - } - }); - - // read out solution - var versionMap = {}; - _.each(solution.getTrueVars(), function (x) { - if (x.indexOf(' ') >= 0) { - var pv = CS.PackageAndVersion.fromString(x); - versionMap[pv.package] = pv.version; - } - }); - - return { - neededToUseUnanticipatedPrereleases: false, // XXX - answer: versionMap - }; + return solver.getSolution(); }; // Exposed for tests. @@ -605,3 +400,9 @@ CS.isConstraintSatisfied = function (package, vConstraint, version, options) { } }); }; + +CS.throwConstraintSolverError = function (message) { + var e = new Error(message); + e.constraintSolverError = true; + throw e; +}; diff --git a/packages/constraint-solver/package.js b/packages/constraint-solver/package.js index 368f40b334..8486109a36 100644 --- a/packages/constraint-solver/package.js +++ b/packages/constraint-solver/package.js @@ -12,7 +12,7 @@ Package.onUse(function (api) { api.use(['underscore', 'ejson', 'check', 'package-version-parser', 'binary-heap', 'random', 'logic-solver']); api.addFiles(['datatypes.js', 'catalog-cache.js', 'catalog-loader.js', - 'constraint-solver-input.js']); + 'constraint-solver-input.js', 'solver.js']); api.addFiles(['constraint-solver.js', 'resolver.js', 'constraints-list.js', 'resolver-state.js', 'priority-queue.js'], ['server']); }); diff --git a/packages/constraint-solver/solver.js b/packages/constraint-solver/solver.js new file mode 100644 index 0000000000..f341a7b5ca --- /dev/null +++ b/packages/constraint-solver/solver.js @@ -0,0 +1,584 @@ +var CS = ConstraintSolver; +var PV = PackageVersion; + +CS.Solver = function (input, options) { + var self = this; + check(input, CS.Input); + + self.input = input; + self.errors = []; // [String] + + self.debugLog = null; + if (options && options.debugLog) { + self.debugLog = []; + } +}; + +CS.Solver.prototype.getSolution = function () { + var self = this; + + self.logic = new Logic.Solver; + + self._requireTopLevelDependencies(); // may throw + + // "bar" -> ["foo 1.0.0", ...] if "foo 1.0.0" requires "bar" + self._requirers = {}; + // package names we come across that aren't in the cache + self._unknownPackages = {}; // package name -> true + // populates _requirers and _unknownPackages: + self._enforceStrongDependencies(); + + // if this is greater than 0, we will throw an error later + // and say what they are, after we run the constraints + // and the cost function. + self._numUnknownPackagesNeeded = self._minimizeUnknownPackages(); + + self._constraintSatisfactionOptions = { + anticipatedPrereleases: self.input.anticipatedPrereleases + }; + self._allConstraints = []; // added to by self._addConstraint(...) + self._numConflicts = self._enforceConstraints(); + + self._costFunction = self._generateCostFunction(); + self._minimizeCostFunction(); + + self._solution = self.logic.solve(); + if (! self._solution) { + // can't get here; should be in a satisfiable state (or have thrown) + throw new Error("Unexpected unsatisfiability"); + } + + self._throwUnknownPackages(); + self._throwConflicts(); + + var versionMap = {}; + _.each(self._solution.getTrueVars(), function (x) { + if (x.indexOf(' ') >= 0) { + var pv = CS.PackageAndVersion.fromString(x); + versionMap[pv.package] = pv.version; + } + }); + + return { + neededToUseUnanticipatedPrereleases: false, // XXX + answer: versionMap + }; +}; + +var getVersionInfo = _.memoize(PV.parse); + +var pvVar = function (p, v) { + return p + ' ' + v; +}; + +CS.Solver.prototype._requireTopLevelDependencies = function () { + var self = this; + var input = self.input; + + _.each(input.dependencies, function (p) { + if (! input.isKnownPackage(p)) { + // Unknown package at top level + self.errors.push('unknown package: ' + p); + } else { + self.logic.require(p); + if (self.debugLog) { + self.debugLog.push('REQUIRE ' + p); + } + } + }); + + self.throwAnyErrors(); +}; + +CS.Solver.prototype._enforceStrongDependencies = function () { + var self = this; + var input = self.input; + var cache = input.catalogCache; + + var unknownPackages = self._unknownPackages; + var requirers = self._requirers; + + cache.eachPackage(function (p, versions) { + // ["foo 1.0.0", "foo 1.0.1", ...] for a given "foo" + var packageAndVersions = _.map(versions, function (v) { + return pvVar(p, v); + }); + // At most one of ["foo 1.0.0", "foo 1.0.1", ...] is true. + self.logic.require(Logic.atMostOne(packageAndVersions)); + if (self.debugLog) { + self.debugLog.push("AT MOST ONE: " + + (packageAndVersions.join(', ') || '[]')); + } + // The variable "foo" is true if and only if at least one of the + // variables ["foo 1.0.0", "foo 1.0.1", ...] is true. + // Note that this doesn't apply to unknown packages (packages + // that aren't in the cache), which aren't visited here. + // We will forbid them later, and generate a good error message + // if that leads to unsatisfiability. + self.logic.require(Logic.equiv(p, Logic.or(packageAndVersions))); + if (self.debugLog) { + self.debugLog.push(p + ' IFF ONE OF: ' + + (packageAndVersions.join(', ') || '[]')); + } + + _.each(versions, function (v) { + var pv = pvVar(p, v); + _.each(cache.getDependencyMap(p, v), function (dep) { + // `dep` is a CS.Dependency + var p2 = dep.pConstraint.name; + if (! input.isKnownPackage(p2)) { + unknownPackages[p2] = true; + } + if (! dep.isWeak) { + requirers[p2] = (requirers[p2] || []); + requirers[p2].push(pv); + } + }); + }); + }); + + _.each(requirers, function (pvs, p) { + // pvs are all the package-versions that require p. + // We want to select p if-and-only-if we select one of the pvs + // (except when p is a root dependency, in which case + // we've already required it). + if (! input.isRootDependency(p)) { + self.logic.require(Logic.equiv(p, Logic.or(pvs))); + if (self.debugLog) { + self.debugLog.push(p + ' IFF ONE OF: ' + + (pvs.join(', ') || '[]')); + } + } + }); +}; + +CS.Solver.prototype.throwAnyErrors = function () { + if (this.errors.length) { + CS.throwConstraintSolverError(this.errors.join('\n')); + } +}; + +CS.Solver.prototype._minimizeUnknownPackages = function () { + var self = this; + var unknownPackages = _.keys(self._unknownPackages); + var useAnyUnknown = Logic.or(unknownPackages); + + var sol = self.logic.solve(); + if (! sol) { + // so far we have no version constraints, and it's a valid solution + // to just select some version of every package, and also select + // all the non-existent packages that are mentioned, since we haven't + // forbid them yet. + throw new Error("Unexpected unsatisfiability"); + } + + if (self.logic.solveAssuming(Logic.not(useAnyUnknown))) { + if (self.debugLog) { + self.debugLog.push('FORBID: ' + + (unknownPackages.join(', ') || '[]')); + } + self.logic.forbid(unknownPackages); + return []; + } else { + // apparently we can't ignore some of the unknown packages; + // we have to use at least one. this will become an error, + // but we don't want to throw it yet so that we can run + // the cost function so that we are showing realistic versions + // in the error. + sol = self.logic.minimize(sol, unknownPackages, 1); + var result = sol.getWeightedSum(unknownPackages, 1); + if (self.debugLog) { + self.debugLog.push('AT MOST ' + result + ' OF: ' + + (unknownPackages.join(', ') || '[]')); + } + return result; + } +}; + +CS.Solver.prototype._generateCostFunction = function () { + var self = this; + // classify packages into categories, which determine what we + // are supposed to be optimizing about the version and with what + // priority. + var costFunc = new CS.Solver.CostFunction(); + // 1 if we change the major version of a root dep with previous version + costFunc.addComponent('previous_root_major'); + // 1 if we move a root dep backwards in the same major version + costFunc.addComponent('previous_root_incompat'); + // 1 if we change a root dep from previous version + costFunc.addComponent('previous_root_change'); + // number of versions forward or backward we move a root dep + costFunc.addComponent('previous_root_distance'); + + costFunc.addComponent('previous_indirect_major'); + costFunc.addComponent('previous_indirect_incompat'); + costFunc.addComponent('previous_indirect_change'); + costFunc.addComponent('previous_indirect_distance'); + + // XXX probably need some more nuance here. + // In general, we want packages we're upgrading and new root dependencies + // (just added or in case of no previous solution) to be as new as possible, + // so we penalize oldness. New indirect dependencies should be as old as + // possible, so we penalize newness. + costFunc.addComponent('upgrade_oldness'); + costFunc.addComponent('new_root_oldness'); + costFunc.addComponent('new_indirect_newness'); + + var input = self.input; + input.catalogCache.eachPackage(function (p, versions) { + if (input.isUpgrading(p)) { + _.each(versions, function (v, i) { + var pv = pvVar(p, v); + costFunc.addToComponent('upgrade_oldness', pv, + versions.length - 1 - i); + }); + } else if (input.isInPreviousSolution(p)) { + var previous = input.previousSolution[p]; + var previousVInfo = getVersionInfo(previous); + if (input.isRootDependency(p)) { + // previous_root + + var firstGteIndex = versions.length; + // previous version should be in versions array, but we don't + // want to assume that + var previousFound = false; + _.each(versions, function (v, i) { + var vInfo = getVersionInfo(v); + var pv = pvVar(p, v); + if (vInfo.major !== previousVInfo.major) { + costFunc.addToComponent('previous_root_major', pv, 1); + } else if (PV.lessThan(vInfo, previousVInfo)) { + costFunc.addToComponent('previous_root_incompat', pv, 1); + } + if (v !== previous) { + costFunc.addToComponent('previous_root_change', pv, 1); + } + if (firstGteIndex === versions.length && + ! PV.lessThan(vInfo, previousVInfo)) { + firstGteIndex = i; + if (v === previous) { + previousFound = true; + } + } + }); + _.each(versions, function (v, i) { + var pv = pvVar(p, v); + if (i < firstGteIndex) { + costFunc.addToComponent('previous_root_distance', pv, + firstGteIndex - i); + } else { + costFunc.addToComponent('previous_root_distance', pv, + i - firstGteIndex + + (previousFound ? 0 : 1)); + } + }); + + } else { + // previous_indirect + + var firstGteIndex = versions.length; + // previous version should be in versions array, but we don't + // want to assume that + var previousFound = false; + _.each(versions, function (v, i) { + var vInfo = getVersionInfo(v); + var pv = pvVar(p, v); + if (vInfo.major !== previousVInfo.major) { + costFunc.addToComponent('previous_indirect_major', pv, 1); + } else if (PV.lessThan(vInfo, previousVInfo)) { + costFunc.addToComponent('previous_indirect_incompat', pv, 1); + } + if (v !== previous) { + costFunc.addToComponent('previous_indirect_change', pv, 1); + } + if (firstGteIndex === versions.length && + ! PV.lessThan(vInfo, previousVInfo)) { + firstGteIndex = i; + if (v === previous) { + previousFound = true; + } + } + }); + _.each(versions, function (v, i) { + var pv = pvVar(p, v); + if (i < firstGteIndex) { + costFunc.addToComponent('previous_indirect_distance', pv, + firstGteIndex - i); + } else { + costFunc.addToComponent('previous_indirect_distance', pv, + i - firstGteIndex + + (previousFound ? 0 : 1)); + } + }); + } + } else { + if (input.isRootDependency(p)) { + // new_root + _.each(versions, function (v, i) { + var pv = pvVar(p, v); + costFunc.addToComponent('new_root_oldness', pv, + versions.length - 1 - i); + }); + } else { + // new_indirect + _.each(versions, function (v, i) { + var pv = pvVar(p, v); + costFunc.addToComponent('new_indirect_newness', pv, i); + }); + } + } + }); + + return costFunc; +}; + +var getDebugLogForWeightedSum = function (solution, terms, weights) { + if (typeof weights === 'number') { + weights = _.map(terms, function () { return weights; }); + } + return 'REQUIRE ' + (_.map(terms, function (t, i) { + return weights[i] + '*(' + t + ')'; + }).join(' + ') || '0')+ ' = ' + solution.getWeightedSum(terms, weights); +}; + +CS.Solver.prototype._minimizeCostFunction = function () { + var self = this; + var sol = self.logic.solve(); + if (! sol) { + // we've already been checking as we go along that the + // problem is still solvable + throw new Error("Unexpected unsatisfiability"); + } + + var costFunc = self._costFunction; + _.each(costFunc.components, function (comp) { + sol = self.logic.minimize(sol, comp.terms, comp.weights); + if (self.debugLog) { + self.debugLog.push(getDebugLogForWeightedSum( + sol, comp.terms, comp.weights)); + } + }); +}; + +CS.Solver.prototype._addConstraint = function (fromVar, toPackage, vConstraint) { + // fromVar is a return value of pvVar(p, v), or null for a top-level constraint + check(fromVar, Match.OneOf(String, null)); + check(toPackage, String); // package name + check(vConstraint, CS.Input.VersionConstraintType); + + var self = this; + var allConstraints = self._allConstraints; + + var newConstraint = new CS.Solver.Constraint( + "constraint#" + allConstraints.length, fromVar, toPackage, vConstraint); + allConstraints.push(newConstraint); + + var targetVersions = self.input.catalogCache.getPackageVersions(toPackage); + var okVersions = _.compact(_.map(targetVersions, function (v) { + if (newConstraint.isSatisfiedBy(v, self._constraintSatisfactionOptions)) { + return pvVar(toPackage, v); + } else { + return null; + } + })); + + // We logically require that IF: + // + // - the constraint var is true, meaning the constraint is active and not + // being skipped for conflict-detection purposes; and + // - fromVar is true, meaning we have selected the package version having + // the constraint, or is non-existent, meaning this is a top-level + // constraint; and + // - toPackage is true, meaning we have selected the package that the + // constraint is about + // + // ... then one of the versions of toPackage that satisfies the constraint + // must be selected. + self.logic.require( + Logic.implies(newConstraint.varName, + Logic.or(fromVar ? Logic.not(fromVar) : [], + Logic.not(toPackage), + okVersions))); + if (self.debugLog) { + var conditions = [newConstraint.varName]; + if (fromVar) { + conditions.push('(' + fromVar + ')'); + } + conditions.push(toPackage); + self.debugLog.push('IF ' + conditions.join(' AND ') + ' THEN ONE OF: ' + + (okVersions.join(', ') || '[]')); + } +}; + +// Register the constraints with the logic solver, but don't actually +// enforce them yet (so we can do conflict detection). +CS.Solver.prototype._enforceConstraints = function () { + var self = this; + var cache = self.input.catalogCache; + + // top-level constraints + _.each(self.input.constraints, function (c) { + self._addConstraint(null, c.name, c.vConstraint); + }); + + // constraints specified by package versions + cache.eachPackage(function (p, versions) { + _.each(versions, function (v) { + var pv = pvVar(p, v); + _.each(cache.getDependencyMap(p, v), function (dep) { + // `dep` is a CS.Dependency + var p2 = dep.pConstraint.name; + if (self.input.isKnownPackage(p2)) { + self._addConstraint(pv, p2, dep.pConstraint.vConstraint); + } + }); + }); + }); + + // minimize conflicts + var allConstraints = self._allConstraints; + var allConstraintVars = _.pluck(allConstraints, 'varName'); + var allConstraintsActive = Logic.and(allConstraintVars); + + var sol = self.logic.solveAssuming(allConstraintsActive); + if (sol) { + self.logic.require(allConstraintVars); + if (self.debugLog) { + self.debugLog.push('REQUIRE: ' + + (allConstraintVars.join(', ') || '[]')); + } + return 0; // no conflicts + } + + // Couldn't solve with all constraints. Figure out how many constraints + // we need to skip to achieve satisfiability, and later we will report + // them as conflicts to the user. + + // First solve with no constraints necessarily active (as a sanity check + // and as a starting point for optimization). + sol = self.logic.solve(); + if (! sol) { + // We should either still be satisfiable or have thrown an error. + throw new Error("Unexpected unsatisfiability"); + } + + sol = self.logic.maximize(sol, allConstraintVars, 1); + if (self.debugLog) { + self.debugLog.push(getDebugLogForWeightedSum( + sol, allConstraintVars, 1)); + } + + return allConstraintVars.length - sol.getWeightedSum(allConstraintVars, 1); +}; + +CS.Solver.prototype._throwUnknownPackages = function () { + var self = this; + + if (! self._numUnknownPackagesNeeded) { + return; + } + + var solution = self._solution; + var unknownPackages = _.keys(self._unknownPackages); + var unknownPackagesNeeded = _.filter(unknownPackages, function (p) { + return solution.evaluate(p); + }); + _.each(unknownPackagesNeeded, function (p) { + self.errors.push('unknown package: ' + p); + }); + self.throwAnyErrors(); +}; + +CS.Solver.prototype._throwConflicts = function () { + var self = this; + + if (! self._numConflicts) { + return; + } + + var allConstraints = self._allConstraints; + + var solution = self._solution; + + _.each(allConstraints, function (c) { + // c is a CS.Solver.Constraint + if (! solution.evaluate(c.varName)) { + // skipped this constraint + var possibleVersions = + self.input.catalogCache.getPackageVersions(c.toPackage); + var chosenVersion = _.find(possibleVersions, function (v) { + return solution.evaluate(pvVar(c.toPackage, v)); + }); + if (! chosenVersion) { + // this can't happen, because for a constraint to be a problem, + // we must have chosen some version of the package it applies to! + throw new Error("Internal error: Version not found"); + } + var error = ( + 'conflict: constraint ' + c.toPackage + '@' + c.vConstraint.raw + + ' is not satisfied by ' + c.toPackage + ' ' + chosenVersion + '.'); + + self.errors.push(error); + // XXX explain further -- what are the other constraints that + // must have conflicted with this one, and what package versions do + // they come from, and by what path were those package versions + // reached? + } + }); + + // always throws, never returns + self.throwAnyErrors(); + + throw new Error("Internal error: conflicts could not be explained"); +}; + +CS.Solver.CostFunction = function () { + this.components = []; + this.componentsByName = {}; +}; + +CS.Solver.CostFunction.prototype.addComponent = function (name, terms, weights) { + check(name, String); + terms = terms || []; + check(terms, [String]); + weights = weights || []; + check(weights, [Logic.WholeNumber]); + var comp = {name: name, + terms: terms, + weights: weights}; + this.components.push(comp); + this.componentsByName[name] = comp; +}; + +CS.Solver.CostFunction.prototype.addToComponent = function ( + compName, term, weight) { + + check(compName, String); + check(term, String); + check(weight, Logic.WholeNumber); + if (! _.has(this.componentsByName, compName)) { + throw new Error("No such cost function component: " + compName); + } + var comp = this.componentsByName[compName]; + comp.terms.push(term); + comp.weights.push(weight); +}; + +CS.Solver.Constraint = function (varName, fromVar, toPackage, vConstraint) { + this.varName = varName; + this.fromVar = fromVar; + this.toPackage = toPackage; + this.vConstraint = vConstraint; + + check(this.varName, String); + // this.fromVar is a return value of pvVar(p, v), or null for a + // top-level constraint + check(this.fromVar, Match.OneOf(String, null)); + check(this.toPackage, String); // package name + check(this.vConstraint, CS.Input.VersionConstraintType); +}; + +CS.Solver.Constraint.prototype.isSatisfiedBy = function (v2, options) { + return CS.isConstraintSatisfied(this.toPackage, this.vConstraint, + v2, options); +}; diff --git a/packages/logic-solver/logic.js b/packages/logic-solver/logic.js index c2102d1c2c..7bb4af25b5 100644 --- a/packages/logic-solver/logic.js +++ b/packages/logic-solver/logic.js @@ -1054,9 +1054,15 @@ Logic.weightedSum = function (formulas, weights) { weights = _.map(formulas, function () { return weights; }); } _check(weights, [Logic.WholeNumber]); - if (! (formulas.length === weights.length && formulas.length)) { - throw new Error("Formula array and weight array must be same length (> 0)"); + if (formulas.length !== weights.length) { + throw new Error("Formula array and weight array must be same length" + + "; they are " + formulas.length + " and " + weights.length); } + + if (formulas.length === 0) { + return new Logic.Bits([]); + } + var binaryWeighted = []; _.each(formulas, function (f, i) { var w = weights[i]; diff --git a/packages/logic-solver/optimize.js b/packages/logic-solver/optimize.js index c495275e36..1324cb3dfc 100644 --- a/packages/logic-solver/optimize.js +++ b/packages/logic-solver/optimize.js @@ -3,6 +3,10 @@ var minMax = function (solver, solution, costTerms, costWeights, optFormula, isM var curCost = curSolution.getWeightedSum(costTerms, costWeights); var weightedSum = (optFormula || Logic.weightedSum(costTerms, costWeights)); + + solver.require((isMin ? Logic.lessThanOrEqual : Logic.greaterThanOrEqual)( + weightedSum, Logic.constantBits(curCost))); + while (isMin ? curCost > 0 : true) { var improvement = (isMin ? Logic.lessThan : Logic.greaterThan)( weightedSum, Logic.constantBits(curCost));