From 579b82261c3989bbbea896483e3b626e0c568005 Mon Sep 17 00:00:00 2001 From: Tony Garnock-Jones Date: Sun, 31 Jan 2016 16:55:24 -0500 Subject: [PATCH] Patches, more tests, fixes --- js/src/main.js | 1 + js/src/patch.js | 229 ++++++++++++++++++++++++++++++++++++++++++ js/src/route.js | 109 +++++++++++++------- js/test/test-patch.js | 127 +++++++++++++++++++++++ js/test/test-route.js | 18 ++++ 5 files changed, 447 insertions(+), 37 deletions(-) create mode 100644 js/src/patch.js create mode 100644 js/test/test-patch.js diff --git a/js/src/main.js b/js/src/main.js index 4be4026..2d7c8d6 100644 --- a/js/src/main.js +++ b/js/src/main.js @@ -6,6 +6,7 @@ module.exports = require("./syndicate.js"); // module.exports.WebSocket = require("./websocket-driver.js"); module.exports.Reflect = require("./reflect.js"); +module.exports.Patch = require("./patch.js"); // module.exports.Ground = require("./ground.js").Ground; // module.exports.Actor = require("./actor.js").Actor; // module.exports.Spy = require("./spy.js").Spy; diff --git a/js/src/patch.js b/js/src/patch.js new file mode 100644 index 0000000..427eb76 --- /dev/null +++ b/js/src/patch.js @@ -0,0 +1,229 @@ +var Route = require("./route.js"); +var Immutable = require("immutable"); + +var __ = Route.__; +var _$ = Route._$; + +function Patch(added, removed) { + this.added = added; + this.removed = removed; +} + +var emptyPatch = new Patch(Route.emptyTrie, Route.emptyTrie); + +var $Observe = new Route.$Special("$Observe"); +var $AtMeta = new Route.$Special("$AtMeta"); +var $Advertise = new Route.$Special("$Advertise"); + +function observe(p) { return [$Observe, p]; } +function atMeta(p) { return [$AtMeta, p]; } +function advertise(p) { return [$Advertise, p]; } + +function prependAtMeta(p, level) { + while (level--) { + p = atMeta(p); + } + return p; +} + +function observeAtMeta(p, level) { + if (level === 0) { + return Route.compilePattern(true, observe(p)); + } else { + return Route._union( + Route.compilePattern(true, observe(prependAtMeta(p, level))), + Route.compilePattern(true, atMeta(Route.embeddedTrie(observeAtMeta(p, level - 1))))); + } +} + +function assert(p, metaLevel) { + return new Patch(Route.compilePattern(true, prependAtMeta(p, metaLevel || 0)), Route.emptyTrie); +} + +function retract(p, metaLevel) { + return new Patch(Route.emptyTrie, Route.compilePattern(true, prependAtMeta(p, metaLevel || 0))); +} + +function sub(p, metaLevel) { + return new Patch(observeAtMeta(p, metaLevel || 0), Route.emptyTrie); +} + +function unsub(p, metaLevel) { + return new Patch(Route.emptyTrie, observeAtMeta(p, metaLevel || 0)); +} + +function pub(p, metaLevel) { + return assert(advertise(p), metaLevel); +} + +function unpub(p, metaLevel) { + return retract(advertise(p), metaLevel); +} + +/////////////////////////////////////////////////////////////////////////// + +Patch.prototype.isEmpty = function () { + return this.added === Route.emptyTrie && this.removed === Route.emptyTrie; +}; + +Patch.prototype.isNonEmpty = function () { + return !this.isEmpty(); +}; + +Patch.prototype.hasAdded = function () { + return this.added !== Route.emptyTrie; +}; + +Patch.prototype.hasRemoved = function () { + return this.removed !== Route.emptyTrie; +}; + +Patch.prototype.lift = function () { + return new Patch(Route.compilePattern(true, atMeta(Route.embeddedTrie(this.added))), + Route.compilePattern(true, atMeta(Route.embeddedTrie(this.removed)))); +}; + +var atMetaProj = Route.compileProjection(atMeta(_$)); +Patch.prototype.drop = function () { + return new Patch(Route.project(this.added, atMetaProj), + Route.project(this.removed, atMetaProj)); +}; + +Patch.prototype.strip = function () { + return new Patch(Route.relabel(this.added, function (v) { return true; }), + Route.relabel(this.removed, function (v) { return true; })); +}; + +Patch.prototype.label = function (labelValue) { + return new Patch(Route.relabel(this.added, function (v) { return labelValue; }), + Route.relabel(this.removed, function (v) { return labelValue; })); +}; + +Patch.prototype.limit = function (bound) { + return new Patch(Route.subtract(this.added, bound, function (v1, v2) { return null; }), + Route.intersect(this.removed, bound, function (v1, v2) { return v1; })); +}; + +var metaLabelSet = Immutable.Set(["meta"]); +Patch.prototype.computeAggregate = function (label, base, removeMeta) { + return new Patch(Route.subtract(this.added, base, addCombiner), + Route.subtract(this.removed, base, removeCombiner)); + + function addCombiner(v1, v2) { + if (removeMeta && Immutable.is(v2, metaLabelSet)) { + return v1; + } else { + return null; + } + } + + function removeCombiner(v1, v2) { + if (v2.size === 1) { + return v1; + } else { + if (removeMeta && v2.size === 2 && v2.has("meta")) { + return v1; + } else { + return null; + } + } + } +}; + +Patch.prototype.applyTo = function (base) { + return Route._union(Route.subtract(base, this.removed), this.added); +}; + +Patch.prototype.updateInterests = function (base) { + return Route._union(Route.subtract(base, this.removed, function (v1, v2) { return null; }), + this.added, + function (v1, v2) { return true; }); +}; + +Patch.prototype.unapplyTo = function (base) { + return Route._union(Route.subtract(base, this.added), this.removed); +}; + +Patch.prototype.andThen = function (nextPatch) { + return new Patch(nextPatch.updateInterests(this.added), + Route._union(Route.subtract(this.removed, + nextPatch.added, + function (v1, v2) { return null; }), + nextPatch.removed, + function (v1, v2) { return true; })); +}; + +function patchSeq(/* patch, patch, ... */) { + var p = emptyPatch; + for (var i = 0; i < arguments.length; i++) { + p = p.andThen(arguments[i]); + } + return p; +} + +function computePatch(oldBase, newBase) { + return new Patch(Route.subtract(newBase, oldBase), + Route.subtract(oldBase, newBase)); +} + +function biasedIntersection(object, subject) { + subject = Route.trieStep(subject, Route.SOA); + subject = Route.trieStep(subject, $Observe); + return Route.intersect(object, subject, + function (v1, v2) { return true; }, + function (v, r) { return Route.trieStep(r, Route.EOA); }); +} + +Patch.prototype.viewFrom = function (interests) { + return new Patch(biasedIntersection(this.added, interests), + biasedIntersection(this.removed, interests)); +}; + +Patch.prototype.unsafeUnion = function (other) { + // Unsafe because does not necessarily preserve invariant that added + // and removed are disjoint. + return new Patch(Route._union(this.added, other.added), + Route._union(this.removed, other.removed)); +}; + +Patch.prototype.project = function (compiledProjection) { + return new Patch(Route.project(this.added, compiledProjection), + Route.project(this.removed, compiledProjection)); +}; + +Patch.prototype.projectObjects = function (compiledProjection) { + return [Route.projectObjects(this.added, compiledProjection), + Route.projectObjects(this.removed, compiledProjection)]; +}; + +function prettyPatch(p) { + return ("<<<<<<<< Removed:\n" + Route.prettyTrie(p.removed) + + "======== Added:\n" + Route.prettyTrie(p.added) + + ">>>>>>>>\n"); +} + +/////////////////////////////////////////////////////////////////////////// + +module.exports.Patch = Patch; +module.exports.emptyPatch = emptyPatch; + +module.exports.$Observe = $Observe; +module.exports.$AtMeta = $AtMeta; +module.exports.$Advertise = $Advertise; +module.exports.observe = observe; +module.exports.atMeta = atMeta; +module.exports.advertise = advertise; + +module.exports.prependAtMeta = prependAtMeta; +module.exports.observeAtMeta = observeAtMeta; +module.exports.assert = assert; +module.exports.retract = retract; +module.exports.sub = sub; +module.exports.unsub = unsub; +module.exports.pub = pub; +module.exports.unpub = unpub; + +module.exports.patchSeq = patchSeq; +module.exports.computePatch = computePatch; +module.exports.biasedIntersection = biasedIntersection; +module.exports.prettyPatch = prettyPatch; diff --git a/js/src/route.js b/js/src/route.js index e4297e9..c5ba6ad 100644 --- a/js/src/route.js +++ b/js/src/route.js @@ -47,7 +47,7 @@ function $Success(value) { $Success.prototype.equals = function (other) { if (!(other instanceof $Success)) return false; return Immutable.is(this.value, other.value); -} +}; function $WildcardSequence(trie) { this.trie = trie; @@ -56,10 +56,10 @@ function $WildcardSequence(trie) { $WildcardSequence.prototype.equals = function (other) { if (!(other instanceof $WildcardSequence)) return false; return Immutable.is(this.trie, other.trie); -} +}; function is_emptyTrie(m) { - return Object.is(m, emptyTrie); + return Immutable.is(m, emptyTrie); } /////////////////////////////////////////////////////////////////////////// @@ -149,7 +149,8 @@ function matchPattern(v, p) { } function rupdate(r, key, k) { - if (is_emptyTrie(k)) { + var oldWild = r.get(__, emptyTrie); + if (Immutable.is(k, oldWild)) { return r.remove(key); } else { return r.set(key, k); @@ -160,6 +161,10 @@ function rlookup(r, key) { return r.get(key, emptyTrie); } +function rlookupWild(r, key) { + return r.get(key, false); +} + function is_keyOpen(k) { return k === SOA; } @@ -174,17 +179,17 @@ function is_keyNormal(k) { /////////////////////////////////////////////////////////////////////////// -var unionSuccesses = function (v1, v2) { +var unionSuccessesDefault = function (v1, v2) { if (v1 === true) return v2; if (v2 === true) return v1; return v1.union(v2); }; -var intersectSuccesses = function (v1, v2) { +var intersectSuccessesDefault = function (v1, v2) { return v1; }; -var subtractSuccesses = function (v1, v2) { +var subtractSuccessesDefault = function (v1, v2) { var r = v1.subtract(v2); if (r.isEmpty()) return null; return r; @@ -204,7 +209,8 @@ function expandWildseq(r) { return union(rwild(rwildseq(r)), rseq(EOA, r)); } -function union(o1, o2) { +function union(o1, o2, unionSuccessesOpt) { + var unionSuccesses = unionSuccessesOpt || unionSuccessesDefault; return merge(o1, o2); function merge(o1, o2) { @@ -223,8 +229,14 @@ function union(o1, o2) { r2 = expandWildseq(r2.trie); } - if (r1 instanceof $Success && r2 instanceof $Success) { - return rsuccess(unionSuccesses(r1.value, r2.value)); + if (r1 instanceof $Success) { + if (r2 instanceof $Success) { + return rsuccess(unionSuccesses(r1.value, r2.value)); + } else { + die("Route.union: left short!"); + } + } else if (r2 instanceof $Success) { + die("Route.union: right short!"); } var w = merge(rlookup(r1, __), rlookup(r2, __)); @@ -270,7 +282,11 @@ function unionN() { return acc; } -function intersect(o1, o2) { +function intersect(o1, o2, intersectSuccessesOpt, leftShortOpt) { + var intersectSuccesses = intersectSuccessesOpt || intersectSuccessesDefault; + var leftShort = leftShortOpt || function (v, r) { + die("Route.intersect: left side short!"); + }; return walk(o1, o2); function walkFlipped(r2, r1) { return walk(r1, r2); } @@ -292,8 +308,12 @@ function intersect(o1, o2) { r2 = expandWildseq(r2.trie); } - if (r1 instanceof $Success && r2 instanceof $Success) { - return rsuccess(intersectSuccesses(r1.value, r2.value)); + if (r1 instanceof $Success) { + if (r2 instanceof $Success) { + return rsuccess(intersectSuccesses(r1.value, r2.value)); + } else { + return leftShort(r1.value, r2); + } } var w1 = rlookup(r1, __); @@ -332,7 +352,7 @@ function intersect(o1, o2) { if (is_emptyTrie(w2)) { r2.forEach(function (val, key) { examineKey(key) }); } else { - target = rupdateInplace(target, __, w); + target = rupdate(target, __, w); r1.forEach(function (val, key) { examineKey(key) }); r2.forEach(function (val, key) { examineKey(key) }); } @@ -351,12 +371,14 @@ function intersect(o1, o2) { } } -// Removes r2's mappings from r1. Assumes r2 has previously been -// union'd into r1. The subtractSuccesses function should return -// null to signal "no remaining success values". -function subtract(o1, o2) { +// The subtractSuccesses function should return null to signal "no +// remaining success values". +function subtract(o1, o2, subtractSuccessesOpt) { + var subtractSuccesses = subtractSuccessesOpt || subtractSuccessesDefault; return walk(o1, o2); + function walkFlipped(r2, r1) { return walk(r1, r2); } + function walk(r1, r2) { if (is_emptyTrie(r1)) { return emptyTrie; @@ -386,11 +408,16 @@ function subtract(o1, o2) { function examineKey(key) { if (key !== __) { - var k1 = rlookup(r1, key); - var k2 = rlookup(r2, key); + var k1 = rlookupWild(r1, key); + var k2 = rlookupWild(r2, key); var updatedK; - if (is_emptyTrie(k2)) { - updatedK = walkWild(key, k1, w2); + if (!k1) { + if (!k2) { + return; + } + updatedK = walkWild(key, k2, w1, walkFlipped); + } else if (!k2) { + updatedK = walkWild(key, k1, w2, walk); } else { updatedK = walk(k1, k2); } @@ -404,15 +431,10 @@ function subtract(o1, o2) { target = rupdate(target, key, ((updatedK instanceof $WildcardSequence) && Immutable.is(updatedK.trie, w)) - ? emptyTrie + ? w : updatedK); - } else if (is_keyClose(key)) { - // We take care of this case later, after the - // target is fully constructed/rebuilt. - target = rupdate(target, key, updatedK); } else { - target = rupdate(target, key, - (Immutable.is(updatedK, w) ? emptyTrie : updatedK)); + target = rupdate(target, key, updatedK); } } } @@ -455,14 +477,14 @@ function subtract(o1, o2) { return target; } - function walkWild(key, k, w) { + function walkWild(key, k, w, walker) { if (is_emptyTrie(w)) return k; - if (is_keyOpen(key)) return walk(k, rwildseq(w)); + if (is_keyOpen(key)) return walker(k, rwildseq(w)); if (is_keyClose(key)) { - if (w instanceof $WildcardSequence) return walk(k, w.trie); + if (w instanceof $WildcardSequence) return walker(k, w.trie); return k; } - return walk(k, w); + return walker(k, w); } } @@ -621,6 +643,13 @@ function appendTrie(m, mTailFn) { } } +function trieStep(m, key) { + if (is_emptyTrie(m)) return emptyTrie; + if (m instanceof $WildcardSequence) return (is_keyClose(key) ? m.trie : m); + if (m instanceof $Success) return emptyTrie; + return rlookupWild(m, key) || rlookup(m, __); +} + function relabel(m, f) { return walk(m); @@ -812,11 +841,11 @@ function project(m, compiledProjection) { if (key !== __) { if (is_keyOpen(key)) { function cont2(mk2) { return captureNested(mk2, cont); } - rupdateInplace(target, key, captureNested(mk, cont2)); + target = rupdate(target, key, captureNested(mk, cont2)); } else if (is_keyClose(key)) { - rupdateInplace(target, key, cont(mk)); + target = rupdate(target, key, cont(mk)); } else { - rupdateInplace(target, key, captureNested(mk, cont)); + target = rupdate(target, key, captureNested(mk, cont)); } } }); @@ -953,7 +982,7 @@ function prettyTrie(m, initialIndent) { } if (m.size === 0) { - acc.push("::: no further matches possible"); + acc.push("::: nothing"); return; } @@ -971,6 +1000,7 @@ function prettyTrie(m, initialIndent) { if (key === __) key = '★'; else if (key === SOA) key = '<'; else if (key === EOA) key = '>'; + else if (key instanceof $Special) key = key.name; else key = JSON.stringify(key); acc.push(key); walk(i + key.length + 1, k); @@ -985,19 +1015,24 @@ function prettyTrie(m, initialIndent) { /////////////////////////////////////////////////////////////////////////// module.exports.__ = __; +module.exports.SOA = SOA; +module.exports.EOA = SOA; module.exports.$Capture = $Capture; +module.exports.$Special = $Special; module.exports._$ = _$; module.exports.is_emptyTrie = is_emptyTrie; module.exports.emptyTrie = emptyTrie; module.exports.embeddedTrie = embeddedTrie; module.exports.compilePattern = compilePattern; module.exports.matchPattern = matchPattern; +module.exports._union = union; module.exports.union = unionN; module.exports.intersect = intersect; module.exports.subtract = subtract; module.exports.matchValue = matchValue; module.exports.matchTrie = matchTrie; module.exports.appendTrie = appendTrie; +module.exports.trieStep = trieStep; module.exports.relabel = relabel; module.exports.compileProjection = compileProjection; module.exports.projectionToPattern = projectionToPattern; diff --git a/js/test/test-patch.js b/js/test/test-patch.js new file mode 100644 index 0000000..d6c5f07 --- /dev/null +++ b/js/test/test-patch.js @@ -0,0 +1,127 @@ +var expect = require('expect.js'); +var Immutable = require('immutable'); + +var Route = require('../src/route.js'); +var Patch = require('../src/patch.js'); + +var __ = Route.__; +var _$ = Route._$; + +function checkPrettyTrie(m, expected) { + expect(r.prettyTrie(m)).to.equal(expected.join('\n')); +} + +function checkPrettyPatch(p, expectedAdded, expectedRemoved) { + expect(Patch.prettyPatch(p)).to.equal( + ('<<<<<<<< Removed:\n' + expectedRemoved.join('\n') + + '======== Added:\n' + expectedAdded.join('\n') + + '>>>>>>>>\n')); +} + +describe('basic patch compilation', function () { + it('should print as expected', function () { + checkPrettyPatch(Patch.assert([1, 2]), + [' < 1 2 > >{true}'], + ['::: nothing']); + checkPrettyPatch(Patch.assert(__), + [' ★ >{true}'], + ['::: nothing']); + checkPrettyPatch(Patch.sub(__), + [' < $Observe ★ > >{true}'], + ['::: nothing']); + checkPrettyPatch(Patch.sub([1, 2]), + [' < $Observe < 1 2 > > >{true}'], + ['::: nothing']); + checkPrettyPatch(Patch.pub('x'), + [' < $Advertise "x" > >{true}'], + ['::: nothing']); + }); + + it('should work at nonzero metalevel', function () { + checkPrettyPatch(Patch.assert([1, 2], 0), + [' < 1 2 > >{true}'], + ['::: nothing']); + checkPrettyPatch(Patch.assert([1, 2], 1), + [' < $AtMeta < 1 2 > > >{true}'], + ['::: nothing']); + checkPrettyPatch(Patch.assert([1, 2], 2), + [' < $AtMeta < $AtMeta < 1 2 > > > >{true}'], + ['::: nothing']); + + checkPrettyPatch(Patch.sub([1, 2], 0), + [' < $Observe < 1 2 > > >{true}'], + ['::: nothing']); + checkPrettyPatch(Patch.sub([1, 2], 1), + [' < $AtMeta < $Observe < 1 2 > > > >{true}', + ' $Observe < $AtMeta < 1 2 > > > >{true}'], + ['::: nothing']); + checkPrettyPatch(Patch.sub([1, 2], 2), + [' < $AtMeta < $AtMeta < $Observe < 1 2 > > > > >{true}', + ' $Observe < $AtMeta < 1 2 > > > > >{true}', + ' $Observe < $AtMeta < $AtMeta < 1 2 > > > > >{true}'], + ['::: nothing']); + }); +}); + +describe('patch sequencing', function () { + it('should do the right thing in simple cases', function () { + checkPrettyPatch(Patch.assert(__).andThen(Patch.retract(3)), + [' ★ >{true}', + ' 3::: nothing'], + [' 3 >{true}']); + checkPrettyPatch(Patch.assert(3).andThen(Patch.retract(__)), + ['::: nothing'], + [' ★ >{true}']); + checkPrettyPatch(Patch.assert(__).andThen(Patch.retract(__)), + ['::: nothing'], + [' ★ >{true}']); + checkPrettyPatch(Patch.assert(3).andThen(Patch.retract(3)), + ['::: nothing'], + [' 3 >{true}']); + checkPrettyPatch(Patch.sub([1, __]).andThen(Patch.unsub([1, 2])), + [' < $Observe < 1 ★ > > >{true}', + ' 2::: nothing'], + [' < $Observe < 1 2 > > >{true}']); + checkPrettyPatch(Patch.sub([__, 2]).andThen(Patch.unsub([1, 2])), + [' < $Observe < ★ 2 > > >{true}', + ' 1::: nothing'], + [' < $Observe < 1 2 > > >{true}']); + checkPrettyPatch(Patch.sub([__, __]).andThen(Patch.unsub([1, 2])), + [' < $Observe < ★ ★ > > >{true}', + ' 1 ★ > > >{true}', + ' 2::: nothing'], + [' < $Observe < 1 2 > > >{true}']); + }); +}); + +describe('patch lifting', function () { + it('should basically work', function () { + checkPrettyPatch(Patch.assert([1, 2]).lift(), + [' < $AtMeta < 1 2 > > >{true}'], + ['::: nothing']); + checkPrettyPatch(Patch.sub([1, 2]).lift(), + [' < $AtMeta < $Observe < 1 2 > > > >{true}'], + ['::: nothing']); + checkPrettyPatch(Patch.assert([1, 2]).andThen(Patch.assert(Patch.atMeta([1, 2]))).lift(), + [' < $AtMeta < $AtMeta < 1 2 > > > >{true}', + ' 1 2 > > >{true}'], + ['::: nothing']); + }); +}); + +describe('patch dropping', function () { + it('should basically work', function () { + checkPrettyPatch(Patch.assert([1, 2]).drop(), + ['::: nothing'], + ['::: nothing']); + checkPrettyPatch(Patch.sub([1, 2]).drop(), + ['::: nothing'], + ['::: nothing']); + checkPrettyPatch(Patch.sub([1, 2], 1).drop(), + [' < $Observe < 1 2 > > >{true}'], + ['::: nothing']); + checkPrettyPatch(Patch.assert([1, 2]).andThen(Patch.assert(Patch.atMeta([1, 2]))).drop(), + [' < 1 2 > >{true}'], + ['::: nothing']); + }); +}); diff --git a/js/test/test-route.js b/js/test/test-route.js index 2e4e25c..5450ffe 100644 --- a/js/test/test-route.js +++ b/js/test/test-route.js @@ -164,6 +164,24 @@ describe("projections", function () { }); }); +describe("subtraction", function () { + it("should basically work", function () { + checkPrettyTrie(r.subtract(r.compilePattern(true, r.__), + r.compilePattern(true, 3), + function (v1, v2) { return null; }), + [" ★ >{true}", + " 3::: nothing"]); + checkPrettyTrie(r.subtract(r.compilePattern(true, r.__), + r.compilePattern(true, [3]), + function (v1, v2) { return null; }), + [" ★ >{true}", + " < ★...> >{true}", + " > >{true}", + " 3 ★...> >{true}", + " >::: nothing"]); + }); +}); + describe("subtract after union", function () { var R1 = r.compilePattern(Immutable.Set(['A']), [r.__, "B"]); var R2 = r.compilePattern(Immutable.Set(['B']), ["A", r.__]);