mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
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!
This commit is contained in:
@@ -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 = {};
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
|
||||
584
packages/constraint-solver/solver.js
Normal file
584
packages/constraint-solver/solver.js
Normal file
@@ -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);
|
||||
};
|
||||
@@ -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];
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user