Patches, more tests, fixes

This commit is contained in:
Tony Garnock-Jones 2016-01-31 16:55:24 -05:00
parent c2fa26f9ed
commit 579b82261c
5 changed files with 447 additions and 37 deletions

View File

@ -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;

229
js/src/patch.js Normal file
View File

@ -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;

View File

@ -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;

127
js/test/test-patch.js Normal file
View File

@ -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']);
});
});

View File

@ -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.__]);