diff --git a/README.md b/README.md
index aa8515a..5ae8ca0 100644
--- a/README.md
+++ b/README.md
@@ -7,3 +7,17 @@ For more information, please visit our [gh-pages](http://babelsberg.github.io/ba
This work is licensed under [3-clause BSD](https://github.com/babelsberg/babelsberg-js/blob/master/LICENSE)
Created under a grant from Hasso Plattner Institute
+
+
+
+Setup for our dev team (Windows)
+=============
+* checkout this repository to ./babelsberg-js
+* download "the windows-2015-04-03 release zip." from https://github.com/LivelyKernel/LivelyKernel
+* unzip, open CMD, run ```start-lively-server.cmd```
+* after it installs packages, you have a working lively kernel at http://localhost:9001
+* navigate to LivelyKernel, this is now your root directory for lively
+* in LivelyKernel create users/
+* rename babelsberg-js to users/timfelgentreff
+* copy ohshima to users
+* open http://localhost:9001/users/timfelgentreff/babelsberg-js.html
\ No newline at end of file
diff --git a/babelsberg/cassowary_ext.js b/babelsberg/cassowary_ext.js
index 3827126..b3f1ba5 100644
--- a/babelsberg/cassowary_ext.js
+++ b/babelsberg/cassowary_ext.js
@@ -26,6 +26,15 @@ ClSimplexSolver.addMethods({
var constraint = new Constraint(func, this);
constraint.priority = priority;
return constraint;
+ },
+ solverName: 'Cassowary',
+ supportsMethods: function() { return false; },
+ supportsSoftConstraints: function() { return true; },
+ supportsFiniteDomains: function() { return false; },
+ supportedDataTypes: function() {
+ // Cassowary does not support strings, but there are actively used scenarios
+ // where js-coercion from string to float is used - these cases would be blown
+ return ['number', 'string']; /* XXX: is this correct? */
}
});
diff --git a/babelsberg/constraintinterpreter.js b/babelsberg/constraintinterpreter.js
index 6ba7d9c..7ea3055 100644
--- a/babelsberg/constraintinterpreter.js
+++ b/babelsberg/constraintinterpreter.js
@@ -17,7 +17,8 @@ toRun(function() {
Object.subclass('Babelsberg', {
initialize: function() {
- this.defaultSolvers = [];
+ this.defaultSolvers = [new ClSimplexSolver(), new DBPlanner(), new csp.Solver()];
+ this.defaultReevaluationInterval = 1000;
this.callbacks = [];
},
@@ -240,38 +241,45 @@ Object.subclass('Babelsberg', {
* If true, allows the use of operations that are not supported by the solver.
* @param {boolean} [opts.debugging=false]
* If true, calls debugger at certain points during constraint construction.
+ * @param {boolean} [opts.logTimings=false]
+ * If true, prints solver timings to console.
+ * @param {boolean} [opts.logReasons=false]
+ * If true, logs why certain solvers are not used for a constraint.
* @param {function} func The constraint to be fulfilled.
*/
always: function(opts, func) {
- var constraint = null,
- solvers = this.chooseSolvers(opts.solver),
+ var solvers = this.chooseSolvers(opts.solver),
errors = [];
func.allowTests = (opts.allowTests === true);
func.allowUnsolvableOperations = (opts.allowUnsolvableOperations === true);
func.debugging = opts.debugging;
func.onError = opts.onError;
+ //TODO: remove this from all solver implementations or move to filterSolvers
+ func.varMapping = opts.ctx;
- solvers.some(function(solver) {
- try {
- constraint = solver.always(opts, func);
- constraint._options = opts;
- } catch (e) {
- errors.push(e);
- return false;
- }
+ solvers = this.filterSolvers(solvers, opts, func);
+ var constraints = this.createEquivalentConstraints(solvers, opts, func, errors);
+ var constraint = this.chooseConstraint(constraints, opts, errors);
+ if (!opts.postponeEnabling && constraint) {
try {
- if (!opts.postponeEnabling) constraint.enable();
+ constraint.isAnyVariableCurrentlySuggested = true; // do not increase
+ // updateCounter
+ try {
+ constraint.enable();
+ } finally {
+ constraint.isAnyVariableCurrentlySuggested = false;
+ }
} catch (e) {
errors.push(e);
constraint.disable();
+ constraint.abandon();
constraint = null;
- return false;
}
- return true;
- });
-
- if (!constraint) {
+ }
+ if (constraint) {
+ this.abandonAllConstraintsExcept(constraint, constraints);
+ } else {
if (typeof opts.onError === 'function') {
bbb.addCallback(opts.onError, opts.onError.constraint, errors);
} else {
@@ -286,6 +294,13 @@ Object.subclass('Babelsberg', {
return constraint;
},
+ abandonAllConstraintsExcept: function(constraintToKeep, constraints) {
+ constraints.each(function(each) {
+ if (each !== constraintToKeep && each !== null)
+ each.abandon();
+ });
+ },
+
stay: function(opts, func) {
func.allowTests = (opts.allowTests === true);
func.allowUnsolvableOperations = (opts.allowUnsolvableOperations === true);
@@ -329,6 +344,198 @@ Object.subclass('Babelsberg', {
}
},
+ filterSolvers: function(solvers, opts, func) {
+ var result = [];
+
+ // FIXME: this global state is ugly
+ bbb.seenTypes = {};
+ bbb.seenFiniteDomain = false;
+ try {
+ cop.withLayers([ConstraintInspectionLayer], function() {
+ func.forInterpretation().apply(undefined, []);
+ });
+ } catch (e) {
+ bbb.seenTypes = {};
+ bbb.seenFiniteDomain = false;
+ if (opts.logReasons) {
+ console.warn('Parsing the expression for types failed, ' +
+ 'will not check types:', e);
+ }
+ }
+
+ solvers.each(function(solver) {
+ if (opts.methods && !solver.supportsMethods()) {
+ if (opts.logReasons) {
+ console.log('Ignoring ' + solver.solverName +
+ ' because it does not support opts.methods');
+ }
+ return false;
+ }
+
+ if (opts.priority && opts.priority != 'required' &&
+ !solver.supportsSoftConstraints()) {
+ if (opts.logReasons) {
+ console.log('Ignoring ' + solver.solverName +
+ ' because it does not support soft constraints');
+ }
+ return false;
+ }
+
+ if (bbb.seenFiniteDomain && !solver.supportsFiniteDomains()) {
+ if (opts.logReasons) {
+ console.log('Ignoring ' + solver.solverName +
+ ' because it does not support finite domains');
+ }
+ return false;
+ }
+
+ for (var type in bbb.seenTypes) {
+ if (solver.supportedDataTypes().indexOf(type) == -1) {
+ if (opts.logReasons) {
+ console.log('Ignoring ' + solver.solverName +
+ ' because it does not support ' + type + ' variables');
+ }
+ return false;
+ }
+ }
+
+ result.push(solver);
+ });
+
+ delete bbb.seenTypes;
+ delete bbb.seenFiniteDomain;
+ return result;
+ },
+
+ /**
+ * Create a Constraint for opts and func for each of the specified solvers.
+ * Return an array of the created Constraints.
+ */
+ createEquivalentConstraints: function(solvers, opts, func, errors) {
+ var constraints = [];
+ solvers.each(function(solver) {
+ try {
+ var optsForSolver = Object.clone(opts);
+ var constraint = solver.always(optsForSolver, func);
+ if (typeof opts.reevaluationInterval === 'number')
+ constraint.reevaluationInterval = opts.reevaluationInterval;
+ constraint.opts = optsForSolver;
+ constraint.originalOpts = opts;
+ constraints.push(constraint);
+ } catch (e) {
+ errors.push(e);
+ return;
+ }
+ });
+ return constraints;
+ },
+
+ /**
+ * Choose one of the specified constraints which performs best according to the
+ * requirements laid out in opts.
+ */
+ chooseConstraint: function(constraints, opts, errors) {
+ if (constraints.length === 1)
+ return constraints[0];
+ var constraint = null;
+ var previouslyEnabledConstraints = [];
+ // make sure all constraints are disabled before the comparison
+ constraints.each(function(each) {
+ if (each._enabled)
+ previouslyEnabledConstraints.push(each);
+ each.disable();
+ });
+ for (var i = 0; i < constraints.length; i++) {
+ try {
+ Constraint.current = constraints[i];
+ constraints[i].enable(true);
+ constraints[i].disable();
+ } catch (e) {
+ errors.push(e);
+ constraints[i].disable();
+ constraints[i].abandon();
+ constraints[i] = null;
+ } finally {
+ Constraint.current = null;
+ }
+ }
+ var minIndex = -1;
+ var constraint = null;
+ if (opts.optimizationPriority === undefined) {
+ opts.optimizationPriority = ['time', 'numberOfChangedVariables'];
+ }
+ var minimumConstraintMetrics = {};
+ for (var i = 0; i < opts.optimizationPriority.length; i++) {
+ minimumConstraintMetrics[opts.optimizationPriority[i]] = Number.MAX_VALUE;
+ }
+ for (var i = 0; i < constraints.length; i++) {
+ if (!constraints[i]) {
+ continue;
+ }
+ for (var m = 0; m < opts.optimizationPriority.length; m++) {
+ var metricName = opts.optimizationPriority[m];
+ var iMetric = constraints[i].comparisonMetrics[metricName];
+ if (typeof iMetric === 'function') {
+ iMetric = iMetric.call(constraints[i].comparisonMetrics);
+ }
+ var currentMinimum = minimumConstraintMetrics[metricName];
+ if (typeof currentMinimum === 'function') {
+ currentMinimum = currentMinimum.call(minimumConstraintMetrics);
+ }
+ if (iMetric > currentMinimum) {
+ break; // do not check further metrics
+ }
+ if (iMetric != currentMinimum) {
+ // iMetric is either smaller or NaN
+ minimumConstraintMetrics = constraints[i].comparisonMetrics;
+ minIndex = i;
+ if (iMetric < currentMinimum) {
+ break; // do not check further metrics
+ }
+ }
+ }
+ }
+ if (minIndex > -1) {
+ constraint = constraints[minIndex];
+ console.log('Selected best solver: ' + constraint.solver.solverName);
+ }
+ return constraint;
+ },
+
+ /**
+ * Creates a constraint equivalent to the given function through
+ * Babelsberg#always, and then disables it immediately
+ * @function Babelsberg#once
+ * @public
+ */
+ once: function(opts, func) {
+ var constraint = this.always(opts, func);
+ constraint.disable();
+ return constraint;
+ },
+
+ reevaluateSolverSelection: function(currentConstraint, updatedConstraintVariable) {
+ var currentSolver = currentConstraint.solver;
+ var func = currentConstraint._predicate;
+ var opts = currentConstraint.originalOpts;
+ var solvers = this.chooseSolvers(opts.solver);
+ solvers = solvers.filter(function(each) { return each !== currentSolver; });
+ solvers = this.filterSolvers(solvers, opts, func);
+ if (solvers.length < 1)
+ return; // no other solver is qualified to enforce this constraint
+ var errors = [];
+ var constraints = this.createEquivalentConstraints(solvers, opts, func, errors);
+ constraints.push(currentConstraint);
+ var constraint = this.chooseConstraint(constraints, opts, errors);
+ if (constraint !== currentConstraint) {
+ currentConstraint.solver = constraint.solver;
+ // yes, constraint does not replace currentConstraint, only its solver
+ currentConstraint.resetDefiningSolverOfVariables();
+ }
+ this.abandonAllConstraintsExcept(currentConstraint, constraints);
+ currentConstraint.enable();
+ },
+
addCallback: function(func, context, args) {
this.callbacks.push({
func: func,
@@ -344,6 +551,11 @@ Object.subclass('Babelsberg', {
cb.func.apply(cb.context, cb.args);
}
}).recursionGuard(bbb, 'isProcessingCallbacks');
+ },
+
+ isValueClass: function(variable) {
+ // TODO: add more value classes
+ return variable instanceof lively.Point;
}
});
@@ -365,8 +577,57 @@ users.timfelgentreff.jsinterpreter.Send.addMethods({
}
});
-cop.create('ConstraintConstructionLayer').
- refineObject(users.timfelgentreff.jsinterpreter, {
+cop.create('ConstraintInspectionLayer')
+.refineClass(users.timfelgentreff.jsinterpreter.InterpreterVisitor, {
+ visitGetSlot: function(node) {
+ var obj = this.visit(node.obj),
+ name = this.visit(node.slotName),
+ value = obj[name];
+
+ if (!(node._parent instanceof users.timfelgentreff.jsinterpreter.GetSlot) &&
+ !(node._parent instanceof users.timfelgentreff.jsinterpreter.Send) &&
+ !(node._parent instanceof users.timfelgentreff.jsinterpreter.Call) &&
+ value != undefined && !bbb.isValueClass(value)) {
+ bbb.seenTypes[typeof value] = true;
+ }
+ return value;
+ },
+ visitNumber: function(node) {
+ if (!(node._parent instanceof users.timfelgentreff.jsinterpreter.GetSlot)) {
+ bbb.seenTypes[typeof node.value] = true;
+ }
+ return node.value;
+ },
+ visitString: function(node) {
+ if (!(node._parent instanceof users.timfelgentreff.jsinterpreter.GetSlot)) {
+ bbb.seenTypes[typeof node.value] = true;
+ }
+ return node.value;
+ },
+ visitBinaryOp: function(node) {
+ if (node.name == 'in' &&
+ node.right instanceof users.timfelgentreff.jsinterpreter.ArrayLiteral) {
+ bbb.seenFiniteDomain = true;
+ }
+ cop.proceed(node);
+ },
+ //FIXME: copy&paste from constraintconstructionlayer
+ shouldInterpret: function(frame, func) {
+ if (func.sourceModule ===
+ Global.users.timfelgentreff.babelsberg.constraintinterpreter) {
+ return false;
+ }
+ if (func.declaredClass === 'Babelsberg') {
+ return false;
+ }
+ var nativeClass = lively.Class.isClass(func) && func.superclass === undefined;
+ return (!(this.isNative(func) || nativeClass)) &&
+ typeof(func.forInterpretation) == 'function';
+ }
+});
+
+cop.create('ConstraintConstructionLayer')
+.refineObject(users.timfelgentreff.jsinterpreter, {
get InterpreterVisitor() {
return ConstraintInterpreterVisitor;
}
@@ -417,6 +678,8 @@ Object.subclass('Constraint', {
this.constraintobjects = [];
this.constraintvariables = [];
this.solver = solver;
+ this.reevaluationInterval = bbb.defaultReevaluationInterval;
+ this.updateCounter = 0;
// FIXME: this global state is ugly
try {
@@ -478,9 +741,11 @@ Object.subclass('Constraint', {
* Enables this constraint. This is done automatically after
* constraint construction by most solvers.
* @function Constraint#enable
+ * @param {boolean} [bCompare] signifies that there are multiple
+ * solvers to be compared
* @public
*/
- enable: function() {
+ enable: function(bCompare) {
if (!this._enabled) {
Constraint.enabledConstraintsGuard.tick();
this.constraintobjects.each(function(ea) {
@@ -490,20 +755,49 @@ Object.subclass('Constraint', {
throw new Error('BUG: No constraintobjects were created.');
}
this._enabled = true;
+ this.constraintvariables.each(function(v) {v._resetIsSolveable();});
+ var begin = performance.now();
this.solver.solve();
+ var end = performance.now();
+ if (this.opts.logTimings) {
+ console.log((this.solver ? this.solver.solverName : '(no solver)') +
+ ' took ' + (end - begin) + ' ms to solve in enable');
+ }
+ var changedVariables = 0;
+ var variableAssigments = {};
this.constraintvariables.each(function(ea) {
var value = ea.getValue();
- if (value != this.storedValue) {
- // solveForConnectedVariables might eventually
- // call updateDownstreamExternalVariables, too.
- // We need this first, however, for the case when
- // this newly enabled constraint is the new
- // highest-weight solver
+ var oldValue = ea.storedValue;
+ if (oldValue !== value) {
+ variableAssigments[ea.ivarname] = {oldValue: oldValue,
+ newValue: value};
+ changedVariables += 1;
+ }
+ // solveForConnectedVariables might eventually
+ // call updateDownstreamExternalVariables, too.
+ // We need this first, however, for the case when
+ // this newly enabled constraint is the new
+ // highest-weight solver
+ if (!bCompare) {
ea.updateDownstreamExternalVariables(value);
ea.solveForConnectedVariables(value);
}
});
+ this.comparisonMetrics = {time: end - begin,
+ numberOfChangedVariables: changedVariables,
+ assignments: variableAssigments};
+ Object.extend(this.comparisonMetrics, {
+ squaredChangeDistance: function() {
+ var sumOfSquaredDistances = 0;
+ for (var varname in this.assignments) {
+ var assignment = this.assignments[varname];
+ var distance = assignment.newValue - assignment.oldValue;
+ sumOfSquaredDistances += distance * distance;
+ }
+ return sumOfSquaredDistances;
+ }
+ });
}
},
@@ -589,6 +883,8 @@ Object.subclass('Constraint', {
});
if (enabled) {
+ this.enable();
+
assignments = this.constraintvariables.select(function(ea) {
// all the cvars that are new after this recalculation
return !cvars.include(ea) && ea.isSolveable();
@@ -625,6 +921,27 @@ Object.subclass('Constraint', {
assignments.invoke('disable');
}
}
+ },
+
+
+ /**
+ * Indicate that this Constraint will never be enabled again.
+ * Causes external variables of related ConstrainedVariables to be detached
+ * if they were connected to their solver only via this Constraint.
+ */
+ abandon: function() {
+ this.constraintvariables.each(function(eachVar) {
+ eachVar.abandonConstraint(this);
+ }, this);
+ // TODO: eject those external variables also from their solvers if possible
+ // because the solvers might be put to use somewhere else and should not be
+ // bothered with old (possibly duplicated) variables, should they?
+ },
+
+ resetDefiningSolverOfVariables: function() {
+ this.constraintvariables.each(function(eachVar) {
+ eachVar.resetDefiningSolver();
+ });
}
});
Object.extend(Constraint, {
@@ -738,25 +1055,51 @@ Object.subclass('ConstrainedVariable', {
var callSetters = !ConstrainedVariable.$$optionalSetters,
oldValue = this.storedValue,
solver = this.definingSolver;
+ var definingConstraint = this.definingConstraint;
ConstrainedVariable.$$optionalSetters =
ConstrainedVariable.$$optionalSetters || [];
try {
+ var isInitiatingSuggestForDefiningConstraint = false;
+ if (definingConstraint !== null) {
+ isInitiatingSuggestForDefiningConstraint =
+ !definingConstraint.isAnyVariableCurrentlySuggested;
+ definingConstraint.isAnyVariableCurrentlySuggested = true;
+ }
+ var begin = performance.now();
+ // never uses multiple solvers, since it gets the defining Solver
this.solveForPrimarySolver(value, oldValue, solver, source, force);
- this.solveForConnectedVariables(value, oldValue, solver, source, force);
+ if (definingConstraint && definingConstraint.opts.logTimings) {
+ console.log((solver ? solver.solverName : '(no solver)') +
+ ' took ' + (performance.now() - begin) + ' ms' +
+ ' to solve for ' + this.ivarname + ' in suggestValue');
+ }
+ if (isInitiatingSuggestForDefiningConstraint) {
+ definingConstraint.updateCounter += 1;
+ if (definingConstraint.updateCounter >=
+ definingConstraint.reevaluationInterval) {
+ bbb.reevaluateSolverSelection(definingConstraint, this);
+ definingConstraint.updateCounter = 0;
+ }
+ }
+ this.solveForConnectedVariables(value, oldValue, source, force);
this.findAndOptionallyCallSetters(callSetters);
} catch (e) {
if (this.getValue() !== oldValue) {
- throw 'solving failed, but variable changed to ' +
- this.getValue() + ' from ' + oldValue;
+ throw new Error('solving failed, but variable changed to ' +
+ this.getValue() + ' from ' + oldValue);
}
this.addErrorCallback(e);
} finally {
this.ensureClearSetters(callSetters);
- if (solver && source) {
+ if (this.isSolveable() && solver && source) {
+ // was bumped up in solveForPrimarySolver
this.bumpSolverWeight(solver, 'down');
}
+ if (isInitiatingSuggestForDefiningConstraint) {
+ definingConstraint.isAnyVariableCurrentlySuggested = false;
+ }
}
bbb.processCallbacks();
}
@@ -769,15 +1112,17 @@ Object.subclass('ConstrainedVariable', {
var wasReadonly = false,
// recursionGuard per externalVariable?
eVar = this.definingExternalVariable;
- try {
- if (solver && source) {
- this.bumpSolverWeight(solver, 'up');
+ if (eVar) {
+ try {
+ if (solver && source) {
+ this.bumpSolverWeight(solver, 'up');
+ }
+ wasReadonly = eVar.isReadonly();
+ eVar.setReadonly(false);
+ eVar.suggestValue(value);
+ } finally {
+ eVar.setReadonly(wasReadonly);
}
- wasReadonly = eVar.isReadonly();
- eVar.setReadonly(false);
- eVar.suggestValue(value);
- } finally {
- eVar.setReadonly(wasReadonly);
}
}).bind(this).recursionGuard(
ConstrainedVariable.isSuggestingValue,
@@ -797,18 +1142,18 @@ Object.subclass('ConstrainedVariable', {
});
},
- solveForConnectedVariables: function(value, priorValue, solver, source, force) {
+ solveForConnectedVariables: function(value, priorValue, source, force) {
if (force || value !== this.storedValue) {
(function() {
try {
// this.setValue(value);
- this.updateDownstreamVariables(value, solver);
- this.updateConnectedVariables(value, solver);
+ this.updateDownstreamVariables(value);
+ this.updateConnectedVariables(value);
} catch (e) {
if (source) {
// is freeing the recursionGuard here necessary?
- this.$$isStoring = false;
this.suggestValue(priorValue, source, 'force');
+ this.$$isStoring = false;
}
throw e; // XXX: Lively checks type, so wrap for top-level
}
@@ -1005,41 +1350,81 @@ Object.subclass('ConstrainedVariable', {
if (Constraint.current || this._hasMultipleSolvers) {
// no fast path for variables with multiple solvers for now
this._definingSolver = null;
- return this._searchDefiningSolver();
+ var defining = this._searchDefiningSolverAndConstraint();
+ return defining.solver;
} else if (!this._definingSolver) {
- return this._definingSolver = this._searchDefiningSolver();
+ var defining = this._searchDefiningSolverAndConstraint();
+ this._definingConstraint = defining.constraint;
+ return this._definingSolver = defining.solver;
} else {
return this._definingSolver;
}
},
- _searchDefiningSolver: function() {
- var solver = {weight: -1000, fake: true};
- this.eachExternalVariableDo(function(eVar) {
- if (eVar) {
- if (!solver.fake) {
- this._hasMultipleSolvers = true;
- }
- var s = eVar.__solver__;
- if (s.weight > solver.weight) {
- solver = s;
+ get definingConstraint() {
+ return this._definingConstraint ||
+ this._searchDefiningSolverAndConstraint().constraint;
+ },
+ _searchDefiningSolverAndConstraint: function() {
+ var solver = {weight: -1000, fake: true, solverName: '(fake)'};
+ var constraint = null;
+ var solvers = [];
+ this.eachExternalVariableDo(function(eVar) {
+ var s = eVar.__solver__;
+
+ if (!s.fake) {
+ solvers.push(s);
+ }
+
+ var hasEnabledConstraint = false;
+ var enabledConstraint = null;
+ for (var i = 0; i < this._constraints.length; i++) {
+ if (this._constraints[i].solver === s &&
+ this._constraints[i]._enabled) {
+ enabledConstraint = this._constraints[i];
+ hasEnabledConstraint = true;
+ break;
}
- }
- }.bind(this));
- return solver;
+ }
+
+ if (this._constraints.length > 0 && !hasEnabledConstraint)
+ return;
+
+ if (!solver.fake && hasEnabledConstraint) {
+ this._hasMultipleSolvers = true;
+ }
+
+
+ if (s.weight > solver.weight) {
+ solver = s;
+ constraint = enabledConstraint;
+ }
+ }.bind(this));
+
+ if (solver.fake) {
+ return {solver: null, constraint: null};
+ }
+
+ return {solver: solver, constraint: constraint};
+ },
+
+ resetDefiningSolver: function() {
+ this._definingSolver = null;
},
get solvers() {
var solvers = [];
this.eachExternalVariableDo(function(eVar) {
- if (eVar) {
- var s = eVar.__solver__;
- solvers.push(s);
- }
+ var s = eVar.__solver__;
+ solvers.push(s);
});
return solvers.uniq();
},
get definingExternalVariable() {
- return this.externalVariables(this.definingSolver);
+ if (this.definingSolver) {
+ return this.externalVariables(this.definingSolver);
+ } else {
+ return null;
+ }
},
isSolveable: function() {
@@ -1051,10 +1436,7 @@ Object.subclass('ConstrainedVariable', {
},
isValueClass: function() {
- // TODO: add more value classes
- return !this.isSolveable() &&
- this.storedValue instanceof lively.Point;
- // return false && this.storedValue instanceof lively.Point;
+ return !this.isSolveable() && bbb.isValueClass(this.storedValue);
},
get storedValue() {
@@ -1086,7 +1468,7 @@ Object.subclass('ConstrainedVariable', {
},
getValue: function() {
- if (this.isSolveable()) {
+ if (this.isSolveable() && this.hasEnabledConstraint()) {
return this.externalValue;
} else {
return this.storedValue;
@@ -1118,6 +1500,40 @@ Object.subclass('ConstrainedVariable', {
this._externalVariables[solver.__uuid__] = value || null;
this._resetIsSolveable();
}
+ },
+
+ /**
+ * Removes all external variables which are used only by the specified Constraint.
+ * @param {Constraint} abandonedConstraint the Constraint about to be purged
+ */
+ abandonConstraint: function(abandonedConstraint) {
+ // remove abandonedConstraint from this._constraints
+ var abandonedIndex = this._constraints.indexOf(abandonedConstraint);
+ if (abandonedIndex !== -1)
+ this._constraints.splice(abandonedIndex, 1);
+ // collect all external variables which can be detached
+ var externalVariableKeysToRemove = Object.keys(this._externalVariables).findAll(
+ function(eachSolverUUID) {
+ var externalVariable = this._externalVariables[eachSolverUUID];
+ if (externalVariable === null)
+ return true; // delete the nulls by the way
+ var hasSomeOtherConstraintForThisSolver = this._constraints.some(
+ function(eachConstraint) {
+ return eachConstraint.solver === externalVariable.__solver__;
+ });
+ return !hasSomeOtherConstraintForThisSolver;
+ }.bind(this));
+ // detach collected external variables
+ externalVariableKeysToRemove.each(function(each) {
+ delete this._externalVariables[each];
+ }.bind(this));
+ },
+
+ hasEnabledConstraint: function() {
+ return this._constraints.length == 0 ||
+ this._constraints.some(function(constraint) {
+ return constraint._enabled;
+ });
}
});
diff --git a/babelsberg/csp_ext.js b/babelsberg/csp_ext.js
index 3ba786c..533173d 100644
--- a/babelsberg/csp_ext.js
+++ b/babelsberg/csp_ext.js
@@ -95,7 +95,14 @@ module('users.timfelgentreff.babelsberg.csp_ext').
}
return cobj;
},
- solve: function() { /* ignored */ }
+ solve: function() { /* ignored */ },
+ solverName: 'CSP',
+ supportsMethods: function() { return false; },
+ supportsSoftConstraints: function() { return false; /* XXX: is this correct? */ },
+ supportsFiniteDomains: function() { return true; },
+ supportedDataTypes: function() {
+ return ['number', 'boolean', 'string', 'object']; /* XXX: is this correct? */
+ }
});
Object.extend(csp.Solver, {
weight: 1000,
diff --git a/babelsberg/deltablue_ext.js b/babelsberg/deltablue_ext.js
index b9fe05c..80e4309 100644
--- a/babelsberg/deltablue_ext.js
+++ b/babelsberg/deltablue_ext.js
@@ -92,6 +92,13 @@ DBPlanner.addMethods({
edit.myOutput.value = newValues[idx];
});
this.currentEditPlan.execute();
+ },
+ solverName: 'DeltaBlue',
+ supportsMethods: function() { return true; },
+ supportsSoftConstraints: function() { return true; },
+ supportsFiniteDomains: function() { return false; },
+ supportedDataTypes: function() {
+ return ['number', 'boolean', 'string', 'object'];
}
});
diff --git a/babelsberg/tests.js b/babelsberg/tests.js
index 2363265..894890c 100644
--- a/babelsberg/tests.js
+++ b/babelsberg/tests.js
@@ -1954,4 +1954,676 @@ TestCase.subclass('users.timfelgentreff.babelsberg.tests.OnErrorTest', {
this.assert(errorMessage === "Could not satisfy constraint", "an unexpected error was thrown, message: " + errorMessage);
}
});
+
+Object.subclass('users.timfelgentreff.babelsberg.tests.DefaultSolversFixture', {
+ saveDefaultSolvers: function(defaultSolvers) {
+ this.previousDefaultSolvers = bbb.defaultSolvers;
+ this.previousDefaultSolver = bbb.defaultSolver;
+ this.previousReevaluationInterval = bbb.defaultReevaluationInterval;
+ },
+ restoreDefaultSolvers: function() {
+ bbb.defaultSolvers = this.previousDefaultSolvers;
+ bbb.defaultSolver = this.previousDefaultSolver;
+ bbb.defaultReevaluationInterval = this.previousReevaluationInterval;
+ },
+});
+
+function preparePatchedSolvers() {
+ // prepare solvers of which the solving time and actions can be dictated
+ patchedSolver = new ClSimplexSolver();
+ patchedSolver.forcedDelay = 0;
+ patchedSolver.solve = function() {
+ var begin = performance.now();
+ while (performance.now() < begin + this.forcedDelay) {
+ ; // busy wait, no sleep in JavaScript
+ // and setTimeout is not what we want
+ }
+ if (typeof this.forcedSolveAction === 'function') {
+ return this.forcedSolveAction();
+ }
+ return ClSimplexSolver.prototype.solve.apply(this, arguments);
+ }
+ PatchedSolver = function() {}
+ PatchedSolver.prototype = patchedSolver;
+ bbb.defaultSolvers = [new PatchedSolver(), new PatchedSolver()];
+}
+
+TestCase.subclass('users.timfelgentreff.babelsberg.tests.AutomaticSolverSelectionDetailsTest', {
+ setUp: function () {
+ this.defaultSolversFixture = new users.timfelgentreff.babelsberg.tests.DefaultSolversFixture();
+ this.defaultSolversFixture.saveDefaultSolvers();
+ bbb.defaultSolvers = [new ClSimplexSolver(), new DBPlanner(), new csp.Solver()];
+ bbb.defaultSolver = null;
+ },
+
+ tearDown: function () {
+ this.defaultSolversFixture.restoreDefaultSolvers();
+ },
+
+ testSquaredChangeDistance: function () {
+ var obj = {a: 2, b: 3};
+ var constraint = bbb.always({
+ ctx: {
+ obj: obj
+ }
+ }, function() {
+ return obj.a + obj.b == 3;
+ });
+ this.assert(constraint.comparisonMetrics.squaredChangeDistance() ==
+ (obj.a - 2) * (obj.a - 2) + (obj.b - 3) * (obj.b - 3),
+ "squaredChangeDistance should be the sum of the squared distances");
+ },
+
+ testChoiceWithTimeOverDistance1: function() {
+ preparePatchedSolvers();
+ bbb.defaultSolvers[0].forcedDelay = 0;
+ bbb.defaultSolvers[1].forcedDelay = 10;
+ // when: create actual constraint
+ var obj = {a: 2, b: 3};
+ var constraint = bbb.always({
+ ctx: {
+ obj: obj
+ },
+ logTimings: true,
+ optimizationPriority: ['time', 'squaredChangeDistance'],
+ }, function() {
+ return obj.a + obj.b == 3;
+ });
+ // then: assert that the faster solver was chosen
+ this.assert(constraint.solver === bbb.defaultSolvers[0], 'The faster solver should have been chosen');
+ },
+
+ testChoiceWithTimeOverDistance2: function() {
+ preparePatchedSolvers();
+ bbb.defaultSolvers[0].forcedDelay = 10;
+ bbb.defaultSolvers[1].forcedDelay = 0;
+ // when: create actual constraint
+ var obj = {a: 2, b: 3};
+ var constraint = bbb.always({
+ ctx: {
+ obj: obj
+ },
+ optimizationPriority: ['time', 'squaredChangeDistance'],
+ }, function() {
+ return obj.a + obj.b == 3;
+ });
+ // then: assert that the faster solver was chosen
+ this.assert(constraint.solver === bbb.defaultSolvers[1], 'The faster solver should have been chosen');
+ },
+
+ testChoiceWithDistanceOverTime1: function() {
+ preparePatchedSolvers();
+ var constraint0 = null, constraint1 = null;
+ bbb.defaultSolvers[0].forcedDelay = 10;
+ bbb.defaultSolvers[0].forcedSolveAction = function () {
+ if (!!Constraint.current) {
+ Constraint.current.enable = arguments.callee;
+ constraint0 = Constraint.current;
+ }
+ constraint0.constraintvariables[0].setValue(2);
+ constraint0.constraintvariables[1].setValue(1);
+ }
+ bbb.defaultSolvers[1].forcedDelay = 0;
+ bbb.defaultSolvers[1].forcedSolveAction = function () {
+ if (!!Constraint.current) {
+ Constraint.current.enable = arguments.callee;
+ constraint1 = Constraint.current;
+ }
+ constraint1.constraintvariables[0].setValue(10);
+ constraint1.constraintvariables[1].setValue(-7);
+ }
+ // when: create actual constraint
+ var obj = {a: 2, b: 3};
+ var constraint = bbb.always({
+ ctx: {
+ obj: obj
+ },
+ optimizationPriority: ['squaredChangeDistance', 'time'],
+ }, function() {
+ return obj.a + obj.b == 3;
+ });
+ // then
+ this.assert(constraint.solver === bbb.defaultSolvers[0], 'The solver with the smaller distance should have been chosen (albeit slower)');
+ },
+
+ testChoiceWithNumberOfChangedVariablesOverTime1: function() {
+ preparePatchedSolvers();
+ var constraint0 = null, constraint1 = null;
+ bbb.defaultSolvers[0].forcedDelay = 10;
+ bbb.defaultSolvers[0].forcedSolveAction = function () {
+ }
+ bbb.defaultSolvers[1].forcedDelay = 0;
+ bbb.defaultSolvers[1].forcedSolveAction = function () {
+ if (!!Constraint.current) {
+ Constraint.current.enable = arguments.callee;
+ constraint1 = Constraint.current;
+ }
+ constraint1.constraintvariables[0].setValue(10);
+ constraint1.constraintvariables[1].setValue(-7);
+ }
+ // when: create actual constraint
+ var obj = {a: 2, b: 1};
+ var constraint = bbb.always({
+ ctx: {
+ obj: obj
+ },
+ optimizationPriority: ['numberOfChangedVariables', 'time'],
+ }, function() {
+ return obj.a + obj.b == 3;
+ });
+ // then
+ this.assert(constraint.solver === bbb.defaultSolvers[0], 'The solver with the smaller distance should have been chosen (albeit slower)');
+ },
+
+ testChoiceWithNumberOfChangedVariablesOverTime2: function() {
+ preparePatchedSolvers();
+ var constraint0 = null, constraint1 = null;
+ bbb.defaultSolvers[0].forcedDelay = 10;
+ bbb.defaultSolvers[0].forcedSolveAction = function () {
+ if (!!Constraint.current) {
+ Constraint.current.enable = arguments.callee;
+ constraint0 = Constraint.current;
+ }
+ constraint0.constraintvariables[0].setValue(10);
+ constraint0.constraintvariables[1].setValue(-7);
+ }
+ bbb.defaultSolvers[1].forcedDelay = 0;
+ bbb.defaultSolvers[1].forcedSolveAction = function () {
+ }
+ // when: create actual constraint
+ var obj = {a: 2, b: 1};
+ var constraint = bbb.always({
+ ctx: {
+ obj: obj
+ },
+ optimizationPriority: ['numberOfChangedVariables', 'time'],
+ }, function() {
+ return obj.a + obj.b == 3;
+ });
+ // then
+ this.assert(constraint.solver === bbb.defaultSolvers[1], 'The solver with the smaller distance should have been chosen (albeit slower)');
+ },
+
+ testChoiceWithDistanceOverTime2: function() {
+ preparePatchedSolvers();
+ var constraint0 = null, constraint1 = null;
+ bbb.defaultSolvers[0].forcedDelay = 0;
+ bbb.defaultSolvers[0].forcedSolveAction = function () {
+ if (!!Constraint.current) {
+ Constraint.current.enable = arguments.callee;
+ constraint0 = Constraint.current;
+ }
+ constraint0.constraintvariables[0].setValue(10);
+ constraint0.constraintvariables[1].setValue(-7);
+ }
+ bbb.defaultSolvers[1].forcedDelay = 10;
+ bbb.defaultSolvers[1].forcedSolveAction = function () {
+ if (!!Constraint.current) {
+ Constraint.current.enable = arguments.callee;
+ constraint1 = Constraint.current;
+ }
+ constraint1.constraintvariables[0].setValue(2);
+ constraint1.constraintvariables[1].setValue(1);
+ }
+ // when: create actual constraint
+ var obj = {a: 2, b: 3};
+ var constraint = bbb.always({
+ ctx: {
+ obj: obj
+ },
+ optimizationPriority: ['squaredChangeDistance', 'time'],
+ }, function() {
+ return obj.a + obj.b == 3;
+ });
+ // then
+ this.assert(constraint.solver === bbb.defaultSolvers[1], 'The solver with the smaller distance should have been chosen (albeit slower)');
+ },
+
+ testStringsAndSquaredChangeDistance: function() {
+ // we do not support a distance for string values
+ // but it should not break the solver selection process
+ var subject = {hat: '', shoes: 'black'};
+ var constraint = bbb.always({
+ ctx: {
+ subject: subject
+ },
+ optimizationPriority: ['squaredChangeDistance', 'time'],
+ }, function () {
+ return subject.hat === subject.shoes;
+ });
+ this.assert(subject.hat === subject.shoes);
+ },
+
+});
+
+TestCase.subclass('users.timfelgentreff.babelsberg.tests.AutomaticSolverSelectionTest', {
+ setUp: function () {
+ this.defaultSolversFixture = new users.timfelgentreff.babelsberg.tests.DefaultSolversFixture();
+ this.defaultSolversFixture.saveDefaultSolvers();
+ bbb.defaultSolvers = [new ClSimplexSolver(), new DBPlanner(), new csp.Solver()];
+ bbb.defaultSolver = null;
+ },
+
+ tearDown: function() {
+ this.defaultSolversFixture.restoreDefaultSolvers();
+ },
+
+ testSimpleConstraintWithoutSolver: function () {
+ var obj = {a: 2, b: 3};
+ bbb.always({
+ ctx: {
+ obj: obj
+ }
+ }, function() {
+ return obj.a + obj.b == 3;
+ });
+ this.assert(obj.a + obj.b == 3, "Automatic solver selection did not produce a working solution");
+ },
+
+ testSuggestingNewValues: function () {
+ var obj = {a: 2, b: 3};
+ bbb.always({
+ ctx: {
+ obj: obj
+ }
+ }, function() {
+ return obj.a + obj.b == 3;
+ });
+ this.assert(obj.a + obj.b == 3, "Automatic solver selection did not produce a " +
+ "working solution");
+ obj.a = 1;
+ this.assert(obj.a === 1, "Assignment should be honored");
+ this.assert(obj.a + obj.b == 3, "Constraint should have adapted the other " +
+ "variable to fulfill the constraint");
+ obj.b = 3;
+ this.assert(obj.b === 3, "Assignment should be honored");
+ this.assert(obj.a + obj.b == 3, "Constraint should have adapted the other " +
+ "variable to fulfill the constraint");
+ },
+
+ testSelfAssignmentOperations: function () {
+ bbb.defaultSolvers = [new ClSimplexSolver(), new ClSimplexSolver()];
+ var obj = {a: 2, b: 3};
+ bbb.always({
+ ctx: {
+ obj: obj
+ }
+ }, function() {
+ return obj.a + obj.b == 3;
+ });
+ this.assert(obj.a + obj.b == 3, "Automatic solver selection did not produce a " +
+ "working solution");
+ var oldA = obj.a;
+ obj.a += 1;
+ this.assert(obj.a === oldA + 1, "Assignment should be honored");
+ this.assert(obj.a + obj.b == 3, "Constraint should have adapted the other " +
+ "variable to fulfill the constraint");
+ obj.a += 1;
+ this.assert(obj.a === oldA + 2, "Assignment should be honored");
+ this.assert(obj.a + obj.b == 3, "Constraint should have adapted the other " +
+ "variable to fulfill the constraint");
+ },
+
+ // TODO: move this to Details test case
+ testConstraintVariableDefiningConstraint: function () {
+ var obj = {a: 2, b: 3};
+ var constraint = bbb.always({
+ ctx: {
+ obj: obj
+ }
+ }, function() {
+ return obj.a + obj.b == 3;
+ });
+ for (var i = 0; i < constraint.constraintvariables.length; i++) {
+ var constraintVariable = constraint.constraintvariables[i];
+ this.assert(constraintVariable.definingConstraint === constraint);
+ }
+ },
+
+ testSimplePropagationShouldChooseDeltaBlue: function() {
+ var o = {string: "0",
+ number: 0};
+
+ bbb.always({
+ ctx: {
+ o: o
+ }, methods: function () {
+ o.string.formula([o.number], function (num) { return num + "" });
+ o.number.formula([o.string], function (str) { return parseInt(str) });
+ }
+ }, function () {
+ return o.string == o.number + "";
+ });
+
+ this.assert(o.string === o.number + "");
+ o.string = "1"
+ this.assert(o.number === 1);
+ o.number = 12
+ this.assert(o.string === "12");
+ },
+ testBacktalkPaperExampleWithAutomaticSolverSelection: function () {
+ var man = {
+ shoes: "foo",
+ shirt: "foo",
+ pants: "foo",
+ hat: "foo"
+ };
+
+ bbb.always({
+ ctx: {
+ bbb: bbb,
+ csp: csp,
+ man: man,
+ _$_self: this.doitContext || this
+ }
+ }, function() {
+ return man.shoes.is in ["brown", "black"];;
+ });
+
+ bbb.always({
+ ctx: {
+ bbb: bbb,
+ csp: csp,
+ man: man,
+ _$_self: this.doitContext || this
+ }
+ }, function() {
+ return man.shirt.is in ["brown", "blue", "white"];;
+ });
+
+ bbb.always({
+ ctx: {
+ bbb: bbb,
+ csp: csp,
+ man: man,
+ _$_self: this.doitContext || this
+ }
+ }, function() {
+ return man.pants.is in ["brown", "blue", "black", "white"];;
+ });
+
+ bbb.always({
+ ctx: {
+ bbb: bbb,
+ csp: csp,
+ man: man,
+ _$_self: this.doitContext || this
+ }
+ }, function() {
+ return man.hat.is in ["brown"];;
+ });
+
+ bbb.always({
+ ctx: {
+ bbb: bbb,
+ csp: csp,
+ man: man,
+ _$_self: this.doitContext || this
+ }
+ }, function() {
+ return man.shoes === man.hat;;
+ });
+
+ bbb.always({
+ ctx: {
+ bbb: bbb,
+ csp: csp,
+ man: man,
+ _$_self: this.doitContext || this
+ }
+ }, function() {
+ return man.shoes !== man.pants;;
+ });
+
+ bbb.always({
+ ctx: {
+ bbb: bbb,
+ csp: csp,
+ man: man,
+ _$_self: this.doitContext || this
+ }
+ }, function() {
+ return man.shoes !== man.shirt;;
+ });
+
+ bbb.always({
+ ctx: {
+ bbb: bbb,
+ csp: csp,
+ man: man,
+ _$_self: this.doitContext || this
+ }
+ }, function() {
+ return man.shirt !== man.pants;;
+ });
+
+ this.assert(man.hat === "brown", "hat's domain is restricted to 'brown' only");
+ this.assert(man.shoes === "brown", "shoes have to be 'brown'");
+ this.assert(man.shirt === "blue" || man.shirt === "white", "shirt has to be 'blue' or 'white'");
+ this.assert(man.shirt !== man.pants, "shirt and pants must not have the same color");
+ this.assert(man.pants === "black" || man.pants === "blue" || man.pants === "white", "pants should be 'black', 'blue' or 'white'");
+ },
+ testFilteringByPriority: function () {
+ var testCase = this;
+ Object.subclass('DummySolver', {
+ always: function(opts, func) { testCase._askedDummySolver = true; throw new Error('will be caught'); },
+ solverName: 'TestDummy',
+ supportsMethods: function() { return true; },
+ supportsSoftConstraints: function() { return false; },
+ supportsFiniteDomains: function() { return false; },
+ supportedDataTypes: function() { return ['number']; }
+ });
+
+ bbb.defaultSolvers = [new DummySolver(), new ClSimplexSolver()];
+ var obj = {a: 2, b: 3};
+ bbb.always({
+ ctx: {
+ obj: obj
+ },
+ logReasons: true,
+ priority: 'low',
+ }, function() {
+ return obj.a + obj.b == 3;
+ });
+ this.assert(!this._askedDummySolver, "should not have asked solver to try");
+ this.assert(obj.a + obj.b == 3, "Automatic solver selection did not produce a working solution");
+ },
+ testFilteringByMethods: function () {
+ var testCase = this;
+ Object.subclass('DummySolver', {
+ always: function(opts, func) { testCase._askedDummySolver = true; throw new Error('will be caught'); },
+ solverName: 'TestDummy',
+ supportsMethods: function() { return false; },
+ supportsSoftConstraints: function() { return false; },
+ supportsFiniteDomains: function() { return false; },
+ supportedDataTypes: function() { return ['number', 'string']; }
+ });
+
+ bbb.defaultSolvers = [new DummySolver(), new DBPlanner()];
+ var o = {string: "0",
+ number: 0};
+
+ bbb.always({
+ ctx: {
+ o: o
+ }, methods: function () {
+ o.string.formula([o.number], function (num) { return num + "" });
+ o.number.formula([o.string], function (str) { return parseInt(str) });
+ },
+ logReasons: true
+ }, function () {
+ return o.string == o.number + "";
+ });
+
+ this.assert(!this._askedDummySolver, "should not have asked solver to try");
+ this.assert(o.string === o.number + "");
+ o.string = "1"
+ this.assert(o.number === 1);
+ o.number = 12
+ this.assert(o.string === "12");
+ },
+ testFilteringByDataTypeOnSlots: function () {
+ var testCase = this;
+ Object.subclass('DummySolver', {
+ always: function(opts, func) { testCase._askedDummySolver = true; throw new Error('will be caught'); },
+ solverName: 'TestDummy',
+ supportsMethods: function() { return true; },
+ supportsSoftConstraints: function() { return true; },
+ supportsFiniteDomains: function() { return false; },
+ supportedDataTypes: function() { return ['string']; }
+ });
+
+ bbb.defaultSolvers = [new DummySolver(), new ClSimplexSolver()];
+ var obj = {a: 2, b: 3};
+ bbb.always({
+ ctx: {
+ obj: obj
+ },
+ logReasons: true
+ }, function() {
+ return obj.a + obj.b == 3;
+ });
+ this.assert(!this._askedDummySolver, "should not have asked solver to try");
+ this.assert(obj.a + obj.b == 3, "Automatic solver selection did not produce a working solution");
+ },
+ testFilteringByDataTypeOnCalls: function () {
+ var testCase = this;
+ Object.subclass('DummySolver', {
+ always: function(opts, func) { testCase._askedDummySolver = true; throw new Error('will be caught'); },
+ solverName: 'TestDummy',
+ supportsMethods: function() { return true; },
+ supportsSoftConstraints: function() { return true; },
+ supportsFiniteDomains: function() { return false; },
+ supportedDataTypes: function() { return ['string']; }
+ });
+
+ bbb.defaultSolvers = [new DummySolver(), new ClSimplexSolver()];
+ var obj = {a: 2, get: function(){ return this.a; }};
+ obj[0] = 3;
+ var inc = function(i) { return i + 1;};
+ bbb.always({
+ ctx: {
+ obj: obj,
+ inc: inc
+ },
+ logReasons: true
+ }, function() {
+ return obj.get() == inc(obj[0]);
+ });
+ this.assert(!this._askedDummySolver, "should not have asked solver to try");
+ this.assert(obj.get() == inc(obj[0]), "Automatic solver selection did not produce a working solution");
+ },
+ testFilteringByFiniteDomains: function () {
+ var testCase = this;
+ Object.subclass('DummySolver', {
+ always: function(opts, func) { testCase._askedDummySolver = true; throw new Error('will be caught'); },
+ solverName: 'TestDummy',
+ supportsMethods: function() { return true; },
+ supportsSoftConstraints: function() { return true; },
+ supportsFiniteDomains: function() { return false; },
+ supportedDataTypes: function() { return ['string']; }
+ });
+
+ bbb.defaultSolvers = [new DummySolver(), new csp.Solver()];
+
+ var man = {
+ shoes: "foo"
+ };
+
+ bbb.always({
+ ctx: {
+ man: man
+ },
+ logReasons: true
+ }, function() {
+ return man.shoes.is in ["brown", "black"];;
+ });
+ this.assert(!this._askedDummySolver, "should not have asked solver to try");
+ this.assert(man.shoes === "brown" || man.shoes === "black", "Automatic solver selection did not produce a working solution");
+ },
+ testReevaluationAfterDefaultNumberOfSolvingOperations: function() {
+ preparePatchedSolvers();
+ var obj = {a: 2, b: 3};
+ bbb.defaultSolvers[0].forcedDelay = 10;
+ bbb.defaultSolvers[1].forcedDelay = 0;
+ bbb.defaultReevaluationInterval = 2; // recalculate after two updates
+ var constraint = bbb.always({
+ ctx: {
+ obj: obj
+ }
+ }, function() {
+ return obj.a + obj.b == 3;
+ });
+ this.assert(constraint.solver === bbb.defaultSolvers[1],
+ "the initially faster solver should have been chosen");
+ bbb.defaultSolvers[0].forcedDelay = 0;
+ bbb.defaultSolvers[1].forcedDelay = 10;
+ for (var i = 0; i < 2; i++) {
+ obj.a += 1;
+ }
+ this.assert(constraint.solver === bbb.defaultSolvers[0],
+ "the solver should have changed to the new faster solver");
+ bbb.defaultSolvers[1].forcedSolveAction = function() {
+ this.assert(false, 'The slower solver should not be called anymore.');
+ }.bind(this);
+ constraint.reevaluationInterval = 1000;
+ obj.a += 1;
+ },
+
+ testCallsToSolvers: function() {
+ preparePatchedSolvers();
+ var obj = {a: 2, b: 3, c: 5};
+ bbb.defaultSolvers[0].forcedDelay = 10;
+ bbb.defaultSolvers[1].forcedDelay = 0;
+ var constraint = bbb.always({
+ ctx: {
+ obj: obj
+ },
+ reevaluationInterval: 3
+ }, function() {
+ return obj.a + obj.b == 3 && obj.c == obj.a + obj.b;
+ });
+ bbb.defaultSolvers[0].solveCalls = 0;
+ bbb.defaultSolvers[0].forcedSolveAction = function() {
+ this.solveCalls += 1;
+ ClSimplexSolver.prototype.solve.call(this);
+ };
+ bbb.defaultSolvers[1].solveCalls = 0;
+ bbb.defaultSolvers[1].forcedSolveAction = bbb.defaultSolvers[0].forcedSolveAction;
+ var otherSolver = bbb.defaultSolvers[constraint.solver === bbb.defaultSolvers[0] ?
+ 1 : 0];
+ for (var i = 0; i < 2; i++) {
+ obj.a += 1;
+ }
+ this.assert(constraint.solver.solveCalls >= 2, 'Chosen solver should have ' +
+ 'been called two times');
+ this.assert(otherSolver.solveCalls === 0, 'Unselected solver should ' +
+ 'not have been called');
+ constraint.solver.solveCalls = 0;
+ otherSolver.solveCalls = 0;
+ obj.a += 1; // should cause reevaluation
+ this.assert(constraint.solver.solveCalls >= 1, 'Chosen solver should have ' +
+ 'been called for reevaluation');
+ this.assert(otherSolver.solveCalls >= 1, 'Unselected solver should ' +
+ 'have been called for reevaluation');
+ // in case the solver has changed, update our otherSolver variable
+ // (it should not, but we do not wish to assert that here)
+ var otherSolver = bbb.defaultSolvers[constraint.solver === bbb.defaultSolvers[0] ?
+ 1 : 0];
+ constraint.solver.solveCalls = 0;
+ otherSolver.solveCalls = 0;
+ for (var i = 0; i < 2; i++) {
+ obj.a += 1;
+ }
+ this.assert(constraint.solver.solveCalls >= 2, 'Chosen solver should be called');
+ this.assert(otherSolver.solveCalls === 0, 'Unchosen solver should not be called');
+ constraint.solver.solveCalls = 0;
+ otherSolver.solveCalls = 0;
+ obj.a += 1; // should cause reevaluation
+ this.assert(constraint.solver.solveCalls >= 1, 'Chosen solver should have ' +
+ 'been called for reevaluation');
+ this.assert(otherSolver.solveCalls >= 1, 'Unselected solver should ' +
+ 'have been called for reevaluation');
+ }
+});
}) // end of module
diff --git a/backtalk/backtalk_ext.js b/backtalk/backtalk_ext.js
index 2466eab..08d0080 100644
--- a/backtalk/backtalk_ext.js
+++ b/backtalk/backtalk_ext.js
@@ -37,7 +37,6 @@ Object.subclass('BacktalkSolver', {
func.allowUnsolvableOperations = true;
func.varMapping = ctx;
var cobj = new Constraint(func, this);
- debugger
if (cobj.constraintobjects.length === 1 && needsFunc) {
this.convertTestToFuncConstraint(cobj, func, opts);
}
@@ -85,6 +84,13 @@ Object.subclass('BacktalkSolver', {
},
weight: 200,
isConstraintObject: true,
+ solverName: 'Backtalk',
+ supportsMethods: function() { return false; },
+ supportsSoftConstraints: function() { return false; /* XXX: is this correct? */ },
+ supportsFiniteDomains: function() { return true; },
+ supportedDataTypes: function() {
+ return ['number', 'boolean', 'string', 'object']; /* XXX: is this correct? */
+ }
});
Object.subclass('BacktalkVariable', {
@@ -155,7 +161,6 @@ Object.subclass('BacktalkVariable', {
},
cnIn: function(ary) {
var domain;
- debugger
if (ary instanceof this.constructor) {
domain = ary.value();
} else {
diff --git a/reactive/reactive.js b/reactive/reactive.js
index f5785d8..9e967eb 100644
--- a/reactive/reactive.js
+++ b/reactive/reactive.js
@@ -13,6 +13,7 @@ module('users.timfelgentreff.reactive.reactive').requires('users.timfelgentreff.
var cobj = new Constraint(func, this);
cobj.allowFailing = true;
cobj.addPrimitiveConstraint(new ReactiveSolver.Constraint(this, cobj, func));
+ cobj.opts = opts;
try {
if(!opts.postponeEnabling) { cobj.enable(); }
} catch(e) {
@@ -31,7 +32,12 @@ module('users.timfelgentreff.reactive.reactive').requires('users.timfelgentreff.
this.constraint.enabled &&
typeof this.constraint.predicate === "function";
},
- weight: 10000
+ weight: 10000,
+ solverName: 'reactive',
+ supportsMethods: function() { return false; },
+ supportsSoftConstraints: function() { return false; },
+ supportsFiniteDomains: function() { return false; },
+ supportedDataTypes: function() { return ['number', 'boolean', 'string', 'object']; /* XXX: is this correct? */ }
});
Object.subclass("ReactiveSolver.Variable", {
diff --git a/standalone/test.html b/standalone/test.html
index 91bdbeb..17ee4d4 100644
--- a/standalone/test.html
+++ b/standalone/test.html
@@ -66,15 +66,31 @@
diff --git a/sutherland/relax_bbb.js b/sutherland/relax_bbb.js
index ee6e1e0..7594e92 100644
--- a/sutherland/relax_bbb.js
+++ b/sutherland/relax_bbb.js
@@ -7,12 +7,12 @@ module('users.timfelgentreff.sutherland.relax_bbb').
Relax.prototype.always = function(opts, func) {
if (opts.priority) {
- throw 'soft constraints not implemented for Z3';
+ throw 'soft constraints not implemented for relax';
}
func.varMapping = opts.ctx;
var constraint = new Constraint(func, this);
this.addConstraint(constraint.constraintobjects[0]);
- this.solve();
+ //this.solve();
return constraint;
};
@@ -43,6 +43,12 @@ Relax.prototype.solve = function() {
Relax.prototype.weight = 100;
+Relax.prototype.solverName = 'Relax';
+Relax.prototype.supportsMethods = function() { return false; };
+Relax.prototype.supportsSoftConstraints = function() { return false; };
+Relax.prototype.supportsFiniteDomains = function() { return false; };
+Relax.prototype.supportedDataTypes = function() { return ['number']; };
+
RelaxNode.prototype.isConstraintObject = function() {
return true;
};
@@ -205,7 +211,7 @@ RelaxNode.prototype.cnOr = function(r) {
return this;
};
-RelaxNode.prototype.enable = function() { /* ignored */ };
+RelaxNode.prototype.enable = function() { this.solver.solve(); };
RelaxNode.prototype.disable = function() { /* ignored */ };
}); // end of module
diff --git a/z3/NaClZ3.js b/z3/NaClZ3.js
index a8141e9..bfefcf7 100644
--- a/z3/NaClZ3.js
+++ b/z3/NaClZ3.js
@@ -203,7 +203,7 @@ module('users.timfelgentreff.z3.NaClZ3').requires().toRun(function() {
}
func.varMapping = opts.ctx;
var constraint = new Constraint(func, this);
- constraint.enable();
+ // constraint.enable();
return constraint;
},
constraintVariableFor: function(value, ivarname, cvar) {
@@ -288,6 +288,11 @@ module('users.timfelgentreff.z3.NaClZ3').requires().toRun(function() {
return acc + "\n" + "(assert " + c.print() + ")";
});
},
+ solverName: 'Z3',
+ supportsMethods: function() { return false; },
+ supportsSoftConstraints: function() { return false; },
+ supportsFiniteDomains: function() { return false; },
+ supportedDataTypes: function() { return ['number', 'boolean']; },
});
if (URL && URL.codeBase && URL.codeBase.withFilename) {
diff --git a/z3/Z3BBBTests.js b/z3/Z3BBBTests.js
index 66e829a..088ae2a 100644
--- a/z3/Z3BBBTests.js
+++ b/z3/Z3BBBTests.js
@@ -5,19 +5,22 @@ module('users.timfelgentreff.z3.Z3BBBTests').requires("users.timfelgentreff.babe
var solver = new CommandLineZ3(true),
res = {major: 0, minor: 0, patch: 0},
req = {major: 3, minor: 1, patch: 0};
- solver.always({
+ var opts = {
ctx: {
res: res,
req: req,
ro: bbb.readonly
}
- }, function () {
+ };
+ var constraint = solver.always(opts, function () {
return (
(res.major >= ro(req.major)) ||
(res.major == ro(req.major) && res.minor >= ro(req.minor)) ||
(res.major == ro(req.major) && res.minor == ro(req.minor) && res.patch >= ro(req.patch))
);
});
+ constraint.opts = opts;
+ constraint.enable();
this.assert((res.major >= req.major) ||
(res.major == req.major && res.minor >= req.minor) ||