From 8ebbba2bc0498acc431af7c4aefbca39d89992cd Mon Sep 17 00:00:00 2001 From: Bob Nystrom Date: Mon, 10 Feb 2014 07:56:11 -0800 Subject: [PATCH] Get delta_blue benchmark working in Wren. --- benchmark/delta_blue.lua.inprogress | 914 ++++++++++++++++++++++++++++ benchmark/delta_blue.py | 639 +++++++++++++++++++ benchmark/delta_blue.wren | 735 ++++++++++++++++++++++ benchmark/run_bench | 2 + 4 files changed, 2290 insertions(+) create mode 100644 benchmark/delta_blue.lua.inprogress create mode 100644 benchmark/delta_blue.py create mode 100644 benchmark/delta_blue.wren diff --git a/benchmark/delta_blue.lua.inprogress b/benchmark/delta_blue.lua.inprogress new file mode 100644 index 00000000..e11f0571 --- /dev/null +++ b/benchmark/delta_blue.lua.inprogress @@ -0,0 +1,914 @@ +-- Copyright 2008 the V8 project authors. All rights reserved. +-- Copyright 1996 John Maloney and Mario Wolczko. + +-- This program is free software; you can redistribute it and/or modify +-- it under the terms of the GNU General Public License as published by +-- the Free Software Foundation; either version 2 of the License, or +-- (at your option) any later version. +-- +-- This program is distributed in the hope that it will be useful, +-- but WITHOUT ANY WARRANTY; without even the implied warranty of +-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +-- GNU General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License +-- along with this program; if not, write to the Free Software +-- Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +-- This implementation of the DeltaBlue benchmark is derived +-- from the Smalltalk implementation by John Maloney and Mario +-- Wolczko. Some parts have been translated directly, whereas +-- others have been modified more aggresively to make it feel +-- more like a JavaScript program. + + +-- +-- A JavaScript implementation of the DeltaBlue constraint-solving +-- algorithm, as described in: +-- +-- "The DeltaBlue Algorithm: An Incremental Constraint Hierarchy Solver" +-- Bjorn N. Freeman-Benson and John Maloney +-- January 1990 Communications of the ACM, +-- also available as University of Washington TR 89-08-06. +-- +-- Beware: this benchmark is written in a grotesque style where +-- the constraint model is built by side-effects from constructors. +-- I've kept it this way to avoid deviating too much from the original +-- implementation. +-- + +-- From: https://github.com/mraleph/deltablue.lua + +local planner + +--- O b j e c t M o d e l --- + +local function alert (...) print(...) end + +local OrderedCollection = class() + +function OrderedCollection:constructor() + self.elms = {} +end + +function OrderedCollection:add(elm) + self.elms[#self.elms + 1] = elm +end + +function OrderedCollection:at (index) + return self.elms[index] +end + +function OrderedCollection:size () + return #self.elms +end + +function OrderedCollection:removeFirst () + local e = self.elms[#self.elms] + self.elms[#self.elms] = nil + return e +end + +function OrderedCollection:remove (elm) + local index = 0 + local skipped = 0 + + for i = 1, #self.elms do + local value = self.elms[i] + if value ~= elm then + self.elms[index] = value + index = index + 1 + else + skipped = skipped + 1 + end + end + + local l = #self.elms + for i = 1, skipped do self.elms[l - i + 1] = nil end +end + +-- +-- S t r e n g t h +-- + +-- +-- Strengths are used to measure the relative importance of constraints. +-- New strengths may be inserted in the strength hierarchy without +-- disrupting current constraints. Strengths cannot be created outside +-- this class, so pointer comparison can be used for value comparison. +-- + +local Strength = class() + +function Strength:constructor(strengthValue, name) + self.strengthValue = strengthValue + self.name = name +end + +function Strength.stronger (s1, s2) + return s1.strengthValue < s2.strengthValue +end + +function Strength.weaker (s1, s2) + return s1.strengthValue > s2.strengthValue +end + +function Strength.weakestOf (s1, s2) + return Strength.weaker(s1, s2) and s1 or s2 +end + +function Strength.strongest (s1, s2) + return Strength.stronger(s1, s2) and s1 or s2 +end + +function Strength:nextWeaker () + local v = self.strengthValue + if v == 0 then return Strength.WEAKEST + elseif v == 1 then return Strength.WEAK_DEFAULT + elseif v == 2 then return Strength.NORMAL + elseif v == 3 then return Strength.STRONG_DEFAULT + elseif v == 4 then return Strength.PREFERRED + elseif v == 5 then return Strength.REQUIRED + end +end + +-- Strength constants. +Strength.REQUIRED = Strength.new(0, "required"); +Strength.STONG_PREFERRED = Strength.new(1, "strongPreferred"); +Strength.PREFERRED = Strength.new(2, "preferred"); +Strength.STRONG_DEFAULT = Strength.new(3, "strongDefault"); +Strength.NORMAL = Strength.new(4, "normal"); +Strength.WEAK_DEFAULT = Strength.new(5, "weakDefault"); +Strength.WEAKEST = Strength.new(6, "weakest"); + +-- +-- C o n s t r a i n t +-- + +-- +-- An abstract class representing a system-maintainable relationship +-- (or "constraint") between a set of variables. A constraint supplies +-- a strength instance variable; concrete subclasses provide a means +-- of storing the constrained variables and other information required +-- to represent a constraint. +-- + +local Constraint = class () + +function Constraint:constructor(strength) + self.strength = strength +end + +-- +-- Activate this constraint and attempt to satisfy it. +-- +function Constraint:addConstraint () + self:addToGraph() + planner:incrementalAdd(self) +end + +-- +-- Attempt to find a way to enforce this constraint. If successful, +-- record the solution, perhaps modifying the current dataflow +-- graph. Answer the constraint that this constraint overrides, if +-- there is one, or nil, if there isn't. +-- Assume: I am not already satisfied. +-- +function Constraint:satisfy (mark) + self:chooseMethod(mark) + if not self:isSatisfied() then + if self.strength == Strength.REQUIRED then + alert("Could not satisfy a required constraint!") + end + return nil + end + self:markInputs(mark) + local out = self:output() + local overridden = out.determinedBy + if overridden ~= nil then overridden:markUnsatisfied() end + out.determinedBy = self + if not planner:addPropagate(self, mark) then alert("Cycle encountered") end + out.mark = mark + return overridden +end + +function Constraint:destroyConstraint () + if self:isSatisfied() + then planner:incrementalRemove(self) + else self:removeFromGraph() + end +end + +-- +-- Normal constraints are not input constraints. An input constraint +-- is one that depends on external state, such as the mouse, the +-- keybord, a clock, or some arbitraty piece of imperative code. +-- +function Constraint:isInput () + return false +end + + +-- +-- U n a r y C o n s t r a i n t +-- + +-- +-- Abstract superclass for constraints having a single possible output +-- variable. +-- + +local UnaryConstraint = class(Constraint) + +function UnaryConstraint:constructor (v, strength) + UnaryConstraint.super.constructor(self, strength) + self.myOutput = v + self.satisfied = false + self:addConstraint() +end + +-- +-- Adds this constraint to the constraint graph +-- +function UnaryConstraint:addToGraph () + self.myOutput:addConstraint(self) + self.satisfied = false +end + +-- +-- Decides if this constraint can be satisfied and records that +-- decision. +-- +function UnaryConstraint:chooseMethod (mark) + self.satisfied = (self.myOutput.mark ~= mark) + and Strength.stronger(self.strength, self.myOutput.walkStrength); +end + +-- +-- Returns true if this constraint is satisfied in the current solution. +-- +function UnaryConstraint:isSatisfied () + return self.satisfied; +end + +function UnaryConstraint:markInputs (mark) + -- has no inputs +end + +-- +-- Returns the current output variable. +-- +function UnaryConstraint:output () + return self.myOutput +end + +-- +-- Calculate the walkabout strength, the stay flag, and, if it is +-- 'stay', the value for the current output of this constraint. Assume +-- this constraint is satisfied. +-- +function UnaryConstraint:recalculate () + self.myOutput.walkStrength = self.strength + self.myOutput.stay = not self:isInput() + if self.myOutput.stay then + self:execute() -- Stay optimization + end +end + +-- +-- Records that this constraint is unsatisfied +-- +function UnaryConstraint:markUnsatisfied () + self.satisfied = false +end + +function UnaryConstraint:inputsKnown () + return true +end + +function UnaryConstraint:removeFromGraph () + if self.myOutput ~= nil then + self.myOutput:removeConstraint(self) + end + self.satisfied = false +end + +-- +-- S t a y C o n s t r a i n t +-- + +-- +-- Variables that should, with some level of preference, stay the same. +-- Planners may exploit the fact that instances, if satisfied, will not +-- change their output during plan execution. This is called "stay +-- optimization". +-- + +local StayConstraint = class(UnaryConstraint) + +function StayConstraint:constructor(v, str) + StayConstraint.super.constructor(self, v, str) +end + +function StayConstraint:execute () + -- Stay constraints do nothing +end + +-- +-- E d i t C o n s t r a i n t +-- + +-- +-- A unary input constraint used to mark a variable that the client +-- wishes to change. +-- + +local EditConstraint = class (UnaryConstraint) + +function EditConstraint:constructor(v, str) + EditConstraint.super.constructor(self, v, str) +end + +-- +-- Edits indicate that a variable is to be changed by imperative code. +-- +function EditConstraint:isInput () + return true +end + +function EditConstraint:execute () + -- Edit constraints do nothing +end + +-- +-- B i n a r y C o n s t r a i n t +-- + +local Direction = {} +Direction.NONE = 0 +Direction.FORWARD = 1 +Direction.BACKWARD = -1 + +-- +-- Abstract superclass for constraints having two possible output +-- variables. +-- + +local BinaryConstraint = class(Constraint) + +function BinaryConstraint:constructor(var1, var2, strength) + BinaryConstraint.super.constructor(self, strength); + self.v1 = var1 + self.v2 = var2 + self.direction = Direction.NONE + self:addConstraint() +end + + +-- +-- Decides if this constraint can be satisfied and which way it +-- should flow based on the relative strength of the variables related, +-- and record that decision. +-- +function BinaryConstraint:chooseMethod (mark) + if self.v1.mark == mark then + self.direction = (self.v2.mark ~= mark and Strength.stronger(self.strength, self.v2.walkStrength)) and Direction.FORWARD or Direction.NONE + end + if self.v2.mark == mark then + self.direction = (self.v1.mark ~= mark and Strength.stronger(self.strength, self.v1.walkStrength)) and Direction.BACKWARD or Direction.NONE + end + if Strength.weaker(self.v1.walkStrength, self.v2.walkStrength) then + self.direction = Strength.stronger(self.strength, self.v1.walkStrength) and Direction.BACKWARD or Direction.NONE + else + self.direction = Strength.stronger(self.strength, self.v2.walkStrength) and Direction.FORWARD or Direction.BACKWARD + end +end + +-- +-- Add this constraint to the constraint graph +-- +function BinaryConstraint:addToGraph () + self.v1:addConstraint(self) + self.v2:addConstraint(self) + self.direction = Direction.NONE +end + +-- +-- Answer true if this constraint is satisfied in the current solution. +-- +function BinaryConstraint:isSatisfied () + return self.direction ~= Direction.NONE +end + +-- +-- Mark the input variable with the given mark. +-- +function BinaryConstraint:markInputs (mark) + self:input().mark = mark +end + +-- +-- Returns the current input variable +-- +function BinaryConstraint:input () + return (self.direction == Direction.FORWARD) and self.v1 or self.v2 +end + +-- +-- Returns the current output variable +-- +function BinaryConstraint:output () + return (self.direction == Direction.FORWARD) and self.v2 or self.v1 +end + +-- +-- Calculate the walkabout strength, the stay flag, and, if it is +-- 'stay', the value for the current output of this +-- constraint. Assume this constraint is satisfied. +-- +function BinaryConstraint:recalculate () + local ihn = self:input() + local out = self:output() + out.walkStrength = Strength.weakestOf(self.strength, ihn.walkStrength); + out.stay = ihn.stay + if out.stay then self:execute() end +end + +-- +-- Record the fact that self constraint is unsatisfied. +-- +function BinaryConstraint:markUnsatisfied () + self.direction = Direction.NONE +end + +function BinaryConstraint:inputsKnown (mark) + local i = self:input() + return i.mark == mark or i.stay or i.determinedBy == nil +end + +function BinaryConstraint:removeFromGraph () + if (self.v1 ~= nil) then self.v1:removeConstraint(self) end + if (self.v2 ~= nil) then self.v2:removeConstraint(self) end + self.direction = Direction.NONE +end + +-- +-- S c a l e C o n s t r a i n t +-- + +-- +-- Relates two variables by the linear scaling relationship: "v2 = +-- (v1 * scale) + offset". Either v1 or v2 may be changed to maintain +-- this relationship but the scale factor and offset are considered +-- read-only. +-- + +local ScaleConstraint = class (BinaryConstraint) + +function ScaleConstraint:constructor(src, scale, offset, dest, strength) + self.direction = Direction.NONE + self.scale = scale + self.offset = offset + ScaleConstraint.super.constructor(self, src, dest, strength) +end + + +-- +-- Adds this constraint to the constraint graph. +-- +function ScaleConstraint:addToGraph () + ScaleConstraint.super.addToGraph(self) + self.scale:addConstraint(self) + self.offset:addConstraint(self) +end + +function ScaleConstraint:removeFromGraph () + ScaleConstraint.super.removeFromGraph(self) + if (self.scale ~= nil) then self.scale:removeConstraint(self) end + if (self.offset ~= nil) then self.offset:removeConstraint(self) end +end + +function ScaleConstraint:markInputs (mark) + ScaleConstraint.super.markInputs(self, mark); + self.offset.mark = mark + self.scale.mark = mark +end + +-- +-- Enforce this constraint. Assume that it is satisfied. +-- +function ScaleConstraint:execute () + if self.direction == Direction.FORWARD then + self.v2.value = self.v1.value * self.scale.value + self.offset.value + else + self.v1.value = (self.v2.value - self.offset.value) / self.scale.value + end +end + +-- +-- Calculate the walkabout strength, the stay flag, and, if it is +-- 'stay', the value for the current output of this constraint. Assume +-- this constraint is satisfied. +-- +function ScaleConstraint:recalculate () + local ihn = self:input() + local out = self:output() + out.walkStrength = Strength.weakestOf(self.strength, ihn.walkStrength) + out.stay = ihn.stay and self.scale.stay and self.offset.stay + if out.stay then self:execute() end +end + +-- +-- E q u a l i t y C o n s t r a i n t +-- + +-- +-- Constrains two variables to have the same value. +-- + +local EqualityConstraint = class (BinaryConstraint) + +function EqualityConstraint:constructor(var1, var2, strength) + EqualityConstraint.super.constructor(self, var1, var2, strength) +end + + +-- +-- Enforce this constraint. Assume that it is satisfied. +-- +function EqualityConstraint:execute () + self:output().value = self:input().value +end + +-- +-- V a r i a b l e +-- + +-- +-- A constrained variable. In addition to its value, it maintain the +-- structure of the constraint graph, the current dataflow graph, and +-- various parameters of interest to the DeltaBlue incremental +-- constraint solver. +-- +local Variable = class () + +function Variable:constructor(name, initialValue) + self.value = initialValue or 0 + self.constraints = OrderedCollection.new() + self.determinedBy = nil + self.mark = 0 + self.walkStrength = Strength.WEAKEST + self.stay = true + self.name = name +end + +-- +-- Add the given constraint to the set of all constraints that refer +-- this variable. +-- +function Variable:addConstraint (c) + self.constraints:add(c) +end + +-- +-- Removes all traces of c from this variable. +-- +function Variable:removeConstraint (c) + self.constraints:remove(c) + if self.determinedBy == c then + self.determinedBy = nil + end +end + +-- +-- P l a n n e r +-- + +-- +-- The DeltaBlue planner +-- +local Planner = class() +function Planner:constructor() + self.currentMark = 0 +end + +-- +-- Attempt to satisfy the given constraint and, if successful, +-- incrementally update the dataflow graph. Details: If satifying +-- the constraint is successful, it may override a weaker constraint +-- on its output. The algorithm attempts to resatisfy that +-- constraint using some other method. This process is repeated +-- until either a) it reaches a variable that was not previously +-- determined by any constraint or b) it reaches a constraint that +-- is too weak to be satisfied using any of its methods. The +-- variables of constraints that have been processed are marked with +-- a unique mark value so that we know where we've been. This allows +-- the algorithm to avoid getting into an infinite loop even if the +-- constraint graph has an inadvertent cycle. +-- +function Planner:incrementalAdd (c) + local mark = self:newMark() + local overridden = c:satisfy(mark) + while overridden ~= nil do + overridden = overridden:satisfy(mark) + end +end + +-- +-- Entry point for retracting a constraint. Remove the given +-- constraint and incrementally update the dataflow graph. +-- Details: Retracting the given constraint may allow some currently +-- unsatisfiable downstream constraint to be satisfied. We therefore collect +-- a list of unsatisfied downstream constraints and attempt to +-- satisfy each one in turn. This list is traversed by constraint +-- strength, strongest first, as a heuristic for avoiding +-- unnecessarily adding and then overriding weak constraints. +-- Assume: c is satisfied. +-- +function Planner:incrementalRemove (c) + local out = c:output() + c:markUnsatisfied() + c:removeFromGraph() + local unsatisfied = self:removePropagateFrom(out) + local strength = Strength.REQUIRED + repeat + for i = 1, unsatisfied:size() do + local u = unsatisfied:at(i) + if u.strength == strength then + self:incrementalAdd(u) + end + end + strength = strength:nextWeaker() + until strength == Strength.WEAKEST +end + +-- +-- Select a previously unused mark value. +-- +function Planner:newMark () + self.currentMark = self.currentMark + 1 + return self.currentMark +end + +-- +-- Extract a plan for resatisfaction starting from the given source +-- constraints, usually a set of input constraints. This method +-- assumes that stay optimization is desired; the plan will contain +-- only constraints whose output variables are not stay. Constraints +-- that do no computation, such as stay and edit constraints, are +-- not included in the plan. +-- Details: The outputs of a constraint are marked when it is added +-- to the plan under construction. A constraint may be appended to +-- the plan when all its input variables are known. A variable is +-- known if either a) the variable is marked (indicating that has +-- been computed by a constraint appearing earlier in the plan), b) +-- the variable is 'stay' (i.e. it is a constant at plan execution +-- time), or c) the variable is not determined by any +-- constraint. The last provision is for past states of history +-- variables, which are not stay but which are also not computed by +-- any constraint. +-- Assume: sources are all satisfied. +-- +local Plan -- FORWARD DECLARATION +function Planner:makePlan (sources) + local mark = self:newMark() + local plan = Plan.new() + local todo = sources + while todo:size() > 0 do + local c = todo:removeFirst() + if c:output().mark ~= mark and c:inputsKnown(mark) then + plan:addConstraint(c) + c:output().mark = mark + self:addConstraintsConsumingTo(c:output(), todo) + end + end + return plan +end + +-- +-- Extract a plan for resatisfying starting from the output of the +-- given constraints, usually a set of input constraints. +-- +function Planner:extractPlanFromConstraints (constraints) + local sources = OrderedCollection.new() + for i = 1, constraints:size() do + local c = constraints:at(i) + if c:isInput() and c:isSatisfied() then + -- not in plan already and eligible for inclusion + sources:add(c) + end + end + return self:makePlan(sources) +end + +-- +-- Recompute the walkabout strengths and stay flags of all variables +-- downstream of the given constraint and recompute the actual +-- values of all variables whose stay flag is true. If a cycle is +-- detected, remove the given constraint and answer +-- false. Otherwise, answer true. +-- Details: Cycles are detected when a marked variable is +-- encountered downstream of the given constraint. The sender is +-- assumed to have marked the inputs of the given constraint with +-- the given mark. Thus, encountering a marked node downstream of +-- the output constraint means that there is a path from the +-- constraint's output to one of its inputs. +-- +function Planner:addPropagate (c, mark) + local todo = OrderedCollection.new() + todo:add(c) + while todo:size() > 0 do + local d = todo:removeFirst() + if d:output().mark == mark then + self:incrementalRemove(c) + return false + end + d:recalculate() + self:addConstraintsConsumingTo(d:output(), todo) + end + return true +end + + +-- +-- Update the walkabout strengths and stay flags of all variables +-- downstream of the given constraint. Answer a collection of +-- unsatisfied constraints sorted in order of decreasing strength. +-- +function Planner:removePropagateFrom (out) + out.determinedBy = nil + out.walkStrength = Strength.WEAKEST + out.stay = true + local unsatisfied = OrderedCollection.new() + local todo = OrderedCollection.new() + todo:add(out) + while todo:size() > 0 do + local v = todo:removeFirst() + for i = 1, v.constraints:size() do + local c = v.constraints:at(i) + if not c:isSatisfied() then unsatisfied:add(c) end + end + local determining = v.determinedBy + for i = 1, v.constraints:size() do + local next = v.constraints:at(i); + if next ~= determining and next:isSatisfied() then + next:recalculate() + todo:add(next:output()) + end + end + end + return unsatisfied +end + +function Planner:addConstraintsConsumingTo (v, coll) + local determining = v.determinedBy + local cc = v.constraints + for i = 1, cc:size() do + local c = cc:at(i) + if c ~= determining and c:isSatisfied() then + coll:add(c) + end + end +end + +-- +-- P l a n +-- + +-- +-- A Plan is an ordered list of constraints to be executed in sequence +-- to resatisfy all currently satisfiable constraints in the face of +-- one or more changing inputs. +-- +Plan = class() +function Plan:constructor() + self.v = OrderedCollection.new() +end + +function Plan:addConstraint (c) + self.v:add(c) +end + +function Plan:size () + return self.v:size() +end + +function Plan:constraintAt (index) + return self.v:at(index) +end + +function Plan:execute () + for i = 1, self:size() do + local c = self:constraintAt(i) + c:execute() + end +end + +-- +-- M a i n +-- + +-- +-- This is the standard DeltaBlue benchmark. A long chain of equality +-- constraints is constructed with a stay constraint on one end. An +-- edit constraint is then added to the opposite end and the time is +-- measured for adding and removing this constraint, and extracting +-- and executing a constraint satisfaction plan. There are two cases. +-- In case 1, the added constraint is stronger than the stay +-- constraint and values must propagate down the entire length of the +-- chain. In case 2, the added constraint is weaker than the stay +-- constraint so it cannot be accomodated. The cost in this case is, +-- of course, very low. Typical situations lie somewhere between these +-- two extremes. +-- +local function chainTest(n) + planner = Planner.new() + local prev = nil + local first = nil + local last = nil + + -- Build chain of n equality constraints + for i = 0, n do + local name = "v" .. i; + local v = Variable.new(name) + if prev ~= nil then EqualityConstraint.new(prev, v, Strength.REQUIRED) end + if i == 0 then first = v end + if i == n then last = v end + prev = v + end + + StayConstraint.new(last, Strength.STRONG_DEFAULT) + local edit = EditConstraint.new(first, Strength.PREFERRED) + local edits = OrderedCollection.new() + edits:add(edit) + local plan = planner:extractPlanFromConstraints(edits) + for i = 0, 99 do + first.value = i + plan:execute() + if last.value ~= i then + alert("Chain test failed.") + end + end +end + +local function change(v, newValue) + local edit = EditConstraint.new(v, Strength.PREFERRED) + local edits = OrderedCollection.new() + edits:add(edit) + local plan = planner:extractPlanFromConstraints(edits) + for i = 1, 10 do + v.value = newValue + plan:execute() + end + edit:destroyConstraint() +end + +-- +-- This test constructs a two sets of variables related to each +-- other by a simple linear transformation (scale and offset). The +-- time is measured to change a variable on either side of the +-- mapping and to change the scale and offset factors. +-- +local function projectionTest(n) + planner = Planner.new(); + local scale = Variable.new("scale", 10); + local offset = Variable.new("offset", 1000); + local src = nil + local dst = nil; + + local dests = OrderedCollection.new(); + for i = 0, n - 1 do + src = Variable.new("src" .. i, i); + dst = Variable.new("dst" .. i, i); + dests:add(dst); + StayConstraint.new(src, Strength.NORMAL); + ScaleConstraint.new(src, scale, offset, dst, Strength.REQUIRED); + end + + change(src, 17) + if dst.value ~= 1170 then alert("Projection 1 failed") end + change(dst, 1050) + if src.value ~= 5 then alert("Projection 2 failed") end + change(scale, 5) + for i = 0, n - 2 do + if dests:at(i + 1).value ~= i * 5 + 1000 then + alert("Projection 3 failed") + end + end + change(offset, 2000) + for i = 0, n - 2 do + if dests:at(i + 1).value ~= i * 5 + 2000 then + alert("Projection 4 failed") + end + end +end + +local function deltaBlue() + chainTest(100); + projectionTest(100); +end + +DeltaBlue = BenchmarkSuite.new('DeltaBlue', 66118, { + Benchmark.new('DeltaBlue', deltaBlue) +}) \ No newline at end of file diff --git a/benchmark/delta_blue.py b/benchmark/delta_blue.py new file mode 100644 index 00000000..f8890f57 --- /dev/null +++ b/benchmark/delta_blue.py @@ -0,0 +1,639 @@ +""" +deltablue.py +============ + +Ported for the PyPy project. + +This implementation of the DeltaBlue benchmark was directly ported +from the `V8's source code`_, which was in turn derived +from the Smalltalk implementation by John Maloney and Mario +Wolczko. The original Javascript implementation was licensed under the GPL. + +It's been updated in places to be more idiomatic to Python (for loops over +collections, a couple magic methods, ``OrderedCollection`` being a list & things +altering those collections changed to the builtin methods) but largely retains +the layout & logic from the original. (Ugh.) + +.. _`V8's source code`: (http://code.google.com/p/v8/source/browse/branches/bleeding_edge/benchmarks/deltablue.js) + +From: https://gist.github.com/toastdriven/6408132 + +""" +from __future__ import print_function +import time + +__author__ = 'Daniel Lindsley' +__license__ = 'BSD' + + +# The JS variant implements "OrderedCollection", which basically completely +# overlaps with ``list``. So we'll cheat. :D +class OrderedCollection(list): + pass + + +class Strength(object): + REQUIRED = None + STRONG_PREFERRED = None + PREFERRED = None + STRONG_DEFAULT = None + NORMAL = None + WEAK_DEFAULT = None + WEAKEST = None + + def __init__(self, strength, name): + super(Strength, self).__init__() + self.strength = strength + self.name = name + + @classmethod + def stronger(cls, s1, s2): + return s1.strength < s2.strength + + @classmethod + def weaker(cls, s1, s2): + return s1.strength > s2.strength + + @classmethod + def weakest_of(cls, s1, s2): + if cls.weaker(s1, s2): + return s1 + + return s2 + + @classmethod + def strongest(cls, s1, s2): + if cls.stronger(s1, s2): + return s1 + + return s2 + + def next_weaker(self): + strengths = { + 0: self.__class__.WEAKEST, + 1: self.__class__.WEAK_DEFAULT, + 2: self.__class__.NORMAL, + 3: self.__class__.STRONG_DEFAULT, + 4: self.__class__.PREFERRED, + # TODO: This looks like a bug in the original code. Shouldn't this be + # ``STRONG_PREFERRED? Keeping for porting sake... + 5: self.__class__.REQUIRED, + } + return strengths[self.strength] + + +# This is a terrible pattern IMO, but true to the original JS implementation. +Strength.REQUIRED = Strength(0, "required") +Strength.STONG_PREFERRED = Strength(1, "strongPreferred") +Strength.PREFERRED = Strength(2, "preferred") +Strength.STRONG_DEFAULT = Strength(3, "strongDefault") +Strength.NORMAL = Strength(4, "normal") +Strength.WEAK_DEFAULT = Strength(5, "weakDefault") +Strength.WEAKEST = Strength(6, "weakest") + + +class Constraint(object): + def __init__(self, strength): + super(Constraint, self).__init__() + self.strength = strength + + def add_constraint(self): + global planner + self.add_to_graph() + planner.incremental_add(self) + + def satisfy(self, mark): + global planner + self.choose_method(mark) + + if not self.is_satisfied(): + if self.strength == Strength.REQUIRED: + print('Could not satisfy a required constraint!') + + return None + + self.mark_inputs(mark) + out = self.output() + overridden = out.determined_by + + if overridden is not None: + overridden.mark_unsatisfied() + + out.determined_by = self + + if not planner.add_propagate(self, mark): + print('Cycle encountered') + + out.mark = mark + return overridden + + def destroy_constraint(self): + global planner + if self.is_satisfied(): + planner.incremental_remove(self) + else: + self.remove_from_graph() + + def is_input(self): + return False + + +class UrnaryConstraint(Constraint): + def __init__(self, v, strength): + super(UrnaryConstraint, self).__init__(strength) + self.my_output = v + self.satisfied = False + self.add_constraint() + + def add_to_graph(self): + self.my_output.add_constraint(self) + self.satisfied = False + + def choose_method(self, mark): + if self.my_output.mark != mark and \ + Strength.stronger(self.strength, self.my_output.walk_strength): + self.satisfied = True + else: + self.satisfied = False + + def is_satisfied(self): + return self.satisfied + + def mark_inputs(self, mark): + # No-ops. + pass + + def output(self): + # Ugh. Keeping it for consistency with the original. So much for + # "we're all adults here"... + return self.my_output + + def recalculate(self): + self.my_output.walk_strength = self.strength + self.my_output.stay = not self.is_input() + + if self.my_output.stay: + self.execute() + + def mark_unsatisfied(self): + self.satisfied = False + + def inputs_known(self, mark): + return True + + def remove_from_graph(self): + if self.my_output is not None: + self.my_output.remove_constraint(self) + self.satisfied = False + + +class StayConstraint(UrnaryConstraint): + def __init__(self, v, string): + super(StayConstraint, self).__init__(v, string) + + def execute(self): + # The methods, THEY DO NOTHING. + pass + + +class EditConstraint(UrnaryConstraint): + def __init__(self, v, string): + super(EditConstraint, self).__init__(v, string) + + def is_input(self): + return True + + def execute(self): + # This constraint also does nothing. + pass + + +class Direction(object): + # Hooray for things that ought to be structs! + NONE = 0 + FORWARD = 1 + BACKWARD = -1 + + +class BinaryConstraint(Constraint): + def __init__(self, v1, v2, strength): + super(BinaryConstraint, self).__init__(strength) + self.v1 = v1 + self.v2 = v2 + self.direction = Direction.NONE + self.add_constraint() + + def choose_method(self, mark): + if self.v1.mark == mark: + if self.v2.mark != mark and Strength.stronger(self.strength, self.v2.walk_strength): + self.direction = Direction.FORWARD + else: + self.direction = Direction.BACKWARD + + if self.v2.mark == mark: + if self.v1.mark != mark and Strength.stronger(self.strength, self.v1.walk_strength): + self.direction = Direction.BACKWARD + else: + self.direction = Direction.NONE + + if Strength.weaker(self.v1.walk_strength, self.v2.walk_strength): + if Strength.stronger(self.strength, self.v1.walk_strength): + self.direction = Direction.BACKWARD + else: + self.direction = Direction.NONE + else: + if Strength.stronger(self.strength, self.v2.walk_strength): + self.direction = Direction.FORWARD + else: + self.direction = Direction.BACKWARD + + def add_to_graph(self): + self.v1.add_constraint(self) + self.v2.add_constraint(self) + self.direction = Direction.NONE + + def is_satisfied(self): + return self.direction != Direction.NONE + + def mark_inputs(self, mark): + self.input().mark = mark + + def input(self): + if self.direction == Direction.FORWARD: + return self.v1 + + return self.v2 + + def output(self): + if self.direction == Direction.FORWARD: + return self.v2 + + return self.v1 + + def recalculate(self): + ihn = self.input() + out = self.output() + out.walk_strength = Strength.weakest_of(self.strength, ihn.walk_strength) + out.stay = ihn.stay + + if out.stay: + self.execute() + + def mark_unsatisfied(self): + self.direction = Direction.NONE + + def inputs_known(self, mark): + i = self.input() + return i.mark == mark or i.stay or i.determined_by == None + + def remove_from_graph(self): + if self.v1 is not None: + self.v1.remove_constraint(self) + + if self.v2 is not None: + self.v2.remove_constraint(self) + + self.direction = Direction.NONE + + +class ScaleConstraint(BinaryConstraint): + def __init__(self, src, scale, offset, dest, strength): + self.direction = Direction.NONE + self.scale = scale + self.offset = offset + super(ScaleConstraint, self).__init__(src, dest, strength) + + def add_to_graph(self): + super(ScaleConstraint, self).add_to_graph() + self.scale.add_constraint(self) + self.offset.add_constraint(self) + + def remove_from_graph(self): + super(ScaleConstraint, self).remove_from_graph() + + if self.scale is not None: + self.scale.remove_constraint(self) + + if self.offset is not None: + self.offset.remove_constraint(self) + + def mark_inputs(self, mark): + super(ScaleConstraint, self).mark_inputs(mark) + self.scale.mark = mark + self.offset.mark = mark + + def execute(self): + if self.direction == Direction.FORWARD: + self.v2.value = self.v1.value * self.scale.value + self.offset.value + else: + self.v1.value = (self.v2.value - self.offset.value) / self.scale.value + + def recalculate(self): + ihn = self.input() + out = self.output() + out.walk_strength = Strength.weakest_of(self.strength, ihn.walk_strength) + out.stay = ihn.stay and self.scale.stay and self.offset.stay + + if out.stay: + self.execute() + + +class EqualityConstraint(BinaryConstraint): + def execute(self): + self.output().value = self.input().value + + +class Variable(object): + def __init__(self, name, initial_value=0): + super(Variable, self).__init__() + self.name = name + self.value = initial_value + self.constraints = OrderedCollection() + self.determined_by = None + self.mark = 0 + self.walk_strength = Strength.WEAKEST + self.stay = True + + def __repr__(self): + # To make debugging this beast from pdb easier... + return '' % ( + self.name, + self.value + ) + + def add_constraint(self, constraint): + self.constraints.append(constraint) + + def remove_constraint(self, constraint): + self.constraints.remove(constraint) + + if self.determined_by == constraint: + self.determined_by = None + + +class Planner(object): + def __init__(self): + super(Planner, self).__init__() + self.current_mark = 0 + + def incremental_add(self, constraint): + mark = self.new_mark() + overridden = constraint.satisfy(mark) + + while overridden is not None: + overridden = overridden.satisfy(mark) + + def incremental_remove(self, constraint): + out = constraint.output() + constraint.mark_unsatisfied() + constraint.remove_from_graph() + unsatisfied = self.remove_propagate_from(out) + strength = Strength.REQUIRED + # Do-while, the Python way. + repeat = True + + while repeat: + for u in unsatisfied: + if u.strength == strength: + self.incremental_add(u) + + strength = strength.next_weaker() + + repeat = strength != Strength.WEAKEST + + def new_mark(self): + self.current_mark += 1 + return self.current_mark + + def make_plan(self, sources): + mark = self.new_mark() + plan = Plan() + todo = sources + + while len(todo): + c = todo.pop(0) + + if c.output().mark != mark and c.inputs_known(mark): + plan.add_constraint(c) + c.output().mark = mark + self.add_constraints_consuming_to(c.output(), todo) + + return plan + + def extract_plan_from_constraints(self, constraints): + sources = OrderedCollection() + + for c in constraints: + if c.is_input() and c.is_satisfied(): + sources.append(c) + + return self.make_plan(sources) + + def add_propagate(self, c, mark): + todo = OrderedCollection() + todo.append(c) + + while len(todo): + d = todo.pop(0) + + if d.output().mark == mark: + self.incremental_remove(c) + return False + + d.recalculate() + self.add_constraints_consuming_to(d.output(), todo) + + return True + + def remove_propagate_from(self, out): + out.determined_by = None + out.walk_strength = Strength.WEAKEST + out.stay = True + unsatisfied = OrderedCollection() + todo = OrderedCollection() + todo.append(out) + + while len(todo): + v = todo.pop(0) + + for c in v.constraints: + if not c.is_satisfied(): + unsatisfied.append(c) + + determining = v.determined_by + + for c in v.constraints: + if c != determining and c.is_satisfied(): + c.recalculate() + todo.append(c.output()) + + return unsatisfied + + def add_constraints_consuming_to(self, v, coll): + determining = v.determined_by + cc = v.constraints + + for c in cc: + if c != determining and c.is_satisfied(): + # I guess we're just updating a reference (``coll``)? Seems + # inconsistent with the rest of the implementation, where they + # return the lists... + coll.append(c) + + +class Plan(object): + def __init__(self): + super(Plan, self).__init__() + self.v = OrderedCollection() + + def add_constraint(self, c): + self.v.append(c) + + def __len__(self): + return len(self.v) + + def __getitem__(self, index): + return self.v[index] + + def execute(self): + for c in self.v: + c.execute() + + +# Main +total = 0 + +def chain_test(n): + """ + This is the standard DeltaBlue benchmark. A long chain of equality + constraints is constructed with a stay constraint on one end. An + edit constraint is then added to the opposite end and the time is + measured for adding and removing this constraint, and extracting + and executing a constraint satisfaction plan. There are two cases. + In case 1, the added constraint is stronger than the stay + constraint and values must propagate down the entire length of the + chain. In case 2, the added constraint is weaker than the stay + constraint so it cannot be accomodated. The cost in this case is, + of course, very low. Typical situations lie somewhere between these + two extremes. + """ + global planner + global total + + planner = Planner() + prev, first, last = None, None, None + + # We need to go up to n inclusively. + for i in range(n + 1): + name = "v%s" % i + v = Variable(name) + + if prev is not None: + EqualityConstraint(prev, v, Strength.REQUIRED) + + if i == 0: + first = v + + if i == n: + last = v + + prev = v + + StayConstraint(last, Strength.STRONG_DEFAULT) + edit = EditConstraint(first, Strength.PREFERRED) + edits = OrderedCollection() + edits.append(edit) + plan = planner.extract_plan_from_constraints(edits) + + for i in range(100): + first.value = i + plan.execute() + + total += last.value + if last.value != i: + print("Chain test failed.") + + +def projection_test(n): + """ + This test constructs a two sets of variables related to each + other by a simple linear transformation (scale and offset). The + time is measured to change a variable on either side of the + mapping and to change the scale and offset factors. + """ + global planner + global total + + planner = Planner() + scale = Variable("scale", 10) + offset = Variable("offset", 1000) + src, dest = None, None + + dests = OrderedCollection() + + for i in range(n): + src = Variable("src%s" % i, i) + dst = Variable("dst%s" % i, i) + dests.append(dst) + StayConstraint(src, Strength.NORMAL) + ScaleConstraint(src, scale, offset, dst, Strength.REQUIRED) + + change(src, 17) + + total += dst.value + if dst.value != 1170: + print("Projection 1 failed") + + change(dst, 1050) + + total += src.value + if src.value != 5: + print("Projection 2 failed") + + change(scale, 5) + + for i in range(n - 1): + total += dests[i].value + if dests[i].value != (i * 5 + 1000): + print("Projection 3 failed") + + change(offset, 2000) + + for i in range(n - 1): + total += dests[i].value + if dests[i].value != (i * 5 + 2000): + print("Projection 4 failed") + + +def change(v, new_value): + global planner + edit = EditConstraint(v, Strength.PREFERRED) + edits = OrderedCollection() + edits.append(edit) + + plan = planner.extract_plan_from_constraints(edits) + + for i in range(10): + v.value = new_value + plan.execute() + + edit.destroy_constraint() + + +# HOORAY FOR GLOBALS... Oh wait. +# In spirit of the original, we'll keep it, but ugh. +planner = None + + +def delta_blue(): + global total + start = time.clock() + for i in range(20): + chain_test(100) + projection_test(100) + print(total) + print("elapsed: " + str(time.clock() - start)) + + +if __name__ == '__main__': + delta_blue() \ No newline at end of file diff --git a/benchmark/delta_blue.wren b/benchmark/delta_blue.wren new file mode 100644 index 00000000..51f82a28 --- /dev/null +++ b/benchmark/delta_blue.wren @@ -0,0 +1,735 @@ +// Copyright 2011 Google Inc. All Rights Reserved. +// Copyright 1996 John Maloney and Mario Wolczko +// +// This file is part of GNU Smalltalk. +// +// GNU Smalltalk is free software; you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation; either version 2, or (at your option) any later version. +// +// GNU Smalltalk is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +// details. +// +// You should have received a copy of the GNU General Public License along with +// GNU Smalltalk; see the file COPYING. If not, write to the Free Software +// Foundation, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +// Translated first from Smalltalk to JavaScript, and finally to +// Dart by Google 2008-2010. +// +// Translated to Wren by Bob Nystrom 2014. + +// A Wren implementation of the DeltaBlue constraint-solving +// algorithm, as described in: +// +// "The DeltaBlue Algorithm: An Incremental Constraint Hierarchy Solver" +// Bjorn N. Freeman-Benson and John Maloney +// January 1990 Communications of the ACM, +// also available as University of Washington TR 89-08-06. +// +// Beware: this benchmark is written in a grotesque style where +// the constraint model is built by side-effects from constructors. +// I've kept it this way to avoid deviating too much from the original +// implementation. + +// TODO: Support forward declarations of globals. +var REQUIRED = null +var STRONG_REFERRED = null +var PREFERRED = null +var STRONG_DEFAULT = null +var NORMAL = null +var WEAK_DEFAULT = null +var WEAKEST = null + +var ORDERED = null + +// Strengths are used to measure the relative importance of constraints. +// New strengths may be inserted in the strength hierarchy without +// disrupting current constraints. Strengths cannot be created outside +// this class, so == can be used for value comparison. +class Strength { + new(value, name) { + _value = value + _name = name + } + + value { return _value } + name { return _name } + + nextWeaker { return ORDERED[_value] } + + static stronger(s1, s2) { return s1.value < s2.value } + static weaker(s1, s2) { return s1.value > s2.value } + + static weakest(s1, s2) { + // TODO: Ternary operator. + if (Strength.weaker(s1, s2)) return s1 + return s2 + } + + static strongest(s1, s2) { + // TODO: Ternary operator. + if (Strength.stronger(s1, s2)) return s1 + return s2 + } +} + +// Compile time computed constants. +REQUIRED = new Strength(0, "required") +STRONG_REFERRED = new Strength(1, "strongPreferred") +PREFERRED = new Strength(2, "preferred") +STRONG_DEFAULT = new Strength(3, "strongDefault") +NORMAL = new Strength(4, "normal") +WEAK_DEFAULT = new Strength(5, "weakDefault") +WEAKEST = new Strength(6, "weakest") + +ORDERED = [ + WEAKEST, WEAK_DEFAULT, NORMAL, STRONG_DEFAULT, PREFERRED, STRONG_REFERRED +] + +// TODO: Forward declarations. +var planner + +class Constraint { + new(strength) { + _strength = strength + } + + strength { return _strength } + + // Activate this constraint and attempt to satisfy it. + addConstraint { + this.addToGraph + planner.incrementalAdd(this) + } + + // Attempt to find a way to enforce this constraint. If successful, + // record the solution, perhaps modifying the current dataflow + // graph. Answer the constraint that this constraint overrides, if + // there is one, or nil, if there isn't. + // Assume: I am not already satisfied. + satisfy(mark) { + this.chooseMethod(mark) + if (!this.isSatisfied) { + if (_strength == REQUIRED) { + IO.print("Could not satisfy a required constraint!") + } + return null + } + + this.markInputs(mark) + var out = this.output + var overridden = out.determinedBy + if (overridden != null) overridden.markUnsatisfied + out.determinedBy = this + if (!planner.addPropagate(this, mark)) IO.print("Cycle encountered") + out.mark = mark + return overridden + } + + destroyConstraint { + if (this.isSatisfied) planner.incrementalRemove(this) + this.removeFromGraph + } + + // Normal constraints are not input constraints. An input constraint + // is one that depends on external state, such as the mouse, the + // keybord, a clock, or some arbitraty piece of imperative code. + isInput { return false } +} + +// Abstract superclass for constraints having a single possible output variable. +class UnaryConstraint is Constraint { + new(myOutput, strength) { + super(strength) + _satisfied = false + _myOutput = myOutput + this.addConstraint + } + + // Adds this constraint to the constraint graph. + addToGraph { + _myOutput.addConstraint(this) + _satisfied = false; + } + + // Decides if this constraint can be satisfied and records that decision. + chooseMethod(mark) { + _satisfied = (_myOutput.mark != mark) && + Strength.stronger(this.strength, _myOutput.walkStrength) + } + + // Returns true if this constraint is satisfied in the current solution. + isSatisfied { return _satisfied } + + markInputs(mark) { + // has no inputs. + } + + // Returns the current output variable. + output { return _myOutput } + + // Calculate the walkabout strength, the stay flag, and, if it is + // 'stay', the value for the current output of this constraint. Assume + // this constraint is satisfied. + recalculate { + _myOutput.walkStrength = this.strength + _myOutput.stay = !this.isInput + if (_myOutput.stay) this.execute // Stay optimization. + } + + // Records that this constraint is unsatisfied. + markUnsatisfied { + _satisfied = false + } + + inputsKnown(mark) { return true } + + removeFromGraph { + if (_myOutput != null) _myOutput.removeConstraint(this) + _satisfied = false + } +} + +// Variables that should, with some level of preference, stay the same. +// Planners may exploit the fact that instances, if satisfied, will not +// change their output during plan execution. This is called "stay +// optimization". +class StayConstraint is UnaryConstraint { + new(variable, strength) { + super(variable, strength) + } + + execute { + // Stay constraints do nothing. + } +} + +// A unary input constraint used to mark a variable that the client +// wishes to change. +class EditConstraint is UnaryConstraint { + EditConstraint(variable, strength) { + super(variable, strength) + } + + // Edits indicate that a variable is to be changed by imperative code. + isInput { return true } + + execute { + // Edit constraints do nothing. + } +} + +// Directions. +var NONE = 1 +var FORWARD = 2 +var BACKWARD = 0 + +// Abstract superclass for constraints having two possible output +// variables. +class BinaryConstraint is Constraint { + new(v1, v2, strength) { + super(strength) + _v1 = v1 + _v2 = v2 + _direction = NONE + this.addConstraint + } + + direction { return _direction } + v1 { return _v1 } + v2 { return _v2 } + + // Decides if this constraint can be satisfied and which way it + // should flow based on the relative strength of the variables related, + // and record that decision. + chooseMethod(mark) { + if (_v1.mark == mark) { + if (_v2.mark != mark && + Strength.stronger(this.strength, _v2.walkStrength)) { + _direction = FORWARD + } else { + _direction = NONE + } + } + + if (_v2.mark == mark) { + if (_v1.mark != mark && + Strength.stronger(this.strength, _v1.walkStrength)) { + _direction = BACKWARD + } else { + _direction = NONE + } + } + + if (Strength.weaker(_v1.walkStrength, _v2.walkStrength)) { + if (Strength.stronger(this.strength, _v1.walkStrength)) { + _direction = BACKWARD + } else { + _direction = NONE + } + } else { + if (Strength.stronger(this.strength, _v2.walkStrength)) { + _direction = FORWARD + } else { + _direction = BACKWARD + } + } + } + + // Add this constraint to the constraint graph. + addToGraph { + _v1.addConstraint(this) + _v2.addConstraint(this) + _direction = NONE + } + + // Answer true if this constraint is satisfied in the current solution. + isSatisfied { return _direction != NONE } + + // Mark the input variable with the given mark. + markInputs(mark) { + this.input.mark = mark + } + + // Returns the current input variable + input { + if (_direction == FORWARD) return _v1 + return _v2 + } + + // Returns the current output variable. + output { + if (_direction == FORWARD) return _v2 + return _v1 + } + + // Calculate the walkabout strength, the stay flag, and, if it is + // 'stay', the value for the current output of this + // constraint. Assume this constraint is satisfied. + recalculate { + var ihn = this.input + var out = this.output + out.walkStrength = Strength.weakest(this.strength, ihn.walkStrength) + out.stay = ihn.stay + if (out.stay) this.execute + } + + // Record the fact that this constraint is unsatisfied. + markUnsatisfied { + _direction = NONE + } + + inputsKnown(mark) { + var i = this.input + return i.mark == mark || i.stay || i.determinedBy == null + } + + removeFromGraph { + if (_v1 != null) _v1.removeConstraint(this) + if (_v2 != null) _v2.removeConstraint(this) + _direction = NONE + } +} + +// Relates two variables by the linear scaling relationship: "v2 = +// (v1 * scale) + offset". Either v1 or v2 may be changed to maintain +// this relationship but the scale factor and offset are considered +// read-only. +class ScaleConstraint is BinaryConstraint { + new(src, scale, offset, dest, strength) { + _scale = scale + _offset = offset + super(src, dest, strength) + } + + // Adds this constraint to the constraint graph. + addToGraph { + super.addToGraph + _scale.addConstraint(this) + _offset.addConstraint(this) + } + + removeFromGraph { + super.removeFromGraph + if (_scale != null) _scale.removeConstraint(this) + if (_offset != null) _offset.removeConstraint(this) + } + + markInputs(mark) { + super.markInputs(mark) + _scale.mark = _offset.mark = mark + } + + // Enforce this constraint. Assume that it is satisfied. + execute { + if (this.direction == FORWARD) { + this.v2.value = this.v1.value * _scale.value + _offset.value; + } else { + // TODO: Is this the same semantics as ~/? + this.v1.value = ((this.v2.value - _offset.value) / _scale.value).floor; + } + } + + // Calculate the walkabout strength, the stay flag, and, if it is + // 'stay', the value for the current output of this constraint. Assume + // this constraint is satisfied. + recalculate { + var ihn = this.input + var out = this.output + out.walkStrength = Strength.weakest(this.strength, ihn.walkStrength) + out.stay = ihn.stay && _scale.stay && _offset.stay + if (out.stay) this.execute + } +} + +// Constrains two variables to have the same value. +class EqualityConstraint is BinaryConstraint { + new(v1, v2, strength) { + super(v1, v2, strength) + } + + // Enforce this constraint. Assume that it is satisfied. + execute { + this.output.value = this.input.value + } +} + +// A constrained variable. In addition to its value, it maintain the +// structure of the constraint graph, the current dataflow graph, and +// various parameters of interest to the DeltaBlue incremental +// constraint solver. +class Variable { + new(name, value) { + _constraints = [] + _determinedBy = null + _mark = 0 + _walkStrength = WEAKEST + _stay = true + _name = name + _value = value + } + + constraints { return _constraints } + determinedBy { return _determinedBy } + determinedBy = value { return _determinedBy = value } + mark { return _mark } + mark = value { return _mark = value } + walkStrength { return _walkStrength } + walkStrength = value { return _walkStrength = value } + stay { return _stay } + stay = value { return _stay = value } + value { return _value } + value = newValue { return _value = newValue } + + // Add the given constraint to the set of all constraints that refer + // this variable. + addConstraint(constraint) { + _constraints.add(constraint) + } + + // Removes all traces of c from this variable. + removeConstraint(constraint) { + // TODO: Better way to filter list. + var i = 0 + while (i < _constraints.count) { + if (_constraints[i] == constraint) { + _constraints.removeAt(i) + } else { + i = i + 1 + } + } + if (_determinedBy == constraint) _determinedBy = null + } +} + +// A Plan is an ordered list of constraints to be executed in sequence +// to resatisfy all currently satisfiable constraints in the face of +// one or more changing inputs. +class Plan { + new { + _list = [] + } + + addConstraint(constraint) { + _list.add(constraint) + } + + size { return _list.count } + + execute { + for (constraint in _list) { + constraint.execute + } + } +} + +class Planner { + new { + _currentMark = 0 + } + + // Attempt to satisfy the given constraint and, if successful, + // incrementally update the dataflow graph. Details: If satifying + // the constraint is successful, it may override a weaker constraint + // on its output. The algorithm attempts to resatisfy that + // constraint using some other method. This process is repeated + // until either a) it reaches a variable that was not previously + // determined by any constraint or b) it reaches a constraint that + // is too weak to be satisfied using any of its methods. The + // variables of constraints that have been processed are marked with + // a unique mark value so that we know where we've been. This allows + // the algorithm to avoid getting into an infinite loop even if the + // constraint graph has an inadvertent cycle. + incrementalAdd(constraint) { + var mark = this.newMark + var overridden = constraint.satisfy(mark) + while (overridden != null) { + overridden = overridden.satisfy(mark) + } + } + + // Entry point for retracting a constraint. Remove the given + // constraint and incrementally update the dataflow graph. + // Details: Retracting the given constraint may allow some currently + // unsatisfiable downstream constraint to be satisfied. We therefore collect + // a list of unsatisfied downstream constraints and attempt to + // satisfy each one in turn. This list is traversed by constraint + // strength, strongest first, as a heuristic for avoiding + // unnecessarily adding and then overriding weak constraints. + // Assume: [c] is satisfied. + incrementalRemove(constraint) { + var out = constraint.output + constraint.markUnsatisfied + constraint.removeFromGraph + var unsatisfied = this.removePropagateFrom(out) + var strength = REQUIRED + while (true) { + for (i in 0...unsatisfied.count) { + var u = unsatisfied[i] + if (u.strength == strength) this.incrementalAdd(u) + } + strength = strength.nextWeaker + if (strength == WEAKEST) break + } + } + + // Select a previously unused mark value. + newMark { + _currentMark = _currentMark + 1 + return _currentMark + } + + // Extract a plan for resatisfaction starting from the given source + // constraints, usually a set of input constraints. This method + // assumes that stay optimization is desired; the plan will contain + // only constraints whose output variables are not stay. Constraints + // that do no computation, such as stay and edit constraints, are + // not included in the plan. + // Details: The outputs of a constraint are marked when it is added + // to the plan under construction. A constraint may be appended to + // the plan when all its input variables are known. A variable is + // known if either a) the variable is marked (indicating that has + // been computed by a constraint appearing earlier in the plan), b) + // the variable is 'stay' (i.e. it is a constant at plan execution + // time), or c) the variable is not determined by any + // constraint. The last provision is for past states of history + // variables, which are not stay but which are also not computed by + // any constraint. + // Assume: [sources] are all satisfied. + makePlan(sources) { + var mark = this.newMark + var plan = new Plan + var todo = sources + while (todo.count > 0) { + var constraint = todo.removeAt(-1) + if (constraint.output.mark != mark && constraint.inputsKnown(mark)) { + plan.addConstraint(constraint) + constraint.output.mark = mark + this.addConstraintsConsumingTo(constraint.output, todo) + } + } + return plan + } + + // Extract a plan for resatisfying starting from the output of the + // given [constraints], usually a set of input constraints. + extractPlanFromConstraints(constraints) { + var sources = [] + for (i in 0...constraints.count) { + var constraint = constraints[i] + // if not in plan already and eligible for inclusion. + if (constraint.isInput && constraint.isSatisfied) sources.add(constraint) + } + return this.makePlan(sources) + } + + // Recompute the walkabout strengths and stay flags of all variables + // downstream of the given constraint and recompute the actual + // values of all variables whose stay flag is true. If a cycle is + // detected, remove the given constraint and answer + // false. Otherwise, answer true. + // Details: Cycles are detected when a marked variable is + // encountered downstream of the given constraint. The sender is + // assumed to have marked the inputs of the given constraint with + // the given mark. Thus, encountering a marked node downstream of + // the output constraint means that there is a path from the + // constraint's output to one of its inputs. + addPropagate(constraint, mark) { + var todo = [constraint] + while (todo.count > 0) { + var d = todo.removeAt(-1) + if (d.output.mark == mark) { + this.incrementalRemove(constraint) + return false + } + + d.recalculate + this.addConstraintsConsumingTo(d.output, todo) + } + + return true + } + + // Update the walkabout strengths and stay flags of all variables + // downstream of the given constraint. Answer a collection of + // unsatisfied constraints sorted in order of decreasing strength. + removePropagateFrom(out) { + out.determinedBy = null + out.walkStrength = WEAKEST + out.stay = true + var unsatisfied = [] + var todo = [out] + while (todo.count > 0) { + var v = todo.removeAt(-1) + for (i in 0...v.constraints.count) { + var constraint = v.constraints[i] + if (!constraint.isSatisfied) unsatisfied.add(constraint) + } + + var determining = v.determinedBy + for (i in 0...v.constraints.count) { + var next = v.constraints[i] + if (next != determining && next.isSatisfied) { + next.recalculate + todo.add(next.output) + } + } + } + + return unsatisfied + } + + addConstraintsConsumingTo(v, coll) { + var determining = v.determinedBy + for (i in 0...v.constraints.count) { + var constraint = v.constraints[i] + if (constraint != determining && constraint.isSatisfied) { + coll.add(constraint) + } + } + } +} + +var total = 0 + +// This is the standard DeltaBlue benchmark. A long chain of equality +// constraints is constructed with a stay constraint on one end. An +// edit constraint is then added to the opposite end and the time is +// measured for adding and removing this constraint, and extracting +// and executing a constraint satisfaction plan. There are two cases. +// In case 1, the added constraint is stronger than the stay +// constraint and values must propagate down the entire length of the +// chain. In case 2, the added constraint is weaker than the stay +// constraint so it cannot be accomodated. The cost in this case is, +// of course, very low. Typical situations lie somewhere between these +// two extremes. +var chainTest = fn(n) { + planner = new Planner + var prev = null + var first = null + var last = null + + // Build chain of n equality constraints. + for (i in 0..n) { + var v = new Variable("v", 0) + if (prev != null) new EqualityConstraint(prev, v, REQUIRED) + if (i == 0) first = v + if (i == n) last = v + prev = v + } + + new StayConstraint(last, STRONG_DEFAULT) + var edit = new EditConstraint(first, PREFERRED) + var plan = planner.extractPlanFromConstraints([edit]) + for (i in 0...100) { + first.value = i + plan.execute + total = total + last.value + } +} + +var change = fn(v, newValue) { + var edit = new EditConstraint(v, PREFERRED) + var plan = planner.extractPlanFromConstraints([edit]) + for (i in 0...10) { + v.value = newValue + plan.execute + } + + edit.destroyConstraint +} + +// This test constructs a two sets of variables related to each +// other by a simple linear transformation (scale and offset). The +// time is measured to change a variable on either side of the +// mapping and to change the scale and offset factors. +var projectionTest = fn(n) { + planner = new Planner + var scale = new Variable("scale", 10) + var offset = new Variable("offset", 1000) + var src = null + var dst = null + + var dests = [] + for (i in 0...n) { + src = new Variable("src", i) + dst = new Variable("dst", i) + dests.add(dst) + new StayConstraint(src, NORMAL) + new ScaleConstraint(src, scale, offset, dst, REQUIRED) + } + + change.call(src, 17) + total = total + dst.value + if (dst.value != 1170) IO.print("Projection 1 failed") + + change.call(dst, 1050) + + total = total + src.value + if (src.value != 5) IO.print("Projection 2 failed") + + change.call(scale, 5) + for (i in 0...n - 1) { + total = total + dests[i].value + if (dests[i].value != i * 5 + 1000) IO.print("Projection 3 failed") + } + + change.call(offset, 2000) + for (i in 0...n - 1) { + total = total + dests[i].value + if (dests[i].value != i * 5 + 2000) IO.print("Projection 4 failed") + } +} + +var start = IO.clock +for (i in 0...20) { + chainTest.call(100) + projectionTest.call(100) +} + +IO.print(total) +IO.print("elapsed: " + (IO.clock - start).toString) + diff --git a/benchmark/run_bench b/benchmark/run_bench index bcd38a3d..6c1dcb3d 100755 --- a/benchmark/run_bench +++ b/benchmark/run_bench @@ -29,6 +29,8 @@ BENCHMARK("binary_trees", """stretch tree of depth 13 check: -1 32 trees of depth 12 check: -32 long lived tree of depth 12 check: -1""") +BENCHMARK("delta_blue", "7032700") + BENCHMARK("fib", r"""317811 317811 317811