Browse Source

Implement deltablue

Hugo Musso Gualandi 3 years ago
parent
commit
7b581f486f
1 changed files with 1314 additions and 0 deletions
  1. 1314 0
      experiments/deltablue.lua

+ 1314 - 0
experiments/deltablue.lua

@@ -0,0 +1,1314 @@
+-- This code is derived from the SOM benchmarks, see AUTHORS.md file.
+--
+-- Copyright (c) 2016 Francois Perrad <[email protected]>
+--
+-- Permission is hereby granted, free of charge, to any person obtaining a copy
+-- of this software and associated documentation files (the 'Software'), to deal
+-- in the Software without restriction, including without limitation the rights
+-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+-- copies of the Software, and to permit persons to whom the Software is
+-- furnished to do so, subject to the following conditions:
+--
+-- The above copyright notice and this permission notice shall be included in
+-- all copies or substantial portions of the Software.
+--
+-- THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+-- THE SOFTWARE.
+
+--[[
+    The module 'bit' is available with:
+      * LuaJIT
+      * LuaBitOp extension which is available for:
+          * Lua 5.1
+          * Lua 5.2
+    The module 'bit32' is available with:
+      * Lua 5.2
+      * Lua 5.3 when compiled with LUA_COMPAT_5_2
+    The bitwise operators are added to Lua 5.3 as new lexemes (there causes
+    lexical error in older version)
+--]]
+local band, bxor, rshift
+if _VERSION < 'Lua 5.3' then
+    local bit = bit32 or require'bit'
+    band = bit.band
+    bxor = bit.bxor
+    rshift = bit.rshift
+else
+    band = assert(load'--[[band]] return function (a, b) return a & b end')()
+    bxor = assert(load'--[[bxor]] return function (a, b) return a ~ b end')()
+    rshift = assert(load'--[[rshift]] return function (a, b) return a >> b end')()
+end
+
+local alloc_array
+local ok, table_new = pcall(require, 'table.new')       -- LuaJIT 2.1 extension
+if ok then
+    alloc_array = function (n)
+        local t = table_new(n, 1)
+        t.n = n
+        return t
+    end
+else
+    alloc_array = function (n)
+        local t = {}
+        t.n = n
+        return t
+    end
+end
+
+local Vector = {_CLASS = 'Vector'} do
+
+local floor = math.floor
+
+function Vector.new (size)
+    local obj = {
+        storage   = alloc_array(size or 50),
+        first_idx = 1,
+        last_idx  = 1,
+    }
+    return setmetatable(obj, {__index = Vector})
+end
+
+function Vector.with (elem)
+    local v = Vector.new(1)
+    v:append(elem)
+    return v
+end
+
+function Vector:at (idx)
+    if idx > self.storage.n then
+        return nil
+    end
+    return self.storage[idx]
+end
+
+function Vector:at_put (idx, val)
+    if idx > self.storage.n then
+        local new_n = self.storage.n
+        while idx > new_n do
+            new_n = new_n * 2
+        end
+
+        local new_storage = alloc_array(new_n)
+        for i = 1, self.storage.n do
+            new_storage[i] = self.storage[i]
+        end
+        self.storage = new_storage
+    end
+    self.storage[idx] = val
+
+    if self.last_idx < idx + 1 then
+        self.last_idx = idx + 1
+    end
+end
+
+function Vector:append (elem)
+    if self.last_idx > self.storage.n then
+        -- Need to expand capacity first
+        local new_storage = alloc_array(2 * self.storage.n)
+        for i = 1, self.storage.n do
+            new_storage[i] = self.storage[i]
+        end
+        self.storage = new_storage
+    end
+
+    self.storage[self.last_idx] = elem
+    self.last_idx = self.last_idx + 1
+end
+
+function Vector:is_empty ()
+    return self.last_idx == self.first_idx
+end
+
+function Vector:each (fn)
+    for i = self.first_idx, self.last_idx - 1 do
+        fn(self.storage[i])
+    end
+end
+
+function Vector:has_some (fn)
+    for i = self.first_idx, self.last_idx - 1 do
+        if fn(self.storage[i]) then
+            return true
+        end
+    end
+    return false
+end
+
+function Vector:get_one (fn)
+    for i = self.first_idx, self.last_idx - 1 do
+        local e = self.storage[i]
+        if fn(e) then
+            return e
+        end
+    end
+    return nil
+end
+
+function Vector:remove_first ()
+    if self:is_empty() then
+        return nil
+    end
+
+    self.first_idx = self.first_idx + 1
+    return self.storage[self.first_idx - 1]
+end
+
+function Vector:remove (obj)
+    local new_array = alloc_array(self:capacity())
+    local new_last = 1
+    local found = false
+
+    self:each(function (it)
+        if it == obj then
+            found = true
+        else
+            new_array[new_last] = it
+            new_last = new_last + 1
+        end
+    end)
+
+    self.storage   = new_array
+    self.last_idx  = new_last
+    self.first_idx = 1
+    return found
+end
+
+function Vector:remove_all ()
+    self.first_idx = 1
+    self.last_idx = 1
+    self.storage = alloc_array(self:capacity())
+end
+
+function Vector:size ()
+    return self.last_idx - self.first_idx
+end
+
+function Vector:capacity ()
+    return self.storage.n
+end
+
+function Vector:sort (fn)
+    -- Make the argument, block, be the criterion for ordering elements of
+    -- the receiver.
+    -- Sort blocks with side effects may not work right.
+    if self:size() > 0 then
+        self:sort_range(self.first_idx, self.last_idx - 1, fn)
+    end
+end
+
+function Vector:sort_range (i, j, fn)
+    assert(fn)
+
+    -- The prefix d means the data at that index.
+    local n = j + 1 - i
+    if n <= 1 then
+        -- Nothing to sort
+        return
+    end
+
+    local storage = self.storage
+    -- Sort di, dj
+    local di = storage[i]
+    local dj = storage[j]
+
+    -- i.e., should di precede dj?
+    if not fn(di, dj) then
+        local tmp = storage[i]
+        storage[i] = storage[j]
+        storage[j] = tmp
+        local tt = di
+        di = dj
+        dj = tt
+    end
+
+    -- NOTE: For DeltaBlue, this is never reached.
+    if n > 2 then               -- More than two elements.
+        local ij  = floor((i + j) / 2)  -- ij is the midpoint of i and j.
+        local dij = storage[ij]         -- Sort di,dij,dj.  Make dij be their median.
+
+        if fn(di, dij) then             -- i.e. should di precede dij?
+            if not fn(dij, dj) then     -- i.e., should dij precede dj?
+               local tmp = storage[j]
+               storage[j] = storage[ij]
+               storage[ij] = tmp
+               dij = dj
+            end
+        else                            -- i.e. di should come after dij
+            local tmp = storage[i]
+            storage[i] = storage[ij]
+            storage[ij] = tmp
+            dij = di
+        end
+
+        if n > 3 then           -- More than three elements.
+            -- Find k>i and l<j such that dk,dij,dl are in reverse order.
+            -- Swap k and l.  Repeat this procedure until k and l pass each other.
+            local k = i
+            local l = j - 1
+
+            while true do
+                -- i.e. while dl succeeds dij
+                while k <= l and fn(dij, storage[l]) do
+                    l = l - 1
+                end
+
+                k = k + 1
+                -- i.e. while dij succeeds dk
+                while k <= l and fn(storage[k], dij) do
+                    k = k + 1
+                end
+
+                if k > l then
+                    break
+                end
+
+                local tmp = storage[k]
+                storage[k] = storage[l]
+                storage[l] = tmp
+            end
+
+            -- Now l < k (either 1 or 2 less), and di through dl are all
+            -- less than or equal to dk through dj.  Sort those two segments.
+            self:sort_range(i, l, fn)
+            self:sort_range(k, j, fn)
+        end
+    end
+end
+
+end -- class Vector
+
+local Set = {_CLASS = 'Set'} do
+
+local INITIAL_SIZE = 10
+
+function Set.new (size)
+    local obj = {
+        items = Vector.new(size or INITIAL_SIZE)
+    }
+    return setmetatable(obj, {__index = Set})
+end
+
+function Set:size ()
+    return self.items:size()
+end
+
+function Set:each (fn)
+    self.items:each(fn)
+end
+
+function Set:has_some (fn)
+    return self.items:has_some(fn)
+end
+
+function Set:get_one (fn)
+    return self.items:get_one(fn)
+end
+
+function Set:add (obj)
+    if not self:contains(obj) then
+        self.items:append(obj)
+    end
+end
+
+function Set:remove_all ()
+    self.items:remove_all()
+end
+
+function Set:collect (fn)
+    local coll = Vector.new()
+    self:each(function (it)
+        coll:append(fn(it))
+    end)
+    return coll
+end
+
+function Set:contains (obj)
+    return self:has_some(function (it) return it == obj end)
+end
+
+end -- class Set
+
+local IdentitySet = {_CLASS = 'IdentitySet'} do
+setmetatable(IdentitySet, {__index = Set})
+
+function IdentitySet.new (size)
+    local obj = Set.new(size)
+    return setmetatable(obj, {__index = IdentitySet})
+end
+
+function IdentitySet:contains (obj)
+    return self:has_some(function (it) return it == obj end)
+end
+
+end -- class IdentitySet
+
+local Entry = {_CLASS = 'Entry'} do
+
+function Entry.new (hash, key, value, next)
+    local obj = {
+        hash  = hash,
+        key   = key,
+        value = value,
+        next  = next,
+    }
+    return setmetatable(obj, {__index = Entry})
+end
+
+function Entry:match (hash, key)
+    return self.hash == hash and self.key == key
+end
+
+end -- class Entry
+
+local Dictionary = {_CLASS = 'Dictionary'} do
+
+local INITIAL_CAPACITY = 16
+
+function Dictionary.new (size)
+    local obj = {
+        buckets = alloc_array(size or INITIAL_CAPACITY),
+        size    = 0,
+    }
+    return setmetatable(obj, {__index = Dictionary})
+end
+
+function Dictionary:hash (key)
+    if not key then
+        return 0
+    end
+    local hash = key:custom_hash()
+    return bxor(hash, rshift(hash, 16))
+end
+
+function Dictionary:is_empty ()
+    return self.size == 0
+end
+
+function Dictionary:get_bucket_idx (hash)
+    return band(self.buckets.n - 1, hash) + 1
+end
+
+function Dictionary:get_bucket (hash)
+    return self.buckets[self:get_bucket_idx(hash)]
+end
+
+function Dictionary:at (key)
+    local hash = self:hash(key)
+    local e = self:get_bucket(hash)
+
+    while e do
+        if e:match(hash, key) then
+            return e.value
+        end
+        e = e.next
+    end
+    return nil
+end
+
+function Dictionary:contains_key (key)
+    local hash = self:hash(key)
+    local e = self:get_bucket(hash)
+
+    while e do
+        if e.match(hash, key) then
+            return true
+        end
+        e = e.next
+    end
+    return false
+end
+
+function Dictionary:at_put (key, value)
+    local hash = self:hash(key)
+    local i = self:get_bucket_idx(hash)
+    local current = self.buckets[i]
+
+    if not current then
+        self.buckets[i] = self:new_entry(key, value, hash)
+        self.size = self.size + 1
+    else
+        self:insert_bucket_entry(key, value, hash, current)
+    end
+
+    if self.size > self.buckets.n then
+        self:resize()
+    end
+end
+
+function Dictionary:new_entry (key, value, hash)
+    return Entry.new(hash, key, value, nil)
+end
+
+function Dictionary:insert_bucket_entry (key, value, hash, head)
+    local current = head
+
+    while true do
+        if current:match(hash, key) then
+            current.value = value
+            return
+        end
+        if not current.next then
+            self.size = self.size + 1
+            current.next = self:new_entry(key, value, hash)
+            return
+        end
+        current = current.next
+    end
+end
+
+function Dictionary:resize ()
+    local old_storage = self.buckets
+    self.buckets = alloc_array(old_storage.n * 2)
+    self:transfer_entries(old_storage)
+end
+
+function Dictionary:transfer_entries (old_storage)
+    local buckets = self.buckets
+    for i = 1, old_storage.n do
+        local current = old_storage[i]
+
+        if current then
+            old_storage[i] = nil
+            if not current.next then
+                local hash = band(current.hash, buckets.n - 1) + 1
+                buckets[hash] = current
+            else
+                self:split_bucket(old_storage, i, current)
+            end
+        end
+    end
+end
+
+function Dictionary:split_bucket (old_storage, i, head)
+    local lo_head, lo_tail = nil, nil
+    local hi_head, hi_tail = nil, nil
+    local current = head
+
+    while current do
+        if band(current.hash, old_storage.n) == 0 then
+            if not lo_tail then
+               lo_head = current
+            else
+                lo_tail.next = current
+            end
+            lo_tail = current
+        else
+            if not hi_tail then
+                hi_head = current
+            else
+                hi_tail.next = current
+            end
+            hi_tail = current
+        end
+       current = current.next
+    end
+
+    if lo_tail then
+        lo_tail.next = nil
+        self.buckets[i] = lo_head
+    end
+    if hi_tail then
+        hi_tail.next = nil
+        self.buckets[i + old_storage.n] = hi_head
+    end
+end
+
+function Dictionary:remove_all ()
+    self.buckets = alloc_array(self.buckets.n)
+    self.size = 0
+end
+
+function Dictionary:keys ()
+    local keys = Vector.new(self.size)
+    local buckets = self.buckets
+    for i = 1, buckets.n do
+        local current = buckets[i]
+        while current do
+            keys:append(current.key)
+            current = current.next
+        end
+    end
+    return keys
+end
+
+function Dictionary:values ()
+    local vals = Vector.new(self.size)
+    local buckets = self.buckets
+    for i = 1, buckets.n do
+        local current = buckets[i]
+        while current do
+            vals:append(current.value)
+            current = current.next
+        end
+    end
+    return vals
+end
+
+end -- class Dictionary
+
+local IdEntry = {_CLASS = 'IdEntry'} do
+setmetatable(IdEntry, {__index = Entry})
+
+function IdEntry.new (hash, key, value, next)
+    local obj = Entry.new (hash, key, value, next)
+    return setmetatable(obj, {__index = IdEntry})
+end
+
+function IdEntry:match (hash, key)
+    return self.hash == hash and self.key == key
+end
+
+end -- class IdEntry
+
+local IdentityDictionary = {_CLASS = 'IdentityDictionary'} do
+setmetatable(IdentityDictionary, {__index = Dictionary})
+
+function IdentityDictionary.new (size)
+    local obj = Dictionary.new (size)
+    return setmetatable(obj, {__index = Dictionary})
+end
+
+function IdentityDictionary:new_entry (key, value, hash)
+    return IdEntry.new(hash, key, value, nil)
+end
+
+end -- class IdentityDictionary
+
+local Random = {_CLASS = 'Random'} do
+
+function Random.new ()
+    local obj = {seed = 74755}
+    return setmetatable(obj, {__index = Random})
+end
+
+function Random:next ()
+  self.seed = band(((self.seed * 1309) + 13849), 65535);
+  return self.seed;
+end
+
+end -- class Random
+
+
+local Sym = {_CLASS = 'Sym'} do
+
+function Sym.new (hash)
+    local obj = {hash = hash}
+    return setmetatable(obj, {__index = Sym})
+end
+
+function Sym:custom_hash ()
+    return self.hash
+end
+
+end -- class Sym
+
+local ABSOLUTE_STRONGEST = Sym.new(0)
+local REQUIRED           = Sym.new(1)
+local STRONG_PREFERRED   = Sym.new(2)
+local PREFERRED          = Sym.new(3)
+local STRONG_DEFAULT     = Sym.new(4)
+local DEFAULT            = Sym.new(5)
+local WEAK_DEFAULT       = Sym.new(6)
+local ABSOLUTE_WEAKEST   = Sym.new(7)
+
+local Strength = {_CLASS = 'Strength'} do
+
+local function create_strength_table ()
+    local dict = IdentityDictionary.new()
+    dict:at_put(ABSOLUTE_STRONGEST,  -10000)
+    dict:at_put(REQUIRED,              -800)
+    dict:at_put(STRONG_PREFERRED,      -600)
+    dict:at_put(PREFERRED,             -400)
+    dict:at_put(STRONG_DEFAULT,        -200)
+    dict:at_put(DEFAULT,                  0)
+    dict:at_put(WEAK_DEFAULT,           500)
+    dict:at_put(ABSOLUTE_WEAKEST,     10000)
+    return dict
+end
+
+local STRENGHT_TABLE = create_strength_table()
+
+function Strength.new (strength_sym)
+    local obj = {
+        symbolic_value   = strength_sym,
+        arithmetic_value = STRENGHT_TABLE:at(strength_sym),
+    }
+    return setmetatable(obj, {__index = Strength})
+end
+
+local function create_strength_constants ()
+    local dict = IdentityDictionary.new()
+    STRENGHT_TABLE:keys():each(function (key)
+        dict:at_put(key, Strength.new(key))
+    end)
+    return dict
+end
+
+local STRENGHT_CONSTANTS = create_strength_constants()
+
+function Strength.of (sym)
+    return STRENGHT_CONSTANTS:at(sym)
+end
+
+Strength.ABSOLUTE_STRONGEST = Strength.of(ABSOLUTE_STRONGEST)
+Strength.ABSOLUTE_WEAKEST   = Strength.of(ABSOLUTE_WEAKEST)
+Strength.REQUIRED           = Strength.of(REQUIRED)
+
+function Strength:same_as (strength)
+    return self.arithmetic_value == strength.arithmetic_value
+end
+
+function Strength:stronger (strength)
+    return self.arithmetic_value < strength.arithmetic_value
+end
+
+function Strength:weaker (strength)
+    return self.arithmetic_value > strength.arithmetic_value
+end
+
+function Strength:strongest (strength)
+    if strength:stronger(self) then
+        return strength
+    else
+        return self
+    end
+end
+
+function Strength:weakest (strength)
+    if strength:weaker(self) then
+        return strength
+    else
+        return self
+    end
+end
+
+end -- class Strength
+
+local AbstractConstraint = {_CLASS = 'AbstractConstraint'} do
+
+function AbstractConstraint:build (strength_sym)
+    self.strength = Strength.of(strength_sym)
+end
+
+function AbstractConstraint:is_input ()
+    return false
+end
+
+function AbstractConstraint:add_constraint (planner)
+    self:add_to_graph()
+    planner:incremental_add(self)
+end
+
+function AbstractConstraint:destroy_constraint (planner)
+    if self:is_satisfied() then
+        planner:incremental_remove(self)
+    end
+    self:remove_from_graph()
+end
+
+function AbstractConstraint:inputs_known (mark)
+    return not self:inputs_has_one(function (v)
+        return not ((v.mark == mark) or v.stay or (v.determined_by == nil))
+    end)
+end
+
+function AbstractConstraint:satisfy (mark, planner)
+    local overridden
+    self:choose_method(mark)
+
+    if self:is_satisfied() then
+        -- constraint can be satisfied
+        -- mark inputs to allow cycle detection in addPropagate
+        self:inputs_do(function (i)
+            i.mark = mark
+        end)
+
+        local out = self:output()
+        overridden = out.determined_by
+        if overridden then
+            overridden:mark_unsatisfied()
+        end
+        out.determined_by = self
+        assert(planner:add_propagate(self, mark),
+               'Cycle encountered adding: Constraint removed')
+        out.mark = mark
+    else
+        overridden = nil
+        assert(not self.strength:same_as(Strength.REQUIRED),
+               'Failed to satisfy a required constraint')
+    end
+    return overridden
+end
+
+end -- abstract class AbstractConstraint
+
+local BinaryConstraint = {_CLASS = 'BinaryConstraint'} do
+setmetatable(BinaryConstraint, {__index = AbstractConstraint})
+
+function BinaryConstraint:build (v1, v2, strength)
+    AbstractConstraint.build(self, strength)
+    self.v1 = v1
+    self.v2 = v2
+    self.direction = nil
+end
+
+function BinaryConstraint:is_satisfied ()
+    return self.direction ~= nil
+end
+
+function BinaryConstraint:add_to_graph ()
+    self.v1:add_constraint(self)
+    self.v2:add_constraint(self)
+    self.direction = nil
+end
+
+function BinaryConstraint:remove_from_graph ()
+    if self.v1 then
+        self.v1:remove_constraint(self)
+    end
+    if self.v2 then
+        self.v2:remove_constraint(self)
+    end
+    self.direction = nil
+end
+
+function BinaryConstraint:choose_method (mark)
+    if self.v1.mark == mark then
+        if (self.v2.mark ~= mark) and self.strength:stronger(self.v2.walk_strength) then
+            self.direction = 'forward'
+            return self.direction
+        else
+            self.direction = nil
+            return nil
+        end
+    end
+
+    if self.v2.mark == mark then
+        if (self.v1.mark ~= mark) and self.strength:stronger(self.v1.walk_strength) then
+            self.direction = 'backward'
+            return self.direction
+        else
+            self.direction = nil
+            return nil
+        end
+    end
+
+    -- If we get here, neither variable is marked, so we have a choice.
+    if self.v1.walk_strength:weaker(self.v2.walk_strength) then
+        if self.strength:stronger(self.v1.walk_strength) then
+            self.direction = 'backward'
+            return self.direction
+        else
+            self.direction = nil
+            return nil
+        end
+    else
+        if self.strength:stronger(self.v2.walk_strength) then
+            self.direction = 'forward'
+            return self.direction
+        else
+            self.direction = nil
+            return nil
+        end
+    end
+end
+
+function BinaryConstraint:inputs_do (fn)
+    if self.direction == 'forward' then
+        fn(self.v1)
+    else
+        fn(self.v2)
+    end
+end
+
+function BinaryConstraint:inputs_has_one (fn)
+    if self.direction == 'forward' then
+        return fn(self.v1)
+    else
+        return fn(self.v2)
+    end
+end
+
+function BinaryConstraint:mark_unsatisfied ()
+    self.direction = nil
+end
+
+function BinaryConstraint:output ()
+    return self.direction == 'forward' and self.v2 or self.v1
+end
+
+function BinaryConstraint:recalculate ()
+    local ihn, out
+    if self.direction == 'forward' then
+        ihn = self.v1
+        out = self.v2
+    else
+        ihn = self.v2
+        out = self.v1
+    end
+    out.walk_strength = self.strength:weakest(ihn.walk_strength)
+    out.stay = ihn.stay
+    if out.stay then
+        self:execute()
+    end
+end
+
+end -- abstract class BinaryConstraint
+
+local UnaryConstraint = {_CLASS = 'UnaryConstraint'} do
+setmetatable(UnaryConstraint, {__index = AbstractConstraint})
+
+function UnaryConstraint:build (v, strength, planner)
+    AbstractConstraint.build(self, strength)
+    self.output_   = v
+    self.satisfied = false
+    self:add_constraint(planner)
+end
+
+function UnaryConstraint:is_satisfied ()
+    return self.satisfied
+end
+
+function UnaryConstraint:add_to_graph ()
+    self.output_:add_constraint(self)
+    self.satisfied = false
+end
+
+function UnaryConstraint:remove_from_graph ()
+    if self.output_ then
+        self.output_:remove_constraint(self)
+    end
+    self.satisfied = false
+end
+
+function UnaryConstraint:choose_method (mark)
+    self.satisfied = (self.output_.mark ~= mark) and
+                     self.strength:stronger(self.output_.walk_strength)
+    return nil
+end
+
+function UnaryConstraint:inputs_do ()
+    -- No-op. I have no input variable.
+end
+
+function UnaryConstraint:inputs_has_one ()
+    return false
+end
+
+function UnaryConstraint:mark_unsatisfied ()
+    self.satisfied = false
+end
+
+function UnaryConstraint:output ()
+    return self.output_
+end
+
+function UnaryConstraint:recalculate ()
+    self.output_.walk_strength = self.strength
+    self.output_.stay          = not self.is_input()
+    if self.output_.stay then
+        self:execute()  -- stay optimization
+    end
+end
+
+end -- abstract class UnaryConstraint
+
+local EditConstraint = {_CLASS = 'EditConstraint'} do
+setmetatable(EditConstraint, {__index = UnaryConstraint})
+
+function EditConstraint.new (v, strength, planner)
+    local obj = setmetatable({}, {__index = EditConstraint})
+    UnaryConstraint.build(obj, v, strength, planner)
+    return obj
+end
+
+function EditConstraint:is_input ()
+    return true
+end
+
+function EditConstraint:execute ()
+    -- Edit constraints does nothing.
+end
+
+end -- class EditConstraint
+
+local EqualityConstraint = {_CLASS = 'EqualityConstraint'} do
+setmetatable(EqualityConstraint, {__index = BinaryConstraint})
+
+function EqualityConstraint.new (var1, var2, strength, planner)
+    local obj = setmetatable({}, {__index = EqualityConstraint})
+    BinaryConstraint.build(obj, var1, var2, strength)
+    obj:add_constraint(planner)
+    return obj
+end
+
+function EqualityConstraint:execute ()
+    if self.direction == 'forward' then
+        self.v2.value = self.v1.value
+    else
+        self.v1.value = self.v2.value
+    end
+end
+
+end -- class EqualityConstraint
+
+local ScaleConstraint = {_CLASS = 'ScaleConstraint'} do
+setmetatable(ScaleConstraint, {__index = BinaryConstraint})
+
+function ScaleConstraint.new (src, scale, offset, dest, strength, planner)
+    local obj = {
+        scale = scale,
+        offset = offset,
+    }
+    setmetatable(obj, {__index = ScaleConstraint})
+    BinaryConstraint.build(obj, src, dest, strength)
+    obj:add_constraint(planner)
+    return obj
+end
+
+function ScaleConstraint:add_to_graph ()
+    self.v1:add_constraint(self)
+    self.v2:add_constraint(self)
+    self.scale:add_constraint(self)
+    self.offset:add_constraint(self)
+    self.direction = nil
+end
+
+function ScaleConstraint:remove_from_graph ()
+    if self.v1 then
+        self.v1:remove_constraint(self)
+    end
+    if self.v2 then
+        self.v2:remove_constraint(self)
+    end
+    if self.scale then
+        self.scale:remove_constraint(self)
+    end
+    if self.offset then
+        self.offset:remove_constraint(self)
+    end
+    self.direction = nil
+end
+
+function ScaleConstraint:execute ()
+    if self.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
+
+function ScaleConstraint:inputs_do (fn)
+    if self.direction == 'forward' then
+        fn(self.v1)
+        fn(self.scale)
+        fn(self.offset)
+    else
+        fn(self.v2)
+        fn(self.scale)
+        fn(self.offset)
+    end
+end
+
+function ScaleConstraint:recalculate ()
+    local ihn, out
+    if self.direction == 'forward' then
+        ihn = self.v1
+        out = self.v2
+    else
+        out = self.v1
+        ihn = self.v2
+    end
+    out.walk_strength = self.strength:weakest(ihn.walk_strength)
+    out.stay = ihn.stay and self.scale.stay and self.offset.stay
+    if out.stay then
+        self:execute()  -- stay optimization
+    end
+end
+
+end -- class ScaleConstraint
+
+local StayConstraint = {_CLASS = 'StayConstraint'} do
+setmetatable(StayConstraint, {__index = UnaryConstraint})
+
+function StayConstraint.new (v, strength, planner)
+    local obj = setmetatable({}, {__index = StayConstraint})
+    UnaryConstraint.build(obj, v, strength, planner)
+    return obj
+end
+
+function StayConstraint:execute ()
+    -- Stay Constraints do nothing
+end
+
+end -- class StayConstraint
+
+local Variable = {_CLASS = 'Variable'} do
+
+function Variable.new (initial_value)
+    local obj = {
+        value         = initial_value or 0,
+        constraints   = Vector.new(2),
+        determined_by = nil,
+        walk_strength = Strength.ABSOLUTE_WEAKEST,
+        stay          = true,
+        mark          = 0,
+    }
+    return setmetatable(obj, {__index = Variable})
+end
+
+function Variable:add_constraint (constraint)
+    self.constraints:append(constraint)
+end
+
+function Variable:remove_constraint (constraint)
+    self.constraints:remove(constraint)
+    if  self.determined_by == constraint then
+        self.determined_by = nil
+    end
+end
+
+end -- class Variable
+
+local Plan = {_CLASS = 'Plan'} do
+setmetatable(Plan, {__index = Vector})
+
+function Plan.new ()
+    local obj = Vector.new(15)
+    return setmetatable(obj, {__index = Plan})
+end
+
+function Plan:execute ()
+    self:each(function (c)
+        c:execute()
+    end)
+end
+
+end -- class Plan
+
+local Planner = {_CLASS = 'Planner'} do
+
+function Planner.new ()
+    local obj = {current_mark = 1}
+    return setmetatable(obj, {__index = Planner})
+end
+
+function Planner:incremental_add (constraint)
+    local mark = self:new_mark()
+    local overridden = constraint:satisfy(mark, self)
+    while overridden do
+        overridden = overridden:satisfy(mark, self)
+    end
+end
+
+function Planner:incremental_remove (constraint)
+    local out = constraint:output()
+    constraint:mark_unsatisfied()
+    constraint:remove_from_graph()
+    local unsatisfied = self:remove_propagate_from(out)
+    unsatisfied:each(function (u)
+        self:incremental_add(u)
+    end)
+end
+
+function Planner:extract_plan_from_constraints (constraints)
+    local sources = Vector.new()
+    constraints:each(function (c)
+        if c:is_input() and c:is_satisfied() then
+            sources:append(c)
+        end
+    end)
+    return self:make_plan(sources)
+end
+
+function Planner:make_plan (sources)
+    local mark = self:new_mark()
+    local plan = Plan.new()
+    local todo = sources
+    while not todo:is_empty () do
+        local c = todo:remove_first()
+        if (c:output().mark ~= mark) and c:inputs_known(mark) then
+            -- not in plan already and eligible for inclusion
+            plan:append(c)
+            c:output().mark = mark
+            self:add_constraints_consuming_to(c:output(), todo)
+        end
+    end
+    return plan
+end
+
+function Planner:propagate_from (v)
+    local todo = Vector.new()
+    self:add_constraints_consuming_to(v, todo)
+    while not todo:is_empty() do
+        local c = todo:remove_first()
+        c:execute()
+        self:add_constraints_consuming_to(c:output(), todo)
+    end
+end
+
+function Planner:add_constraints_consuming_to (v, coll)
+    local determining_c = v.determined_by
+    v.constraints:each(function (c)
+        if (c ~= determining_c) and c:is_satisfied() then
+            coll:append(c)
+        end
+    end)
+end
+
+function Planner:add_propagate (c, mark)
+    local todo = Vector.with(c)
+    while not todo:is_empty() do
+        local d = todo:remove_first()
+        if d:output().mark == mark then
+            self:incremental_remove(c)
+            return false
+        end
+        d:recalculate()
+        self:add_constraints_consuming_to(d:output(), todo)
+    end
+    return true
+end
+
+function Planner:change_var (var, val)
+    local edit_constraint = EditConstraint.new(var, PREFERRED, self)
+    local plan = self:extract_plan_from_constraints(Vector.with(edit_constraint))
+    for _ = 1, 10 do
+        var.value = val
+        plan:execute()
+    end
+    edit_constraint:destroy_constraint(self)
+end
+
+function Planner:constraints_consuming (v, fn)
+    local determining_c = v.determined_by
+    v.constraints:each(function (c)
+        if (c ~= determining_c) and c:is_satisfied() then
+            fn(c)
+        end
+    end)
+end
+
+function Planner:new_mark ()
+    local current_mark = self.current_mark
+    self.current_mark = current_mark + 1
+    return current_mark
+end
+
+function Planner:remove_propagate_from (out)
+    local unsatisfied = Vector.new()
+
+    out.determined_by = nil
+    out.walk_strength = Strength.ABSOLUTE_WEAKEST
+    out.stay = true
+
+    local todo = Vector.with(out)
+    while not todo:is_empty() do
+        local v = todo:remove_first()
+
+        v.constraints:each(function (c)
+            if not c:is_satisfied() then
+                unsatisfied:append(c)
+            end
+        end)
+
+        self:constraints_consuming(v, function (c)
+            c:recalculate()
+            todo:append(c:output())
+        end)
+    end
+
+    unsatisfied:sort(function (c1, c2)
+        return c1.strength:stronger(c2.strength)
+    end)
+    return unsatisfied
+end
+
+function Planner.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.
+
+    local planner = Planner.new()
+    local vars = {}
+    for i = 1, n + 1 do
+        vars[i] = Variable.new()
+    end
+
+    -- thread a chain of equality constraints through the variables
+    for i = 1, n do
+        local v1, v2 = vars[i], vars[i + 1]
+        EqualityConstraint.new(v1, v2, REQUIRED, planner)
+    end
+
+    StayConstraint.new(vars[n + 1], STRONG_DEFAULT, planner)
+    local edit = EditConstraint.new(vars[1], PREFERRED, planner)
+    local plan = planner:extract_plan_from_constraints(Vector.with(edit))
+
+    for v = 1, 100 do
+        vars[1].value = v
+        plan:execute()
+        assert(vars[n + 1].value == v, 'Chain test failed!')
+    end
+
+    edit:destroy_constraint(planner)
+end
+
+function Planner.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.
+
+    local planner = Planner.new()
+    local dests   = Vector.new()
+    local scale   = Variable.new(10)
+    local offset  = Variable.new(1000)
+
+    local src = nil
+    local dst = nil
+
+    for i = 1, n do
+        src = Variable.new(i)
+        dst = Variable.new(i)
+        dests:append(dst)
+        StayConstraint.new(src, DEFAULT, planner)
+        ScaleConstraint.new(src, scale, offset, dst, REQUIRED, planner)
+    end
+
+    planner:change_var(src, 17)
+    assert(dst.value == 1170, 'Projection 1 failed')
+
+    planner:change_var(dst, 1050)
+    assert(src.value == 5, 'Projection 2 failed')
+
+    planner:change_var(scale, 5)
+    for i = 1, n - 1 do
+        assert(dests:at(i).value == i * 5 + 1000, 'Projection 3 failed')
+    end
+
+    planner:change_var(offset, 2000)
+    for i = 1, n - 1 do
+        assert(dests:at(i).value == i * 5 + 2000, 'Projection 4 failed')
+    end
+end
+
+end -- class Planner
+
+return function(N)
+    Planner.chain_test(N)
+    Planner.projection_test(N)
+end