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 HPI Logo + + + +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) ||