diff --git a/hs/.gitignore b/hs/.gitignore new file mode 100644 index 0000000..849ddff --- /dev/null +++ b/hs/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/hs/LICENSE b/hs/LICENSE new file mode 100644 index 0000000..31afd6d --- /dev/null +++ b/hs/LICENSE @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/hs/Setup.hs b/hs/Setup.hs new file mode 100755 index 0000000..cd7dc32 --- /dev/null +++ b/hs/Setup.hs @@ -0,0 +1,3 @@ +#!/usr/bin/env runhaskell +import Distribution.Simple +main = defaultMain diff --git a/hs/src/Syndicate/Dataspace/Trie/ESOP2016.hs b/hs/src/Syndicate/Dataspace/Trie/ESOP2016.hs new file mode 100644 index 0000000..be8ed6f --- /dev/null +++ b/hs/src/Syndicate/Dataspace/Trie/ESOP2016.hs @@ -0,0 +1,103 @@ +module Syndicate.Dataspace.Trie.ESOP2016 where +-- Implementation of dataspace tries, following our ESOP 2016 paper, +-- "Coordinated Concurrent Programming in Syndicate" (Tony +-- Garnock-Jones and Matthias Felleisen). +-- +-- Includes bug fixes wrt the paper: +-- +-- - combine now has parameters leftEmpty and rightEmpty. In the +-- paper, these were missing, and in some cases combine could fail +-- to terminate, since it had missing "br(∅)" checks. +-- +-- - we use the smart constructor `tl` throughout, to avoid +-- constructing `Tl` atop an empty trie. In the paper, this can +-- happen in the definition of `get` when `get(h,★)` is the empty +-- trie, but σ=<< and no mapping for σ exists in h. +-- +-- Also, there are problems with the algorithm as described; it is +-- roughly correct, but does not collapse away as much redundancy as +-- it could. These problems are remedied in ESOP2016v2.hs. +-- +-- Here is an example of a pair of inputs that could be given to +-- combine() as written in the paper that would cause nontermination: +-- combine (Tl (Ok (Set.singleton 1))) (Br Map.empty) f_union +-- To see the nontermination, comment out the lines +-- g r1 r2 | null r1 = dedup $ leftEmpty r2 +-- g r1 r2 | null r2 = dedup $ rightEmpty r1 +-- from combine below. + +import Prelude hiding (null, seq) +import qualified Data.Map.Strict as Map +import qualified Data.Set as Set + +data Sigma = Open + | Close + | Wild + | Ch Char + deriving (Eq, Ord, Show) + +data Trie a = Ok a + | Tl (Trie a) + | Br (Map.Map Sigma (Trie a)) + deriving (Eq, Show) + +empty = Br Map.empty + +null (Br h) = Map.null h +null _ = False + +tl r = if null r then empty else Tl r + +untl (Tl r) = r +untl _ = empty + +route [] (Ok v) f = v +route [] _ f = f +route (_ : _) (Ok v) f = f +route (x : s) (Br h) f = if Map.null h + then f + else route s (get h x) f +route (Close : s) (Tl r) f = route s r f +route (Open : s) (Tl r) f = route s (tl (tl r)) f +route (x : s) (Tl r) f = route s (tl r) f + +get h x = case Map.lookup x h of + Just r -> r + Nothing -> case x of + Open -> tl (get h Wild) + Close -> untl (get h Wild) + Wild -> empty + x -> get h Wild + +combine r1 r2 f leftEmpty rightEmpty = g r1 r2 + where g (Tl r1) (Tl r2) = tl (g r1 r2) + g (Tl r1) r2 = g (expand r1) r2 + g r1 (Tl r2) = g r1 (expand r2) + g (Ok v) r2 = f (Ok v) r2 + g r1 (Ok v) = f r1 (Ok v) + g r1 r2 | null r1 = dedup $ leftEmpty r2 + g r1 r2 | null r2 = dedup $ rightEmpty r1 + g (Br h1) (Br h2) = dedup $ Br (foldKeys g h1 h2) + +foldKeys g h1 h2 = Set.foldr f Map.empty keys + where f x acc = Map.insert x (g (get h1 x) (get h2 x)) acc + keys = Set.union (Map.keysSet h1) (Map.keysSet h2) + +expand r = Br (Map.fromList [(Wild, tl r), (Close, r)]) + +dedup (Br h) = Br (Map.filterWithKey (distinct h) h) + +distinct h Wild r = not (null r) +distinct h Open (Tl r) = r /= get h Wild +distinct h Open r = not (null r) +distinct h Close r = r /= untl (get h Wild) +distinct h x r = r /= get h Wild + +--------------------------------------------------------------------------- + +union r1 r2 = combine r1 r2 unionCombine id id +unionCombine (Ok vs) (Ok ws) = Ok (Set.union vs ws) +unionCombine r1 r2 | null r1 = r2 +unionCombine r1 r2 | null r2 = r1 + +unions rs = foldr union empty rs diff --git a/hs/src/Syndicate/Dataspace/Trie/ESOP2016v2.hs b/hs/src/Syndicate/Dataspace/Trie/ESOP2016v2.hs new file mode 100644 index 0000000..a2494f8 --- /dev/null +++ b/hs/src/Syndicate/Dataspace/Trie/ESOP2016v2.hs @@ -0,0 +1,98 @@ +module Syndicate.Dataspace.Trie.ESOP2016v2 where +-- Builds on ESOP2016.hs, but collapses additional redundancy from +-- combine results by using collapse/update instead of dedup/distinct. + +import Prelude hiding (null, seq) +import qualified Data.Map.Strict as Map +import qualified Data.Set as Set + +data Sigma = Open + | Close + | Wild + | Ch Char + deriving (Eq, Ord, Show) + +data Trie a = Ok a + | Tl (Trie a) + | Br (Map.Map Sigma (Trie a)) + deriving (Eq, Show) + +empty = Br Map.empty + +null (Br h) = Map.null h +null _ = False + +tl r = if null r then empty else Tl r + +untl (Tl r) = r +untl _ = empty + +route [] (Ok v) f = v +route [] _ f = f +route (_ : _) (Ok v) f = f +route (x : s) (Br h) f = if Map.null h + then f + else route s (get h x) f +route (Close : s) (Tl r) f = route s r f +route (Open : s) (Tl r) f = route s (tl (tl r)) f +route (x : s) (Tl r) f = route s (tl r) f + +get h x = case Map.lookup x h of + Just r -> r + Nothing -> case x of + Open -> tl (get h Wild) + Close -> untl (get h Wild) + Wild -> empty + x -> get h Wild + +combine r1 r2 f leftEmpty rightEmpty = g r1 r2 + where g (Tl r1) (Tl r2) = tl (g r1 r2) + g (Tl r1) r2 = g (expand r1) r2 + g r1 (Tl r2) = g r1 (expand r2) + g (Ok v) r2 = f (Ok v) r2 + g r1 (Ok v) = f r1 (Ok v) + g r1 r2 | null r1 = collapse $ leftEmpty r2 + g r1 r2 | null r2 = collapse $ rightEmpty r1 + g (Br h1) (Br h2) = collapse $ Br (foldKeys g h1 h2) + +foldKeys g h1 h2 = Set.foldr f Map.empty keys + where f x acc = update x (g (get h1 x) (get h2 x)) acc + keys = Set.union (Map.keysSet h1) (Map.keysSet h2) + +expand r = Br (Map.fromList [(Wild, tl r), (Close, r)]) + +collapse (Br h) = if Map.size h == 2 + then case (Map.lookup Wild h, Map.lookup Close h) of + (Just (w @ (Tl k1)), Just k2) | k1 == k2 -> w + _ -> Br h + else if Map.size h == 1 + then case Map.lookup Wild h of + Just (w @ (Tl _)) -> w + _ -> Br h + else Br h + +update Wild k h = + if null k + then Map.delete Wild h + else Map.insert Wild k h +update Open (Tl k) h = + case Map.lookup Wild h of + Just k' | k' == k -> Map.delete Open h + _ -> Map.insert Open (Tl k) h +update Close k h = + case Map.lookup Wild h of + Just (Tl k') | k' == k -> Map.delete Close h + _ -> Map.insert Close k h +update x k h = + case Map.lookup Wild h of + Just k' | k' == k -> Map.delete x h + _ -> Map.insert x k h + +--------------------------------------------------------------------------- + +union r1 r2 = combine r1 r2 unionCombine id id +unionCombine (Ok vs) (Ok ws) = Ok (Set.union vs ws) +unionCombine r1 r2 | null r1 = r2 +unionCombine r1 r2 | null r2 = r1 + +unions rs = foldr union empty rs diff --git a/hs/src/Syndicate/Dataspace/Trie/ESOP2016v3.hs b/hs/src/Syndicate/Dataspace/Trie/ESOP2016v3.hs new file mode 100644 index 0000000..4d05dff --- /dev/null +++ b/hs/src/Syndicate/Dataspace/Trie/ESOP2016v3.hs @@ -0,0 +1,84 @@ +module Syndicate.Dataspace.Trie.ESOP2016v3 where +-- Explicitly separate Open/Close/Wild from other edges in Br nodes. +-- This gives an elegant presentation. + +import Prelude hiding (null, seq) +import qualified Data.Map.Strict as Map +import qualified Data.Set as Set + +data Trie a = Mt + | Ok a + | Tl (Trie a) + | Br (Trie a, Trie a, Trie a, Map.Map Char (Trie a)) -- Open, Close, Wild, rest + deriving (Eq, Show) + +empty = Mt + +null Mt = True +null _ = False + +tl r = if null r then empty else Tl r + +untl (Tl r) = r +untl _ = empty + +route _ Mt f = f +route [] (Ok v) f = v +route [] _ f = f +route (_ : _) (Ok v) f = f +route ('<' : s) (Br (r, _, _, _)) f = route s r f +route ('>' : s) (Br (_, r, _, _)) f = route s r f +route (x : s) (Br (_, _, w, h)) f = route s (Map.findWithDefault w x h) f +route ('<' : s) (Tl r) f = route s (tl (tl r)) f +route ('>' : s) (Tl r) f = route s r f +route (x : s) (Tl r) f = route s (tl r) f + +get w h x = Map.findWithDefault w x h + +combine f leftEmpty rightEmpty r1 r2 = g r1 r2 + where g (Tl r1) (Tl r2) = tl (g r1 r2) + g (Tl r1) r2 = g (expand r1) r2 + g r1 (Tl r2) = g r1 (expand r2) + g (Ok v) r2 = f (Ok v) r2 + g r1 (Ok v) = f r1 (Ok v) + g r1 r2 | null r1 = collapse $ leftEmpty r2 + g r1 r2 | null r2 = collapse $ rightEmpty r1 + g r1 r2 = collapse $ foldKeys g r1 r2 + +foldKeys g (Br (o1, c1, w1, h1)) (Br (o2, c2, w2, h2)) = + Br (g o1 o2, g c1 c2, w, Set.foldr f Map.empty keys) + where w = g w1 w2 + f x acc = update x (g (get w1 h1 x) (get w2 h2 x)) w acc + keys = Set.union (Map.keysSet h1) (Map.keysSet h2) + +expand r = Br (Mt, r, tl r, Map.empty) + +collapse (Br (Mt, k, Tl k', h)) | Map.null h && k == k' = tl k +collapse (Br (Mt, Mt, Tl k, h)) | Map.null h = tl k +collapse (Br (Mt, Mt, Mt, h)) | Map.null h = empty +collapse r = r + +update x k w h = if k == w then Map.delete x h else Map.insert x k h + +--------------------------------------------------------------------------- + +union :: Ord t => Trie (Set.Set t) -> Trie (Set.Set t) -> Trie (Set.Set t) +union = combine unionCombine id id +unionCombine (Ok vs) (Ok ws) = Ok (Set.union vs ws) +unionCombine r1 r2 | null r1 = r2 +unionCombine r1 r2 | null r2 = r1 + +unions rs = foldr union empty rs + +intersection :: Ord t => Trie (Set.Set t) -> Trie (Set.Set t) -> Trie (Set.Set t) +intersection = combine intersectionCombine (const empty) (const empty) +intersectionCombine (Ok vs) (Ok ws) = Ok (Set.union vs ws) +intersectionCombine r1 r2 | null r1 = empty +intersectionCombine r1 r2 | null r2 = empty + +difference :: Ord t => Trie (Set.Set t) -> Trie (Set.Set t) -> Trie (Set.Set t) +difference = combine differenceCombine (const empty) id +differenceCombine (Ok vs) (Ok ws) = let xs = Set.difference vs ws in + if Set.null xs then empty else (Ok xs) +differenceCombine r1 r2 | null r1 = empty +differenceCombine r1 r2 | null r2 = r1 diff --git a/hs/src/Syndicate/Dataspace/Trie/Prefix.hs b/hs/src/Syndicate/Dataspace/Trie/Prefix.hs new file mode 100644 index 0000000..a2f424d --- /dev/null +++ b/hs/src/Syndicate/Dataspace/Trie/Prefix.hs @@ -0,0 +1,88 @@ +{-# LANGUAGE FlexibleInstances #-} +module Syndicate.Dataspace.Trie.Prefix where +-- Alternate representation, where Open has an explicit *arity* +-- attached to it, and matching close-parens are implicitly tracked. +-- Where ESOP2016-style implementations have "", this style has +-- "<3xyz". + +import Prelude hiding (null, seq) +import qualified Data.Map.Strict as Map +import qualified Data.Set as Set + +data Trie a = Mt + | Ok a + | Br (Map.Map Integer (Trie a), Trie a, Map.Map Char (Trie a)) -- Opens, Wild, rest + deriving (Eq, Show) + +empty = Mt + +null Mt = True +null _ = False + +makeTail _ r | null r = r +makeTail 0 r = r +makeTail n r = Br (Map.empty, makeTail (n - 1) r, Map.empty) + +stripTail _ r | null r = Just r +stripTail 0 r = Just r +stripTail n (Br (os, r, h)) | Map.null os && Map.null h = stripTail (n - 1) r +stripTail _ _ = Nothing + +route _ Mt f = f +route [] (Ok v) f = v +route [] _ f = f +route (_ : _) (Ok v) f = f +route ('<' : nc : s) (Br (os, w, _)) f = + let n = (read (nc : []) :: Integer) in + case Map.lookup n os of + Just r -> route s r f + Nothing -> route s (makeTail n w) f +route (x : s) (Br (_, w, h)) f = route s (Map.findWithDefault w x h) f + +get w h x = Map.findWithDefault w x h + +combine f leftEmpty rightEmpty r1 r2 = g r1 r2 + where g (Ok v) r2 = f (Ok v) r2 + g r1 (Ok v) = f r1 (Ok v) + g r1 r2 | null r1 = collapse $ leftEmpty r2 + g r1 r2 | null r2 = collapse $ rightEmpty r1 + g r1 r2 = collapse $ foldKeys g r1 r2 + +foldKeys g (Br (os1, w1, h1)) (Br (os2, w2, h2)) = + Br (Set.foldr fo Map.empty sizes, w, Set.foldr f Map.empty keys) + where sizes = Set.union (Map.keysSet os1) (Map.keysSet os2) + w = g w1 w2 + fo size acc = let o1 = Map.findWithDefault (makeTail size w1) size os1 in + let o2 = Map.findWithDefault (makeTail size w2) size os2 in + let o = g o1 o2 in + if stripTail size o == Just w then acc else Map.insert size o acc + f x acc = update x (g (get w1 h1 x) (get w2 h2 x)) w acc + keys = Set.union (Map.keysSet h1) (Map.keysSet h2) + +collapse (Br (os, Mt, h)) | Map.null os && Map.null h = empty +collapse r = r + +update x k w h = if k == w then Map.delete x h else Map.insert x k h + +--------------------------------------------------------------------------- + +union :: Ord t => Trie (Set.Set t) -> Trie (Set.Set t) -> Trie (Set.Set t) +union = combine unionCombine id id +unionCombine (Ok vs) (Ok ws) = Ok (Set.union vs ws) +unionCombine r1 r2 | null r1 = r2 +unionCombine r1 r2 | null r2 = r1 + +unions rs = foldr union empty rs + +intersection :: Ord t => Trie (Set.Set t) -> Trie (Set.Set t) -> Trie (Set.Set t) +intersection = combine intersectionCombine (const empty) (const empty) +intersectionCombine (Ok vs) (Ok ws) = Ok (Set.union vs ws) +intersectionCombine r1 r2 | null r1 = empty +intersectionCombine r1 r2 | null r2 = empty + +difference :: Ord t => Trie (Set.Set t) -> Trie (Set.Set t) -> Trie (Set.Set t) +difference = combine differenceCombine (const empty) id +differenceCombine (Ok vs) (Ok ws) = let xs = Set.difference vs ws in + if Set.null xs then empty else (Ok xs) +differenceCombine r1 r2 | null r1 = empty +differenceCombine r1 r2 | null r2 = r1 diff --git a/hs/syndicate.cabal b/hs/syndicate.cabal new file mode 100644 index 0000000..9924e7c --- /dev/null +++ b/hs/syndicate.cabal @@ -0,0 +1,37 @@ +name: syndicate +version: 0.1.0.0 +synopsis: An Actor-based language with multicast, managed shared state, and grouping. +copyright: Copyright © 2016 Tony Garnock-Jones +homepage: http://syndicate-lang.org/ +license: LGPL-3 +license-file: LICENSE +author: Tony Garnock-Jones +maintainer: tonyg@leastfixedpoint.com +category: Concurrency +build-type: Simple +cabal-version: >=1.10 + +library + exposed-modules: Syndicate.Dataspace.Trie.ESOP2016 + , Syndicate.Dataspace.Trie.ESOP2016v2 + , Syndicate.Dataspace.Trie.ESOP2016v3 + , Syndicate.Dataspace.Trie.Prefix + build-depends: base + , containers + hs-source-dirs: src + default-language: Haskell2010 + +test-suite syndicate-dataspace-testsuite + type: exitcode-stdio-1.0 + main-is: Main.hs + build-depends: base + , containers + , QuickCheck + , HUnit + , Cabal + , test-framework + , test-framework-hunit + , test-framework-quickcheck2 + , syndicate + hs-source-dirs: test + default-language: Haskell2010 diff --git a/hs/test/Main.hs b/hs/test/Main.hs new file mode 100644 index 0000000..d1b47ca --- /dev/null +++ b/hs/test/Main.hs @@ -0,0 +1,27 @@ +{-# LANGUAGE FlexibleInstances #-} +module Main where + +import Test.Framework +import Test.Framework.Providers.HUnit +import Test.Framework.Providers.QuickCheck2 + +import Syndicate.Dataspace.Trie.Tests.ESOP2016 as ESOP2016 +import Syndicate.Dataspace.Trie.Tests.ESOP2016v2 as ESOP2016v2 +import Syndicate.Dataspace.Trie.Tests.ESOP2016v3 as ESOP2016v3 +import Syndicate.Dataspace.Trie.Tests.Prefix as Prefix + +testOpts = (mempty :: TestOptions) + { topt_maximum_generated_tests = Just 1000 + , topt_maximum_unsuitable_generated_tests = Just 10000 + } +runnerOpts = (mempty :: RunnerOptions) { ropt_test_options = Just testOpts } +runTests tests = defaultMainWithOpts tests runnerOpts + +main = runTests + [ testGroup "ESOP2016" $ hUnitTestToTests ESOP2016.hUnitSuite + , testGroup "ESOP2016v2" $ hUnitTestToTests ESOP2016v2.hUnitSuite + , testGroup "ESOP2016v3" $ hUnitTestToTests ESOP2016v3.hUnitSuite + , testGroup "Prefix" [ testGroup "HUnit tests" $ hUnitTestToTests Prefix.hUnitSuite + , testGroup "QuickCheck tests" Prefix.quickCheckSuite + ] + ] diff --git a/hs/test/Syndicate/Dataspace/Trie/Tests/ESOP2016.hs b/hs/test/Syndicate/Dataspace/Trie/Tests/ESOP2016.hs new file mode 100644 index 0000000..e472788 --- /dev/null +++ b/hs/test/Syndicate/Dataspace/Trie/Tests/ESOP2016.hs @@ -0,0 +1,55 @@ +module Syndicate.Dataspace.Trie.Tests.ESOP2016 where + +import Prelude hiding (null, seq) +import Syndicate.Dataspace.Trie.ESOP2016 +import qualified Data.Map.Strict as Map +import qualified Data.Set as Set +import Test.HUnit + +ok vs = Ok (Set.fromList vs) +seq x r = if null r then r else Br (Map.singleton x r) + +seqCh '<' = Open +seqCh '>' = Close +seqCh '*' = Wild +seqCh x = Ch x + +seqs s r = foldr (\ x r -> seq (seqCh x) r) r s + +hUnitSuite = test + [ "seqs simple" ~: seq Open (seq Close (Ok (Set.singleton 1))) ~=? seqs "<>" (ok [1]), + "union simple1" ~: Br (Map.fromList [(Ch 'a', ok [1]), + (Ch 'b', ok [2])]) ~=? + union (seqs "a" (ok [1])) (seqs "b" (ok [2])), + "union simple2" ~: Br (Map.fromList [(Ch 'a', ok [1,2]), + (Ch 'b', ok [2])]) ~=? + unions [seqs "a" (ok [1]), + seqs "b" (ok [2]), + seqs "a" (ok [2])], + "union idem" ~: (seqs "abc" (ok [1])) ~=? + union (seqs "abc" (ok [1])) (seqs "abc" (ok [1])), + "union wild" ~: + -- This is noisier than it needs to be. + Br (Map.fromList [(Open,Br (Map.fromList [(Close, ok [1]), + (Wild,Br (Map.fromList [(Wild,Tl (ok [1]))])), + (Ch 'a',Br (Map.fromList [(Close, ok [1,2]), + (Wild,Br (Map.fromList [(Wild,Tl (ok [1]))]))]))])), + (Wild, ok [1])]) + ~=? union (seqs "*" (ok [1])) (seqs "" (ok [2])), + "route union wild1" ~: Set.fromList [1,2] ~=? + route [Open, Ch 'a', Close] (union + (seqs "*" (ok [1])) + (seqs "" (ok [2]))) Set.empty, + "route union wild2" ~: Set.fromList [1] ~=? + route [Open, Ch 'b', Close] (union + (seqs "*" (ok [1])) + (seqs "" (ok [2]))) Set.empty, + "route union wild3" ~: Set.fromList [1] ~=? + route [Open, Close] (union + (seqs "*" (ok [1])) + (seqs "" (ok [2]))) Set.empty, + "route union wild4" ~: Set.fromList [1] ~=? + route [Open, Ch 'a', Ch 'a', Close] (union + (seqs "*" (ok [1])) + (seqs "" (ok [2]))) Set.empty + ] diff --git a/hs/test/Syndicate/Dataspace/Trie/Tests/ESOP2016v2.hs b/hs/test/Syndicate/Dataspace/Trie/Tests/ESOP2016v2.hs new file mode 100644 index 0000000..a5e2779 --- /dev/null +++ b/hs/test/Syndicate/Dataspace/Trie/Tests/ESOP2016v2.hs @@ -0,0 +1,55 @@ +module Syndicate.Dataspace.Trie.Tests.ESOP2016v2 where +-- Close to the ESOP 2016 implementation of dataspace tries, but takes +-- a step toward efficiency by using collapse/update instead of dedup/distinct. + +import Prelude hiding (null, seq) +import Syndicate.Dataspace.Trie.ESOP2016v2 +import qualified Data.Map.Strict as Map +import qualified Data.Set as Set +import Test.HUnit + +ok vs = Ok (Set.fromList vs) +seq x r = if null r then r else Br (Map.singleton x r) + +seqCh '<' = Open +seqCh '>' = Close +seqCh '*' = Wild +seqCh x = Ch x + +seqs s r = foldr (\ x r -> seq (seqCh x) r) r s + +hUnitSuite = test + [ "seqs simple" ~: seq Open (seq Close (Ok (Set.singleton 1))) ~=? seqs "<>" (ok [1]), + "union simple1" ~: Br (Map.fromList [(Ch 'a', ok [1]), + (Ch 'b', ok [2])]) ~=? + union (seqs "a" (ok [1])) (seqs "b" (ok [2])), + "union simple2" ~: Br (Map.fromList [(Ch 'a', ok [1,2]), + (Ch 'b', ok [2])]) ~=? + unions [seqs "a" (ok [1]), + seqs "b" (ok [2]), + seqs "a" (ok [2])], + "union idem" ~: (seqs "abc" (ok [1])) ~=? + union (seqs "abc" (ok [1])) (seqs "abc" (ok [1])), + "union wild" ~: + Br (Map.fromList [(Open,Br (Map.fromList [(Wild,Tl (ok [1])), + (Ch 'a',Br (Map.fromList [(Close,ok [1,2]), + (Wild,Tl (ok [1]))]))])), + (Wild,ok [1])]) + ~=? union (seqs "*" (ok [1])) (seqs "" (ok [2])), + "route union wild1" ~: Set.fromList [1,2] ~=? + route [Open, Ch 'a', Close] (union + (seqs "*" (ok [1])) + (seqs "" (ok [2]))) Set.empty, + "route union wild2" ~: Set.fromList [1] ~=? + route [Open, Ch 'b', Close] (union + (seqs "*" (ok [1])) + (seqs "" (ok [2]))) Set.empty, + "route union wild3" ~: Set.fromList [1] ~=? + route [Open, Close] (union + (seqs "*" (ok [1])) + (seqs "" (ok [2]))) Set.empty, + "route union wild4" ~: Set.fromList [1] ~=? + route [Open, Ch 'a', Ch 'a', Close] (union + (seqs "*" (ok [1])) + (seqs "" (ok [2]))) Set.empty + ] diff --git a/hs/test/Syndicate/Dataspace/Trie/Tests/ESOP2016v3.hs b/hs/test/Syndicate/Dataspace/Trie/Tests/ESOP2016v3.hs new file mode 100644 index 0000000..e3b763e --- /dev/null +++ b/hs/test/Syndicate/Dataspace/Trie/Tests/ESOP2016v3.hs @@ -0,0 +1,95 @@ +module Syndicate.Dataspace.Trie.Tests.ESOP2016v3 where +-- Explicitly separate Open/Close/Wild from other edges in Br nodes. +-- This gives an elegant presentation. + +import Prelude hiding (null, seq) +import Syndicate.Dataspace.Trie.ESOP2016v3 +import qualified Data.Map.Strict as Map +import qualified Data.Set as Set +import Test.HUnit + +ok vs = Ok (Set.fromList vs) + +seq _ r | null r = r +seq '<' r = Br (r, Mt, Mt, Map.empty) +seq '>' r = Br (Mt, r, Mt, Map.empty) +seq '*' r = Br (tl r, untl r, r, Map.empty) +seq x r = Br (Mt, Mt, Mt, Map.singleton x r) + +seqs s r = foldr seq r s + +hUnitSuite = test + [ "seqs simple" ~: + Br (Br (Mt, ok [1], Mt, Map.empty), Mt, Mt, Map.empty) ~=? seqs "<>" (ok [1]), + "union simple1" ~: + Br (Mt, Mt, Mt, + Map.fromList [('a', ok [1]), + ('b', ok [2])]) ~=? + union (seqs "a" (ok [1])) (seqs "b" (ok [2])), + "union simple2" ~: + Br (Mt, Mt, Mt, + Map.fromList [('a', ok [1,2]), + ('b', ok [2])]) ~=? + unions [seqs "a" (ok [1]), + seqs "b" (ok [2]), + seqs "a" (ok [2])], + "union idem" ~: + (seqs "abc" (ok [1])) ~=? + union (seqs "abc" (ok [1])) (seqs "abc" (ok [1])), + "union wild" ~: + Br (Br (Mt, + ok [1], + Tl (ok [1]), + Map.fromList [('a', Br (Mt, + ok [1,2], + Tl (ok [1]), + Map.empty))]), + Mt, + ok [1], + Map.empty) ~=? + union (seqs "*" (ok [1])) (seqs "" (ok [2])), + "route union wild1" ~: Set.fromList [1,2] ~=? + route "" (union + (seqs "*" (ok [1])) + (seqs "" (ok [2]))) Set.empty, + "route union wild2" ~: Set.fromList [1] ~=? + route "" (union + (seqs "*" (ok [1])) + (seqs "" (ok [2]))) Set.empty, + "route union wild3" ~: Set.fromList [1] ~=? + route "<>" (union + (seqs "*" (ok [1])) + (seqs "" (ok [2]))) Set.empty, + "route union wild4" ~: Set.fromList [1] ~=? + route "" (union + (seqs "*" (ok [1])) + (seqs "" (ok [2]))) Set.empty, + "intersection simple1" ~: + seqs "a" (ok [1,2]) ~=? intersection (seqs "a" (ok [1])) (seqs "a" (ok [2])), + "intersection simple2" ~: + empty ~=? intersection (seqs "a" (ok [1])) (seqs "b" (ok [2])), + "intersection idem" ~: + (seqs "abc" (ok [1])) ~=? + intersection (seqs "abc" (ok [1])) (seqs "abc" (ok [1])), + "difference simple1" ~: + seqs "a" (ok [1]) ~=? difference (seqs "a" (ok [1,2])) (seqs "a" (ok [2])), + "difference simple1a" ~: + seqs "ab" (ok [1]) ~=? difference (seqs "ab" (ok [1,2])) (seqs "ab" (ok [2])), + "difference simple2" ~: + empty ~=? difference (seqs "a" (ok [1])) (seqs "a" (ok [1])), + "difference wild" ~: + Br (Tl (ok [1]), + Mt, + ok [1], + Map.fromList [('a', Mt)]) ~=? + difference (seqs "*" (ok [1])) (seqs "a" (ok [1])), + "union after difference" ~: + seqs "*" (ok [1]) ~=? + union (difference (seqs "*" (ok [1])) (seqs "a" (ok [1]))) (seqs "a" (ok [1])), + "union after difference 2" ~: + Br (Tl (ok [1]), + Mt, + ok [1], + Map.fromList [('a', ok [2])]) ~=? + union (difference (seqs "*" (ok [1])) (seqs "a" (ok [1]))) (seqs "a" (ok [2])) + ] diff --git a/hs/test/Syndicate/Dataspace/Trie/Tests/Prefix.hs b/hs/test/Syndicate/Dataspace/Trie/Tests/Prefix.hs new file mode 100644 index 0000000..21c46e7 --- /dev/null +++ b/hs/test/Syndicate/Dataspace/Trie/Tests/Prefix.hs @@ -0,0 +1,210 @@ +{-# LANGUAGE FlexibleInstances #-} +module Syndicate.Dataspace.Trie.Tests.Prefix where +-- Alternate representation, where Open has an explicit *arity* +-- attached to it, and matching close-parens are implicitly tracked. +-- Where ESOP2016-style implementations have "", this style has +-- "<3xyz". + +import Prelude hiding (null) +import Syndicate.Dataspace.Trie.Prefix +import qualified Data.Map.Strict as Map +import qualified Data.Set as Set + +import Test.HUnit +import Test.QuickCheck +import Test.Framework +import Test.Framework.Providers.HUnit +import Test.Framework.Providers.QuickCheck2 +import Control.Monad + +ok vs = Ok (Set.fromList vs) + +seqs _ r | null r = r +seqs [] r = r +seqs ('<' : n : s) r = Br (Map.singleton (read (n : []) :: Integer) (seqs s r), Mt, Map.empty) +seqs ('*' : s) r = Br (Map.empty, seqs s r, Map.empty) +seqs (x : s) r = Br (Map.empty, Mt, Map.singleton x (seqs s r)) + +hUnitSuite = test + [ "seqs simple" ~: + Br (Map.singleton 0 (ok [1]), Mt, Map.empty) ~=? seqs "<0" (ok [1]), + "union simple1" ~: + Br (Map.empty, Mt, + Map.fromList [('a', ok [1]), + ('b', ok [2])]) ~=? + union (seqs "a" (ok [1])) (seqs "b" (ok [2])), + "union simple2" ~: + Br (Map.empty, Mt, + Map.fromList [('a', ok [1,2]), + ('b', ok [2])]) ~=? + unions [seqs "a" (ok [1]), + seqs "b" (ok [2]), + seqs "a" (ok [2])], + "union idem" ~: + (seqs "abc" (ok [1])) ~=? + union (seqs "abc" (ok [1])) (seqs "abc" (ok [1])), + "union wild" ~: + Br (Map.singleton 1 (Br (Map.empty, + ok [1], + Map.singleton 'a' (ok [1,2]))), + ok [1], + Map.empty) ~=? + union (seqs "*" (ok [1])) (seqs "<1a" (ok [2])), + "route union wild1" ~: Set.fromList [1,2] ~=? + route "<1a" (union + (seqs "*" (ok [1])) + (seqs "<1a" (ok [2]))) Set.empty, + "route union wild2" ~: Set.fromList [1] ~=? + route "<1b" (union + (seqs "*" (ok [1])) + (seqs "<1a" (ok [2]))) Set.empty, + "route union wild3" ~: Set.fromList [1] ~=? + route "<0" (union + (seqs "*" (ok [1])) + (seqs "<1a" (ok [2]))) Set.empty, + "route union wild4" ~: Set.fromList [1] ~=? + route "<2aa" (union + (seqs "*" (ok [1])) + (seqs "<1a" (ok [2]))) Set.empty, + "intersection simple1" ~: + seqs "a" (ok [1,2]) ~=? intersection (seqs "a" (ok [1])) (seqs "a" (ok [2])), + "intersection simple2" ~: + empty ~=? intersection (seqs "a" (ok [1])) (seqs "b" (ok [2])), + "intersection idem" ~: + (seqs "abc" (ok [1])) ~=? + intersection (seqs "abc" (ok [1])) (seqs "abc" (ok [1])), + "difference simple1" ~: + seqs "a" (ok [1]) ~=? difference (seqs "a" (ok [1,2])) (seqs "a" (ok [2])), + "difference simple1a" ~: + seqs "ab" (ok [1]) ~=? difference (seqs "ab" (ok [1,2])) (seqs "ab" (ok [2])), + "difference simple2" ~: + empty ~=? difference (seqs "a" (ok [1])) (seqs "a" (ok [1])), + "difference wild" ~: + Br (Map.empty, + ok [1], + Map.fromList [('a', Mt)]) ~=? + difference (seqs "*" (ok [1])) (seqs "a" (ok [1])), + "difference wild 2" ~: + Br (Map.singleton 1 (Br (Map.empty, + ok [1], + Map.singleton 'a' Mt)), + ok [1], + Map.empty) ~=? + difference (seqs "*" (ok [1])) (seqs "<1a" (ok [1])), + "difference wild 3" ~: + Br (Map.singleton 0 Mt, + ok [1], + Map.empty) ~=? + difference (seqs "*" (ok [1])) (seqs "<0" (ok [1])), + "union after difference" ~: + seqs "*" (ok [1]) ~=? + union (difference (seqs "*" (ok [1])) (seqs "a" (ok [1]))) (seqs "a" (ok [1])), + "union after difference 2" ~: + Br (Map.empty, + ok [1], + Map.fromList [('a', ok [2])]) ~=? + union (difference (seqs "*" (ok [1])) (seqs "a" (ok [1]))) (seqs "a" (ok [2])), + "intersection no overlap opens" ~: + empty ~=? + intersection (seqs "<2aa" (ok [1])) (seqs "<1b" (ok [2])), + "intersection no overlap opens 2" ~: + Br (Map.empty, Mt, Map.singleton 'x' (ok [1,2])) ~=? + (intersection + (union (seqs "x" (ok [1])) (seqs "<2aa" (ok [1]))) + (union (seqs "x" (ok [2])) (seqs "<1b" (ok [2])))), + "intersection no overlap opens 3" ~: + Br (Map.fromList [(1,Br (Map.empty, + ok [3,4], + Map.fromList [('b', ok [2,3,4])])), + (2,Br (Map.empty, + Br (Map.empty, ok [3,4], Map.empty), + Map.fromList [('a',Br (Map.empty, + ok [3,4], + Map.fromList [('a', + ok [1,3,4])]))]))], + ok [3,4], + Map.empty) ~=? + (intersection + (union (seqs "*" (ok [3])) (seqs "<2aa" (ok [1]))) + (union (seqs "*" (ok [4])) (seqs "<1b" (ok [2])))) + ] + +--------------------------------------------------------------------------- + +newtype Pattern = Pattern { getPattern :: String } deriving (Eq, Ord, Show) +newtype Message = Message { getMessage :: String } deriving (Eq, Ord, Show) + +instance Arbitrary Pattern where + arbitrary = liftM Pattern $ sized $ trieNoLargerThan + where leaf = oneof $ [return "x", + return "y", + return "z", + return "*"] + trieNoLargerThan leafLimit = + if leafLimit >= 1 + then frequency [(2, leaf), (3, node leafLimit)] + else leaf + node leafLimit = + do degree <- choose (0, min 4 leafLimit) + kids <- genChildren leafLimit degree + return $ "<" ++ show degree ++ concat kids + genChildren leafLimit 0 = return [] + genChildren leafLimit degree = + do childLimit <- choose (1, leafLimit - (degree - 1)) + child <- trieNoLargerThan childLimit + rest <- genChildren (leafLimit - childLimit) (degree - 1) + return (child : rest) + +instance Arbitrary Message where + arbitrary = do Pattern p <- arbitrary + m <- sequence $ [if c == '*' + then do Message m <- scale (`div` 2) arbitrary + return m + else return (c : []) + | c <- p] + return $ Message $ concat m + +instance Arbitrary (Set.Set Integer) where + arbitrary = resize 5 $ sized set + where set 0 = return Set.empty + set n = do v <- arbitrary `suchThat` (\v -> v >= 0) + s <- set (n - 1) + return $ Set.insert v s + +genTrie k 0 = return Mt +genTrie k n = do Pattern p <- arbitrary + rest <- genTrie k (n - 1) + return $ union (seqs p k) rest + +type TrieOfPids = Trie (Set.Set Integer) + +instance Arbitrary TrieOfPids where + -- arbitrary = do vs <- arbitrary + -- resize 6 $ sized $ genTrie (Ok vs) + arbitrary = resize 6 $ sized $ genTrie (ok [1]) + +isWild (Br (os, w, h)) = Map.null os && Map.null h +isWild _ = False + +trieContains t (Message m) = not $ Set.null $ route m t Set.empty + +combineBasics :: (TrieOfPids -> TrieOfPids -> TrieOfPids) -> + (Bool -> Bool -> Bool) -> + (TrieOfPids, TrieOfPids, Message) -> + Property +combineBasics tf bf (trie1, trie2, element) = + not (isWild trie1) && not (isWild trie2) && (p || q1 || q2) ==> p == q + where p = combined `trieContains` element + q1 = trie1 `trieContains` element + q2 = trie2 `trieContains` element + q = bf q1 q2 + combined = tf trie1 trie2 + +unionBasics = combineBasics union (||) +intersectionBasics = combineBasics intersection (&&) +differenceBasics = combineBasics difference (\ x y -> x && not y) + +quickCheckSuite = [ testProperty "differenceBasics" differenceBasics + , testProperty "intersectionBasics" intersectionBasics + , testProperty "unionBasics" unionBasics + ] diff --git a/js/.gitignore b/js/.gitignore new file mode 100644 index 0000000..911168a --- /dev/null +++ b/js/.gitignore @@ -0,0 +1,2 @@ +scratch/ +node_modules/ diff --git a/js/Makefile b/js/Makefile new file mode 100644 index 0000000..aae362f --- /dev/null +++ b/js/Makefile @@ -0,0 +1,23 @@ +all: + npm install . + +keys: private-key.pem server-cert.pem + +private-key.pem: + openssl genrsa -des3 -passout pass:a -out $@ 1024 + openssl rsa -passin pass:a -in $@ -out $@ + +server-cert.pem: private-key.pem + openssl req -new -x509 -nodes -sha1 -days 365 \ + -subj /CN=server.minimart.leastfixedpoint.com \ + -passin pass:a \ + -key private-key.pem > $@ + +clean-keys: + rm -f private-key.pem server-cert.pem + +clean: + rm -f dist/*.js + +veryclean: clean + rm -rf node_modules/ diff --git a/js/README.md b/js/README.md new file mode 100644 index 0000000..581c7ca --- /dev/null +++ b/js/README.md @@ -0,0 +1,24 @@ +# Syndicate-JS: Syndicate for Javascript environments + +## A walk through the code + +Source files in `src/`, from most general to most specific: + + - `reflect.js`: Reflection on function formal parameter lists. + - `util.js`: Functions `extend` and `kwApply`. + - `randomid.js`: Generation of (cryptographically) random base64 strings. + + - `route.js`: Implementation of dataspace trie structure. + - `patch.js`: Implementation of patches over dataspace tries. + - `mux.js`: Use of tries plus patches to build a (de)multiplexing routing structure. + - `network.js`: Implementation of core leaf actors and networks. + - `ground.js`: Pseudo-network acting as the global outermost context for Syndicate actors. + + - `ack.js`: Utility for detecting when a previous state change has taken effect. + - `seal.js`: Immutable container for data, used to hide structure from dataspace tries. + + - `demand-matcher.js`: Tracking and responding to demand and supply expressed as assertions. + - `dom-driver.js`: Syndicate driver for displaying DOM fragments on a webpage. + - `jquery-driver.js`: Syndicate driver for soliciting jQuery-based DOM events. + + - `main.js`: Main package entry point. diff --git a/js/bin/syndicatec b/js/bin/syndicatec new file mode 100755 index 0000000..f7311d0 --- /dev/null +++ b/js/bin/syndicatec @@ -0,0 +1,23 @@ +#!/usr/bin/env node +// -*- javascript -*- + +var fs = require('fs'); +var compiler = require('../compiler/compiler.js'); + +function compileAndPrint(inputSource) { + var translatedSource = compiler.compileSyndicateSource(inputSource); + if (translatedSource) { + console.log(translatedSource); + } +} + +if (process.argv.length < 3 || process.argv[2] === '-') { + var inputSource = ''; + process.stdin.resume(); + process.stdin.setEncoding('utf8'); + process.stdin.on('data', function(buf) { inputSource += buf; }); + process.stdin.on('end', function() { compileAndPrint(inputSource); }); +} else { + var inputSource = fs.readFileSync(process.argv[2]).toString(); + compileAndPrint(inputSource); +} diff --git a/js/compiler/README.md b/js/compiler/README.md new file mode 100644 index 0000000..d9031f8 --- /dev/null +++ b/js/compiler/README.md @@ -0,0 +1,4 @@ +# Syndicate/js compiler + +Translates ES5 + Syndicate extensions to plain ES5 using +[Ohm](https://github.com/cdglabs/ohm#readme). diff --git a/js/compiler/compiler.js b/js/compiler/compiler.js new file mode 100644 index 0000000..2292d81 --- /dev/null +++ b/js/compiler/compiler.js @@ -0,0 +1,365 @@ +// Compile ES5+Syndicate to plain ES5. + +'use strict'; + +var fs = require('fs'); +var path = require('path'); + +var ohm = require('ohm-js'); +var ES5 = require('./es5.js'); + +var grammarSource = fs.readFileSync(path.join(__dirname, 'syndicate.ohm')).toString(); +var grammar = ohm.grammar(grammarSource, { ES5: ES5.grammar }); +var semantics = grammar.extendSemantics(ES5.semantics); + +var gensym_start = Math.floor(new Date() * 1); +var gensym_counter = 0; +function gensym(label) { + return '_' + (label || 'g') + gensym_start + '_' + (gensym_counter++); +} + +var forEachChild = (function () { + function flattenIterNodes(nodes, acc) { + for (var i = 0; i < nodes.length; ++i) { + if (nodes[i].isIteration()) { + flattenIterNodes(nodes[i].children, acc); + } else { + acc.push(nodes[i]); + } + } + } + + function compareByInterval(node, otherNode) { + return node.interval.startIdx - otherNode.interval.startIdx; + } + + function forEachChild(children, f) { + var nodes = []; + flattenIterNodes(children, nodes); + nodes.sort(compareByInterval).forEach(f); + } + + return forEachChild; +})(); + +function buildActor(constructorES5, block) { + return 'Syndicate.Actor.spawnActor(new '+constructorES5+', '+ + 'function() ' + block.asES5 + ');'; +} + +function buildFacet(facetBlock, transitionBlock) { + return 'Syndicate.Actor.createFacet()' + + (facetBlock ? facetBlock.asES5 : '') + + (transitionBlock ? transitionBlock.asES5 : '') + + '.completeBuild();'; +} + +function buildOnEvent(isTerminal, eventType, subscription, projection, bindings, body) { + return '\n.onEvent(' + isTerminal + ', ' + JSON.stringify(eventType) + ', ' + + subscription + ', ' + projection + + ', (function(' + bindings.join(', ') + ') ' + body + '))'; +} + +function buildCaseEvent(eventPattern, body) { + if (eventPattern.eventType === 'risingEdge') { + return buildOnEvent(true, + eventPattern.eventType, + 'function() { return (' + eventPattern.asES5 + '); }', + 'null', + [], + body); + } else { + return buildOnEvent(true, + eventPattern.eventType, + eventPattern.subscription, + eventPattern.projection, + eventPattern.bindings, + body); + } +} + +var modifiedSourceActions = { + ActorStatement_noConstructor: function(_actor, block) { + return buildActor('Object()', block); + }, + ActorStatement_withConstructor: function(_actor, ctorExp, block) { + return buildActor(ctorExp.asES5, block); + }, + + NetworkStatement_ground: function(_ground, _network, maybeId, block) { + var code = 'new Syndicate.Ground(function () ' + block.asES5 + ').startStepping();'; + if (maybeId.numChildren === 1) { + return 'var ' + maybeId.children[0].interval.contents + ' = ' + code; + } else { + return code; + } + }, + NetworkStatement_normal: function(_network, block) { + return 'Syndicate.Network.spawn(new Network(function () ' + block.asES5 + '));'; + }, + + ActorFacetStatement_state: function(_state, facetBlock, _until, transitionBlock) { + return buildFacet(facetBlock, transitionBlock); + }, + ActorFacetStatement_until: function(_until, transitionBlock) { + return buildFacet(null, transitionBlock); + }, + ActorFacetStatement_forever: function(_forever, facetBlock) { + return buildFacet(facetBlock, null); + }, + + AssertionTypeDeclarationStatement: function(_assertion, + _type, + typeName, + _leftParen, + formalsRaw, + _rightParen, + _maybeEquals, + maybeLabel, + _maybeSc) + { + var formals = formalsRaw.asSyndicateStructureArguments; + var label = maybeLabel.numChildren === 1 + ? maybeLabel.children[0].interval.contents + : JSON.stringify(typeName.interval.contents); + var fragments = []; + fragments.push( + 'var ' + typeName.asES5 + ' = (function() {', + ' var $SyndicateMeta$ = {', + ' label: ' + label + ',', + ' arguments: ' + JSON.stringify(formals), + ' };', + ' return function ' + typeName.asES5 + '(' + formalsRaw.asES5 + ') {', + ' return {'); + formals.forEach(function(f) { + fragments.push(' ' + JSON.stringify(f) + ': ' + f + ','); + }); + fragments.push( + ' "$SyndicateMeta$": $SyndicateMeta$', + ' };', + ' };', + '})();'); + return fragments.join('\n'); + }, + + SendMessageStatement: function(_colons, expr, sc) { + return 'Syndicate.Network.send(' + expr.asES5 + ')' + sc.interval.contents; + }, + + FacetBlock: function(_leftParen, init, situations, done, _rightParen) { + return (init ? init.asES5 : '') + situations.asES5.join('') + (done ? done.asES5 : ''); + }, + FacetStateTransitionBlock: function(_leftParen, transitions, _rightParen) { + return transitions.asES5; + }, + + FacetInitBlock: function(_init, block) { + return '\n.addInitBlock((function() ' + block.asES5 + '))'; + }, + FacetDoneBlock: function(_done, block) { + return '\n.addDoneBlock((function() ' + block.asES5 + '))'; + }, + + FacetSituation_assert: function(_assert, expr, _sc) { + return '\n.addAssertion(' + buildSubscription([expr], 'assert', 'pattern') + ')'; + }, + FacetSituation_event: function(_on, eventPattern, block) { + return buildOnEvent(false, + eventPattern.eventType, + eventPattern.subscription, + eventPattern.projection, + eventPattern.bindings, + block.asES5); + }, + FacetSituation_during: function(_during, pattern, facetBlock) { + return buildOnEvent(false, + 'asserted', + pattern.subscription, + pattern.projection, + pattern.bindings, + '{ Syndicate.Actor.createFacet()' + + facetBlock.asES5 + + buildOnEvent(true, + 'retracted', + pattern.instantiatedSubscription, + pattern.instantiatedProjection, + [], + '{}') + + '.completeBuild(); }'); + }, + + FacetStateTransition_withContinuation: function(_case, eventPattern, block) { + return buildCaseEvent(eventPattern, block.asES5); + }, + FacetStateTransition_noContinuation: function(_case, eventPattern, _sc) { + return buildCaseEvent(eventPattern, '{}'); + } +}; + +semantics.extendAttribute('modifiedSource', modifiedSourceActions); + +semantics.addAttribute('asSyndicateStructureArguments', { + FormalParameterList: function(formals) { + return formals.asIteration().asSyndicateStructureArguments; + }, + identifier: function(_name) { + return this.interval.contents; + } +}); + +semantics.addAttribute('eventType', { + FacetEventPattern_messageEvent: function(_kw, _pattern) { return 'message'; }, + FacetEventPattern_assertedEvent: function(_kw, _pattern) { return 'asserted'; }, + FacetEventPattern_retractedEvent: function(_kw, _pattern) { return 'retracted'; }, + + FacetTransitionEventPattern_facetEvent: function (pattern) { return pattern.eventType; }, + FacetTransitionEventPattern_risingEdge: function (_lp, expr, _rp) { return 'risingEdge'; } +}); + +function buildSubscription(children, patchMethod, mode) { + var fragments = []; + fragments.push('(function() { var _ = Syndicate.__; return '); + if (patchMethod) { + fragments.push('Syndicate.Patch.' + patchMethod + '('); + } else { + fragments.push('{ assertion: '); + } + children.forEach(function (c) { c.buildSubscription(fragments, mode); }); + if (patchMethod) { + fragments.push(', '); + } else { + fragments.push(', metalevel: '); + } + children.forEach(function (c) { fragments.push(c.metalevel) }); + if (patchMethod) { + fragments.push(')'); + } else { + fragments.push(' }'); + } + fragments.push('; })'); + return fragments.join(''); +} + +semantics.addAttribute('subscription', { + _default: function(children) { + return buildSubscription(children, 'sub', 'pattern'); + } +}); + +semantics.addAttribute('instantiatedSubscription', { + _default: function(children) { + return buildSubscription(children, 'sub', 'instantiated'); + } +}); + +semantics.addAttribute('instantiatedProjection', { + _default: function(children) { + return buildSubscription(children, null, 'instantiated'); + } +}); + +semantics.addAttribute('projection', { + _default: function(children) { + return buildSubscription(children, null, 'projection'); + } +}); + +semantics.addAttribute('metalevel', { + FacetEventPattern_messageEvent: function(_kw, p) { return p.metalevel; }, + FacetEventPattern_assertedEvent: function(_kw, p) { return p.metalevel; }, + FacetEventPattern_retractedEvent: function(_kw, p) { return p.metalevel; }, + + FacetTransitionEventPattern_facetEvent: function (pattern) { return pattern.metalevel; }, + + FacetPattern_withMetalevel: function(_expr, _kw, metalevel) { + return metalevel.interval.contents; + }, + FacetPattern_noMetalevel: function(_expr) { + return 0; + } +}); + +semantics.addOperation('buildSubscription(acc,mode)', { + FacetEventPattern_messageEvent: function(_kw, pattern) { + pattern.buildSubscription(this.args.acc, this.args.mode); + }, + FacetEventPattern_assertedEvent: function(_kw, pattern) { + pattern.buildSubscription(this.args.acc, this.args.mode); + }, + FacetEventPattern_retractedEvent: function(_kw, pattern) { + pattern.buildSubscription(this.args.acc, this.args.mode); + }, + + FacetTransitionEventPattern_facetEvent: function (pattern) { + pattern.buildSubscription(this.args.acc, this.args.mode); + }, + + FacetPattern: function (v) { + v.children[0].buildSubscription(this.args.acc, this.args.mode); // both branches! + }, + + identifier: function(_name) { + var i = this.interval.contents; + if (i[0] === '$') { + switch (this.args.mode) { + case 'pattern': this.args.acc.push('_'); break; + case 'instantiated': this.args.acc.push(i.slice(1)); break; + case 'projection': this.args.acc.push('(Syndicate._$(' + JSON.stringify(i.slice(1)) + '))'); break; + default: throw new Error('Unexpected buildSubscription mode ' + this.args.mode); + } + } else { + this.args.acc.push(i); + } + }, + _terminal: function() { + this.args.acc.push(this.interval.contents); + }, + _nonterminal: function(children) { + var self = this; + forEachChild(children, function (c) { + c.buildSubscription(self.args.acc, self.args.mode); + }); + } +}); + +semantics.addAttribute('bindings', { + _default: function(children) { + var result = []; + this.pushBindings(result); + return result; + } +}); + +semantics.addOperation('pushBindings(accumulator)', { + identifier: function(_name) { + var i = this.interval.contents; + if (i[0] === '$') { + this.args.accumulator.push(i.slice(1)); + } + }, + _terminal: function () {}, + _nonterminal: function(children) { + var self = this; + children.forEach(function (c) { c.pushBindings(self.args.accumulator); }); + } +}) + +function compileSyndicateSource(inputSource, onError) { + var parseResult = grammar.match(inputSource); + if (parseResult.failed()) { + if (onError) { + return onError(parseResult.message, parseResult); + } else { + console.error(parseResult.message); + return false; + } + } else { + return '"use strict";\n' + semantics(parseResult).asES5; + } +} + +//--------------------------------------------------------------------------- + +module.exports.grammar = grammar; +module.exports.semantics = semantics; +module.exports.compileSyndicateSource = compileSyndicateSource; diff --git a/js/compiler/demo-bankaccount.js b/js/compiler/demo-bankaccount.js new file mode 100644 index 0000000..aa70f37 --- /dev/null +++ b/js/compiler/demo-bankaccount.js @@ -0,0 +1,43 @@ +// bin/syndicatec compiler/demo-bankaccount.js | node + +var Syndicate = require('./src/main.js'); + +assertion type account(balance); +assertion type deposit(amount); + +ground network { + actor { + this.balance = 0; + + forever { + assert account(this.balance); + on message deposit($amount) { + this.balance += amount; + } + } + } + + actor { + forever { + on asserted account($balance) { + console.log("Balance is now", balance); + } + } + } + + actor { + state { + init { + console.log("Waiting for account."); + } + done { + console.log("Account became ready."); + } + } until { + case asserted Syndicate.observe(deposit(_)) { + :: deposit(+100); + :: deposit(-30); + } + } + } +} diff --git a/js/compiler/demo-filesystem.js b/js/compiler/demo-filesystem.js new file mode 100644 index 0000000..1542209 --- /dev/null +++ b/js/compiler/demo-filesystem.js @@ -0,0 +1,66 @@ +// bin/syndicatec compiler/demo-filesystem.js | node + +var Syndicate = require('./src/main.js'); + +assertion type file(name, content) = "file"; +assertion type saveFile(name, content) = "save"; +assertion type deleteFile(name) = "delete"; + +ground network { + /////////////////////////////////////////////////////////////////////////// + // The file system actor + + actor { + this.files = {}; + forever { + during Syndicate.observe(file($name, _)) { + init { + console.log("At least one reader exists for:", name); + } + assert file(name, this.files[name]); + done { + console.log("No remaining readers exist for:", name); + } + } + on message saveFile($name, $newcontent) { + this.files[name] = newcontent; + } + on message deleteFile($name) { + delete this.files[name]; + } + } + } + + /////////////////////////////////////////////////////////////////////////// + // A simple demo client of the file system + + actor { + state { + on asserted file("hello.txt", $content) { + console.log("hello.txt has content", JSON.stringify(content)); + } + } until { + case asserted file("hello.txt", "quit demo") { + console.log("The hello.txt file contained 'quit demo', so we will quit"); + } + } + + until { + case asserted Syndicate.observe(saveFile(_, _)) { + :: saveFile("hello.txt", "a"); + :: deleteFile("hello.txt"); + :: saveFile("hello.txt", "c"); + :: saveFile("hello.txt", "quit demo"); + :: saveFile("hello.txt", "final contents"); + actor { + until { + case asserted file("hello.txt", $content) { + console.log("second observer sees that hello.txt content is", + JSON.stringify(content)); + } + } + } + } + } + } +} diff --git a/js/compiler/es5.js b/js/compiler/es5.js new file mode 100644 index 0000000..c453618 --- /dev/null +++ b/js/compiler/es5.js @@ -0,0 +1,94 @@ +//=========================================================================== +// Copy of ohm-js/examples/ecmascript/es5.js to get browserify+brfs to work +//=========================================================================== + +/* eslint-env node */ + +'use strict'; + +// -------------------------------------------------------------------- +// Imports +// -------------------------------------------------------------------- + +var fs = require('fs'); +var path = require('path'); + +var ohm = require('ohm-js'); + +// -------------------------------------------------------------------- +// Helpers +// -------------------------------------------------------------------- + +function isUndefined(x) { return x === void 0; } + +// Take an Array of nodes, and whenever an _iter node is encountered, splice in its +// recursively-flattened children instead. +function flattenIterNodes(nodes) { + var result = []; + for (var i = 0; i < nodes.length; ++i) { + if (nodes[i]._node.ctorName === '_iter') { + result.push.apply(result, flattenIterNodes(nodes[i].children)); + } else { + result.push(nodes[i]); + } + } + return result; +} + +// Comparison function for sorting nodes based on their interval's start index. +function compareByInterval(node, otherNode) { + return node.interval.startIdx - otherNode.interval.startIdx; +} + +// Semantic actions for the `modifiedSource` attribute (see below). +var modifiedSourceActions = { + _nonterminal: function(children) { + var flatChildren = flattenIterNodes(children).sort(compareByInterval); + var childResults = flatChildren.map(function(n) { return n.modifiedSource; }); + if (flatChildren.length === 0 || childResults.every(isUndefined)) { + return undefined; + } + var code = ''; + var interval = flatChildren[0].interval.collapsedLeft(); + for (var i = 0; i < flatChildren.length; ++i) { + if (childResults[i] == null) { + // Grow the interval to include this node. + interval = interval.coverageWith(flatChildren[i].interval.collapsedRight()); + } else { + interval = interval.coverageWith(flatChildren[i].interval.collapsedLeft()); + code += interval.contents + childResults[i]; + interval = flatChildren[i].interval.collapsedRight(); + } + } + code += interval.contents; + return code; + }, + _iter: function(_) { + throw new Error('_iter semantic action should never be hit'); + }, + _terminal: function() { + return undefined; + } +}; + +// Instantiate the ES5 grammar. +var contents = fs.readFileSync(path.join(__dirname, 'es5.ohm')); +var g = ohm.grammars(contents).ES5; +var semantics = g.semantics(); + +// An attribute whose value is either a string representing the modified source code for the +// node, or undefined (which means that the original source code should be used). +semantics.addAttribute('modifiedSource', modifiedSourceActions); + +// A simple wrapper around the `modifiedSource` attribute, which always returns a string +// containing the ES5 source code for the node. +semantics.addAttribute('asES5', { + _nonterminal: function(children) { + return isUndefined(this.modifiedSource) ? this.interval.contents : this.modifiedSource; + } +}); + +module.exports = { + grammar: g, + semantics: semantics +}; diff --git a/js/compiler/es5.ohm b/js/compiler/es5.ohm new file mode 100644 index 0000000..74cf233 --- /dev/null +++ b/js/compiler/es5.ohm @@ -0,0 +1,502 @@ +/* + This grammar was originally based on Tom Van Cutsem's ES5 parser from the + es-lab project (https://github.com/tvcutsem/es-lab/blob/master/src/parser/es5parser.ojs), + and was adapted to Ohm by Tony Garnock-Jones in 2014. + + The original copyright and license follows: +*/ + +// Copyright (C) 2009 Google Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* (end of original copyright and license) */ + +ES5 { + + Program = Directive* SourceElement* + + // §A.1 Lexical Grammar -- http://ecma-international.org/ecma-262/5.1/#sec-A.1 + + /* + Note: the following lexical conventions (see http://ecma-international.org/ecma-262/5.1/#sec-7) + are not implemented in this parser. + + // Goal production in contexts where a leading "/" or "/=" is permitted: + InputElementDiv = whitespace | lineTerminator | comment | token | DivPunctuator + + // Goal production in contexts where a leading "/" or "/=' is not permitted: + InputElementRegExp = whitespace | lineTerminator | comment | token | regularExpressionLiteral + */ + + sourceCharacter = any + + // Override Ohm's built-in definition of space. + space := whitespace | lineTerminator | comment + + whitespace = "\t" + | "\x0B" -- verticalTab + | "\x0C" -- formFeed + | " " + | "\u00A0" -- noBreakSpace + | "\uFEFF" -- byteOrderMark + | unicodeSpaceSeparator + + lineTerminator = "\n" | "\r" | "\u2028" | "\u2029" + lineTerminatorSequence = "\n" | "\r" ~"\n" | "\u2028" | "\u2029" | "\r\n" + + comment = multiLineComment | singleLineComment + + multiLineComment = "/*" (~"*/" sourceCharacter)* "*/" + singleLineComment = "//" (~lineTerminator sourceCharacter)* + + identifier (an indentifier) = ~reservedWord identifierName + identifierName = identifierStart identifierPart* + + identifierStart = letter | "$" | "_" + | "\\" unicodeEscapeSequence -- escaped + identifierPart = identifierStart | unicodeCombiningMark + | unicodeDigit | unicodeConnectorPunctuation + | "\u200C" | "\u200D" + letter += unicodeCategoryNl + unicodeCategoryNl + = "\u2160".."\u2182" | "\u3007" | "\u3021".."\u3029" + unicodeDigit (a digit) + = "\u0030".."\u0039" | "\u0660".."\u0669" | "\u06F0".."\u06F9" | "\u0966".."\u096F" | "\u09E6".."\u09EF" | "\u0A66".."\u0A6F" | "\u0AE6".."\u0AEF" | "\u0B66".."\u0B6F" | "\u0BE7".."\u0BEF" | "\u0C66".."\u0C6F" | "\u0CE6".."\u0CEF" | "\u0D66".."\u0D6F" | "\u0E50".."\u0E59" | "\u0ED0".."\u0ED9" | "\u0F20".."\u0F29" | "\uFF10".."\uFF19" + + unicodeCombiningMark (a Unicode combining mark) + = "\u0300".."\u0345" | "\u0360".."\u0361" | "\u0483".."\u0486" | "\u0591".."\u05A1" | "\u05A3".."\u05B9" | "\u05BB".."\u05BD" | "\u05BF".."\u05BF" | "\u05C1".."\u05C2" | "\u05C4".."\u05C4" | "\u064B".."\u0652" | "\u0670".."\u0670" | "\u06D6".."\u06DC" | "\u06DF".."\u06E4" | "\u06E7".."\u06E8" | "\u06EA".."\u06ED" | "\u0901".."\u0902" | "\u093C".."\u093C" | "\u0941".."\u0948" | "\u094D".."\u094D" | "\u0951".."\u0954" | "\u0962".."\u0963" | "\u0981".."\u0981" | "\u09BC".."\u09BC" | "\u09C1".."\u09C4" | "\u09CD".."\u09CD" | "\u09E2".."\u09E3" | "\u0A02".."\u0A02" | "\u0A3C".."\u0A3C" | "\u0A41".."\u0A42" | "\u0A47".."\u0A48" | "\u0A4B".."\u0A4D" | "\u0A70".."\u0A71" | "\u0A81".."\u0A82" | "\u0ABC".."\u0ABC" | "\u0AC1".."\u0AC5" | "\u0AC7".."\u0AC8" | "\u0ACD".."\u0ACD" | "\u0B01".."\u0B01" | "\u0B3C".."\u0B3C" | "\u0B3F".."\u0B3F" | "\u0B41".."\u0B43" | "\u0B4D".."\u0B4D" | "\u0B56".."\u0B56" | "\u0B82".."\u0B82" | "\u0BC0".."\u0BC0" | "\u0BCD".."\u0BCD" | "\u0C3E".."\u0C40" | "\u0C46".."\u0C48" | "\u0C4A".."\u0C4D" | "\u0C55".."\u0C56" | "\u0CBF".."\u0CBF" | "\u0CC6".."\u0CC6" | "\u0CCC".."\u0CCD" | "\u0D41".."\u0D43" | "\u0D4D".."\u0D4D" | "\u0E31".."\u0E31" | "\u0E34".."\u0E3A" | "\u0E47".."\u0E4E" | "\u0EB1".."\u0EB1" | "\u0EB4".."\u0EB9" | "\u0EBB".."\u0EBC" | "\u0EC8".."\u0ECD" | "\u0F18".."\u0F19" | "\u0F35".."\u0F35" | "\u0F37".."\u0F37" | "\u0F39".."\u0F39" | "\u0F71".."\u0F7E" | "\u0F80".."\u0F84" | "\u0F86".."\u0F87" | "\u0F90".."\u0F95" | "\u0F97".."\u0F97" | "\u0F99".."\u0FAD" | "\u0FB1".."\u0FB7" | "\u0FB9".."\u0FB9" | "\u20D0".."\u20DC" | "\u20E1".."\u20E1" | "\u302A".."\u302F" | "\u3099".."\u309A" | "\uFB1E".."\uFB1E" | "\uFE20".."\uFE23" + + unicodeConnectorPunctuation = "\u005F" | "\u203F".."\u2040" | "\u30FB" | "\uFE33".."\uFE34" | "\uFE4D".."\uFE4F" | "\uFF3F" | "\uFF65" + unicodeSpaceSeparator = "\u2000".."\u200B" | "\u3000" + + reservedWord = keyword | futureReservedWord | nullLiteral | booleanLiteral + + // Note: keywords that are the complete prefix of another keyword should + // be prioritized (e.g. 'in' should come before 'instanceof') + keyword = break | do | instanceof | typeof + | case | else | new | var + | catch | finally | return | void + | continue | for | switch | while + | debugger | function | this | with + | default | if | throw + | delete | in | try + + futureReservedWordLax = class | enum | extends + | super | const | export + | import + + futureReservedWordStrict = futureReservedWordLax + | implements | let | private | public + | interface | package | protected | static + | yield + + futureReservedWord = futureReservedWordStrict + + /* + Note: Punctuator and DivPunctuator (see https://es5.github.io/x7.html#x7.7) are + not currently used by this grammar. + */ + + literal = nullLiteral | booleanLiteral | numericLiteral + | stringLiteral | regularExpressionLiteral // spec forgot Regexp literals in appendix? + nullLiteral = "null" ~identifierPart + booleanLiteral = ("true" | "false") ~identifierPart + + // For semantics on how decimal literals are constructed, see section 7.8.3 + + // Note that the ordering of hexIntegerLiteral and decimalLiteral is reversed w.r.t. the spec + // This is intentional: the order decimalLiteral | hexIntegerLiteral will parse + // "0x..." as a decimal literal "0" followed by "x..." + numericLiteral = octalIntegerLiteral | hexIntegerLiteral | decimalLiteral + + decimalLiteral = decimalIntegerLiteral "." decimalDigit* exponentPart -- bothParts + | "." decimalDigit+ exponentPart -- decimalsOnly + | decimalIntegerLiteral exponentPart -- integerOnly + + decimalIntegerLiteral = nonZeroDigit decimalDigit* -- nonZero + | "0" -- zero + decimalDigit = "0".."9" + nonZeroDigit = "1".."9" + + exponentPart = exponentIndicator signedInteger -- present + | -- absent + exponentIndicator = "e" | "E" + signedInteger = "+" decimalDigit* -- positive + | "-" decimalDigit* -- negative + | decimalDigit+ -- noSign + + hexIntegerLiteral = "0x" hexDigit+ + | "0X" hexDigit+ + + // hexDigit defined in Ohm's built-in rules (otherwise: hexDigit = "0".."9" | "a".."f" | "A".."F") + + octalIntegerLiteral = "0" octalDigit+ + + octalDigit = "0".."7" + + // For semantics on how string literals are constructed, see section 7.8.4 + stringLiteral = "\"" doubleStringCharacter* "\"" + | "'" singleStringCharacter* "'" + doubleStringCharacter = ~("\"" | "\\" | lineTerminator) sourceCharacter -- nonEscaped + | "\\" escapeSequence -- escaped + | lineContinuation -- lineContinuation + singleStringCharacter = ~("'" | "\\" | lineTerminator) sourceCharacter -- nonEscaped + | "\\" escapeSequence -- escaped + | lineContinuation -- lineContinuation + lineContinuation = "\\" lineTerminatorSequence + escapeSequence = unicodeEscapeSequence + | hexEscapeSequence + | octalEscapeSequence + | characterEscapeSequence // Must come last. + characterEscapeSequence = singleEscapeCharacter + | nonEscapeCharacter + singleEscapeCharacter = "'" // -> ( String.fromCharCode(0039) ) /*\u0027*/ + | "\"" // -> ( String.fromCharCode(0034) ) /*\u0022*/ + | "\\" // -> ( String.fromCharCode(0092) ) /*\u005C*/ + | "b" // -> ( String.fromCharCode(0008) ) /*\u0008*/ + | "f" // -> ( String.fromCharCode(0012) ) /*\u000C*/ + | "n" // -> ( String.fromCharCode(0010) ) /*\u000A*/ + | "r" // -> ( String.fromCharCode(0013) ) /*\u000D*/ + | "t" // -> ( String.fromCharCode(0009) ) /*\u0009*/ + | "v" // -> ( String.fromCharCode(0011) ) /*\u000B*/ + nonEscapeCharacter = ~(escapeCharacter | lineTerminator) sourceCharacter + escapeCharacter = singleEscapeCharacter | decimalDigit | "x" | "u" + octalEscapeSequence = zeroToThree octalDigit octalDigit -- whole + | fourToSeven octalDigit -- eightTimesfourToSeven + | zeroToThree octalDigit ~decimalDigit -- eightTimesZeroToThree + | octalDigit ~decimalDigit -- octal + hexEscapeSequence = "x" hexDigit hexDigit + unicodeEscapeSequence = "u" hexDigit hexDigit hexDigit hexDigit + + zeroToThree = "0".."3" + fourToSeven = "4".."7" + + // §7.8.5 Regular Expression Literals -- http://ecma-international.org/ecma-262/5.1/#sec-7.8.5 + + regularExpressionLiteral = "/" regularExpressionBody "/" regularExpressionFlags + regularExpressionBody = regularExpressionFirstChar regularExpressionChar* + regularExpressionFirstChar = ~("*" | "\\" | "/" | "[") regularExpressionNonTerminator + | regularExpressionBackslashSequence + | regularExpressionClass + regularExpressionChar = ~("\\" | "/" | "[") regularExpressionNonTerminator + | regularExpressionBackslashSequence + | regularExpressionClass + regularExpressionBackslashSequence = "\\" regularExpressionNonTerminator + regularExpressionNonTerminator = ~(lineTerminator) sourceCharacter + regularExpressionClass = "[" regularExpressionClassChar* "]" + regularExpressionClassChar = ~("]" | "\\") regularExpressionNonTerminator + | regularExpressionBackslashSequence + regularExpressionFlags = identifierPart* + + // === Implementation-level rules (not part of the spec) === + + multiLineCommentNoNL = "/*" (~("*/" | lineTerminator) sourceCharacter)* "*/" + + // does not accept lineTerminators, not even implicit ones in a multiLineComment (cf. section 7.4) + spacesNoNL = (whitespace | singleLineComment | multiLineCommentNoNL)* + + // A semicolon is "automatically inserted" if a newline is reached the end of the input stream + // is reached, or the offending token is "}". + // See http://ecma-international.org/ecma-262/5.1/#sec-7.9 for more information. + // NOTE: Applications of this rule *must* appear in a lexical context -- either in the body of a + // lexical rule, or inside `#()`. + sc = space* (";" | end) + | spacesNoNL (lineTerminator | ~multiLineCommentNoNL multiLineComment | &"}") + + // Convenience rules for parsing keyword tokens. + break = "break" ~identifierPart + do = "do" ~identifierPart + instanceof = "instanceof" ~identifierPart + typeof = "typeof" ~identifierPart + case = "case" ~identifierPart + else = "else" ~identifierPart + new = "new" ~identifierPart + var = "var" ~identifierPart + catch = "catch" ~identifierPart + finally = "finally" ~identifierPart + return = "return" ~identifierPart + void = "void" ~identifierPart + continue = "continue" ~identifierPart + for = "for" ~identifierPart + switch = "switch" ~identifierPart + while = "while" ~identifierPart + debugger = "debugger" ~identifierPart + function = "function" ~identifierPart + this = "this" ~identifierPart + with = "with" ~identifierPart + default = "default" ~identifierPart + if = "if" ~identifierPart + throw = "throw" ~identifierPart + delete = "delete" ~identifierPart + in = "in" ~identifierPart + try = "try" ~identifierPart + get = "get" ~identifierPart + set = "set" ~identifierPart + class = "class" ~identifierPart + enum = "enum" ~identifierPart + extends = "extends" ~identifierPart + super = "super" ~identifierPart + const = "const" ~identifierPart + export = "export" ~identifierPart + import = "import" ~identifierPart + implements = "implements" ~identifierPart + let = "let" ~identifierPart + private = "private" ~identifierPart + public = "public" ~identifierPart + interface = "interface" ~identifierPart + package = "package" ~identifierPart + protected = "protected" ~identifierPart + static = "static" ~identifierPart + yield = "yield" ~identifierPart + + // end of lexical rules + + noIn = ~in + withIn = + + // §A.3 Expressions -- http://ecma-international.org/ecma-262/5.1/#sec-A.3 + + PrimaryExpression = this + | identifier + | literal + // ( litToken.type === "regexp" + // ? this.ast(_fromIdx, "RegExpExpr",{body: litToken.value.body + // flags: litToken.value.flags}, []) + // : this.ast(_fromIdx, "LiteralExpr",{type: litToken.type + // value: litToken.value}, []) ) + | ArrayLiteral + | ObjectLiteral + | "(" Expression ")" -- parenExpr + + ArrayLiteral = "[" ListOf "]" + AssignmentExpressionOrElision = AssignmentExpression + | -- elision + + ObjectLiteral = "{" ListOf "}" -- noTrailingComma + | "{" NonemptyListOf "," "}" -- trailingComma + + PropertyAssignment = get PropertyName "(" ")" "{" FunctionBody "}" -- getter + | set PropertyName "(" FormalParameter ")" "{" FunctionBody "}" -- setter + | PropertyName ":" AssignmentExpression -- simple + + PropertyName = identifierName + | stringLiteral + | numericLiteral + + MemberExpression = MemberExpression "[" Expression "]" -- arrayRefExp + | MemberExpression "." identifierName -- propRefExp + | new MemberExpression Arguments -- newExp + | FunctionExpression + | PrimaryExpression + + NewExpression = MemberExpression + | new NewExpression -- newExp + + CallExpression = CallExpression "[" Expression "]" -- arrayRefExp + | CallExpression "." identifierName -- propRefExp + | CallExpression Arguments -- callExpExp + | MemberExpression Arguments -- memberExpExp + + Arguments = "(" ListOf, ","> ")" + + LeftHandSideExpression = CallExpression + | NewExpression + + PostfixExpression = LeftHandSideExpression #(spacesNoNL "++") -- postIncrement + | LeftHandSideExpression #(spacesNoNL "--") -- postDecrement + | LeftHandSideExpression + + UnaryExpression = delete UnaryExpression -- deleteExp + | void UnaryExpression -- voidExp + | typeof UnaryExpression -- typeofExp + | "++" UnaryExpression -- preIncrement + | "--" UnaryExpression -- preDecrement + | "+" UnaryExpression -- unaryPlus + | "-" UnaryExpression -- unaryMinus + | "~" UnaryExpression -- bnot + | "!" UnaryExpression -- lnot + | PostfixExpression + + MultiplicativeExpression = MultiplicativeExpression "*" UnaryExpression -- mul + | MultiplicativeExpression "/" UnaryExpression -- div + | MultiplicativeExpression "%" UnaryExpression -- mod + | UnaryExpression + + AdditiveExpression = AdditiveExpression "+" MultiplicativeExpression -- add + | AdditiveExpression "-" MultiplicativeExpression -- sub + | MultiplicativeExpression + + ShiftExpression = ShiftExpression "<<" AdditiveExpression -- lsl + | ShiftExpression ">>>" AdditiveExpression -- lsr + | ShiftExpression ">>" AdditiveExpression -- asr + | AdditiveExpression + + RelationalExpression + = RelationalExpression "<" ShiftExpression -- lt + | RelationalExpression ">" ShiftExpression -- gt + | RelationalExpression "<=" ShiftExpression -- le + | RelationalExpression ">=" ShiftExpression -- ge + | RelationalExpression "instanceof" ShiftExpression -- instanceOfExp + | RelationalExpression guardIn "in" ShiftExpression -- inExp + | ShiftExpression + + EqualityExpression + = EqualityExpression "==" RelationalExpression -- equal + | EqualityExpression "!=" RelationalExpression -- notEqual + | EqualityExpression "===" RelationalExpression -- eq + | EqualityExpression "!==" RelationalExpression -- notEq + | RelationalExpression + + BitwiseANDExpression + = BitwiseANDExpression "&" EqualityExpression -- band + | EqualityExpression + + BitwiseXORExpression + = BitwiseXORExpression "^" BitwiseANDExpression -- bxor + | BitwiseANDExpression + + BitwiseORExpression + = BitwiseORExpression "|" BitwiseXORExpression -- bor + | BitwiseXORExpression + + LogicalANDExpression + = LogicalANDExpression "&&" BitwiseORExpression -- land + | BitwiseORExpression + + LogicalORExpression + = LogicalORExpression "||" LogicalANDExpression -- lor + | LogicalANDExpression + + ConditionalExpression + = LogicalORExpression "?" AssignmentExpression ":" AssignmentExpression -- conditional + | LogicalORExpression + + AssignmentExpression + = LeftHandSideExpression AssignmentOperator AssignmentExpression -- assignment + | ConditionalExpression + + Expression (an expression) + = Expression "," AssignmentExpression -- commaExp + | AssignmentExpression + + AssignmentOperator = "=" | ">>>=" | "<<=" | ">>=" + | "*=" | "/=" | "%=" | "+=" | "-=" | "&=" | "^=" | "|=" + + // §A.4 Statements -- http://ecma-international.org/ecma-262/5.1/#sec-A.4 + + Statement (a statement) + = Block + | VariableStatement + | EmptyStatement + | ExpressionStatement + | IfStatement + | IterationStatement + | ContinueStatement + | BreakStatement + | ReturnStatement + | WithStatement + | LabelledStatement + | SwitchStatement + | ThrowStatement + | TryStatement + | DebuggerStatement + + Block = "{" StatementList "}" + + StatementList = Statement* + + VariableStatement = var VariableDeclarationList #(sc) + + VariableDeclarationList = NonemptyListOf, ","> + + VariableDeclaration = identifier Initialiser? + + Initialiser = "=" AssignmentExpression + + EmptyStatement = ";" // note: this semicolon eats newlines + + ExpressionStatement = ~("{" | function) Expression #(sc) + + IfStatement = if "(" Expression ")" Statement (else Statement)? + + IterationStatement = do Statement while "(" Expression ")" #(sc) -- doWhile + | while "(" Expression ")" Statement -- whileDo + | for "(" Expression? ";" + Expression? ";" + Expression? ")" Statement -- for3 + | for "(" var VariableDeclarationList ";" + Expression? ";" + Expression? ")" Statement -- for3var + | for "(" LeftHandSideExpression in + Expression ")" Statement -- forIn + | for "(" var VariableDeclaration in + Expression ")" Statement -- forInVar + + ContinueStatement = continue #((spacesNoNL identifier)? sc) + + BreakStatement = break #((spacesNoNL identifier)? sc) + + ReturnStatement = return (#(spacesNoNL ~space) Expression)? #(sc) + + WithStatement = with "(" Expression ")" Statement + + SwitchStatement = switch "(" Expression ")" CaseBlock + + CaseBlock = "{" CaseClause* DefaultClause CaseClause* "}" -- withDefault + | "{" CaseClause* "}" -- withoutDefault + + CaseClause = case Expression ":" Statement* + + DefaultClause = default ":" Statement* + + LabelledStatement = identifier ":" Statement + + ThrowStatement = throw Expression #(sc) -- throwExpr + + TryStatement = try Block Catch Finally -- tryCatchFinally + | try Block Finally -- tryFinally + | try Block Catch -- tryCatch + + Catch = catch "(" FormalParameter ")" Block + + Finally = finally Block + + DebuggerStatement = #(debugger sc) + + // §A.5 Functions and Programs -- http://ecma-international.org/ecma-262/5.1/#sec-A.5 + + FunctionDeclaration + = function identifier "(" FormalParameterList ")" "{" FunctionBody "}" + + FunctionExpression + = function identifier "(" FormalParameterList ")" "{" FunctionBody "}" -- named + | function "(" FormalParameterList ")" "{" FunctionBody "}" -- anonymous + + FormalParameterList = ListOf + + FormalParameter = identifier + + FunctionBody = Directive* SourceElement* + + SourceElement = Declaration | Statement + + // Broken out so es6 can override to include ConstDecl and LetDecl + Declaration = FunctionDeclaration + + Directive = stringLiteral #(sc) +} + +ES5Lax <: ES5 { + futureReservedWord := futureReservedWordLax +} diff --git a/js/compiler/inbrowser.js b/js/compiler/inbrowser.js new file mode 100644 index 0000000..751fc4c --- /dev/null +++ b/js/compiler/inbrowser.js @@ -0,0 +1,38 @@ +'use strict'; + +var compiler = require('./compiler.js'); + +function getUrlContent(url) { + var req = new XMLHttpRequest(); + req.open('GET', url, false); + try { + req.send(); + if (req.status === 0 || req.status === 200) { + return req.responseText; + } + } catch (e) { + console.error("Error while loading " + url, e); + } + return false; +} + +function translateSyndicateScripts() { + var scriptNodes = document.querySelectorAll('script[type="text/syndicate-js"]'); + var allSources = []; + for (var i = 0; i < scriptNodes.length; i++) { + var n = scriptNodes[i]; + var srcUrl = n.getAttribute('src'); + allSources.push(srcUrl ? getUrlContent(srcUrl) : n.innerHTML); + } + var allSourceText = allSources.join('\n;\n'); + + var output = compiler.compileSyndicateSource(allSourceText); + var f = new Function(output); + f(); +} + +document.addEventListener('DOMContentLoaded', translateSyndicateScripts, false); + +//--------------------------------------------------------------------------- + +module.exports = compiler; diff --git a/js/compiler/syndicate.ohm b/js/compiler/syndicate.ohm new file mode 100644 index 0000000..f6e1106 --- /dev/null +++ b/js/compiler/syndicate.ohm @@ -0,0 +1,87 @@ +// -*- javascript -*- +// Syntactic extensions to ES5 for Syndicate/js. See compiler.js for +// the rest of the translator. + +Syndicate <: ES5 { + //--------------------------------------------------------------------------- + // Extensions to expressions. + + Statement + += ActorStatement + | NetworkStatement + | ActorFacetStatement + | AssertionTypeDeclarationStatement + | SendMessageStatement + + ActorStatement + = actor CallExpression Block -- withConstructor + | actor Block -- noConstructor + + NetworkStatement + = ground network identifier? Block -- ground + | network Block -- normal + + ActorFacetStatement + = state FacetBlock until FacetStateTransitionBlock -- state + | until FacetStateTransitionBlock -- until + | forever FacetBlock -- forever + + AssertionTypeDeclarationStatement + = assertion type identifier "(" FormalParameterList ")" ("=" stringLiteral)? #(sc) + + SendMessageStatement = "::" Expression #(sc) + + //--------------------------------------------------------------------------- + // Ongoing event handlers. + + FacetBlock = "{" FacetInitBlock? FacetSituation* FacetDoneBlock? "}" + FacetStateTransitionBlock = "{" FacetStateTransition* "}" + + FacetInitBlock = init Block + FacetDoneBlock = done Block + + FacetSituation + = assert FacetPattern #(sc) -- assert + | on FacetEventPattern Block -- event + | during FacetPattern FacetBlock -- during + + FacetEventPattern + = message FacetPattern -- messageEvent + | asserted FacetPattern -- assertedEvent + | retracted FacetPattern -- retractedEvent + + FacetTransitionEventPattern + = FacetEventPattern -- facetEvent + | "(" Expression ")" -- risingEdge + + FacetStateTransition + = case FacetTransitionEventPattern Block -- withContinuation + | case FacetTransitionEventPattern #(sc) -- noContinuation + + FacetPattern + = LeftHandSideExpression metalevel decimalIntegerLiteral -- withMetalevel + | LeftHandSideExpression -- noMetalevel + + //--------------------------------------------------------------------------- + // Keywords. We don't add them to the "keyword" production because + // we don't want to make them unavailable to programs as + // identifiers. + + actor = "actor" ~identifierPart + assert = "assert" ~identifierPart + asserted = "asserted" ~identifierPart + assertion = "assertion" ~identifierPart + done = "done" ~identifierPart + during = "during" ~identifierPart + forever = "forever" ~identifierPart + ground = "ground" ~identifierPart + init = "init" ~identifierPart + message = "message" ~identifierPart + metalevel = "metalevel" ~identifierPart + network = "network" ~identifierPart + on = "on" ~identifierPart + retracted = "retracted" ~identifierPart + state = "state" ~identifierPart + type = "type" ~identifierPart + until = "until" ~identifierPart +} diff --git a/js/dist/README.md b/js/dist/README.md new file mode 100644 index 0000000..b1239b6 --- /dev/null +++ b/js/dist/README.md @@ -0,0 +1 @@ +Directory for build products, checked in to the repo for ease-of-use. diff --git a/js/examples/button/index.expanded.js b/js/examples/button/index.expanded.js new file mode 100644 index 0000000..1d21de6 --- /dev/null +++ b/js/examples/button/index.expanded.js @@ -0,0 +1,45 @@ +"use strict"; +var DOM = (function() { + var $SyndicateMeta$ = { + label: "DOM", + arguments: ["containerSelector","fragmentClass","spec"] + }; + return function DOM(containerSelector, fragmentClass, spec) { + return { + "containerSelector": containerSelector, + "fragmentClass": fragmentClass, + "spec": spec, + "$SyndicateMeta$": $SyndicateMeta$ + }; + }; +})(); +var jQuery = (function() { + var $SyndicateMeta$ = { + label: "jQuery", + arguments: ["selector","eventType","event"] + }; + return function jQuery(selector, eventType, event) { + return { + "selector": selector, + "eventType": eventType, + "event": event, + "$SyndicateMeta$": $SyndicateMeta$ + }; + }; +})(); + +$(document).ready(function() { + new Syndicate.Ground(function () { + Syndicate.DOM.spawnDOMDriver(); + Syndicate.JQuery.spawnJQueryDriver(); + + Syndicate.Actor.spawnActor(new Object(), function() { + this.counter = 0; + Syndicate.Actor.createFacet() +.addAssertion((function() { var _ = Syndicate.__; return Syndicate.Patch.assert(DOM('#button-label','',Syndicate.seal(this.counter)), 0); })) +.onEvent(false, "message", (function() { var _ = Syndicate.__; return Syndicate.Patch.sub(jQuery('#counter','click',_), 0); }), (function() { var _ = Syndicate.__; return { assertion: jQuery('#counter','click',_), metalevel: 0 }; }), (function() { + this.counter++; + })).completeBuild(); + }); + }).startStepping(); +}); diff --git a/js/examples/button/index.html b/js/examples/button/index.html new file mode 100644 index 0000000..184d544 --- /dev/null +++ b/js/examples/button/index.html @@ -0,0 +1,22 @@ + + + + Syndicate: Button Example + + + + + + + + +

Button Example

+ +

+ Source code: index.js +

+

+ Expanded source code: index.expanded.js +

+ + diff --git a/js/examples/button/index.js b/js/examples/button/index.js new file mode 100644 index 0000000..b4bda3c --- /dev/null +++ b/js/examples/button/index.js @@ -0,0 +1,19 @@ +assertion type DOM(containerSelector, fragmentClass, spec); +assertion type jQuery(selector, eventType, event); + +$(document).ready(function() { + ground network { + Syndicate.DOM.spawnDOMDriver(); + Syndicate.JQuery.spawnJQueryDriver(); + + actor { + this.counter = 0; + forever { + assert DOM('#button-label', '', Syndicate.seal(this.counter)); + on message jQuery('#counter', 'click', _) { + this.counter++; + } + } + } + } +}); diff --git a/js/examples/dom/index.html b/js/examples/dom/index.html new file mode 100644 index 0000000..3b90a38 --- /dev/null +++ b/js/examples/dom/index.html @@ -0,0 +1,16 @@ + + + + Syndicate: DOM Example + + + + + + +

DOM example

+
+
+

+  
+
diff --git a/js/examples/dom/index.js b/js/examples/dom/index.js
new file mode 100644
index 0000000..de51c5f
--- /dev/null
+++ b/js/examples/dom/index.js
@@ -0,0 +1,56 @@
+var G;
+$(document).ready(function () {
+  var Network = Syndicate.Network;
+  var sub = Syndicate.sub;
+  var assert = Syndicate.assert;
+  var retract = Syndicate.retract;
+  var seal = Syndicate.seal;
+  var __ = Syndicate.__;
+  var _$ = Syndicate._$;
+
+  G = new Syndicate.Ground(function () {
+    console.log('starting ground boot');
+
+    Syndicate.DOM.spawnDOMDriver();
+
+    Network.spawn({
+      boot: function () {
+	return assert(["DOM", "#clicker-holder", "clicker",
+		       seal(["button", ["span", [["style", "font-style: italic"]], "Click me!"]])])
+	  .andThen(sub(["jQuery", "button.clicker", "click", __]));
+      },
+      handleEvent: function (e) {
+	if (e.type === "message" && e.message[0] === "jQuery") {
+	  Network.send("bump_count");
+	}
+      }
+    });
+
+    Network.spawn({
+      counter: 0,
+      boot: function () {
+	this.updateState();
+	return sub("bump_count");
+      },
+      updateState: function () {
+	Network.stateChange(retract(["DOM", __, __, __])
+			    .andThen(assert(["DOM", "#counter-holder", "counter",
+					     seal(["div",
+						   ["p", "The current count is: ",
+						    this.counter]])])));
+      },
+      handleEvent: function (e) {
+	if (e.type === "message" && e.message === "bump_count") {
+	  this.counter++;
+	  this.updateState();
+	}
+      }
+    });
+  });
+
+  G.network.onStateChange = function (mux, patch) {
+    $("#spy-holder").text(Syndicate.prettyTrie(mux.routingTable));
+  };
+
+  G.startStepping();
+});
diff --git a/js/examples/jquery/index.html b/js/examples/jquery/index.html
new file mode 100644
index 0000000..d5bea5b
--- /dev/null
+++ b/js/examples/jquery/index.html
@@ -0,0 +1,16 @@
+
+
+  
+    Syndicate: jQuery Example
+    
+    
+    
+    
+  
+  
+    

jQuery example

+ +
0
+

+  
+
diff --git a/js/examples/jquery/index.js b/js/examples/jquery/index.js
new file mode 100644
index 0000000..1377c27
--- /dev/null
+++ b/js/examples/jquery/index.js
@@ -0,0 +1,31 @@
+"use strict";
+
+var G;
+$(document).ready(function () {
+    var Network = Syndicate.Network;
+    var sub = Syndicate.sub;
+    var __ = Syndicate.__;
+    var _$ = Syndicate._$;
+
+    G = new Syndicate.Ground(function () {
+      console.log('starting ground boot');
+
+      Syndicate.JQuery.spawnJQueryDriver();
+
+      Network.spawn({
+	boot: function () {
+	  return sub(['jQuery', '#clicker', 'click', __]);
+	},
+	handleEvent: function (e) {
+	  if (e.type === 'message' && e.message[0] === 'jQuery' && e.message[1] === '#clicker') {
+	    var r = $('#result');
+	    r.html(Number(r.html()) + 1);
+	  }
+	}
+      });
+    });
+    G.network.onStateChange = function (mux, patch) {
+      $("#spy-holder").text(Syndicate.prettyTrie(mux.routingTable));
+    };
+    G.startStepping();
+});
diff --git a/js/examples/smoketest-dsl/index.html b/js/examples/smoketest-dsl/index.html
new file mode 100644
index 0000000..b8208b7
--- /dev/null
+++ b/js/examples/smoketest-dsl/index.html
@@ -0,0 +1,14 @@
+
+
+  
+    Syndicate: Smoketest with DSL
+    
+    
+    
+    
+    
+  
+  
+    

Smoketest

+ + diff --git a/js/examples/smoketest-dsl/index.js b/js/examples/smoketest-dsl/index.js new file mode 100644 index 0000000..3f16981 --- /dev/null +++ b/js/examples/smoketest-dsl/index.js @@ -0,0 +1,31 @@ +assertion type beep(counter); + +ground network { + console.log('starting ground boot'); + + actor { + until { + case asserted Syndicate.observe(beep(_)) { + var counter = 0; + state { + init { + :: beep(counter++); + } + on message beep(_) { + :: beep(counter++); + } + } until { + case (counter >= 10); + } + } + } + } + + actor { + forever { + on message $m { + console.log("Got message:", m); + } + } + } +} diff --git a/js/examples/smoketest/index.html b/js/examples/smoketest/index.html new file mode 100644 index 0000000..f5a020e --- /dev/null +++ b/js/examples/smoketest/index.html @@ -0,0 +1,13 @@ + + + + Syndicate: Smoketest + + + + + + +

Smoketest

+ + diff --git a/js/examples/smoketest/index.js b/js/examples/smoketest/index.js new file mode 100644 index 0000000..f4f90e4 --- /dev/null +++ b/js/examples/smoketest/index.js @@ -0,0 +1,33 @@ +"use strict"; + +var G; +$(document).ready(function () { + var Network = Syndicate.Network; + var sub = Syndicate.sub; + var __ = Syndicate.__; + var _$ = Syndicate._$; + + G = new Syndicate.Ground(function () { + console.log('starting ground boot'); + + Network.spawn({ + counter: 0, + boot: function () {}, + handleEvent: function (e) {}, + step: function () { + Network.send(["beep", this.counter++]); + return this.counter <= 10; + } + }); + + Network.spawn({ + boot: function () { return sub(["beep", __]); }, + handleEvent: function (e) { + if (e.type === 'message') { + console.log("beep!", e.message[1]); + } + } + }); + }); + G.startStepping(); +}); diff --git a/js/examples/textfield-dsl/index.html b/js/examples/textfield-dsl/index.html new file mode 100644 index 0000000..9684bd4 --- /dev/null +++ b/js/examples/textfield-dsl/index.html @@ -0,0 +1,23 @@ + + + + Syndicate: Textfield Example (DSL variation) + + + + + + + + +

Textfield Example (DSL variation)

+

+ After Hesam + Samimi's paper. +

+

Field contents:

+

Search

+ +

+  
+
diff --git a/js/examples/textfield-dsl/index.js b/js/examples/textfield-dsl/index.js
new file mode 100644
index 0000000..5f33832
--- /dev/null
+++ b/js/examples/textfield-dsl/index.js
@@ -0,0 +1,169 @@
+///////////////////////////////////////////////////////////////////////////
+// GUI
+
+assertion type jQuery(selector, eventType, event);
+assertion type fieldCommand(detail);
+assertion type fieldContents(text, pos);
+assertion type highlight(state);
+
+function escapeText(text) {
+  text = text.replace(/&/g, '&');
+  text = text.replace(//g, '>');
+  text = text.replace(/ /g, ' ');
+  return text;
+}
+
+function piece(text, pos, lo, hi, cls) {
+  return ""+
+    ((pos >= lo && pos < hi)
+     ? (escapeText(text.substring(lo, pos)) +
+	"" +
+	escapeText(text.substring(pos, hi)))
+     : escapeText(text.substring(lo, hi)))
+    + "";
+}
+
+function spawnGui() {
+  actor {
+    this.text = '';
+    this.pos = 0;
+    this.highlightState = false;
+
+    this.updateDisplay = function () {
+      var text = this.text;
+      var pos = this.pos;
+      var highlight = this.highlightState;
+      var hLeft = highlight ? highlight.get(0) : 0;
+      var hRight = highlight ? highlight.get(1) : 0;
+      $("#fieldContents")[0].innerHTML = highlight
+	? piece(text, pos, 0, hLeft, "normal") +
+	piece(text, pos, hLeft, hRight, "highlight") +
+	piece(text, pos, hRight, text.length + 1, "normal")
+	: piece(text, pos, 0, text.length + 1, "normal");
+    };
+
+    forever {
+      on message jQuery("#inputRow", "+keypress", $event) {
+        var keycode = event.keyCode;
+        var character = String.fromCharCode(event.charCode);
+        if (keycode === 37 /* left */) {
+          :: fieldCommand("cursorLeft");
+        } else if (keycode === 39 /* right */) {
+	  :: fieldCommand("cursorRight");
+        } else if (keycode === 9 /* tab */) {
+	  // ignore
+        } else if (keycode === 8 /* backspace */) {
+	  :: fieldCommand("backspace");
+        } else if (character) {
+	  :: fieldCommand(["insert", character]);
+        }
+      }
+
+      on asserted fieldContents($text, $pos) {
+        this.text = text;
+        this.pos = pos;
+        this.updateDisplay();
+      }
+
+      on asserted highlight($state) {
+        this.highlightState = state;
+        this.updateDisplay();
+      }
+    }
+  }
+}
+
+///////////////////////////////////////////////////////////////////////////
+// Textfield Model
+
+function spawnModel() {
+  actor {
+    this.fieldContents = "initial";
+    this.cursorPos = this.fieldContents.length; /* positions address gaps between characters */
+
+    forever {
+      assert fieldContents(this.fieldContents, this.cursorPos);
+
+      on message fieldCommand("cursorLeft") {
+	this.cursorPos--;
+	if (this.cursorPos < 0)
+	  this.cursorPos = 0;
+      }
+
+      on message fieldCommand("cursorRight") {
+	this.cursorPos++;
+	if (this.cursorPos > this.fieldContents.length)
+	  this.cursorPos = this.fieldContents.length;
+      }
+
+      on message fieldCommand("backspace") {
+        if (this.cursorPos > 0) {
+	  this.fieldContents =
+	    this.fieldContents.substring(0, this.cursorPos - 1) +
+	    this.fieldContents.substring(this.cursorPos);
+	  this.cursorPos--;
+        }
+      }
+
+      on message fieldCommand(["insert", $newText]) {
+	this.fieldContents =
+	  this.fieldContents.substring(0, this.cursorPos) +
+	  newText +
+	  this.fieldContents.substring(this.cursorPos);
+	this.cursorPos += newText.length;
+      }
+    }
+  }
+}
+
+///////////////////////////////////////////////////////////////////////////
+// Search engine
+
+function spawnSearch() {
+  actor {
+    this.fieldContents = "";
+    this.highlight = false;
+
+    this.search = function () {
+      var searchtext = $("#searchBox")[0].value;
+      if (searchtext) {
+	var pos = this.fieldContents.indexOf(searchtext);
+	this.highlight = (pos !== -1) && [pos, pos + searchtext.length];
+      } else {
+	this.highlight = false;
+      }
+    };
+
+    forever {
+      assert highlight(this.highlight);
+
+      on message jQuery("#searchBox", "input", $event) {
+        this.search();
+      }
+
+      on asserted fieldContents($text, _) {
+        this.fieldContents = text;
+        this.search();
+      }
+    }
+  }
+}
+
+///////////////////////////////////////////////////////////////////////////
+// Main
+
+$(document).ready(function () {
+  ground network G {
+    Syndicate.JQuery.spawnJQueryDriver();
+    Syndicate.DOM.spawnDOMDriver();
+
+    spawnGui();
+    spawnModel();
+    spawnSearch();
+  }
+
+  G.network.onStateChange = function (mux, patch) {
+    $("#spy-holder").text(Syndicate.prettyTrie(mux.routingTable));
+  };
+});
diff --git a/js/examples/textfield-dsl/style.css b/js/examples/textfield-dsl/style.css
new file mode 100644
index 0000000..d612510
--- /dev/null
+++ b/js/examples/textfield-dsl/style.css
@@ -0,0 +1,12 @@
+#fieldContents {
+    font-family: monospace;
+}
+
+.cursor {
+    border-left: solid red 1px;
+    border-right: solid red 1px;
+}
+
+.highlight {
+    background-color: yellow;
+}
diff --git a/js/examples/textfield/index.html b/js/examples/textfield/index.html
new file mode 100644
index 0000000..42c64ad
--- /dev/null
+++ b/js/examples/textfield/index.html
@@ -0,0 +1,22 @@
+
+
+  
+    Syndicate: Textfield Example
+    
+    
+    
+    
+    
+  
+  
+    

Textfield Example

+

+ After Hesam + Samimi's paper. +

+

Field contents:

+

Search

+ +

+  
+
diff --git a/js/examples/textfield/index.js b/js/examples/textfield/index.js
new file mode 100644
index 0000000..ab8991f
--- /dev/null
+++ b/js/examples/textfield/index.js
@@ -0,0 +1,206 @@
+///////////////////////////////////////////////////////////////////////////
+// GUI
+
+var Network = Syndicate.Network;
+var Route = Syndicate.Route;
+var Patch = Syndicate.Patch;
+var __ = Syndicate.__;
+var _$ = Syndicate._$;
+
+function escapeText(text) {
+  text = text.replace(/&/g, '&');
+  text = text.replace(//g, '>');
+  text = text.replace(/ /g, ' ');
+  return text;
+}
+
+function piece(text, pos, lo, hi, cls) {
+  return ""+
+    ((pos >= lo && pos < hi)
+     ? (escapeText(text.substring(lo, pos)) +
+	"" +
+	escapeText(text.substring(pos, hi)))
+     : escapeText(text.substring(lo, hi)))
+    + "";
+}
+
+function spawnGui() {
+  Network.spawn({
+    field: { text: '', pos: 0 },
+    highlight: { state: false },
+
+    boot: function () {
+      return Patch.sub(["jQuery", "#inputRow", "+keypress", __])
+	.andThen(Patch.sub(["fieldContents", __, __]))
+	.andThen(Patch.sub(["highlight", __]));
+    },
+
+    fieldContentsProjection: Route.compileProjection(["fieldContents", _$("text"), _$("pos")]),
+    highlightProjection: Route.compileProjection(["highlight", _$("state")]),
+    handleEvent: function (e) {
+      var self = this;
+      switch (e.type) {
+      case "message":
+	var event = e.message[3];
+	var keycode = event.keyCode;
+	var character = String.fromCharCode(event.charCode);
+	if (keycode === 37 /* left */) {
+	  Network.send(["fieldCommand", "cursorLeft"]);
+	} else if (keycode === 39 /* right */) {
+	  Network.send(["fieldCommand", "cursorRight"]);
+	} else if (keycode === 9 /* tab */) {
+	  // ignore
+	} else if (keycode === 8 /* backspace */) {
+	  Network.send(["fieldCommand", "backspace"]);
+	} else if (character) {
+	  Network.send(["fieldCommand", ["insert", character]]);
+	}
+	break;
+      case "stateChange":
+	Route.projectObjects(e.patch.added, this.fieldContentsProjection).forEach(function (c) {
+	  self.field = c;
+	});
+	Route.projectObjects(e.patch.added, this.highlightProjection).forEach(function (c) {
+	  self.highlight = c;
+	});
+	this.updateDisplay();
+	break;
+      }
+    },
+
+    updateDisplay: function () {
+      var text = this.field ? this.field.text : "";
+      var pos = this.field ? this.field.pos : 0;
+      var highlight = this.highlight ? this.highlight.state : false;
+      var hLeft = highlight ? highlight.get(0) : 0;
+      var hRight = highlight ? highlight.get(1) : 0;
+      $("#fieldContents")[0].innerHTML = highlight
+	? piece(text, pos, 0, hLeft, "normal") +
+	piece(text, pos, hLeft, hRight, "highlight") +
+	piece(text, pos, hRight, text.length + 1, "normal")
+	: piece(text, pos, 0, text.length + 1, "normal");
+    }
+  });
+}
+
+///////////////////////////////////////////////////////////////////////////
+// Textfield Model
+
+function spawnModel() {
+  var initialContents = "initial";
+  Network.spawn({
+    fieldContents: initialContents,
+    cursorPos: initialContents.length, /* positions address gaps between characters */
+
+    boot: function () {
+      this.publishState();
+      return Patch.sub(["fieldCommand", __]);
+    },
+
+    handleEvent: function (e) {
+      if (e.type === "message" && e.message[0] === "fieldCommand") {
+	var command = e.message[1];
+	if (command === "cursorLeft") {
+	  this.cursorPos--;
+	  if (this.cursorPos < 0)
+	    this.cursorPos = 0;
+	} else if (command === "cursorRight") {
+	  this.cursorPos++;
+	  if (this.cursorPos > this.fieldContents.length)
+	    this.cursorPos = this.fieldContents.length;
+	} else if (command === "backspace" && this.cursorPos > 0) {
+	  this.fieldContents =
+	    this.fieldContents.substring(0, this.cursorPos - 1) +
+	    this.fieldContents.substring(this.cursorPos);
+	  this.cursorPos--;
+	} else if (command.constructor === Array && command[0] === "insert") {
+	  var newText = command[1];
+	  this.fieldContents =
+	    this.fieldContents.substring(0, this.cursorPos) +
+	    newText +
+	    this.fieldContents.substring(this.cursorPos);
+	  this.cursorPos += newText.length;
+	}
+	this.publishState();
+      }
+    },
+
+    publishState: function () {
+      Network.stateChange(
+	Patch.retract(["fieldContents", __, __])
+	  .andThen(Patch.assert(["fieldContents", this.fieldContents, this.cursorPos])));
+    }
+  });
+}
+
+///////////////////////////////////////////////////////////////////////////
+// Search engine
+
+function spawnSearch() {
+  Network.spawn({
+    fieldContents: "",
+    highlight: false,
+
+    boot: function () {
+      this.publishState();
+      return Patch.sub(["jQuery", "#searchBox", "input", __])
+	.andThen(Patch.sub(["fieldContents", __, __]));
+    },
+
+    fieldContentsProjection: Route.compileProjection(["fieldContents", _$("text"), _$("pos")]),
+    handleEvent: function (e) {
+      var self = this;
+      if (e.type === "message" && e.message[0] === "jQuery") {
+	this.search();
+      }
+      if (e.type === "stateChange") {
+	Route.projectObjects(e.patch.added, this.fieldContentsProjection).forEach(function (c) {
+	  self.fieldContents = c.text;
+	});
+	this.search();
+      }
+    },
+
+    publishState: function () {
+      Network.stateChange(
+	Patch.retract(["highlight", __])
+	  .andThen(Patch.assert(["highlight", this.highlight])));
+    },
+
+    search: function () {
+      var searchtext = $("#searchBox")[0].value;
+      var oldHighlight = this.highlight;
+      if (searchtext) {
+	var pos = this.fieldContents.indexOf(searchtext);
+	this.highlight = (pos !== -1) && [pos, pos + searchtext.length];
+      } else {
+	this.highlight = false;
+      }
+      if (JSON.stringify(oldHighlight) !== JSON.stringify(this.highlight)) {
+	this.publishState();
+      }
+    }
+  });
+}
+
+///////////////////////////////////////////////////////////////////////////
+// Main
+
+var G;
+$(document).ready(function () {
+  G = new Syndicate.Ground(function () {
+    Syndicate.JQuery.spawnJQueryDriver();
+    Syndicate.DOM.spawnDOMDriver();
+
+    spawnGui();
+    spawnModel();
+    spawnSearch();
+  });
+
+  G.network.onStateChange = function (mux, patch) {
+    $("#spy-holder").text(Syndicate.prettyTrie(mux.routingTable));
+  };
+
+  G.startStepping();
+});
diff --git a/js/examples/textfield/style.css b/js/examples/textfield/style.css
new file mode 100644
index 0000000..d612510
--- /dev/null
+++ b/js/examples/textfield/style.css
@@ -0,0 +1,12 @@
+#fieldContents {
+    font-family: monospace;
+}
+
+.cursor {
+    border-left: solid red 1px;
+    border-right: solid red 1px;
+}
+
+.highlight {
+    background-color: yellow;
+}
diff --git a/js/package.json b/js/package.json
new file mode 100644
index 0000000..6f364f4
--- /dev/null
+++ b/js/package.json
@@ -0,0 +1,36 @@
+{
+  "name": "syndicate-js",
+  "version": "0.0.0",
+  "description": "Syndicate in the browser",
+  "homepage": "https://github.com/tonyg/syndicate",
+  "main": "src/main.js",
+  "repository": {
+    "type": "git",
+    "url": "git://github.com/tonyg/syndicate"
+  },
+  "directories": {
+    "bin": "./bin"
+  },
+  "scripts": {
+    "clean": "rm -f dist/*",
+    "build-debug": "browserify src/main.js -d -s Syndicate -o dist/syndicate.js",
+    "build-min": "browserify src/main.js -s Syndicate -o dist/_syndicate.js && uglifyjs dist/_syndicate.js -o dist/syndicate.min.js && rm dist/_syndicate.js",
+    "build-compiler-debug": "browserify -t brfs compiler/inbrowser.js -s SyndicateCompiler -o dist/syndicatecompiler.js",
+    "build-compiler": "browserify -t brfs compiler/inbrowser.js -s SyndicateCompiler -o dist/_syndicatecompiler.js && uglifyjs dist/_syndicatecompiler.js -o dist/syndicatecompiler.min.js && rm dist/_syndicatecompiler.js",
+    "build": "npm run build-debug && npm run build-compiler-debug && npm run build-min && npm run build-compiler",
+    "watch": "watchify src/main.js -d -s Syndicate -o dist/syndicate.js",
+    "test": "mocha",
+    "prepublish": "npm run build"
+  },
+  "author": "Tony Garnock-Jones ",
+  "devDependencies": {
+    "watchify": "^3.7.0",
+    "uglify-js": "^2.6.1",
+    "browserify": "^13.0.0",
+    "mocha": "^2.4.5",
+    "expect.js": "^0.3.1",
+    "immutable": "^3.7.6",
+    "brfs": "^1.4.3",
+    "ohm-js": "cdglabs/ohm"
+  }
+}
diff --git a/js/src/ack.js b/js/src/ack.js
new file mode 100644
index 0000000..45e22fe
--- /dev/null
+++ b/js/src/ack.js
@@ -0,0 +1,41 @@
+// Utility protocol for measuring when a stateChange takes effect.
+
+var RandomID = require('./randomid.js');
+var Network = require('./network.js').Network;
+var Route = require('./route.js');
+var Patch = require('./patch.js');
+
+var $Ack = new Route.$Special('ack');
+
+function Ack(metaLevel, id) {
+  this.metaLevel = metaLevel || 0;
+  this.id = id || RandomID.randomId(16);
+  this.done = false;
+}
+
+Ack.prototype.arm = function () {
+  Network.stateChange(Patch.sub([$Ack, this.id], this.metaLevel));
+  Network.send([$Ack, this.id], this.metaLevel);
+};
+
+Ack.prototype.disarm = function () {
+  Network.stateChange(Patch.unsub([$Ack, this.id], this.metaLevel));
+};
+
+Ack.prototype.check = function (e) {
+  if (!this.done) {
+    if (e.type === 'message') {
+      var m = Patch.stripAtMeta(e.message, this.metaLevel);
+      if (m && m[0] === $Ack && m[1] === this.id) {
+	this.disarm();
+	this.done = true;
+      }
+    }
+  }
+  return this.done;
+};
+
+///////////////////////////////////////////////////////////////////////////
+
+module.exports.$Ack = $Ack;
+module.exports.Ack = Ack;
diff --git a/js/src/actor.js b/js/src/actor.js
new file mode 100644
index 0000000..3fa192f
--- /dev/null
+++ b/js/src/actor.js
@@ -0,0 +1,187 @@
+'use strict';
+
+var Immutable = require('immutable');
+var Network = require('./network.js').Network;
+var Mux = require('./mux.js');
+var Patch = require('./patch.js');
+var Route = require('./route.js');
+var Util = require('./util.js');
+
+//---------------------------------------------------------------------------
+
+function spawnActor(state, bootFn) {
+  Network.spawn(new Actor(state, bootFn));
+}
+
+function Actor(state, bootFn) {
+  this.state = state;
+  this.facets = Immutable.Set();
+  this.mux = new Mux.Mux();
+
+  this.boot = function() {
+    bootFn.call(this.state);
+    this.checkForTermination();
+  };
+}
+
+Actor.prototype.handleEvent = function(e) {
+  this.facets.forEach(function (f) {
+    f.handleEvent(e);
+  });
+  this.checkForTermination();
+};
+
+Actor.prototype.addFacet = function(facet) {
+  this.facets = this.facets.add(facet);
+};
+
+Actor.prototype.removeFacet = function(facet) {
+  this.facets = this.facets.remove(facet);
+};
+
+Actor.prototype.checkForTermination = function() {
+  if (this.facets.isEmpty()) {
+    Network.exit();
+  }
+};
+
+//---------------------------------------------------------------------------
+
+function createFacet() {
+  return new Facet(Network.activeBehavior());
+}
+
+function Facet(actor) {
+  this.actor = actor;
+  this.endpoints = Immutable.Map();
+  this.initBlocks = Immutable.List();
+  this.doneBlocks = Immutable.List();
+}
+
+Facet.prototype.handleEvent = function(e) {
+  var facet = this;
+  this.endpoints.forEach(function(endpoint) {
+    endpoint.handlerFn.call(facet.actor.state, e);
+  });
+  this.refresh();
+};
+
+Facet.prototype.addAssertion = function(assertionFn) {
+  return this.addEndpoint(new Endpoint(assertionFn, function(e) {}));
+};
+
+Facet.prototype.onEvent = function(isTerminal, eventType, subscriptionFn, projectionFn, handlerFn) {
+  var facet = this;
+  switch (eventType) {
+
+  case 'message':
+    return this.addEndpoint(new Endpoint(subscriptionFn, function(e) {
+      if (e.type === 'message') {
+        var proj = projectionFn.call(facet.actor.state);
+        var spec = Patch.prependAtMeta(proj.assertion, proj.metalevel);
+        var match = Route.matchPattern(e.message, spec);
+        // console.log(match);
+        if (match) {
+          if (isTerminal) { facet.terminate(); }
+          Util.kwApply(handlerFn, facet.actor.state, match);
+        }
+      }
+    }));
+
+  case 'asserted': /* fall through */
+  case 'retracted':
+    return this.addEndpoint(new Endpoint(subscriptionFn, function(e) {
+      if (e.type === 'stateChange') {
+        var proj = projectionFn.call(facet.actor.state);
+        var spec = Patch.prependAtMeta(proj.assertion, proj.metalevel);
+        var compiledSpec = Route.compileProjection(spec);
+        var objects = Route.projectObjects(eventType === 'asserted'
+                                           ? e.patch.added
+                                           : e.patch.removed,
+                                           compiledSpec);
+        if (objects && objects.size > 0) {
+          // console.log(objects.toArray());
+          if (isTerminal) { facet.terminate(); }
+          objects.forEach(function (o) { Util.kwApply(handlerFn, facet.actor.state, o); });
+        }
+      }
+    }));
+
+  case 'risingEdge':
+    var endpoint = new Endpoint(function() { return Patch.emptyPatch; },
+                                function(e) {
+                                  var newValue = subscriptionFn.call(facet.actor.state);
+                                  if (newValue && !this.currentValue) {
+                                    if (isTerminal) { facet.terminate(); }
+                                    handlerFn.call(facet.actor.state);
+                                  }
+                                  this.currentValue = newValue;
+                                });
+    endpoint.currentValue = false;
+    return this.addEndpoint(endpoint);
+
+  default:
+    throw new Error("Unsupported Facet eventType: " + eventType);
+  }
+};
+
+Facet.prototype.addEndpoint = function(endpoint) {
+  var patch = endpoint.subscriptionFn.call(this.actor.state);
+  var r = this.actor.mux.addStream(patch);
+  this.endpoints = this.endpoints.set(r.pid, endpoint);
+  Network.stateChange(r.deltaAggregate);
+  return this; // for chaining
+};
+
+Facet.prototype.addInitBlock = function(thunk) {
+  this.initBlocks = this.initBlocks.push(thunk);
+  return this;
+};
+
+Facet.prototype.addDoneBlock = function(thunk) {
+  this.doneBlocks = this.doneBlocks.push(thunk);
+  return this;
+};
+
+Facet.prototype.refresh = function() {
+  var facet = this;
+  var aggregate = Patch.emptyPatch;
+  this.endpoints.forEach(function(endpoint, eid) {
+    var patch =
+        Patch.retract(Syndicate.__).andThen(endpoint.subscriptionFn.call(facet.actor.state));
+    var r = facet.actor.mux.updateStream(eid, patch);
+    aggregate = aggregate.andThen(r.deltaAggregate);
+  });
+  Network.stateChange(aggregate);
+};
+
+Facet.prototype.completeBuild = function() {
+  var facet = this;
+  this.actor.addFacet(this);
+  this.initBlocks.forEach(function(b) { b.call(facet.actor.state); });
+};
+
+Facet.prototype.terminate = function() {
+  var facet = this;
+  var aggregate = Patch.emptyPatch;
+  this.endpoints.forEach(function(endpoint, eid) {
+    var r = facet.actor.mux.removeStream(eid);
+    aggregate = aggregate.andThen(r.deltaAggregate);
+  });
+  Network.stateChange(aggregate);
+  this.endpoints = Immutable.Map();
+  this.actor.removeFacet(this);
+  this.doneBlocks.forEach(function(b) { b.call(facet.actor.state); });
+};
+
+//---------------------------------------------------------------------------
+
+function Endpoint(subscriptionFn, handlerFn) {
+  this.subscriptionFn = subscriptionFn;
+  this.handlerFn = handlerFn;
+}
+
+//---------------------------------------------------------------------------
+
+module.exports.spawnActor = spawnActor;
+module.exports.createFacet = createFacet;
diff --git a/js/src/demand-matcher.js b/js/src/demand-matcher.js
new file mode 100644
index 0000000..d5bba33
--- /dev/null
+++ b/js/src/demand-matcher.js
@@ -0,0 +1,79 @@
+var Immutable = require('immutable');
+var Route = require('./route.js');
+var Patch = require('./patch.js');
+var Util = require('./util.js');
+
+function DemandMatcher(demandSpec, supplySpec, options) {
+  options = Util.extend({
+    metaLevel: 0,
+    onDemandIncrease: function (captures) {
+      console.error("Syndicate: Unhandled increase in demand", captures);
+    },
+    onSupplyDecrease: function (captures) {
+      console.error("Syndicate: Unhandled decrease in supply", captures);
+    }
+  }, options);
+  this.metaLevel = options.metaLevel;
+  this.onDemandIncrease = options.onDemandIncrease;
+  this.onSupplyDecrease = options.onSupplyDecrease;
+  this.demandSpec = demandSpec;
+  this.supplySpec = supplySpec;
+  this.demandPattern = Route.projectionToPattern(demandSpec);
+  this.supplyPattern = Route.projectionToPattern(supplySpec);
+  this.demandProjection = Route.compileProjection(Patch.prependAtMeta(demandSpec, this.metaLevel));
+  this.supplyProjection = Route.compileProjection(Patch.prependAtMeta(supplySpec, this.metaLevel));
+  this.currentDemand = Immutable.Set();
+  this.currentSupply = Immutable.Set();
+}
+
+DemandMatcher.prototype.boot = function () {
+  return Patch.sub(this.demandPattern, this.metaLevel)
+    .andThen(Patch.sub(this.supplyPattern, this.metaLevel));
+};
+
+DemandMatcher.prototype.handleEvent = function (e) {
+  if (e.type === "stateChange") {
+    this.handlePatch(e.patch);
+  }
+};
+
+DemandMatcher.prototype.handlePatch = function (p) {
+  var self = this;
+
+  var addedDemand = Route.trieKeys(Route.project(p.added, self.demandProjection));
+  var removedDemand = Route.trieKeys(Route.project(p.removed, self.demandProjection));
+  var addedSupply = Route.trieKeys(Route.project(p.added, self.supplyProjection));
+  var removedSupply = Route.trieKeys(Route.project(p.removed, self.supplyProjection));
+
+  if (addedDemand === null) {
+    throw new Error("Syndicate: wildcard demand detected:\n" +
+		    self.demandSpec + "\n" +
+		    p.pretty());
+  }
+  if (addedSupply === null) {
+    throw new Error("Syndicate: wildcard supply detected:\n" +
+		    self.supplySpec + "\n" +
+		    p.pretty());
+  }
+
+  self.currentSupply = self.currentSupply.union(addedSupply);
+  self.currentDemand = self.currentDemand.subtract(removedDemand);
+
+  removedSupply.forEach(function (captures) {
+    if (self.currentDemand.has(captures)) {
+      self.onSupplyDecrease(Route.captureToObject(captures, self.supplyProjection));
+    }
+  });
+  addedDemand.forEach(function (captures) {
+    if (!self.currentSupply.has(captures)) {
+      self.onDemandIncrease(Route.captureToObject(captures, self.demandProjection));
+    }
+  });
+
+  self.currentSupply = self.currentSupply.subtract(removedSupply);
+  self.currentDemand = self.currentDemand.union(addedDemand);
+};
+
+///////////////////////////////////////////////////////////////////////////
+
+module.exports.DemandMatcher = DemandMatcher;
diff --git a/js/src/dom-driver.js b/js/src/dom-driver.js
new file mode 100644
index 0000000..11d63a4
--- /dev/null
+++ b/js/src/dom-driver.js
@@ -0,0 +1,141 @@
+// DOM fragment display driver
+var Patch = require("./patch.js");
+var DemandMatcher = require('./demand-matcher.js').DemandMatcher;
+var Ack = require('./ack.js').Ack;
+var Seal = require('./seal.js').Seal;
+
+var Network_ = require("./network.js");
+var Network = Network_.Network;
+var __ = Network_.__;
+var _$ = Network_._$;
+
+function spawnDOMDriver(domWrapFunction, jQueryWrapFunction) {
+  domWrapFunction = domWrapFunction || defaultWrapFunction;
+  var spec = domWrapFunction(_$('selector'), _$('fragmentClass'), _$('fragmentSpec'));
+  Network.spawn(
+    new DemandMatcher(spec,
+		      Patch.advertise(spec),
+		      {
+			onDemandIncrease: function (c) {
+			  Network.spawn(new DOMFragment(c.selector,
+							c.fragmentClass,
+							c.fragmentSpec,
+							domWrapFunction,
+							jQueryWrapFunction));
+			}
+		      }));
+}
+
+function defaultWrapFunction(selector, fragmentClass, fragmentSpec) {
+  return ["DOM", selector, fragmentClass, fragmentSpec];
+}
+
+function DOMFragment(selector, fragmentClass, fragmentSpec, domWrapFunction, jQueryWrapFunction) {
+  this.selector = selector;
+  this.fragmentClass = fragmentClass;
+  this.fragmentSpec = fragmentSpec;
+  this.domWrapFunction = domWrapFunction;
+  this.jQueryWrapFunction = jQueryWrapFunction;
+  this.demandExists = false;
+  this.subscriptionEstablished = new Ack();
+  this.nodes = this.buildNodes();
+}
+
+DOMFragment.prototype.boot = function () {
+  var self = this;
+  var specification = self.domWrapFunction(self.selector, self.fragmentClass, self.fragmentSpec);
+
+  Network.spawn(new Network(function () {
+    Syndicate.JQuery.spawnJQueryDriver(self.selector+" > ."+self.fragmentClass,
+				       1,
+				       self.jQueryWrapFunction);
+    Network.spawn({
+      demandExists: false,
+      subscriptionEstablished: new Ack(1),
+      boot: function () {
+	this.subscriptionEstablished.arm();
+	return Patch.sub(Patch.advertise(specification), 1);
+      },
+      handleEvent: function (e) {
+	this.subscriptionEstablished.check(e);
+	if (e.type === "stateChange") {
+	  if (e.patch.hasAdded()) this.demandExists = true;
+	  if (e.patch.hasRemoved()) this.demandExists = false;
+	}
+	if (this.subscriptionEstablished.done && !this.demandExists) {
+	  Network.exitNetwork();
+	}
+      }
+    });
+  }));
+
+  this.subscriptionEstablished.arm();
+  return Patch.sub(specification).andThen(Patch.pub(specification));
+};
+
+DOMFragment.prototype.handleEvent = function (e) {
+  this.subscriptionEstablished.check(e);
+  if (e.type === "stateChange") {
+    if (e.patch.hasAdded()) this.demandExists = true;
+    if (e.patch.hasRemoved()) this.demandExists = false;
+  }
+  if (this.subscriptionEstablished.done && !this.demandExists) {
+    for (var i = 0; i < this.nodes.length; i++) {
+      var n = this.nodes[i];
+      n.parentNode.removeChild(n);
+    }
+    Network.exit();
+  }
+};
+
+///////////////////////////////////////////////////////////////////////////
+
+function isAttributes(x) {
+  return Array.isArray(x) && ((x.length === 0) || Array.isArray(x[0]));
+}
+
+DOMFragment.prototype.interpretSpec = function (spec) {
+  // Fragment specs are roughly JSON-equivalents of SXML.
+  // spec ::== ["tag", [["attr", "value"], ...], spec, spec, ...]
+  //         | ["tag", spec, spec, ...]
+  //         | "cdata"
+  if (typeof(spec) === "string" || typeof(spec) === "number") {
+    return document.createTextNode(spec);
+  } else if ($.isArray(spec)) {
+    var tagName = spec[0];
+    var hasAttrs = isAttributes(spec[1]);
+    var attrs = hasAttrs ? spec[1] : {};
+    var kidIndex = hasAttrs ? 2 : 1;
+
+    // Wow! Such XSS! Many hacks! So vulnerability! Amaze!
+    var n = document.createElement(tagName);
+    for (var i = 0; i < attrs.length; i++) {
+      n.setAttribute(attrs[i][0], attrs[i][1]);
+    }
+    for (var i = kidIndex; i < spec.length; i++) {
+      n.appendChild(this.interpretSpec(spec[i]));
+    }
+    return n;
+  } else {
+    throw new Error("Ill-formed DOM specification");
+  }
+};
+
+DOMFragment.prototype.buildNodes = function () {
+  var self = this;
+  var nodes = [];
+  $(self.selector).each(function (index, domNode) {
+    var n = self.interpretSpec(self.fragmentSpec.sealContents);
+    if ('classList' in n) {
+      n.classList.add(self.fragmentClass);
+    }
+    domNode.appendChild(n);
+    nodes.push(n);
+  });
+  return nodes;
+};
+
+///////////////////////////////////////////////////////////////////////////
+
+module.exports.spawnDOMDriver = spawnDOMDriver;
+module.exports.defaultWrapFunction = defaultWrapFunction;
diff --git a/js/src/ground.js b/js/src/ground.js
new file mode 100644
index 0000000..a6ea0a9
--- /dev/null
+++ b/js/src/ground.js
@@ -0,0 +1,79 @@
+"use strict";
+
+var Immutable = require('immutable');
+var Network = require('./network.js').Network;
+
+function Ground(bootFn) {
+  var self = this;
+  this.stepperId = null;
+  this.baseStack = Immutable.List.of({ network: this, activePid: -1 });
+  Network.withNetworkStack(this.baseStack, function () {
+    self.network = new Network(bootFn);
+  });
+}
+
+Ground.prototype.step = function () {
+  var self = this;
+  return Network.withNetworkStack(this.baseStack, function () {
+    return self.network.step();
+  });
+};
+
+Ground.prototype.checkPid = function (pid) {
+  if (pid !== -1) console.error('Weird pid in Ground', pid);
+};
+
+Ground.prototype.markRunnable = function (pid) {
+  this.checkPid(pid);
+  this.startStepping();
+};
+
+Ground.prototype.startStepping = function () {
+  var self = this;
+  if (this.stepperId) return;
+  if (this.step()) {
+    this.stepperId = setTimeout(function () {
+      self.stepperId = null;
+      self.startStepping();
+    }, 0);
+  }
+  return this; // because the syndicatec compiler chains startStepping after the ctor
+};
+
+Ground.prototype.stopStepping = function () {
+  if (this.stepperId) {
+    clearTimeout(this.stepperId);
+    this.stepperId = null;
+  }
+};
+
+Ground.prototype.kill = function (pid, exn) {
+  this.checkPid(pid);
+  console.log("Ground network terminated");
+  this.stopStepping();
+};
+
+Ground.prototype.enqueueAction = function (pid, action) {
+  this.checkPid(pid);
+
+  switch (action.type) {
+  case 'stateChange':
+    if (action.patch.isNonEmpty()) {
+      console.error('You have subscribed to a nonexistent event source.',
+		    action.patch.pretty());
+    }
+    break;
+
+  case 'message':
+    console.error('You have sent a message into the outer void.', action);
+    break;
+
+  default:
+    console.error('Internal error: unexpected action at ground level', action);
+    break;
+  }
+};
+
+///////////////////////////////////////////////////////////////////////////
+
+module.exports.Ground = Ground;
diff --git a/js/src/jquery-driver.js b/js/src/jquery-driver.js
new file mode 100644
index 0000000..c3c77a5
--- /dev/null
+++ b/js/src/jquery-driver.js
@@ -0,0 +1,91 @@
+// JQuery event driver
+var Patch = require("./patch.js");
+var DemandMatcher = require('./demand-matcher.js').DemandMatcher;
+
+var Network_ = require("./network.js");
+var Network = Network_.Network;
+var __ = Network_.__;
+var _$ = Network_._$;
+
+function spawnJQueryDriver(baseSelector, metaLevel, wrapFunction) {
+  metaLevel = metaLevel || 0;
+  wrapFunction = wrapFunction || defaultWrapFunction;
+  Network.spawn(
+    new DemandMatcher(Patch.observe(wrapFunction(_$('selector'), _$('eventName'), __)),
+		      Patch.advertise(wrapFunction(_$('selector'), _$('eventName'), __)),
+		      {
+			metaLevel: metaLevel,
+			onDemandIncrease: function (c) {
+			  Network.spawn(new JQueryEventRouter(baseSelector,
+							      c.selector,
+							      c.eventName,
+							      metaLevel,
+							      wrapFunction));
+			}
+		      }));
+}
+
+function defaultWrapFunction(selector, eventName, eventValue) {
+  return ["jQuery", selector, eventName, eventValue];
+}
+
+function JQueryEventRouter(baseSelector, selector, eventName, metaLevel, wrapFunction) {
+  var self = this;
+  this.baseSelector = baseSelector || null;
+  this.selector = selector;
+  this.eventName = eventName;
+  this.metaLevel = metaLevel || 0;
+  this.wrapFunction = wrapFunction || defaultWrapFunction;
+  this.preventDefault = (this.eventName.charAt(0) !== "+");
+  this.handler =
+    Network.wrap(function (e) {
+      Network.send(self.wrapFunction(self.selector, self.eventName, e), self.metaLevel);
+      if (self.preventDefault) e.preventDefault();
+      return !self.preventDefault;
+    });
+  this.computeNodes().on(this.preventDefault ? this.eventName : this.eventName.substring(1),
+			 this.handler);
+}
+
+JQueryEventRouter.prototype.boot = function () {
+  return Patch.pub(this.wrapFunction(this.selector, this.eventName, __), this.metaLevel)
+    .andThen(Patch.sub(Patch.observe(this.wrapFunction(this.selector, this.eventName, __)),
+		       this.metaLevel));
+};
+
+JQueryEventRouter.prototype.handleEvent = function (e) {
+  if (e.type === "stateChange" && e.patch.hasRemoved()) {
+    this.computeNodes().off(this.eventName, this.handler);
+    Network.exit();
+  }
+};
+
+JQueryEventRouter.prototype.computeNodes = function () {
+  if (this.baseSelector) {
+    return $(this.baseSelector).children(this.selector).addBack(this.selector);
+  } else {
+    return $(this.selector);
+  }
+};
+
+function simplifyDOMEvent(e) {
+  var keys = [];
+  for (var k in e) {
+    var v = e[k];
+    if (typeof v === 'object') continue;
+    if (typeof v === 'function') continue;
+    keys.push(k);
+  }
+  keys.sort();
+  var simplified = [];
+  for (var i = 0; i < keys.length; i++) {
+    simplified.push([keys[i], e[keys[i]]]);
+  }
+  return simplified;
+}
+
+///////////////////////////////////////////////////////////////////////////
+
+module.exports.spawnJQueryDriver = spawnJQueryDriver;
+module.exports.simplifyDOMEvent = simplifyDOMEvent;
+module.exports.defaultWrapFunction = defaultWrapFunction;
diff --git a/js/src/main.js b/js/src/main.js
new file mode 100644
index 0000000..8e383a1
--- /dev/null
+++ b/js/src/main.js
@@ -0,0 +1,51 @@
+"use strict";
+
+function copyKeys(keys, to, from) {
+  for (var i = 0; i < keys.length; i++) {
+    to[keys[i]] = from[keys[i]];
+  }
+}
+
+module.exports = require("./network.js");
+
+module.exports.Route = require("./route.js");
+copyKeys(['__', '_$', '$Capture', '$Special',
+	  'is_emptyTrie', 'emptyTrie',
+	  'embeddedTrie', 'compilePattern',
+	  'compileProjection', 'project', 'projectObjects',
+	  'prettyTrie'],
+	 module.exports,
+	 module.exports.Route);
+
+var Seal = require('./seal.js')
+copyKeys(['Seal', 'seal'],
+	 module.exports,
+	 Seal);
+
+module.exports.DemandMatcher = require('./demand-matcher.js').DemandMatcher;
+module.exports.Ack = require('./ack.js').Ack;
+
+module.exports.RandomID = require('./randomid.js');
+module.exports.DOM = require("./dom-driver.js");
+module.exports.JQuery = require("./jquery-driver.js");
+// module.exports.RoutingTableWidget = require("./routing-table-widget.js");
+// module.exports.WebSocket = require("./websocket-driver.js");
+module.exports.Reflect = require("./reflect.js");
+
+module.exports.Patch = require("./patch.js");
+copyKeys(['emptyPatch',
+	  'observe', 'atMeta', 'advertise',
+	  'isObserve', 'isAtMeta', 'isAdvertise',
+	  'assert', 'retract', 'sub', 'unsub', 'pub', 'unpub',
+	  'patchSeq'],
+	 module.exports,
+	 module.exports.Patch);
+
+module.exports.Ground = require("./ground.js").Ground;
+module.exports.Actor = require("./actor.js");
+// module.exports.Spy = require("./spy.js").Spy;
+// module.exports.WakeDetector = require("./wake-detector.js").WakeDetector;
+
+// var Worker = require("./worker.js");
+// module.exports.Worker = Worker.Worker;
+// module.exports.WorkerGround = Worker.WorkerGround;
diff --git a/js/src/mux.js b/js/src/mux.js
new file mode 100644
index 0000000..70c274c
--- /dev/null
+++ b/js/src/mux.js
@@ -0,0 +1,123 @@
+"use strict";
+
+var Immutable = require('immutable');
+var Route = require('./route.js');
+var Patch = require('./patch.js');
+
+function Mux(nextPid, routingTable, interestTable) {
+  this.nextPid = nextPid || 0;
+  this.routingTable = routingTable || Route.emptyTrie;
+  this.interestTable = interestTable || Immutable.Map(); // pid -> Trie
+}
+
+Mux.prototype.shallowCopy = function () {
+  return new Mux(this.nextPid, this.routingTable, this.interestTable);
+};
+
+Mux.prototype.addStream = function (initialPatch) {
+  var newPid = this.nextPid++;
+  return this.updateStream(newPid, initialPatch);
+};
+
+Mux.prototype.removeStream = function (pid) {
+  return this.updateStream(pid, Patch.removeEverythingPatch);
+};
+
+Mux.prototype.updateStream = function (pid, unclampedPatch) {
+  var oldInterests = this.interestsOf(pid);
+  var oldRoutingTable = this.routingTable;
+  var delta = unclampedPatch.label(Immutable.Set.of(pid)).limit(oldInterests);
+  var deltaAggregate = delta.computeAggregate(pid, oldRoutingTable);
+  var newInterests = delta.applyTo(oldInterests);
+  var newRoutingTable = delta.applyTo(oldRoutingTable);
+
+  this.routingTable = newRoutingTable;
+
+  if (Route.is_emptyTrie(newInterests)) {
+    this.interestTable = this.interestTable.remove(pid);
+  } else {
+    this.interestTable = this.interestTable.set(pid, newInterests);
+  }
+
+  return { pid: pid,
+	   delta: delta,
+	   deltaAggregate: deltaAggregate };
+};
+
+var atMetaEverything = Route.compilePattern(true, Patch.atMeta(Route.__));
+var atMetaBranchKeys = Immutable.List([Route.SOA, Patch.$AtMeta]);
+var onlyMeta = Immutable.Set.of("meta");
+
+function echoCancelledTrie(t) {
+  return Route.subtract(t, atMetaEverything, function (v1, v2) {
+    return v1.has("meta") ? onlyMeta : null;
+  });
+}
+
+function computeEvents(oldMux, newMux, updateStreamResult) {
+  var actingPid = updateStreamResult.pid;
+  var delta = updateStreamResult.delta;
+  var deltaAggregate = updateStreamResult.deltaAggregate;
+  var deltaAggregateNoEcho = (actingPid === "meta")
+      ? delta // because echo-cancellation means that meta-SCNs are always new information
+      : new Patch.Patch(Route.triePruneBranch(deltaAggregate.added, atMetaBranchKeys),
+			Route.triePruneBranch(deltaAggregate.removed, atMetaBranchKeys));
+  var oldRoutingTable = oldMux.routingTable;
+  var newRoutingTable = newMux.routingTable;
+  var affectedPids =
+      computeAffectedPids(oldRoutingTable, deltaAggregateNoEcho).add(actingPid).remove("meta");
+  return {
+    eventMap: Immutable.Map().withMutations(function (result) {
+      affectedPids.forEach(function (pid) {
+	var patchForPid;
+	if (pid === actingPid) {
+	  var part1 = new Patch.Patch(
+	    echoCancelledTrie(Patch.biasedIntersection(newRoutingTable, delta.added)),
+	    echoCancelledTrie(Patch.biasedIntersection(oldRoutingTable, delta.removed)));
+	  var part2 = new Patch.Patch(Patch.biasedIntersection(deltaAggregateNoEcho.added,
+							       newMux.interestsOf(pid)),
+				      Patch.biasedIntersection(deltaAggregateNoEcho.removed,
+							       oldMux.interestsOf(pid)));
+	  patchForPid = part1.unsafeUnion(part2);
+	} else {
+	  patchForPid = deltaAggregateNoEcho.viewFrom(oldMux.interestsOf(pid));
+	}
+	if (patchForPid.isNonEmpty()) {
+	  result.set(pid, patchForPid);
+	}
+      });
+    }),
+    metaEvents: (actingPid === "meta")
+      ? Immutable.List()
+      : Immutable.List.of(delta.computeAggregate(actingPid, oldRoutingTable, true).drop())
+  };
+}
+
+function computeAffectedPids(routingTable, delta) {
+  var cover = Route._union(delta.added, delta.removed);
+  routingTable = Route.trieStep(routingTable, Route.SOA);
+  routingTable = Route.trieStep(routingTable, Patch.$Observe);
+  return Route.matchTrie(cover, routingTable, Immutable.Set(),
+			 function (v, r, acc) {
+			   return acc.union(Route.trieStep(r, Route.EOA).value);
+			 });
+}
+
+Mux.prototype.routeMessage = function (body) {
+  if (Route.matchValue(this.routingTable, body) === null) {
+    return Route.matchValue(this.routingTable, Patch.observe(body)) || Immutable.Set();
+  } else {
+    // Some other stream has declared body
+    return Immutable.Set();
+  }
+};
+
+Mux.prototype.interestsOf = function (pid) {
+  return this.interestTable.get(pid, Route.emptyTrie);
+};
+
+///////////////////////////////////////////////////////////////////////////
+
+module.exports.Mux = Mux;
+module.exports.computeEvents = computeEvents;
+module.exports.computeAffectedPids = computeAffectedPids;
diff --git a/js/src/network.js b/js/src/network.js
new file mode 100644
index 0000000..151e54d
--- /dev/null
+++ b/js/src/network.js
@@ -0,0 +1,299 @@
+"use strict";
+
+var Immutable = require('immutable');
+var Route = require('./route.js');
+var Patch = require('./patch.js');
+var Mux = require('./mux.js');
+
+/*---------------------------------------------------------------------------*/
+/* Events and Actions */
+
+function stateChange(patch) {
+  return { type: 'stateChange', patch: patch };
+}
+
+function message(body) {
+  return { type: 'message', message: body };
+}
+
+function spawn(behavior) {
+  return { type: 'spawn', behavior: behavior };
+}
+
+function terminate() {
+  return { type: 'terminate' };
+}
+
+function terminateNetwork() {
+  return { type: 'terminateNetwork' };
+}
+
+/*---------------------------------------------------------------------------*/
+/* Network */
+
+function Network(bootFn) {
+  this.pendingActions = Immutable.List(); // of [pid, Action]
+  this.processTable = Immutable.Map(); // pid -> Behavior
+  this.runnablePids = Immutable.Set(); // of pid
+  this.mux = new Mux.Mux();
+  this.onStateChange = function (mux, deltaAggregate) {};
+  this.asChild('meta', function () { return bootFn() }, true);
+}
+
+// Class state and methods
+
+Network.stack = Immutable.List();
+
+Network.current = function () {
+  return Network.stack.last().network;
+};
+
+Network.activePid = function () {
+  return Network.stack.last().activePid;
+};
+
+Network.activeBehavior = function () {
+  var entry = Network.stack.last();
+  var p = entry.network.processTable.get(entry.activePid);
+  return p ? p.behavior : null;
+};
+
+Network.withNetworkStack = function (stack, f) {
+  var oldStack = Network.stack;
+  Network.stack = stack;
+  var result;
+  try {
+    result = f();
+  } catch (e) {
+    Network.stack = oldStack;
+    throw e;
+  }
+  Network.stack = oldStack;
+  return result;
+};
+
+Network.wrap = function (f) {
+  var savedStack = Network.stack;
+  return function () {
+    var actuals = arguments;
+    return Network.withNetworkStack(savedStack, function () {
+      var result = Network.current().asChild(Network.activePid(), function () {
+	return f.apply(null, actuals);
+      });
+      Network.stack.reverse().forEach(function (entry) {
+	entry.network.markRunnable(entry.activePid);
+      });
+      return result;
+    });
+  };
+};
+
+Network.enqueueAction = function (action) {
+  var entry = Network.stack.last();
+  entry.network.enqueueAction(entry.activePid, action);
+};
+
+Network.send = function (body, metaLevel) {
+  Network.enqueueAction(message(Patch.prependAtMeta(body, metaLevel || 0)));
+};
+
+Network.stateChange = function (patch) {
+  Network.enqueueAction(stateChange(patch));
+};
+
+Network.spawn = function (behavior) {
+  Network.enqueueAction(spawn(behavior));
+};
+
+Network.exit = function (exn) {
+  Network.current().kill(Network.activePid(), exn);
+};
+
+Network.exitNetwork = function () {
+  Network.enqueueAction(terminateNetwork());
+};
+
+Network.inertBehavior = {
+  handleEvent: function (e) {}
+};
+
+// Instance methods
+
+Network.prototype.asChild = function (pid, f, omitLivenessCheck) {
+  var self = this;
+  var p = this.processTable.get(pid, null);
+  if (!omitLivenessCheck && (p === null)) {
+    console.warn("Network.asChild eliding invocation of dead process", pid);
+    return;
+  }
+
+  return Network.withNetworkStack(
+    Network.stack.push({ network: this, activePid: pid }),
+    function () {
+      try {
+	return f(p);
+      } catch (e) {
+	self.kill(pid, e);
+      }
+    });
+};
+
+Network.prototype.kill = function (pid, exn) {
+  if (exn && exn.stack) {
+    console.log("Process exiting", pid, exn, exn.stack);
+  } else {
+    console.log("Process exiting", pid, exn);
+  }
+  var p = this.processTable.get(pid);
+  this.processTable = this.processTable.set(pid, { behavior: Network.inertBehavior });
+  if (p) {
+    if (p.behavior.trapexit) {
+      this.asChild(pid, function () { return p.behavior.trapexit(exn); }, true);
+    }
+    this.enqueueAction(pid, terminate());
+  }
+};
+
+Network.prototype.boot = function () {
+  // Needed in order for a new Network to be marked as "runnable", so
+  // its initial actions get performed.
+};
+
+Network.prototype.handleEvent = function (e) {
+  switch (e.type) {
+  case 'stateChange':
+    this.enqueueAction('meta', stateChange(e.patch.lift()));
+    break;
+  case 'message':
+    this.enqueueAction('meta', message(Patch.atMeta(e.message)));
+    break;
+  default:
+    var exn = new Error("Event type " + e.type + " not understood");
+    exn.event = e;
+    throw exn;
+  }
+  return true;
+};
+
+Network.prototype.step = function () {
+  return this.dispatchActions()
+    && this.runRunnablePids()
+    && ((this.pendingActions.size > 0) || (this.runnablePids.size > 0));
+};
+
+Network.prototype.enqueueAction = function (pid, action) {
+  this.pendingActions = this.pendingActions.push([pid, action]);
+};
+
+Network.prototype.dispatchActions = function () {
+  var self = this;
+  var actionQueue = this.pendingActions;
+  this.pendingActions = Immutable.List();
+  var alive = true;
+  actionQueue.forEach(function (entry) {
+    var pid = entry[0];
+    var action = entry[1];
+    if (!self.interpretAction(pid, action)) {
+      alive = false;
+      return false;
+    }
+  });
+  return alive;
+};
+
+Network.prototype.markRunnable = function (pid) {
+  this.runnablePids = this.runnablePids.add(pid);
+};
+
+Network.prototype.runRunnablePids = function () {
+  var self = this;
+  var pidSet = this.runnablePids;
+  this.runnablePids = Immutable.Set();
+  pidSet.forEach(function (pid) {
+    var childBusy = self.asChild(pid, function (p) {
+      return p.behavior.step // exists, haven't called it yet
+	&& p.behavior.step();
+    });
+    if (childBusy) self.markRunnable(pid);
+  });
+  return true;
+};
+
+Network.prototype.interpretAction = function (pid, action) {
+  var self = this;
+
+  switch (action.type) {
+  case 'stateChange':
+    var oldMux = this.mux.shallowCopy();
+    this.deliverPatches(oldMux, this.mux.updateStream(pid, action.patch));
+    return true;
+
+  case 'message':
+    if (Patch.isObserve(action.message)) {
+      console.warn('Process ' + pid + ' send message containing query', action.message);
+    }
+    if (pid !== 'meta' && Patch.isAtMeta(action.message)) {
+      Network.send(action.message[1]);
+    } else {
+      this.mux.routeMessage(action.message).forEach(function (pid) {
+	self.deliverEvent(pid, action);
+      });
+    }
+    return true;
+
+  case 'spawn':
+    var oldMux = this.mux.shallowCopy();
+    var p = { behavior: action.behavior };
+    var pid = this.mux.nextPid;
+    this.processTable = this.processTable.set(pid, p);
+    var initialPatch = Patch.emptyPatch;
+    if (p.behavior.boot) {
+      initialPatch = this.asChild(pid, function () { return p.behavior.boot() });
+      initialPatch = initialPatch || Patch.emptyPatch;
+      this.markRunnable(pid);
+    }
+    this.deliverPatches(oldMux, this.mux.addStream(initialPatch));
+    return true;
+
+  case 'terminate':
+    var oldMux = this.mux.shallowCopy();
+    this.deliverPatches(oldMux, this.mux.removeStream(pid));
+    console.log("Process exit complete", pid);
+    this.processTable = this.processTable.remove(pid);
+    return true;
+
+  case 'terminateNetwork':
+    Network.exit();
+    return false;
+
+  default:
+    var exn = new Error("Action type " + action.type + " not understood");
+    exn.action = action;
+    throw exn;
+  }
+};
+
+Network.prototype.deliverPatches = function (oldMux, updateStreamResult) {
+  var self = this;
+  var events = Mux.computeEvents(oldMux, this.mux, updateStreamResult);
+  events.eventMap.forEach(function (patch, pid) {
+    self.deliverEvent(pid, stateChange(patch));
+  });
+  events.metaEvents.forEach(Network.stateChange);
+  this.onStateChange(this.mux, updateStreamResult.deltaAggregate);
+};
+
+Network.prototype.deliverEvent = function (pid, event) {
+  var childBusy = this.asChild(pid, function (p) { return p.behavior.handleEvent(event); });
+  if (childBusy) this.markRunnable(pid);
+};
+
+///////////////////////////////////////////////////////////////////////////
+
+module.exports.stateChange = stateChange;
+module.exports.message = message;
+module.exports.spawn = spawn;
+module.exports.terminate = terminate;
+module.exports.terminateNetwork = terminateNetwork;
+
+module.exports.Network = Network;
diff --git a/js/src/patch.js b/js/src/patch.js
new file mode 100644
index 0000000..93ed59f
--- /dev/null
+++ b/js/src/patch.js
@@ -0,0 +1,265 @@
+"use strict";
+
+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 removeEverythingPatch = new Patch(Route.emptyTrie, Route.compilePattern(true, __));
+
+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 isObserve(p) { return p[0] === $Observe; }
+function isAtMeta(p) { return p[0] === $AtMeta; }
+function isAdvertise(p) { return p[0] === $Advertise; }
+
+function prependAtMeta(p, level) {
+  while (level--) {
+    p = atMeta(p);
+  }
+  return p;
+}
+
+function stripAtMeta(p, level) {
+  while (level--) {
+    if (p.length === 2 && p[0] === $AtMeta) {
+      p = p[1];
+    } else {
+      return null;
+    }
+  }
+  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 _check(p) {
+  if (p instanceof Patch) {
+    throw new Error("Cannot construct patch pattern using an embedded patch");
+  }
+  return p;
+}
+
+function assert(p, metaLevel) {
+  return new Patch(Route.compilePattern(true, prependAtMeta(_check(p), metaLevel || 0)),
+		   Route.emptyTrie);
+}
+
+function retract(p, metaLevel) {
+  return new Patch(Route.emptyTrie,
+		   Route.compilePattern(true, prependAtMeta(_check(p), metaLevel || 0)));
+}
+
+function sub(p, metaLevel) {
+  return new Patch(observeAtMeta(_check(p), metaLevel || 0), Route.emptyTrie);
+}
+
+function unsub(p, metaLevel) {
+  return new Patch(Route.emptyTrie, observeAtMeta(_check(p), metaLevel || 0));
+}
+
+function pub(p, metaLevel) {
+  return assert(advertise(_check(p)), metaLevel);
+}
+
+function unpub(p, metaLevel) {
+  return retract(advertise(_check(p)), metaLevel);
+}
+
+///////////////////////////////////////////////////////////////////////////
+
+Patch.prototype.equals = function (other) {
+  if (!(other instanceof Patch)) return false;
+  return Immutable.is(this.added, other.added) && Immutable.is(this.removed, other.removed);
+};
+
+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 /* optional flag */) {
+  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)];
+};
+
+Patch.prototype.pretty = function () {
+  return ("<<<<<<<< Removed:\n" + Route.prettyTrie(this.removed) + "\n" +
+	  "======== Added:\n" + Route.prettyTrie(this.added) + "\n" +
+	  ">>>>>>>>");
+}
+
+///////////////////////////////////////////////////////////////////////////
+
+module.exports.Patch = Patch;
+module.exports.emptyPatch = emptyPatch;
+module.exports.removeEverythingPatch = removeEverythingPatch;
+
+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.isObserve = isObserve;
+module.exports.isAtMeta = isAtMeta;
+module.exports.isAdvertise = isAdvertise;
+
+module.exports.prependAtMeta = prependAtMeta;
+module.exports.stripAtMeta = stripAtMeta;
+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;
diff --git a/js/src/randomid.js b/js/src/randomid.js
new file mode 100644
index 0000000..18b6061
--- /dev/null
+++ b/js/src/randomid.js
@@ -0,0 +1,26 @@
+var randomId;
+
+if ((typeof window !== 'undefined') &&
+    (typeof window.crypto !== 'undefined') &&
+    (typeof window.crypto.getRandomValues !== 'undefined')) {
+  randomId = function (byteCount) {
+    var buf = new Uint8Array(byteCount);
+    window.crypto.getRandomValues(buf);
+    return btoa(String.fromCharCode.apply(null, buf)).replace(/=/g,'');
+  };
+} else {
+  var crypto;
+  try {
+    crypto = require('crypto');
+  } catch (e) {}
+  if ((typeof crypto !== 'undefined') &&
+      (typeof crypto.randomBytes !== 'undefined')) {
+    randomId = function (byteCount) {
+      return crypto.randomBytes(byteCount).base64Slice().replace(/=/g,'');
+    };
+  } else {
+    console.warn('No suitable implementation for RandomID.randomId available.');
+  }
+}
+
+module.exports.randomId = randomId;
diff --git a/js/src/reflect.js b/js/src/reflect.js
new file mode 100644
index 0000000..43ef62b
--- /dev/null
+++ b/js/src/reflect.js
@@ -0,0 +1,28 @@
+"use strict";
+
+// Reflection on function formal parameter lists.
+// This module is based on Angular's "injector" code,
+// https://github.com/angular/angular.js/blob/master/src/auto/injector.js,
+// MIT licensed, and hence:
+// Copyright (c) 2010-2014 Google, Inc. http://angularjs.org
+// Copyright (c) 2014 Tony Garnock-Jones
+
+var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
+var FN_ARG_SPLIT = /,/;
+var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
+
+function formalParameters(fn) {
+    var result = [];
+
+    var fnText = fn.toString().replace(STRIP_COMMENTS, '');
+    var argDecl = fnText.match(FN_ARGS);
+    var args = argDecl[1].split(FN_ARG_SPLIT);
+    for (var i = 0; i < args.length; i++) {
+	var trimmed = args[i].trim();
+	if (trimmed) { result.push(trimmed); }
+    }
+
+    return result;
+}
+
+module.exports.formalParameters = formalParameters;
diff --git a/js/src/route.js b/js/src/route.js
new file mode 100644
index 0000000..efdb218
--- /dev/null
+++ b/js/src/route.js
@@ -0,0 +1,1117 @@
+"use strict";
+
+var Immutable = require("immutable");
+
+function makeStructureConstructor(label, argumentNames) {
+  var $SyndicateMeta$ = {
+    label: label,
+    arguments: argumentNames
+  };
+  return function() {
+    var result = {"$SyndicateMeta$": $SyndicateMeta$};
+    for (var i = 0; i < argumentNames.length; i++) {
+      result[argumentNames[i]] = arguments[i];
+    }
+    return result;
+  };
+}
+
+function isStructure(s) {
+  return (s !== null) && (typeof s === 'object') && ("$SyndicateMeta$" in s);
+}
+
+function structureToArray(s) {
+  var result = [s.$SyndicateMeta$.label];
+  var args = s.$SyndicateMeta$.arguments;
+  for (var i = 0; i < args.length; i++) {
+    result.push(s[args[i]]);
+  }
+  return result;
+}
+
+function $Special(name) {
+  this.name = name;
+}
+
+var __ = new $Special("wildcard"); /* wildcard marker */
+
+var SOA = new $Special("["); // start of array
+var EOA = new $Special("]"); // end of array
+
+function die(message) {
+  throw new Error(message);
+}
+
+function $Embedded(trie) {
+  this.trie = trie;
+}
+
+function embeddedTrie(trie) {
+  return new $Embedded(trie);
+}
+
+// The name argument should be a string or null; it defaults to null.
+// The pattern argument defaults to wildcard, __.
+function $Capture(name, pattern) {
+  this.name = name || null;
+  this.pattern = (typeof pattern === 'undefined' ? __ : pattern);
+}
+
+// Abbreviation: _$(...) <==> new $Capture(...)
+function _$(name, pattern) {
+  return new $Capture(name, pattern);
+}
+
+function isCapture(x) { return x instanceof $Capture || x === _$; }
+function captureName(x) { return x instanceof $Capture ? x.name : null; }
+function capturePattern(x) { return x instanceof $Capture ? x.pattern : __; }
+
+var SOC = new $Special("{"); // start of capture
+var EOC = new $Special("}"); // end of capture
+
+function $Success(value) {
+  this.value = 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;
+}
+
+$WildcardSequence.prototype.equals = function (other) {
+  if (!(other instanceof $WildcardSequence)) return false;
+  return Immutable.is(this.trie, other.trie);
+};
+
+function is_emptyTrie(m) {
+  return Immutable.is(m, emptyTrie);
+}
+
+///////////////////////////////////////////////////////////////////////////
+// Constructors
+
+var emptyTrie = Immutable.Map();
+
+function rsuccess(v) {
+  return (v === null) ? emptyTrie : new $Success(v);
+}
+
+function rseq(e, r) {
+  if (r === emptyTrie) return emptyTrie;
+  return emptyTrie.set(e, r);
+}
+
+function rwild(r) {
+  return rseq(__, r);
+}
+
+function rwildseq(r) {
+  return (r === emptyTrie) ? emptyTrie : new $WildcardSequence(r);
+}
+
+///////////////////////////////////////////////////////////////////////////
+
+function compilePattern(v, p) {
+  if (!p) die("compilePattern: missing pattern");
+  return walk(p, rseq(EOA, rsuccess(v)));
+
+  function walk(p, acc) {
+    if (p === __) return rwild(acc);
+
+    if (Array.isArray(p)) {
+      acc = rseq(EOA, acc);
+      for (var i = p.length - 1; i >= 0; i--) {
+	acc = walk(p[i], acc);
+      }
+      return rseq(SOA, acc);
+    }
+
+    if (Immutable.List.isList(p)) {
+      acc = rseq(EOA, acc);
+      p.reverse().forEach(function (element) {
+	acc = walk(element, acc);
+      });
+      return rseq(SOA, acc);
+    }
+
+    if (isStructure(p)) {
+      var args = p.$SyndicateMeta$.arguments;
+      acc = rseq(EOA, acc);
+      for (var i = args.length - 1; i >= 0; i--) {
+        acc = walk(p[args[i]], acc);
+      }
+      acc = rseq(p.$SyndicateMeta$.label, acc);
+      return rseq(SOA, acc);
+    }
+
+    if (p instanceof $Embedded) {
+      return appendTrie(p.trie, function (v) { return acc; });
+    } else {
+      return rseq(p, acc);
+    }
+  }
+}
+
+function matchPattern(v, p) {
+  var captureCount = 0;
+  var result = {};
+  try {
+    walk(v, p);
+  } catch (e) {
+    if (e.matchPatternFailed) return null;
+    throw e;
+  }
+  result.length = captureCount;
+  return result;
+
+  function walk(v, p) {
+    if (p === v) return;
+
+    if (p === __) return;
+
+    if (isStructure(p)) { p = structureToArray(p); }
+    if (isStructure(v)) { v = structureToArray(v); }
+
+    if (Array.isArray(p) && Array.isArray(v) && p.length === v.length) {
+      for (var i = 0; i < p.length; i++) {
+        walk(v[i], p[i]);
+      }
+      return;
+    }
+
+    if (isCapture(p)) {
+      var thisCapture = captureCount++;
+      walk(v, capturePattern(p));
+      result[captureName(p) || ('$' + thisCapture)] = v;
+      return;
+    }
+
+    if (p instanceof $Embedded) {
+      die("$Embedded patterns not supported in matchPattern()");
+    }
+
+    throw {matchPatternFailed: true};
+  }
+}
+
+function rupdate(r, key, k) {
+  var oldWild = r.get(__, emptyTrie);
+  if (Immutable.is(k, oldWild)) {
+    return r.remove(key);
+  } else {
+    return r.set(key, k);
+  }
+}
+
+function rlookup(r, key) {
+  return r.get(key, emptyTrie);
+}
+
+function rlookupWild(r, key) {
+  var result = r.get(key, false);
+  if (result) return result;
+  var wildEdge = rlookup(r, __);
+  if (is_keyOpen(key)) return rwildseq(wildEdge);
+  if (is_keyClose(key)) return (wildEdge instanceof $WildcardSequence) ? wildEdge.trie : emptyTrie;
+  return wildEdge;
+}
+
+function is_keyOpen(k) {
+  return k === SOA;
+}
+
+function is_keyClose(k) {
+  return k === EOA;
+}
+
+function is_keyNormal(k) {
+  return !(is_keyOpen(k) || is_keyClose(k));
+}
+
+///////////////////////////////////////////////////////////////////////////
+
+var unionSuccessesDefault = function (v1, v2) {
+  if (v1 === true) return v2;
+  if (v2 === true) return v1;
+  return v1.union(v2);
+};
+
+var intersectSuccessesDefault = function (v1, v2) {
+  return v1;
+};
+
+var subtractSuccessesDefault = function (v1, v2) {
+  var r = v1.subtract(v2);
+  if (r.isEmpty()) return null;
+  return r;
+};
+
+var matchTrieSuccesses = function (v1, v2, acc) {
+  return acc.union(v2);
+};
+
+var projectSuccess = function (v) {
+  return v;
+};
+
+///////////////////////////////////////////////////////////////////////////
+
+function expandWildseq(r) {
+  return union(rwild(rwildseq(r)), rseq(EOA, r));
+}
+
+function union(o1, o2, unionSuccessesOpt) {
+  var unionSuccesses = unionSuccessesOpt || unionSuccessesDefault;
+  return merge(o1, o2);
+
+  function merge(o1, o2) {
+    if (is_emptyTrie(o1)) return o2;
+    if (is_emptyTrie(o2)) return o1;
+    return walk(o1, o2);
+  }
+
+  function walk(r1, r2) {
+    if (r1 instanceof $WildcardSequence) {
+      if (r2 instanceof $WildcardSequence) {
+	return rwildseq(walk(r1.trie, r2.trie));
+      }
+      r1 = expandWildseq(r1.trie);
+    } else if (r2 instanceof $WildcardSequence) {
+      r2 = expandWildseq(r2.trie);
+    }
+
+    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, __));
+    var target;
+
+    function examineKey(key) {
+      if ((key !== __) && !target.has(key)) {
+	target = rupdate(target, key, merge(rlookupWild(r1, key), rlookupWild(r2, key)));
+      }
+    }
+
+    if (is_emptyTrie(w)) {
+      var smaller = r1.size < r2.size ? r1 : r2;
+      var larger  = r1.size < r2.size ? r2 : r1;
+      target = larger;
+      smaller.forEach(function (val, key) {
+	var k = merge(rlookup(smaller, key), rlookup(larger, key));
+	target = rupdate(target, key, k);
+      });
+    } else {
+      target = rwild(w);
+      r1.forEach(function (val, key) { examineKey(key) });
+      r2.forEach(function (val, key) { examineKey(key) });
+    }
+
+    return target;
+  }
+}
+
+function unionN() {
+  var acc = emptyTrie;
+  for (var i = 0; i < arguments.length; i++) {
+    acc = union(acc, arguments[i]);
+  }
+  return acc;
+}
+
+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); }
+
+  function walk(r1, r2) {
+    // INVARIANT: r1 is a part of the original o1, and
+    // likewise for r2. This is so that the first arg to
+    // intersectSuccesses always comes from r1, and the second
+    // from r2.
+    if (is_emptyTrie(r1)) return emptyTrie;
+    if (is_emptyTrie(r2)) return emptyTrie;
+
+    if (r1 instanceof $WildcardSequence) {
+      if (r2 instanceof $WildcardSequence) {
+	return rwildseq(walk(r1.trie, r2.trie));
+      }
+      r1 = expandWildseq(r1.trie);
+    } else if (r2 instanceof $WildcardSequence) {
+      r2 = expandWildseq(r2.trie);
+    }
+
+    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, __);
+    var w2 = rlookup(r2, __);
+    var w = walk(w1, w2);
+
+    var target = emptyTrie;
+
+    function examineKey(key) {
+      if ((key !== __) && !target.has(key)) {
+	target = rupdate(target, key, walk(rlookupWild(r1, key), rlookupWild(r2, key)));
+      }
+    }
+
+    if (is_emptyTrie(w1)) {
+      if (is_emptyTrie(w2)) {
+	(r1.size < r2.size ? r1 : r2).forEach(function (val, key) { examineKey(key) });
+      } else {
+	r1.forEach(function (val, key) { examineKey(key) });
+      }
+    } else {
+      if (is_emptyTrie(w2)) {
+	r2.forEach(function (val, key) { examineKey(key) });
+      } else {
+	target = rupdate(target, __, w);
+	r1.forEach(function (val, key) { examineKey(key) });
+	r2.forEach(function (val, key) { examineKey(key) });
+      }
+    }
+    return target;
+  }
+}
+
+// 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;
+    } else {
+      if (is_emptyTrie(r2)) {
+	return r1;
+      }
+    }
+
+    if (r1 instanceof $WildcardSequence) {
+      if (r2 instanceof $WildcardSequence) {
+	return rwildseq(walk(r1.trie, r2.trie));
+      }
+      r1 = expandWildseq(r1.trie);
+    } else if (r2 instanceof $WildcardSequence) {
+      r2 = expandWildseq(r2.trie);
+    }
+
+    if (r1 instanceof $Success && r2 instanceof $Success) {
+      return rsuccess(subtractSuccesses(r1.value, r2.value));
+    }
+
+    var w1 = rlookup(r1, __);
+    var w2 = rlookup(r2, __);
+    var w = walk(w1, w2);
+    var target;
+
+    function examineKey(key) {
+      if (key !== __) {
+	var k1 = rlookupWild(r1, key);
+	var k2 = rlookupWild(r2, key);
+	var updatedK = walk(k1, k2);
+
+	// Here we ensure a "minimal" remainder in cases
+	// where after an erasure, a particular key's
+	// continuation is the same as the wildcard's
+	// continuation. TODO: the equals check may
+	// be expensive. If so, how can it be made
+	// cheaper?
+	if (is_keyOpen(key)) {
+	  target = rupdate(target, key,
+			   ((updatedK instanceof $WildcardSequence) &&
+			    Immutable.is(updatedK.trie, w))
+			   ? w
+			   : updatedK);
+	} else {
+	  target = rupdate(target, key, updatedK);
+	}
+      }
+    }
+
+    if (is_emptyTrie(w2)) {
+      target = r1;
+      r2.forEach(function (val, key) { examineKey(key) });
+    } else {
+      target = emptyTrie;
+      target = rupdate(target, __, w);
+      r1.forEach(function (val, key) { examineKey(key) });
+      r2.forEach(function (val, key) { examineKey(key) });
+    }
+
+    return collapseWildcardSequences(target);
+  }
+}
+
+function collapseWildcardSequences(target) {
+  // Here, the target is complete. If it has only two keys,
+  // one wild and one is_keyClose, and wild's continuation
+  // is a $WildcardSequence and the other continuation is
+  // identical to the sequence's continuation, then replace
+  // the whole thing with a nested $WildcardSequence.
+  // (We know w === rlookup(target, __) from before.)
+  //
+  // TODO: I suspect actually this applies even if there are
+  // more than two keys, so long as all their continuations
+  // are identical and there's at least one is_keyClose
+  // alongside a wild.
+  if (target.size === 2) {
+    var finalW = rlookup(target, __);
+    if (finalW instanceof $WildcardSequence) {
+      target.forEach(function (k, key) {
+	if ((key !== __) && is_keyClose(key)) {
+	  if (Immutable.is(k, finalW.trie)) {
+	    target = finalW;
+	    return false; // terminate the iteration early
+	  }
+	}
+      });
+    }
+  }
+  return target;
+}
+
+// Returns null on failed match, otherwise the appropriate success
+// value contained in the trie r.
+function matchValue(r, v) {
+  var failureResult = null;
+
+  var vs = Immutable.List.of(v);
+  var stack = [Immutable.List()];
+
+  while (!is_emptyTrie(r)) {
+    if (r instanceof $WildcardSequence) {
+      if (stack.length === 0) return failureResult;
+      vs = stack.pop();
+      r = r.trie;
+      continue;
+    }
+
+    if (r instanceof $Success) {
+      if (vs.size === 0 && stack.length === 0) return r.value;
+      return failureResult;
+    }
+
+    if (vs.size === 0) {
+      if (stack.length === 0) return failureResult;
+      vs = stack.pop();
+      r = rlookup(r, EOA);
+      continue;
+    }
+
+    var v = vs.first();
+    vs = vs.shift();
+
+    if (typeof v === 'string' && v.substring(0, 2) === '__') {
+      die("Cannot match special string starting with __");
+    }
+
+    if (Array.isArray(v)) {
+      if (r.has(SOA)) {
+	r = rlookup(r, SOA);
+	stack.push(vs);
+	vs = Immutable.List(v);
+      } else {
+	r = rlookup(r, __);
+      }
+    } else if (isStructure(v)) {
+      if (r.has(SOA)) {
+        r = rlookup(r, SOA);
+        stack.push(vs);
+        vs = Immutable.List.of(v.$SyndicateMeta$.label);
+        var args = v.$SyndicateMeta$.arguments;
+        for (var i = 0; i < args.length; i++) {
+          vs = vs.push(v[args[i]]);
+        }
+      } else {
+        r = rlookup(r, __);
+      }
+    } else {
+      if (r.has(v)) {
+	r = rlookup(r, v);
+      } else {
+	r = rlookup(r, __);
+      }
+    }
+  }
+
+  return failureResult;
+}
+
+function matchTrie(o1, o2, seed, leftShortOpt) {
+  var acc = typeof seed === 'undefined' ? Immutable.Set() : seed; // variable updated imperatively
+  var leftShort = leftShortOpt || function (v, r, acc) {
+    die("Route.matchTrie: left side short!");
+  };
+  walk(o1, o2);
+  return acc;
+
+  function walkFlipped(r2, r1) { return walk(r1, r2); }
+
+  function walk(r1, r2) {
+    if (is_emptyTrie(r1) || is_emptyTrie(r2)) return;
+
+    if (r1 instanceof $WildcardSequence) {
+      if (r2 instanceof $WildcardSequence) {
+	walk(r1.trie, r2.trie);
+	return;
+      }
+      r1 = expandWildseq(r1.trie);
+    } else if (r2 instanceof $WildcardSequence) {
+      r2 = expandWildseq(r2.trie);
+    }
+
+    if (r1 instanceof $Success) {
+      if (r2 instanceof $Success) {
+	acc = matchTrieSuccesses(r1.value, r2.value, acc);
+      } else {
+	acc = leftShort(r1.value, r2, acc);
+      }
+      return;
+    } else if (r2 instanceof $Success) {
+      die("Route.matchTrie: right side short!");
+    }
+
+    var w1 = rlookup(r1, __);
+    var w2 = rlookup(r2, __);
+    walk(w1, w2);
+
+    function examineKey(key) {
+      if (key !== __) {
+	var k1 = rlookup(r1, key);
+	var k2 = rlookup(r2, key);
+	if (is_emptyTrie(k1)) {
+	  if (is_emptyTrie(k2)) {
+	    return;
+	  } else {
+	    walkWild(walk, w1, key, k2);
+	  }
+	} else {
+	  if (is_emptyTrie(k2)) {
+	    walkWild(walkFlipped, w2, key, k1);
+	  } else {
+	    walk(k1, k2);
+	  }
+	}
+      }
+    }
+
+    // Optimize similarly to intersect().
+    if (is_emptyTrie(w1)) {
+      if (is_emptyTrie(w2)) {
+	(r1.size < r2.size ? r1 : r2).forEach(function (val, key) { examineKey(key) });
+      } else {
+	r1.forEach(function (val, key) { examineKey(key) });
+      }
+    } else {
+      if (is_emptyTrie(w2)) {
+	r2.forEach(function (val, key) { examineKey(key) });
+      } else {
+	r1.forEach(function (val, key) { examineKey(key) });
+	r2.forEach(function (val, key) { examineKey(key) });
+      }
+    }
+  }
+
+  function walkWild(walker, w, key, k) {
+    if (is_emptyTrie(w)) return;
+    if (is_keyOpen(key)) {
+      walker(rwildseq(w), k);
+      return;
+    }
+    if (is_keyClose(key)) {
+      if (w instanceof $WildcardSequence) walker(w.trie, k);
+      return;
+    }
+    walker(w, k);
+  }
+}
+
+function appendTrie(m, mTailFn) {
+  return walk(m);
+
+  function walk(m) {
+    if (is_emptyTrie(m)) return emptyTrie;
+    if (m instanceof $WildcardSequence) return rwildseq(walk(m.trie));
+    if (m instanceof $Success) die("Ill-formed trie");
+
+    var target = emptyTrie;
+    m.forEach(function (k, key) {
+      if (is_keyClose(key) && (k instanceof $Success)) {
+	target = union(target, mTailFn(k.value));
+      } else {
+	target = rupdate(target, key, walk(k));
+      }
+    });
+    return target;
+  }
+}
+
+function triePruneBranch(m, keys) {
+  if (keys.isEmpty()) return emptyTrie;
+
+  if (is_emptyTrie(m)) return emptyTrie;
+  if (m instanceof $WildcardSequence) {
+    return collapseWildcardSequences(triePruneBranch(expandWildseq(m.trie), keys));
+  }
+  if (m instanceof $Success) return m;
+
+  var key = keys.first();
+  var rest = keys.shift();
+  return rupdate(m, key, triePruneBranch(rlookupWild(m, key), rest));
+}
+
+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);
+}
+
+function relabel(m, f) {
+  return walk(m);
+
+  function walk(m) {
+    if (is_emptyTrie(m)) return emptyTrie;
+    if (m instanceof $WildcardSequence) return rwildseq(walk(m.trie));
+    if (m instanceof $Success) return rsuccess(f(m.value));
+
+    var target = emptyTrie;
+    m.forEach(function (k, key) {
+      target = rupdate(target, key, walk(k));
+    });
+    return target;
+  }
+}
+
+function compileProjection(/* projection, projection, ... */) {
+  var names = [];
+  var acc = [];
+  for (var i = 0; i < arguments.length; i++) {
+    walk(arguments[i]);
+  }
+  acc.push(EOA);
+  return {names: names, spec: acc};
+
+  function walk(p) {
+    if (isCapture(p)) {
+      names.push(captureName(p));
+      acc.push(SOC);
+      walk(capturePattern(p));
+      acc.push(EOC);
+      return;
+    }
+
+    if (Array.isArray(p)) {
+      acc.push(SOA);
+      for (var i = 0; i < p.length; i++) {
+	walk(p[i]);
+      }
+      acc.push(EOA);
+      return;
+    }
+
+    if (isStructure(p)) {
+      acc.push(SOA);
+      acc.push(p.$SyndicateMeta$.label);
+      var args = p.$SyndicateMeta$.arguments;
+      for (var i = 0; i < args.length; i++) {
+        walk(p[args[i]]);
+      }
+      acc.push(EOA);
+      return;
+    }
+
+    if (p instanceof $Embedded) {
+      die("Cannot embed trie in projection");
+    } else {
+      acc.push(p);
+    }
+  }
+}
+
+function projectionToPattern(p) {
+  return walk(p);
+
+  function walk(p) {
+    if (isCapture(p)) return walk(capturePattern(p));
+
+    if (Array.isArray(p)) {
+      var result = [];
+      for (var i = 0; i < p.length; i++) {
+	result.push(walk(p[i]));
+      }
+      return result;
+    }
+
+    if (isStructure(p)) {
+      var result = {"$SyndicateMeta$": p.$SyndicateMeta$};
+      var args = p.$SyndicateMeta$.arguments;
+      for (var i = 0; i < args.length; i++) {
+        result[args[i]] = walk(p[args[i]]);
+      }
+      return result;
+    }
+
+    if (p instanceof $Embedded) {
+      return p.trie;
+    } else {
+      return p;
+    }
+  }
+}
+
+function project(m, compiledProjection) {
+  var spec = compiledProjection.spec;
+  return walk(false, m, 0);
+
+  function walk(isCapturing, m, specIndex) {
+    if (specIndex >= spec.length) {
+      if (isCapturing) die("Bad specification: unclosed capture");
+      if (m instanceof $Success) {
+	return rseq(EOA, rsuccess(projectSuccess(m.value)));
+      } else {
+	return emptyTrie;
+      }
+    }
+
+    if (is_emptyTrie(m)) return emptyTrie;
+
+    var item = spec[specIndex];
+    var nextIndex = specIndex + 1;
+
+    if (item === EOC) {
+      if (!isCapturing) die("Bad specification: unexpected EOC");
+      return walk(false, m, nextIndex);
+    }
+
+    if (item === SOC) {
+      if (isCapturing) die("Bad specification: nested capture");
+      return walk(true, m, nextIndex);
+    }
+
+    if (item === __) {
+      if (m instanceof $WildcardSequence) {
+	if (isCapturing) {
+	  return rwild(walk(isCapturing, m, nextIndex));
+	} else {
+	  return walk(isCapturing, m, nextIndex);
+	}
+      }
+
+      if (m instanceof $Success) {
+	return emptyTrie;
+      }
+
+      var target;
+      if (isCapturing) {
+	target = emptyTrie;
+	target = rupdate(target, __, walk(isCapturing, rlookup(m, __), nextIndex));
+	m.forEach(function (mk, key) {
+	  if (key !== __) {
+	    if (is_keyOpen(key)) {
+	      target = rupdate(target, key, captureNested(mk, function (mk2) {
+		return walk(isCapturing, mk2, nextIndex);
+	      }));
+	    } else if (is_keyClose(key)) {
+	      // do nothing
+	    } else {
+	      target = rupdate(target, key, walk(isCapturing, mk, nextIndex));
+	    }
+	  }
+	});
+      } else {
+	target = walk(isCapturing, rlookup(m, __), nextIndex);
+	m.forEach(function (mk, key) {
+	  if (key !== __) {
+	    if (is_keyOpen(key)) {
+	      target = union(target, skipNested(mk, function (mk2) {
+		return walk(isCapturing, mk2, nextIndex);
+	      }));
+	    } else if (is_keyClose(key)) {
+	      // do nothing
+	    } else {
+	      target = union(target, walk(isCapturing, mk, nextIndex));
+	    }
+	  }
+	});
+      }
+      return target;
+    }
+
+    var result;
+    if (m instanceof $WildcardSequence) {
+      if (is_keyOpen(item)) {
+	result = walk(isCapturing, rwildseq(m), nextIndex);
+      } else if (is_keyClose(item)) {
+	result = walk(isCapturing, m.trie, nextIndex);
+      } else {
+	result = walk(isCapturing, m, nextIndex);
+      }
+    } else if (m instanceof $Success) {
+      result = emptyTrie;
+    } else {
+      if (is_keyOpen(item)) {
+	result = walk(isCapturing, rwildseq(rlookup(m, __)), nextIndex);
+      } else if (is_keyClose(item)) {
+	result = emptyTrie;
+      } else {
+	result = walk(isCapturing, rlookup(m, __), nextIndex);
+      }
+      result = union(result, walk(isCapturing, rlookup(m, item), nextIndex));
+    }
+    if (isCapturing) {
+      result = rseq(item, result);
+    }
+    return result;
+  }
+
+  function captureNested(m, cont) {
+    if (m instanceof $WildcardSequence) {
+      return rwildseq(cont(m.trie));
+    }
+
+    if (is_emptyTrie(m) || (m instanceof $Success)) {
+      return emptyTrie;
+    }
+
+    var target = emptyTrie;
+    target = rupdate(target, __, captureNested(rlookup(m, __), cont));
+    m.forEach(function (mk, key) {
+      if (key !== __) {
+	if (is_keyOpen(key)) {
+	  target = rupdate(target, key, captureNested(mk, function (mk2) {
+	    return captureNested(mk2, cont);
+	  }));
+	} else if (is_keyClose(key)) {
+	  target = rupdate(target, key, cont(mk));
+	} else {
+	  target = rupdate(target, key, captureNested(mk, cont));
+	}
+      }
+    });
+    return target;
+  }
+
+  function skipNested(m, cont) {
+    if (m instanceof $WildcardSequence) {
+      return cont(m.trie);
+    }
+
+    if (is_emptyTrie(m) || (m instanceof $Success)) {
+      return emptyTrie;
+    }
+
+    var target = skipNested(rlookup(m, __), cont);
+    m.forEach(function (mk, key) {
+      if (key !== __) {
+	if (is_keyOpen(key)) {
+	  target = union(target, skipNested(mk, function (mk2) {
+	    return skipNested(mk2, cont)
+	  }));
+	} else if (is_keyClose(key)) {
+	  target = union(target, cont(mk));
+	} else {
+	  target = union(target, skipNested(mk, cont));
+	}
+      }
+    });
+    return target;
+  }
+}
+
+function trieKeys(m) {
+  if (is_emptyTrie(m)) return Immutable.Set();
+  var result = walkSeq(m, function (vss, vsk) { return vss; });
+  if (result === null) return null;
+  return Immutable.Set(result);
+
+  function walk(m, k) {
+    if (m instanceof $WildcardSequence) return null;
+    if (m instanceof $Success) return [];
+    if (m.has(__)) return null;
+    var acc = [];
+    m.forEach(function (mk, key) {
+      var piece;
+      if (is_keyOpen(key)) {
+	piece = walkSeq(mk, function (vss, vsk) {
+	  var acc = [];
+	  for (var i = 0; i < vss.length; i++) {
+	    var vs = vss[i];
+	    acc = acc.concat(k(transformSeqs(vs, key), vsk));
+	  }
+	  return acc;
+	});
+      } else if (is_keyClose(key)) {
+	die("trieKeys: internal error: unexpected key-close");
+      } else {
+	piece = k(key, mk);
+      }
+      if (piece === null) return null;
+      acc = acc.concat(piece);
+    });
+    return acc;
+  }
+
+  function walkSeq(m, k) {
+    if (m instanceof $WildcardSequence) return null;
+    if (m instanceof $Success) return k([], emptyTrie); // TODO: ??
+    if (m.has(__)) return null;
+    var acc = [];
+    m.forEach(function (mk, key) {
+      var piece;
+      if (is_keyClose(key)) {
+	piece = k([Immutable.List()], mk);
+      } else {
+	piece = walk(rseq(key, mk), function (v, vk) {
+	  return walkSeq(vk, function (vss, vsk) {
+	    var acc = [];
+	    for (var i = 0; i < vss.length; i++) {
+	      acc.push(vss[i].unshift(v));
+	    }
+	    return k(acc, vsk);
+	  });
+	});
+      }
+      if (piece === null) return null;
+      acc = acc.concat(piece);
+    });
+    return acc;
+  }
+
+  function transformSeqs(vs, opener) {
+    if (opener === SOA) return vs;
+    die("Internal error: unknown opener " + opener);
+  }
+}
+
+function captureToObject(captures, compiledProjection) {
+  var d = {};
+  captures.forEach(function (key, index) {
+    d[compiledProjection.names[index] || ('$' + index)] = key;
+  });
+  return d;
+}
+
+function trieKeysToObjects(trieKeysResult, compiledProjection) {
+  if (trieKeysResult === null) return null;
+  return trieKeysResult.toList().map(function (e) { return captureToObject(e, compiledProjection); });
+}
+
+function projectObjects(m, compiledProjection) {
+  return trieKeysToObjects(trieKeys(project(m, compiledProjection)), compiledProjection);
+}
+
+function prettyTrie(m, initialIndent) {
+  var acc = [];
+  walk(initialIndent || 0, m);
+  return acc.join('');
+
+  function walk(i, m) {
+    if (m instanceof $WildcardSequence) {
+      acc.push("...>");
+      walk(i + 4, m.trie);
+      return;
+    }
+    if (m instanceof $Success) {
+      var v = m.value;
+      if (Immutable.Set.isSet(v)) { v = v.toArray(); }
+      acc.push("{" + JSON.stringify(v) + "}");
+      return;
+    }
+
+    if (m.size === 0) {
+      acc.push("::: nothing");
+      return;
+    }
+
+    var needSep = false;
+    m.toOrderedMap()
+      .sortBy(function (k, key) { return key })
+      .forEach(function (k, key) {
+	if (needSep) {
+	  acc.push("\n");
+	  acc.push(indentStr(i));
+	} else {
+	  needSep = true;
+	}
+	acc.push(" ");
+	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);
+      });
+  }
+
+  function indentStr(i) {
+    return new Array(i + 1).join(' '); // eww
+  }
+}
+
+///////////////////////////////////////////////////////////////////////////
+
+module.exports.__ = __;
+module.exports.SOA = SOA;
+module.exports.EOA = EOA;
+module.exports.$Capture = $Capture;
+module.exports.$Special = $Special;
+module.exports.makeStructureConstructor = makeStructureConstructor;
+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.triePruneBranch = triePruneBranch;
+module.exports.trieStep = trieStep;
+module.exports.relabel = relabel;
+module.exports.compileProjection = compileProjection;
+module.exports.projectionToPattern = projectionToPattern;
+module.exports.project = project;
+module.exports.trieKeys = trieKeys;
+module.exports.captureToObject = captureToObject;
+module.exports.trieKeysToObjects = trieKeysToObjects;
+module.exports.projectObjects = projectObjects;
+module.exports.prettyTrie = prettyTrie;
+
+// For testing
+module.exports._testing = {
+  rsuccess: rsuccess,
+  rseq: rseq,
+  rwild: rwild,
+  rwildseq: rwildseq
+};
diff --git a/js/src/seal.js b/js/src/seal.js
new file mode 100644
index 0000000..9cde479
--- /dev/null
+++ b/js/src/seal.js
@@ -0,0 +1,18 @@
+var Immutable = require('immutable');
+
+function Seal(contents) {
+  this.sealContents = contents;
+  Object.freeze(this);
+}
+
+Seal.prototype.equals = function (other) {
+  if (!(other instanceof Seal)) return false;
+  return Immutable.is(this.sealContents, other.sealContents);
+};
+
+function seal(contents) {
+  return new Seal(contents);
+}
+
+module.exports.Seal = Seal;
+module.exports.seal = seal;
diff --git a/js/src/util.js b/js/src/util.js
new file mode 100644
index 0000000..0961ca9
--- /dev/null
+++ b/js/src/util.js
@@ -0,0 +1,25 @@
+"use strict";
+
+var Reflect = require("./reflect.js");
+
+module.exports.extend = function (what, _with) {
+  for (var prop in _with) {
+    if (_with.hasOwnProperty(prop)) {
+      what[prop] = _with[prop];
+    }
+  }
+  return what;
+};
+
+module.exports.kwApply = function (f, thisArg, args) {
+  var formals = Reflect.formalParameters(f);
+  var actuals = []
+  for (var i = 0; i < formals.length; i++) {
+    var formal = formals[i];
+    if (!(formal in args)) {
+      throw new Error("Function parameter '"+formal+"' not present in args");
+    }
+    actuals.push(args[formal]);
+  }
+  return f.apply(thisArg, actuals);
+};
diff --git a/js/test/test-mux.js b/js/test/test-mux.js
new file mode 100644
index 0000000..24eb348
--- /dev/null
+++ b/js/test/test-mux.js
@@ -0,0 +1,275 @@
+"use strict";
+
+var expect = require('expect.js');
+var Immutable = require('immutable');
+
+var Route = require('../src/route.js');
+var Patch = require('../src/patch.js');
+var Mux = require('../src/mux.js');
+
+var __ = Route.__;
+var _$ = Route._$;
+
+function checkPrettyTrie(m, expected) {
+  expect(Route.prettyTrie(m)).to.equal(expected.join('\n'));
+}
+
+function checkPrettyPatch(p, expectedAdded, expectedRemoved) {
+  expect(p.pretty()).to.equal(
+    ('<<<<<<<< Removed:\n' + expectedRemoved.join('\n') + '\n' +
+     '======== Added:\n' + expectedAdded.join('\n') + '\n' +
+     '>>>>>>>>'));
+}
+
+function getM() {
+  var m = new Mux.Mux();
+  expect(m.addStream(Patch.assert(1).andThen(Patch.assert(2))).pid).to.equal(0);
+  expect(m.addStream(Patch.assert(3).andThen(Patch.assert(2))).pid).to.equal(1);
+  return m;
+}
+
+describe('mux stream', function () {
+  describe('addition', function () {
+    it('should union interests appropriately', function () {
+      var m = getM();
+      checkPrettyTrie(m.routingTable, [' 1 >{[0]}',
+				       ' 2 >{[0,1]}',
+				       ' 3 >{[1]}']);
+      checkPrettyTrie(m.interestsOf(0), [' 1 >{[0]}',
+					 ' 2 >{[0]}']);
+      checkPrettyTrie(m.interestsOf(1), [' 2 >{[1]}',
+					 ' 3 >{[1]}']);
+    });
+  });
+
+  describe('update', function () {
+    it('should update interests appropriately', function () {
+      var rawPatch =
+	  Patch.assert(1)
+	  .andThen(Patch.retract(2))
+	  .andThen(Patch.retract(3))
+	  .andThen(Patch.assert(4))
+	  .andThen(Patch.retract(99));
+      checkPrettyPatch(rawPatch,
+		       [' 1 >{true}',
+		        ' 4 >{true}'],
+		       [' 2 >{true}',
+		        ' 3 >{true}',
+		        ' 99 >{true}']);
+
+      var m = getM();
+      var updateStreamResult = m.updateStream(1, rawPatch);
+      expect(updateStreamResult.pid).to.equal(1);
+      checkPrettyPatch(updateStreamResult.delta,
+		       [' 1 >{[1]}',
+			' 4 >{[1]}'],
+		       [' 2 >{[1]}',
+			' 3 >{[1]}']);
+      checkPrettyTrie(m.routingTable, [' 1 >{[0,1]}',
+				       ' 2 >{[0]}',
+				       ' 4 >{[1]}']);
+      checkPrettyTrie(m.interestsOf(0), [' 1 >{[0]}',
+					 ' 2 >{[0]}']);
+      checkPrettyTrie(m.interestsOf(1), [' 1 >{[1]}',
+					 ' 4 >{[1]}']);
+      checkPrettyPatch(updateStreamResult.deltaAggregate,
+		       [' 4 >{[1]}'],
+		       [' 3 >{[1]}']);
+    });
+  });
+
+  describe('removal', function () {
+    it('should remove streams properly', function () {
+      var m = getM();
+      var updateStreamResult = m.removeStream(1);
+      expect(updateStreamResult.pid).to.equal(1);
+      checkPrettyPatch(updateStreamResult.delta,
+		       ['::: nothing'],
+		       [' 2 >{[1]}',
+			' 3 >{[1]}']);
+      checkPrettyTrie(m.routingTable, [' 1 >{[0]}',
+				       ' 2 >{[0]}']);
+      checkPrettyTrie(m.interestsOf(0), [' 1 >{[0]}',
+					 ' 2 >{[0]}']);
+      checkPrettyTrie(m.interestsOf(1), ['::: nothing']);
+      checkPrettyPatch(updateStreamResult.deltaAggregate,
+		       ['::: nothing'],
+		       [' 3 >{[1]}']);
+    });
+  });
+});
+
+describe('computeEvents', function () {
+  describe('for new assertions and existing specific interest', function () {
+    var oldM = new Mux.Mux();
+    oldM.addStream(Patch.sub(1));
+    var newM = oldM.shallowCopy();
+    var updateStreamResult = newM.addStream(Patch.assert(1).andThen(Patch.assert(2)));
+    var events = Mux.computeEvents(oldM, newM, updateStreamResult);
+
+    it('should yield just the assertion of interest', function () {
+      expect(events.eventMap.size).to.be(1);
+      expect(events.metaEvents.size).to.be(1);
+      expect(events.eventMap.get(0).strip().equals(Patch.assert(1))).to.be(true);
+      expect(events.metaEvents.get(0).equals(Patch.emptyPatch)).to.be(true);
+    });
+  });
+
+  describe('for removed assertions', function () {
+    var oldM = new Mux.Mux();
+    oldM.addStream(Patch.sub([__]));
+    var newM = oldM.shallowCopy();
+    newM.addStream(Patch.assert([1]).andThen(Patch.assert([2])));
+    var updateStreamResult = newM.updateStream(1, Patch.retract([1]));
+    var events = Mux.computeEvents(oldM, newM, updateStreamResult);
+
+    it('should yield just the specific retraction', function () {
+      expect(events.eventMap.size).to.be(1);
+      expect(events.metaEvents.size).to.be(1);
+      expect(events.eventMap.get(0).strip().equals(Patch.retract([1]))).to.be(true);
+      expect(events.metaEvents.get(0).equals(Patch.emptyPatch)).to.be(true);
+    });
+  });
+
+  describe('for new assertions and existing general interest', function () {
+    var oldM = new Mux.Mux();
+    oldM.addStream(Patch.sub([__]));
+    var newM = oldM.shallowCopy();
+    var updateStreamResult = newM.addStream(Patch.assert([1]).andThen(Patch.assert([2])));
+    var events = Mux.computeEvents(oldM, newM, updateStreamResult);
+
+    it('should yield both new assertions', function () {
+      expect(events.eventMap.size).to.be(1);
+      expect(events.metaEvents.size).to.be(1);
+      expect(events.eventMap.get(0).strip().equals(Patch.assert([1]).andThen(Patch.assert([2]))))
+	.to.be(true);
+      expect(events.metaEvents.get(0).equals(Patch.emptyPatch)).to.be(true);
+    });
+  })
+
+  describe('for new specific interest and existing assertion', function () {
+    var oldM = new Mux.Mux();
+    oldM.addStream(Patch.assert(1).andThen(Patch.assert(2)));
+    var newM = oldM.shallowCopy();
+    var updateStreamResult = newM.addStream(Patch.sub(1));
+    var events = Mux.computeEvents(oldM, newM, updateStreamResult);
+
+    it('should yield just the assertion of interest', function () {
+      expect(events.eventMap.size).to.be(1);
+      expect(events.metaEvents.size).to.be(1);
+      expect(events.eventMap.get(1).strip().equals(Patch.assert(1))).to.be(true);
+      expect(events.metaEvents.get(0).equals(Patch.emptyPatch)).to.be(true);
+    });
+  });
+
+  describe('for new general interest and existing assertion', function () {
+    var oldM = new Mux.Mux();
+    oldM.addStream(Patch.assert([1]).andThen(Patch.assert([2])));
+    var newM = oldM.shallowCopy();
+    var updateStreamResult = newM.addStream(Patch.sub([__]));
+    var events = Mux.computeEvents(oldM, newM, updateStreamResult);
+
+    it('should yield just the assertion of interest', function () {
+      expect(events.eventMap.size).to.be(1);
+      expect(events.metaEvents.size).to.be(1);
+      expect(events.eventMap.get(1).strip().equals(Patch.assert([1]).andThen(Patch.assert([2]))))
+	.to.be(true);
+      expect(events.metaEvents.get(0).equals(Patch.emptyPatch)).to.be(true);
+    });
+  });
+
+  describe('for removed general interest', function () {
+    var oldM = new Mux.Mux();
+    oldM.addStream(Patch.assert([1]).andThen(Patch.assert([2])));
+    oldM.addStream(Patch.sub([__]));
+    var newM = oldM.shallowCopy();
+    var updateStreamResult = newM.updateStream(1, Patch.unsub([__]));
+    var events = Mux.computeEvents(oldM, newM, updateStreamResult);
+
+    it('should yield both retractions', function () {
+      expect(events.eventMap.size).to.be(1);
+      expect(events.metaEvents.size).to.be(1);
+      expect(events.eventMap.get(1).strip().equals(Patch.retract([1]).andThen(Patch.retract([2]))))
+	.to.be(true);
+      expect(events.metaEvents.get(0).equals(Patch.emptyPatch)).to.be(true);
+    });
+  });
+
+  describe('for removed specific interest', function () {
+    it('should yield three appropriate events for three intercessions', function () {
+      var oldM = new Mux.Mux();
+      oldM.addStream(Patch.assert([1]).andThen(Patch.assert([2])));
+      oldM.addStream(Patch.sub([__]));
+      var newM = oldM.shallowCopy();
+      var updateStreamResult = newM.updateStream(1, Patch.unsub([1]));
+      var events = Mux.computeEvents(oldM, newM, updateStreamResult);
+
+      expect(events.eventMap.size).to.be(1);
+      expect(events.metaEvents.size).to.be(1);
+      expect(events.eventMap.get(1).strip().equals(Patch.retract([1]))).to.be(true);
+      expect(events.metaEvents.get(0).equals(Patch.emptyPatch)).to.be(true);
+
+      oldM = newM;
+      newM = oldM.shallowCopy();
+      events = Mux.computeEvents(oldM, newM, newM.updateStream(1, Patch.unsub([2])));
+
+      expect(events.eventMap.size).to.be(1);
+      expect(events.metaEvents.size).to.be(1);
+      expect(events.eventMap.get(1).strip().equals(Patch.retract([2])))
+	.to.be(true);
+      expect(events.metaEvents.get(0).equals(Patch.emptyPatch)).to.be(true);
+
+      oldM = newM;
+      newM = oldM.shallowCopy();
+      events = Mux.computeEvents(oldM, newM, newM.updateStream(0, Patch.assert([3])));
+
+      expect(events.eventMap.size).to.be(1);
+      expect(events.metaEvents.size).to.be(1);
+      expect(events.eventMap.get(1).strip().equals(Patch.assert([3]))).to.be(true);
+      expect(events.metaEvents.get(0).equals(Patch.emptyPatch)).to.be(true);
+    });
+  });
+
+  describe('for new meta assertion', function () {
+    var oldM = new Mux.Mux();
+    var newM = oldM.shallowCopy();
+    var updateStreamResult = newM.addStream(Patch.assert(Patch.atMeta(1)).andThen(Patch.assert(2)));
+    var events = Mux.computeEvents(oldM, newM, updateStreamResult);
+
+    it('should yield no local events and one meta event', function () {
+      expect(events.eventMap.size).to.be(0);
+      expect(events.metaEvents.size).to.be(1);
+      expect(events.metaEvents.get(0).strip().equals(Patch.assert(1))).to.be(true);
+    });
+  });
+
+  describe('for adding supply and demand simultaneously', function () {
+    var oldM = new Mux.Mux();
+    var newM = oldM.shallowCopy();
+    var updateStreamResult = newM.addStream(Patch.assert(1).andThen(Patch.sub(1)));
+    var events = Mux.computeEvents(oldM, newM, updateStreamResult);
+
+    it('should send a patch back', function () {
+      expect(events.eventMap.size).to.be(1);
+      expect(events.metaEvents.size).to.be(1);
+      expect(events.eventMap.get(0).strip().equals(Patch.assert(1))).to.be(true);
+      expect(events.metaEvents.get(0).equals(Patch.emptyPatch)).to.be(true);
+    });
+  });
+
+  describe('for a no-op but nonempty patch', function () {
+    var oldM = new Mux.Mux();
+    var pid1 = oldM.addStream(Patch.assert(["fieldContents", "initial", 7])).pid;
+    var pid2 = oldM.addStream(Patch.sub(["fieldContents", __, __])).pid;
+    var newM = oldM.shallowCopy();
+    var unclampedPatch =
+	Patch.retract(["fieldContents", __, __])
+	.andThen(Patch.assert(["fieldContents", "initial", 7]));
+    var updateStreamResult = newM.updateStream(pid1, unclampedPatch);
+    var events = Mux.computeEvents(oldM, newM, updateStreamResult);
+
+    it('should send no patch to the subscriber', function () {
+      expect(events.eventMap.size).to.be(0);
+    });
+  });
+});
diff --git a/js/test/test-patch.js b/js/test/test-patch.js
new file mode 100644
index 0000000..e622487
--- /dev/null
+++ b/js/test/test-patch.js
@@ -0,0 +1,141 @@
+"use strict";
+
+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 checkPrettyPatch(p, expectedAdded, expectedRemoved) {
+  expect(p.pretty()).to.equal(
+    ('<<<<<<<< Removed:\n' + expectedRemoved.join('\n') + '\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}']);
+  });
+
+  it('works for longer chains of asserts and retracts', function () {
+    var rawPatch =
+	Patch.assert(1)
+	.andThen(Patch.retract(2))
+	.andThen(Patch.retract(3))
+	.andThen(Patch.assert(4))
+	.andThen(Patch.retract(99));
+      checkPrettyPatch(rawPatch,
+		       [' 1 >{true}',
+		        ' 4 >{true}'],
+		       [' 2 >{true}',
+		        ' 3 >{true}',
+		        ' 99 >{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
new file mode 100644
index 0000000..569a6ca
--- /dev/null
+++ b/js/test/test-route.js
@@ -0,0 +1,527 @@
+"use strict";
+
+var Immutable = require('immutable');
+var expect = require('expect.js');
+var util = require('util');
+var r = require("../src/route.js");
+
+function checkPrettyTrie(m, expected) {
+  expect(r.prettyTrie(m)).to.equal(expected.join('\n'));
+}
+
+function checkTrieKeys(actual, expected) {
+  expect(actual.equals(Immutable.Set(expected).map(Immutable.List))).to.be(true);
+}
+
+describe("basic pattern compilation", function () {
+  var sAny = Immutable.Set(['mAny']);
+  var sAAny = Immutable.Set(['mAAny']);
+  var mAny = r.compilePattern(sAny, r.__);
+  var mAAny = r.compilePattern(sAAny, ['A', r.__]);
+
+  it("should print as expected", function () {
+    checkPrettyTrie(mAny, [' ★ >{["mAny"]}']);
+    checkPrettyTrie(mAAny, [' < "A" ★ > >{["mAAny"]}']);
+  });
+
+  describe("of wildcard", function () {
+    it("should match anything", function () {
+      expect(r.matchValue(mAny, 'hi')).to.eql(sAny);
+      expect(r.matchValue(mAny, ['A', 'hi'])).to.eql(sAny);
+      expect(r.matchValue(mAny, ['B', 'hi'])).to.eql(sAny);
+      expect(r.matchValue(mAny, ['A', [['hi']]])).to.eql(sAny);
+    });
+  });
+
+  describe("of A followed by wildcard", function () {
+    it("should match A followed by anything", function () {
+      expect(r.matchValue(mAAny, 'hi')).to.be(null);
+      expect(r.matchValue(mAAny, ['A', 'hi'])).to.eql(sAAny);
+      expect(r.matchValue(mAAny, ['B', 'hi'])).to.be(null);
+      expect(r.matchValue(mAAny, ['A', [['hi']]])).to.eql(sAAny);
+    });
+  });
+
+  it("should observe basic (in)equivalences", function () {
+    expect(Immutable.is(mAny, mAAny)).to.be(false);
+    expect(Immutable.is(mAny, mAny)).to.be(true);
+    expect(Immutable.is(mAAny, mAAny)).to.be(true);
+  });
+});
+
+describe("unions", function () {
+  it("should collapse common prefix wildcard", function () {
+    checkPrettyTrie(r.union(r.compilePattern(Immutable.Set(['A']), [r.__, 'A']),
+			    r.compilePattern(Immutable.Set(['B']), [r.__, 'B'])),
+		    [' < ★ "A" > >{["A"]}',
+		     '     "B" > >{["B"]}']);
+  });
+
+  it("should unroll wildcard unioned with nonwildcard", function () {
+    checkPrettyTrie(r.union(r.compilePattern(Immutable.Set(['A']), [r.__, 'A']),
+			    r.compilePattern(Immutable.Set(['W']), r.__)),
+		    [' ★ >{["W"]}',
+		     ' < ★ "A" ★...> >{["W"]}',
+		     '         > >{["W","A"]}',
+		     '     ★...> >{["W"]}',
+		     '     > >{["W"]}',
+		     '   > >{["W"]}']);
+  });
+
+  it("should properly multiply out", function () {
+    checkPrettyTrie(r.union(r.compilePattern(Immutable.Set(['A']), [r.__, 2]),
+			    r.compilePattern(Immutable.Set(['C']), [1, 3]),
+			    r.compilePattern(Immutable.Set(['B']), [3, 4])),
+		    [' < ★ 2 > >{["A"]}',
+		     '   1 2 > >{["A"]}',
+		     '     3 > >{["C"]}',
+		     '   3 2 > >{["A"]}',
+		     '     4 > >{["B"]}']);
+
+    checkPrettyTrie(r.union(r.compilePattern(Immutable.Set(['C']), [1, 3]),
+			    r.compilePattern(Immutable.Set(['B']), [3, 4])),
+		    [' < 1 3 > >{["C"]}',
+		     '   3 4 > >{["B"]}']);
+
+    checkPrettyTrie(r.union(r.compilePattern(Immutable.Set(['A']), [r.__, 2]),
+			    r.compilePattern(Immutable.Set(['C']), [1, 3])),
+		    [' < ★ 2 > >{["A"]}',
+		     '   1 2 > >{["A"]}',
+		     '     3 > >{["C"]}']);
+
+    checkPrettyTrie(r.union(r.compilePattern(Immutable.Set(['A']), [r.__, 2]),
+			    r.compilePattern(Immutable.Set(['B']), [3, 4])),
+		    [' < ★ 2 > >{["A"]}',
+		     '   3 2 > >{["A"]}',
+		     '     4 > >{["B"]}']);
+  });
+
+  it("should correctly construct intermediate values", function () {
+    var MU = r.emptyTrie;
+    MU = r.union(MU, r.compilePattern(Immutable.Set(['A']), [r.__, 2]));
+    checkPrettyTrie(MU, [' < ★ 2 > >{["A"]}']);
+    MU = r.union(MU, r.compilePattern(Immutable.Set(['C']), [1, 3]));
+    checkPrettyTrie(MU, [' < ★ 2 > >{["A"]}',
+			 '   1 2 > >{["A"]}',
+			 '     3 > >{["C"]}']);
+    MU = r.union(MU, r.compilePattern(Immutable.Set(['B']), [3, 4]));
+    checkPrettyTrie(MU, [' < ★ 2 > >{["A"]}',
+			 '   1 2 > >{["A"]}',
+			 '     3 > >{["C"]}',
+			 '   3 2 > >{["A"]}',
+			 '     4 > >{["B"]}']);
+  });
+
+  it("should handle identical patterns with different pids", function () {
+    var m = r.union(r.compilePattern(Immutable.Set('B'), [2]),
+		    r.compilePattern(Immutable.Set('C'), [3]));
+    checkPrettyTrie(m, [' < 2 > >{["B"]}',
+			'   3 > >{["C"]}']);
+    m = r.union(r.compilePattern(Immutable.Set('A'), [2]), m);
+    checkPrettyTrie(m, [' < 2 > >{["A","B"]}',
+			'   3 > >{["C"]}']);
+  });
+
+  it('should work with subtraction and wildcards', function () {
+    var x = r.compilePattern(Immutable.Set(["A"]), [r.__]);
+    var y = r.compilePattern(Immutable.Set(["A"]), ["Y"]);
+    var z = r.compilePattern(Immutable.Set(["A"]), ["Z"]);
+    var expected = [' < "Y"::: nothing',
+		    '   ★ > >{["A"]}'];
+    checkPrettyTrie(r.subtract(r.union(x, z), y), expected);
+    checkPrettyTrie(r.union(r.subtract(x, y), z), expected);
+  });
+});
+
+describe("projections", function () {
+  describe("with picky structure", function () {
+    var proj = r.compileProjection(r._$("v", [[r.__]]));
+
+    it("should include things that match as well as wildcards", function () {
+      checkPrettyTrie(r.project(r.union(r.compilePattern(Immutable.Set(['A']), r.__),
+					r.compilePattern(Immutable.Set(['B']), [['b']])),
+				proj),
+		      [' < < ★ > > >{["A"]}',
+		       '     "b" > > >{["A","B"]}']);
+    });
+
+    it("should exclude things that lack the required structure", function () {
+      checkPrettyTrie(r.project(r.union(r.compilePattern(Immutable.Set(['A']), r.__),
+					r.compilePattern(Immutable.Set(['B']), ['b'])),
+				proj),
+		      [' < < ★ > > >{["A"]}']);
+    });
+  });
+
+  describe("simple positional", function () {
+    var proj = r.compileProjection([r._$, r._$]);
+
+    it("should collapse common prefixes", function () {
+      checkPrettyTrie(r.project(r.union(r.compilePattern(Immutable.Set(['A']), [1, 2]),
+					r.compilePattern(Immutable.Set(['C']), [1, 3]),
+					r.compilePattern(Immutable.Set(['B']), [3, 4])),
+				proj),
+		      [' 1 2 >{["A"]}',
+		       '   3 >{["C"]}',
+		       ' 3 4 >{["B"]}']);
+    });
+
+    it("should yield a correct set of results", function () {
+      var u = r.union(r.compilePattern(Immutable.Set(['A']), [1, 2]),
+		      r.compilePattern(Immutable.Set(['C']), [1, 3]),
+		      r.compilePattern(Immutable.Set(['B']), [3, 4]));
+      checkTrieKeys(r.trieKeys(r.project(u, proj)), [[1, 2], [1, 3], [3, 4]]);
+    });
+  });
+});
+
+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"]);
+  });
+
+  it("should be idempotent if the subtrahend doesn't overlap the minuend", function () {
+    checkPrettyTrie(r.compilePattern(true, 1),
+		    [' 1 >{true}']);
+    checkPrettyTrie(r.subtract(r.compilePattern(true, 1),
+			       r.compilePattern(true, 2)),
+		    [' 1 >{true}']);
+    checkPrettyTrie(r.subtract(r.compilePattern(true, 1),
+			       r.compilePattern(true, 2),
+			       function (v1, v2) { return null; }),
+		    [' 1 >{true}']);
+  });
+});
+
+describe("subtract after union", function () {
+  var R1 = r.compilePattern(Immutable.Set(['A']), [r.__, "B"]);
+  var R2 = r.compilePattern(Immutable.Set(['B']), ["A", r.__]);
+  var R12 = r.union(R1, R2);
+
+  it("should have sane preconditions", function () { // Am I doing this right?
+    checkPrettyTrie(R1, [' < ★ "B" > >{["A"]}']);
+    checkPrettyTrie(R2, [' < "A" ★ > >{["B"]}']);
+    checkPrettyTrie(R12, [' < "A" "B" > >{["B","A"]}',
+			  '       ★ > >{["B"]}',
+			  '   ★ "B" > >{["A"]}']);
+  });
+
+  it("should yield the remaining ingredients of the union", function () {
+    expect(Immutable.is(r.subtract(R12, R1), R2)).to.be(true);
+    expect(Immutable.is(r.subtract(R12, R2), R1)).to.be(true);
+    expect(Immutable.is(r.subtract(R12, R1), R1)).to.be(false);
+  });
+});
+
+describe("trie equality", function () {
+  it("should not rely on object identity", function () {
+    expect(Immutable.is(r.union(r.compilePattern(Immutable.Set(['A']), [r.__, 'A']),
+				r.compilePattern(Immutable.Set(['B']), [r.__, 'B'])),
+			r.union(r.compilePattern(Immutable.Set(['A']), [r.__, 'A']),
+				r.compilePattern(Immutable.Set(['B']), [r.__, 'B']))))
+      .to.be(true);
+  });
+
+  it("should respect commutativity of union", function () {
+    expect(Immutable.is(r.union(r.compilePattern(Immutable.Set(['A']), [r.__, 'A']),
+				r.compilePattern(Immutable.Set(['B']), [r.__, 'B'])),
+			r.union(r.compilePattern(Immutable.Set(['B']), [r.__, 'B']),
+				r.compilePattern(Immutable.Set(['A']), [r.__, 'A']))))
+      .to.be(true);
+  });
+});
+
+describe("trieKeys on wild tries", function () {
+  var M = r.union(r.compilePattern(Immutable.Set(['A']), [r.__, 2]),
+		  r.compilePattern(Immutable.Set(['C']), [1, 3]),
+		  r.compilePattern(Immutable.Set(['B']), [3, 4]));
+
+  it("should yield null to signal an infinite result", function () {
+    expect(r.trieKeys(r.project(M, r.compileProjection([r._$, r._$])))).to.be(null);
+  });
+
+  it("should extract just the second array element successfully", function () {
+    checkTrieKeys(r.trieKeys(r.project(M, r.compileProjection([r.__, r._$]))),
+		  [[2],[3],[4]]);
+  });
+
+  var M2 = r.project(M, r.compileProjection([r._$, r._$]));
+
+  it("should survive double-projection", function () {
+    checkTrieKeys(r.trieKeys(r.project(M2, r.compileProjection(r.__, r._$))),
+		  [[2],[3],[4]]);
+  });
+
+  it("should survive embedding and reprojection", function () {
+    checkTrieKeys(r.trieKeys(r.project(r.compilePattern(true, [r.embeddedTrie(M2)]),
+				       r.compileProjection([r.__, r._$]))),
+		  [[2],[3],[4]]);
+    checkTrieKeys(r.trieKeys(r.project(r.compilePattern(true, [[r.embeddedTrie(M2)]]),
+				       r.compileProjection([[r.__, r._$]]))),
+		  [[2],[3],[4]]);
+  });
+});
+
+describe("trieKeys using multiple-values in projections", function () {
+  var M = r.union(r.compilePattern(Immutable.Set(['A']), [1, 2]),
+		  r.compilePattern(Immutable.Set(['C']), [1, 3]),
+		  r.compilePattern(Immutable.Set(['B']), [3, 4]));
+  var proj = r.compileProjection([r._$, r._$]);
+  var M2 = r.project(M, proj);
+
+  it("should be able to extract ordinary values", function () {
+    checkTrieKeys(r.trieKeys(M2), [[1,2],[1,3],[3,4]]);
+  });
+
+  it("should be able to be reprojected as a sequence of more than one value", function () {
+    checkTrieKeys(r.trieKeys(r.project(M2, r.compileProjection(r._$, r._$))),
+		  [[1,2],[1,3],[3,4]]);
+  });
+
+  it("should be convertible into objects with $-indexed fields", function () {
+    expect(r.trieKeysToObjects(r.trieKeys(M2), proj).toArray())
+      .to.eql([{'$0': 3, '$1': 4}, {'$0': 1, '$1': 2}, {'$0': 1, '$1': 3}]);
+    expect(r.projectObjects(M, proj).toArray())
+      .to.eql([{'$0': 3, '$1': 4}, {'$0': 1, '$1': 2}, {'$0': 1, '$1': 3}]);
+  });
+});
+
+describe("trieKeys using multiple-values in projections, with names", function () {
+  var M = r.union(r.compilePattern(Immutable.Set(['A']), [1, 2]),
+		  r.compilePattern(Immutable.Set(['C']), [1, 3]),
+		  r.compilePattern(Immutable.Set(['B']), [3, 4]));
+
+  it("should yield named fields", function () {
+    expect(r.projectObjects(M, r.compileProjection([r._$("fst"), r._$("snd")])).toArray())
+      .to.eql([{'fst': 3, 'snd': 4}, {'fst': 1, 'snd': 2}, {'fst': 1, 'snd': 3}]);
+  });
+
+  it("should yield numbered fields where names are missing", function () {
+    expect(r.projectObjects(M, r.compileProjection([r._$, r._$("snd")])).toArray())
+      .to.eql([{'$0': 3, 'snd': 4}, {'$0': 1, 'snd': 2}, {'$0': 1, 'snd': 3}]);
+    expect(r.projectObjects(M, r.compileProjection([r._$("fst"), r._$])).toArray())
+      .to.eql([{'fst': 3, '$1': 4}, {'fst': 1, '$1': 2}, {'fst': 1, '$1': 3}]);
+  });
+});
+
+describe("complex erasure", function () {
+  var A = r.compilePattern(Immutable.Set(['A']), r.__);
+  var B = r.union(r.compilePattern(Immutable.Set(['B']), [[[["foo"]]]]),
+		  r.compilePattern(Immutable.Set(['B']), [[[["bar"]]]]));
+  describe("after a union", function () {
+    var R0 = r.union(A, B);
+    var R1a = r.subtract(R0, B);
+    var R1b = r.subtract(R0, A);
+
+    it("should yield the other parts of the union", function () {
+      expect(Immutable.is(R1a, A)).to.be(true);
+      expect(Immutable.is(R1b, B)).to.be(true);
+    });
+  });
+});
+
+describe("embedding tries in patterns", function () {
+  var M1a =
+      r.compilePattern(Immutable.Set(['A']),
+		       [1, r.embeddedTrie(r.compilePattern(Immutable.Set(['B']), [2, 3])), 4]);
+  var M1b =
+      r.compilePattern(Immutable.Set(['A']), [1, [2, 3], 4]);
+  var M2a =
+      r.compilePattern(Immutable.Set(['A']),
+		       [r.embeddedTrie(r.compilePattern(Immutable.Set(['B']), [1, 2])),
+		        r.embeddedTrie(r.compilePattern(Immutable.Set(['C']), [3, 4]))]);
+  var M2b =
+      r.compilePattern(Immutable.Set(['A']), [[1, 2], [3, 4]]);
+
+  it("should yield tries equivalent to the original patterns", function () {
+    expect(Immutable.is(M1a, M1b)).to.be(true);
+    expect(Immutable.is(M2a, M2b)).to.be(true);
+  });
+});
+
+describe("calls to matchPattern", function () {
+  it("should yield appropriately-named/-numbered fields", function () {
+    expect(r.matchPattern([1, 2, 3], [r.__, 2, r._$])).to.eql({'$0': 3, 'length': 1});
+    expect(r.matchPattern([1, 2, 3], [r.__, 2, r._$("three")])).to.eql({'three': 3, 'length': 1});
+    expect(r.matchPattern([1, 2, 3], [r._$, 2, r._$("three")]))
+      .to.eql({'$0': 1, 'three': 3, 'length': 2});
+    expect(r.matchPattern([1, 2, 3], [r._$("one"), 2, r._$]))
+      .to.eql({'one': 1, '$1': 3, 'length': 2});
+    expect(r.matchPattern([1, 2, 3], [r._$("one"), 2, r._$("three")]))
+      .to.eql({'one': 1, 'three': 3, 'length': 2});
+  });
+
+  it("should fail on value mismatch", function () {
+    expect(r.matchPattern([1, 2, 3], [r.__, 999, r._$("three")])).to.be(null);
+  });
+
+  it("should fail on array length mismatch", function () {
+    expect(r.matchPattern([1, 2, 3], [r.__, 2, r._$("three"), 4])).to.be(null);
+  });
+
+  it("matches substructure", function () {
+    expect(r.matchPattern([1, [2, 999], 3], [r._$("one"), r._$(null, [2, r.__]), r._$("three")]))
+      .to.eql({ one: 1, '$1': [ 2, 999 ], three: 3, length: 3 });
+    expect(r.matchPattern([1, [2, 999], 3], [r._$("one"), r._$("two", [2, r.__]), r._$("three")]))
+      .to.eql({ one: 1, two: [ 2, 999 ], three: 3, length: 3 });
+    expect(r.matchPattern([1, [999, 2], 3], [r._$("one"), r._$(null, [2, r.__]), r._$("three")]))
+      .to.be(null);
+    expect(r.matchPattern([1, [999, 2], 3], [r._$("one"), r._$("two", [2, r.__]), r._$("three")]))
+      .to.be(null);
+  });
+
+  it("matches nested captures", function () {
+    expect(r.matchPattern([1, [2, 999], 3], [r._$("one"), r._$(null, [2, r._$]), r._$("three")]))
+      .to.eql({ one: 1, '$2': 999, '$1': [ 2, 999 ], three: 3, length: 4 });
+    expect(r.matchPattern([1, [2, 999], 3], [r._$("one"), r._$("two", [2, r._$]), r._$("three")]))
+      .to.eql({ one: 1, '$2': 999, two: [ 2, 999 ], three: 3, length: 4 });
+  });
+
+  it("matches structures", function () {
+    var ctor = r.makeStructureConstructor('foo', ['bar', 'zot']);
+    expect(r.matchPattern(ctor(123, 234), ctor(r._$("bar"), r._$("zot"))))
+      .to.eql({ bar: 123, zot: 234, length: 2 });
+    expect(r.matchPattern(["foo", 123, 234], ctor(r._$("bar"), r._$("zot"))))
+      .to.eql({ bar: 123, zot: 234, length: 2 });
+    expect(r.matchPattern(ctor(123, 234), ["foo", r._$("bar"), r._$("zot")]))
+      .to.eql({ bar: 123, zot: 234, length: 2 });
+  });
+});
+
+describe("Projection with no captures", function () {
+  it("should yield the empty sequence when there's a match", function () {
+    var emptySequence = [' >{["A"]}'];
+
+    checkPrettyTrie(r.project(r.compilePattern(Immutable.Set(['A']), ["X", r.__]),
+			      r.compileProjection(r.__)),
+		    emptySequence);
+    checkPrettyTrie(r.project(r.compilePattern(Immutable.Set(['A']), ["X", r.__]),
+			      r.compileProjection([r.__, r.__])),
+		    emptySequence);
+    checkPrettyTrie(r.project(r.compilePattern(Immutable.Set(['A']), ["X", r.__]),
+			      r.compileProjection(["X", r.__])),
+		    emptySequence);
+  });
+
+  it("should yield the empty trie when there's no match", function () {
+    expect(r.project(r.compilePattern(Immutable.Set(['A']), ["X", r.__]),
+		     r.compileProjection(["Y", r.__]))).to.be(r.emptyTrie);
+  });
+
+  it("should yield nonempty sequences when there are captures after all", function () {
+    checkPrettyTrie(r.project(r.compilePattern(Immutable.Set(['A']), ["X", r.__]),
+			      r.compileProjection([r.__, r._$])),
+		    [' ★ >{["A"]}']);
+    checkPrettyTrie(r.project(r.compilePattern(Immutable.Set(['A']), ["X", r.__]),
+			      r.compileProjection([r._$, r._$])),
+		    [' "X" ★ >{["A"]}']);
+  });
+});
+
+describe('trieStep', function () {
+  it('should expand wildcard when given SOA', function () {
+    expect(Immutable.is(r.trieStep(r.compilePattern(true, r.__), r.SOA),
+			r._testing.rwildseq(r._testing.rseq(r.EOA, r._testing.rsuccess(true)))))
+      .to.be(true);
+  });
+});
+
+describe('intersect', function () {
+  it('should compute no-op patch limits properly', function () {
+    var x = r.compilePattern(Immutable.Set([0]), ["fieldContents", r.__, r.__]);
+    var y = r.compilePattern(Immutable.Set([0]), ["fieldContents", "initial", 7]);
+    checkPrettyTrie(r.subtract(x, y), [
+      ' < "fieldContents" ★ ★ > >{[0]}',
+      '                   "initial" ★ > >{[0]}',
+      '                             7::: nothing']);
+    checkPrettyTrie(r.intersect(r.subtract(x, y), y), ['::: nothing']);
+  });
+});
+
+describe('triePruneBranch', function () {
+  it('should not affect empty trie', function () {
+    checkPrettyTrie(r.triePruneBranch(r.emptyTrie, Immutable.List([])), ['::: nothing']);
+    checkPrettyTrie(r.triePruneBranch(r.emptyTrie, Immutable.List([r.SOA])), ['::: nothing']);
+    checkPrettyTrie(r.triePruneBranch(r.emptyTrie, Immutable.List(["x"])), ['::: nothing']);
+    checkPrettyTrie(r.triePruneBranch(r.emptyTrie, Immutable.List([r.SOA, "x"])), ['::: nothing']);
+  });
+
+  it('should leave a hole in a full trie', function () {
+    var full = r.compilePattern(true, r.__);
+    checkPrettyTrie(r.triePruneBranch(full, Immutable.List([])), ['::: nothing']);
+    checkPrettyTrie(r.triePruneBranch(full, Immutable.List([r.SOA])),
+		    [' ★ >{true}',
+		     ' <::: nothing']);
+    checkPrettyTrie(r.triePruneBranch(full, Immutable.List(["x"])),
+		    [' ★ >{true}',
+		     ' "x"::: nothing']);
+    checkPrettyTrie(r.triePruneBranch(full, Immutable.List([r.SOA, "x"])),
+		    [' ★ >{true}',
+		     ' < ★...> >{true}',
+		     '   > >{true}',
+		     '   "x"::: nothing']);
+  });
+
+  it('should prune in a finite tree and leave the rest alone', function () {
+    var A = r.compilePattern(true, ["y"])
+    var B = r.union(r.compilePattern(true, ["x"]), A);
+    var C = r.union(r.compilePattern(true, "z"), B);
+    checkPrettyTrie(r.triePruneBranch(A, Immutable.List([])), ['::: nothing']);
+    checkPrettyTrie(r.triePruneBranch(B, Immutable.List([])), ['::: nothing']);
+    checkPrettyTrie(r.triePruneBranch(C, Immutable.List([])), ['::: nothing']);
+    checkPrettyTrie(r.triePruneBranch(A, Immutable.List(["z"])), [' < "y" > >{true}']);
+    checkPrettyTrie(r.triePruneBranch(B, Immutable.List(["z"])), [' < "x" > >{true}',
+								  '   "y" > >{true}']);
+    checkPrettyTrie(r.triePruneBranch(C, Immutable.List(["z"])), [' < "x" > >{true}',
+								  '   "y" > >{true}']);
+    checkPrettyTrie(r.triePruneBranch(A, Immutable.List([r.SOA])), ['::: nothing']);
+    checkPrettyTrie(r.triePruneBranch(B, Immutable.List([r.SOA])), ['::: nothing']);
+    checkPrettyTrie(r.triePruneBranch(C, Immutable.List([r.SOA])), [' "z" >{true}']);
+    checkPrettyTrie(r.triePruneBranch(A, Immutable.List([r.SOA, "x"])), [' < "y" > >{true}']);
+    checkPrettyTrie(r.triePruneBranch(B, Immutable.List([r.SOA, "x"])), [' < "y" > >{true}']);
+    checkPrettyTrie(r.triePruneBranch(C, Immutable.List([r.SOA, "x"])), [' < "y" > >{true}',
+									 ' "z" >{true}']);
+    checkPrettyTrie(r.triePruneBranch(A, Immutable.List([r.SOA, "y"])), ['::: nothing']);
+    checkPrettyTrie(r.triePruneBranch(B, Immutable.List([r.SOA, "y"])), [' < "x" > >{true}']);
+    checkPrettyTrie(r.triePruneBranch(C, Immutable.List([r.SOA, "y"])), [' < "x" > >{true}',
+									 ' "z" >{true}']);
+  });
+});
+
+describe('makeStructureConstructor', function () {
+  it('should produce the right metadata', function () {
+    var ctor = r.makeStructureConstructor('foo', ['bar', 'zot']);
+    var inst = ctor(123, 234);
+    expect(inst.$SyndicateMeta$.label).to.equal('foo');
+    expect(inst.$SyndicateMeta$.arguments).to.eql(['bar', 'zot']);
+  });
+
+  it('should produce the right instance data', function () {
+    var ctor = r.makeStructureConstructor('foo', ['bar', 'zot']);
+    var inst = ctor(123, 234);
+    expect(inst.bar).to.equal(123);
+    expect(inst.zot).to.equal(234);
+  });
+
+  it('should work with compilePattern and matchValue', function () {
+    var sA = Immutable.Set(["A"]);
+    var ctor = r.makeStructureConstructor('foo', ['bar', 'zot']);
+    var inst = ctor(123, 234);
+    var x = r.compilePattern(sA, ctor(123, r.__));
+    checkPrettyTrie(x, [' < "foo" 123 ★ > >{["A"]}']);
+    expect(r.matchValue(x, ctor(123, 234))).to.eql(sA);
+    expect(r.matchValue(x, ctor(234, 123))).to.eql(null);
+  });
+});
diff --git a/js/test/test-syndicate.js b/js/test/test-syndicate.js
new file mode 100644
index 0000000..823a6b1
--- /dev/null
+++ b/js/test/test-syndicate.js
@@ -0,0 +1,152 @@
+"use strict";
+
+var expect = require('expect.js');
+var Immutable = require('immutable');
+
+var Syndicate = require('../src/main.js');
+var Network = Syndicate.Network;
+var Patch = Syndicate.Patch;
+
+var __ = Syndicate.__;
+var _$ = Syndicate._$;
+
+function configurationTrace(bootConfiguration) {
+  var eventLog = [];
+  function trace(item) {
+    eventLog.push(item);
+  }
+
+  var G = new Syndicate.Ground(function () {
+    bootConfiguration(trace);
+  });
+
+  while (G.step()) {
+    // do nothing until G becomes inert
+  }
+
+  return eventLog;
+}
+
+function traceEvent(trace) {
+  return function(item) {
+    trace((item.type === "stateChange") ? item.patch.pretty() : item);
+  }
+}
+
+function checkTrace(bootConfiguration, expected) {
+  expect(configurationTrace(bootConfiguration)).to.eql(expected);
+}
+
+describe("configurationTrace", function() {
+  describe("with an inert configuration", function () {
+    it("should yield an empty trace", function () {
+      checkTrace(function (trace) {}, []);
+    });
+  });
+
+  describe("with a single trace in an inert configuration", function () {
+    it("should yield that trace", function () {
+      checkTrace(function (trace) { trace(1) }, [1]);
+    });
+  });
+
+  describe("with some traced communication", function () {
+    it("should yield an appropriate trace", function () {
+      checkTrace(function (trace) {
+	Network.spawn({
+	  boot: function () { return Syndicate.sub(__); },
+	  handleEvent: traceEvent(trace)
+	});
+	Network.send(123);
+	Network.send(234);
+      }, ['<<<<<<<< Removed:\n'+
+	  '::: nothing\n'+
+	  '======== Added:\n'+
+	  ' < $Observe ★ > >{[0]}\n'
+	  +'            >::: nothing\n'+
+	  '>>>>>>>>',
+	  Syndicate.message(123),
+	  Syndicate.message(234)]);
+    });
+  });
+});
+
+describe("nonempty initial routes", function () {
+  it("should be immediately signalled to the process", function () {
+    // Specifically, no Syndicate.updateRoutes([]) first.
+    checkTrace(function (trace) {
+      Network.spawn({
+	boot: function () { return Patch.assert(["A", __]); },
+	handleEvent: function (e) {}
+      });
+      Network.spawn({
+	boot: function () { return Patch.sub(["A", __]); },
+	handleEvent: traceEvent(trace)
+      });
+    }, ['<<<<<<<< Removed:\n'+
+	'::: nothing\n'+
+	'======== Added:\n'+
+	' < "A" ★ > >{[1]}\n'+
+	'>>>>>>>>']);
+  });
+});
+
+describe("nested actor with an echoey protocol", function () {
+  it("shouldn't see an echoed assertion", function () {
+    checkTrace(function (trace) {
+      Network.spawn(new Network(function () {
+	Network.spawn({
+	  boot: function () {
+	    Network.stateChange(Patch.retract("X", 1)); // happens after subs on next line!
+	    return Patch.sub("X", 1).andThen(Patch.assert("X", 1));
+	  },
+	  handleEvent: traceEvent(trace)
+	});
+      }));
+    }, ['<<<<<<<< Removed:\n'+
+	'::: nothing\n'+
+	'======== Added:\n'+
+	' < $AtMeta "X" > >{[0]}\n'+
+	'>>>>>>>>',
+	'<<<<<<<< Removed:\n'+
+	' < $AtMeta "X" > >{[0]}\n'+
+	'======== Added:\n'+
+	'::: nothing\n'+
+	'>>>>>>>>']);
+  })
+  it("shouldn't see an echoed message", function () {
+    checkTrace(function (trace) {
+      Network.spawn(new Network(function () {
+	Network.spawn({
+	  boot: function () {
+	    Network.send("X", 1); // happens after subs on next line!
+	    return Patch.sub("X", 1);
+	  },
+	  handleEvent: traceEvent(trace)
+	});
+      }));
+    }, [Syndicate.message(Patch.atMeta("X"))]);
+  });
+  it("shouldn't see an echoed assertion", function () {
+  });
+});
+
+// describe("actor with nonempty initial routes", function () {
+//   it("shouldn't see initial empty conversational context", function () {
+//     checkTrace(function (trace) {
+//       Network.spawn({
+// 	boot: function () { return [pub(["A", __])] },
+// 	handleEvent: function (e) {
+// 	  Network.spawn(new Actor(function () {
+// 	    Actor.observeAdvertisers(
+// 	      function () { return ["A", __] },
+// 	      { presence: "isPresent" },
+// 	      function () {
+// 		trace(["isPresent", this.isPresent]);
+// 	      });
+// 	  }));
+// 	}
+//       });
+//     }, [["isPresent", true]]);
+//   });
+// });
diff --git a/js/third-party/jquery-2.2.0.min.js b/js/third-party/jquery-2.2.0.min.js
new file mode 100644
index 0000000..06ac263
--- /dev/null
+++ b/js/third-party/jquery-2.2.0.min.js
@@ -0,0 +1,4 @@
+/*! jQuery v2.2.0 | (c) jQuery Foundation | jquery.org/license */
+!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=a.document,e=c.slice,f=c.concat,g=c.push,h=c.indexOf,i={},j=i.toString,k=i.hasOwnProperty,l={},m="2.2.0",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return e.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:e.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a){return n.each(this,a)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(e.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor()},push:g,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){var b=a&&a.toString();return!n.isArray(a)&&b-parseFloat(b)+1>=0},isPlainObject:function(a){return"object"!==n.type(a)||a.nodeType||n.isWindow(a)?!1:a.constructor&&!k.call(a.constructor.prototype,"isPrototypeOf")?!1:!0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?i[j.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=d.createElement("script"),b.text=a,d.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b){var c,d=0;if(s(a)){for(c=a.length;c>d;d++)if(b.call(a[d],d,a[d])===!1)break}else for(d in a)if(b.call(a[d],d,a[d])===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):g.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:h.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,e,g=0,h=[];if(s(a))for(d=a.length;d>g;g++)e=b(a[g],g,c),null!=e&&h.push(e);else for(g in a)e=b(a[g],g,c),null!=e&&h.push(e);return f.apply([],h)},guid:1,proxy:function(a,b){var c,d,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(d=e.call(arguments,2),f=function(){return a.apply(b||this,d.concat(e.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:l}),"function"==typeof Symbol&&(n.fn[Symbol.iterator]=c[Symbol.iterator]),n.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "),function(a,b){i["[object "+b+"]"]=b.toLowerCase()});function s(a){var b=!!a&&"length"in a&&a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ga(),z=ga(),A=ga(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+M+"))|)"+L+"*\\]",O=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+N+")*)|.*)\\)|)",P=new RegExp(L+"+","g"),Q=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),R=new RegExp("^"+L+"*,"+L+"*"),S=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),T=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),U=new RegExp(O),V=new RegExp("^"+M+"$"),W={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M+"|[*])"),ATTR:new RegExp("^"+N),PSEUDO:new RegExp("^"+O),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},X=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Z=/^[^{]+\{\s*\[native \w/,$=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,_=/[+~]/,aa=/'|\\/g,ba=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),ca=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},da=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(ea){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function fa(a,b,d,e){var f,h,j,k,l,o,r,s,w=b&&b.ownerDocument,x=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==x&&9!==x&&11!==x)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==x&&(o=$.exec(a)))if(f=o[1]){if(9===x){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(w&&(j=w.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(o[2])return H.apply(d,b.getElementsByTagName(a)),d;if((f=o[3])&&c.getElementsByClassName&&b.getElementsByClassName)return H.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==x)w=b,s=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(aa,"\\$&"):b.setAttribute("id",k=u),r=g(a),h=r.length,l=V.test(k)?"#"+k:"[id='"+k+"']";while(h--)r[h]=l+" "+qa(r[h]);s=r.join(","),w=_.test(a)&&oa(b.parentNode)||b}if(s)try{return H.apply(d,w.querySelectorAll(s)),d}catch(y){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(Q,"$1"),b,d,e)}function ga(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ha(a){return a[u]=!0,a}function ia(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ja(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function ka(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function la(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function na(a){return ha(function(b){return b=+b,ha(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function oa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=fa.support={},f=fa.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=fa.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ia(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ia(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Z.test(n.getElementsByClassName),c.getById=ia(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ba,ca);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return"undefined"!=typeof b.getElementsByClassName&&p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=Z.test(n.querySelectorAll))&&(ia(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ia(function(a){var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Z.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ia(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",O)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Z.test(o.compareDocumentPosition),t=b||Z.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return ka(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?ka(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},fa.matches=function(a,b){return fa(a,null,null,b)},fa.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(T,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return fa(b,n,null,[a]).length>0},fa.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},fa.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},fa.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},fa.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=fa.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=fa.selectors={cacheLength:50,createPseudo:ha,match:W,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ba,ca),a[3]=(a[3]||a[4]||a[5]||"").replace(ba,ca),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||fa.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&fa.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return W.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&U.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ba,ca).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=fa.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(P," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||fa.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ha(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ha(function(a){var b=[],c=[],d=h(a.replace(Q,"$1"));return d[u]?ha(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ha(function(a){return function(b){return fa(a,b).length>0}}),contains:ha(function(a){return a=a.replace(ba,ca),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ha(function(a){return V.test(a||"")||fa.error("unsupported lang: "+a),a=a.replace(ba,ca).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Y.test(a.nodeName)},input:function(a){return X.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:na(function(){return[0]}),last:na(function(a,b){return[b-1]}),eq:na(function(a,b,c){return[0>c?c+b:c]}),even:na(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:na(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:na(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:na(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function ra(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j,k=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(j=b[u]||(b[u]={}),i=j[b.uniqueID]||(j[b.uniqueID]={}),(h=i[d])&&h[0]===w&&h[1]===f)return k[2]=h[2];if(i[d]=k,k[2]=a(b,c,g))return!0}}}function sa(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ta(a,b,c){for(var d=0,e=b.length;e>d;d++)fa(a,b[d],c);return c}function ua(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function va(a,b,c,d,e,f){return d&&!d[u]&&(d=va(d)),e&&!e[u]&&(e=va(e,f)),ha(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ta(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:ua(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=ua(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=ua(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function wa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ra(function(a){return a===b},h,!0),l=ra(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[ra(sa(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return va(i>1&&sa(m),i>1&&qa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(Q,"$1"),c,e>i&&wa(a.slice(i,e)),f>e&&wa(a=a.slice(e)),f>e&&qa(a))}m.push(c)}return sa(m)}function xa(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=F.call(i));u=ua(u)}H.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&fa.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ha(f):f}return h=fa.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=wa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,xa(e,d)),f.selector=a}return f},i=fa.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ba,ca),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=W.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ba,ca),_.test(j[0].type)&&oa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&qa(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,!b||_.test(a)&&oa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ia(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ia(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ja("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ia(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ja("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ia(function(a){return null==a.getAttribute("disabled")})||ja(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),fa}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.uniqueSort=n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},v=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},w=n.expr.match.needsContext,x=/^<([\w-]+)\s*\/?>(?:<\/\1>|)$/,y=/^.[^:#\[\.,]*$/;function z(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(y.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return h.call(b,a)>-1!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(z(this,a||[],!1))},not:function(a){return this.pushStack(z(this,a||[],!0))},is:function(a){return!!z(this,"string"==typeof a&&w.test(a)?n(a):a||[],!1).length}});var A,B=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,C=n.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||A,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:B.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),x.test(e[1])&&n.isPlainObject(b))for(e in b)n.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&f.parentNode&&(this.length=1,this[0]=f),this.context=d,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?void 0!==c.ready?c.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};C.prototype=n.fn,A=n(d);var D=/^(?:parents|prev(?:Until|All))/,E={children:!0,contents:!0,next:!0,prev:!0};n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=w.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?h.call(n(a),this[0]):h.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.uniqueSort(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function F(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return u(a,"parentNode")},parentsUntil:function(a,b,c){return u(a,"parentNode",c)},next:function(a){return F(a,"nextSibling")},prev:function(a){return F(a,"previousSibling")},nextAll:function(a){return u(a,"nextSibling")},prevAll:function(a){return u(a,"previousSibling")},nextUntil:function(a,b,c){return u(a,"nextSibling",c)},prevUntil:function(a,b,c){return u(a,"previousSibling",c)},siblings:function(a){return v((a.parentNode||{}).firstChild,a)},children:function(a){return v(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(E[a]||n.uniqueSort(e),D.test(a)&&e.reverse()),this.pushStack(e)}});var G=/\S+/g;function H(a){var b={};return n.each(a.match(G)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?H(a):n.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),h>=c&&h--}),this},has:function(a){return a?n.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().progress(c.notify).done(c.resolve).fail(c.reject):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=e.call(arguments),d=c.length,f=1!==d||a&&n.isFunction(a.promise)?d:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?e.call(arguments):d,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(d>1)for(i=new Array(d),j=new Array(d),k=new Array(d);d>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().progress(h(b,j,i)).done(h(b,k,c)).fail(g.reject):--f;return f||g.resolveWith(k,c),g.promise()}});var I;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(I.resolveWith(d,[n]),n.fn.triggerHandler&&(n(d).triggerHandler("ready"),n(d).off("ready"))))}});function J(){d.removeEventListener("DOMContentLoaded",J),a.removeEventListener("load",J),n.ready()}n.ready.promise=function(b){return I||(I=n.Deferred(),"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(n.ready):(d.addEventListener("DOMContentLoaded",J),a.addEventListener("load",J))),I.promise(b)},n.ready.promise();var K=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)K(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},L=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function M(){this.expando=n.expando+M.uid++}M.uid=1,M.prototype={register:function(a,b){var c=b||{};return a.nodeType?a[this.expando]=c:Object.defineProperty(a,this.expando,{value:c,writable:!0,configurable:!0}),a[this.expando]},cache:function(a){if(!L(a))return{};var b=a[this.expando];return b||(b={},L(a)&&(a.nodeType?a[this.expando]=b:Object.defineProperty(a,this.expando,{value:b,configurable:!0}))),b},set:function(a,b,c){var d,e=this.cache(a);if("string"==typeof b)e[b]=c;else for(d in b)e[d]=b[d];return e},get:function(a,b){return void 0===b?this.cache(a):a[this.expando]&&a[this.expando][b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=a[this.expando];if(void 0!==f){if(void 0===b)this.register(a);else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in f?d=[b,e]:(d=e,d=d in f?[d]:d.match(G)||[])),c=d.length;while(c--)delete f[d[c]]}(void 0===b||n.isEmptyObject(f))&&(a.nodeType?a[this.expando]=void 0:delete a[this.expando])}},hasData:function(a){var b=a[this.expando];return void 0!==b&&!n.isEmptyObject(b)}};var N=new M,O=new M,P=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Q=/[A-Z]/g;function R(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(Q,"-$&").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:P.test(c)?n.parseJSON(c):c}catch(e){}O.set(a,b,c);
+}else c=void 0;return c}n.extend({hasData:function(a){return O.hasData(a)||N.hasData(a)},data:function(a,b,c){return O.access(a,b,c)},removeData:function(a,b){O.remove(a,b)},_data:function(a,b,c){return N.access(a,b,c)},_removeData:function(a,b){N.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=O.get(f),1===f.nodeType&&!N.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),R(f,d,e[d])));N.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){O.set(this,a)}):K(this,function(b){var c,d;if(f&&void 0===b){if(c=O.get(f,a)||O.get(f,a.replace(Q,"-$&").toLowerCase()),void 0!==c)return c;if(d=n.camelCase(a),c=O.get(f,d),void 0!==c)return c;if(c=R(f,d,void 0),void 0!==c)return c}else d=n.camelCase(a),this.each(function(){var c=O.get(this,d);O.set(this,d,b),a.indexOf("-")>-1&&void 0!==c&&O.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){O.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=N.get(a,b),c&&(!d||n.isArray(c)?d=N.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return N.get(a,c)||N.access(a,c,{empty:n.Callbacks("once memory").add(function(){N.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length",""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};$.optgroup=$.option,$.tbody=$.tfoot=$.colgroup=$.caption=$.thead,$.th=$.td;function _(a,b){var c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function aa(a,b){for(var c=0,d=a.length;d>c;c++)N.set(a[c],"globalEval",!b||N.get(b[c],"globalEval"))}var ba=/<|&#?\w+;/;function ca(a,b,c,d,e){for(var f,g,h,i,j,k,l=b.createDocumentFragment(),m=[],o=0,p=a.length;p>o;o++)if(f=a[o],f||0===f)if("object"===n.type(f))n.merge(m,f.nodeType?[f]:f);else if(ba.test(f)){g=g||l.appendChild(b.createElement("div")),h=(Y.exec(f)||["",""])[1].toLowerCase(),i=$[h]||$._default,g.innerHTML=i[1]+n.htmlPrefilter(f)+i[2],k=i[0];while(k--)g=g.lastChild;n.merge(m,g.childNodes),g=l.firstChild,g.textContent=""}else m.push(b.createTextNode(f));l.textContent="",o=0;while(f=m[o++])if(d&&n.inArray(f,d)>-1)e&&e.push(f);else if(j=n.contains(f.ownerDocument,f),g=_(l.appendChild(f),"script"),j&&aa(g),c){k=0;while(f=g[k++])Z.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),l.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",l.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var da=/^key/,ea=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,fa=/^([^.]*)(?:\.(.+)|)/;function ga(){return!0}function ha(){return!1}function ia(){try{return d.activeElement}catch(a){}}function ja(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ja(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=ha;else if(!e)return this;return 1===f&&(g=e,e=function(a){return n().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=n.guid++)),a.each(function(){n.event.add(this,b,e,d,c)})}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=N.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return"undefined"!=typeof n&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(G)||[""],j=b.length;while(j--)h=fa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=N.hasData(a)&&N.get(a);if(r&&(i=r.events)){b=(b||"").match(G)||[""],j=b.length;while(j--)if(h=fa.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&N.remove(a,"handle events")}},dispatch:function(a){a=n.event.fix(a);var b,c,d,f,g,h=[],i=e.call(arguments),j=(N.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())(!a.rnamespace||a.rnamespace.test(g.namespace))&&(a.handleObj=g,a.data=g.data,d=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==d&&(a.result=d)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&("click"!==a.type||isNaN(a.button)||a.button<1))for(;i!==this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>-1:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]*)\/>/gi,la=/\s*$/g;function pa(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a:a}function qa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function ra(a){var b=na.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function sa(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(N.hasData(a)&&(f=N.access(a),g=N.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}O.hasData(a)&&(h=O.access(a),i=n.extend({},h),O.set(b,i))}}function ta(a,b){var c=b.nodeName.toLowerCase();"input"===c&&X.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}function ua(a,b,c,d){b=f.apply([],b);var e,g,h,i,j,k,m=0,o=a.length,p=o-1,q=b[0],r=n.isFunction(q);if(r||o>1&&"string"==typeof q&&!l.checkClone&&ma.test(q))return a.each(function(e){var f=a.eq(e);r&&(b[0]=q.call(this,e,f.html())),ua(f,b,c,d)});if(o&&(e=ca(b,a[0].ownerDocument,!1,a,d),g=e.firstChild,1===e.childNodes.length&&(e=g),g||d)){for(h=n.map(_(e,"script"),qa),i=h.length;o>m;m++)j=e,m!==p&&(j=n.clone(j,!0,!0),i&&n.merge(h,_(j,"script"))),c.call(a[m],j,m);if(i)for(k=h[h.length-1].ownerDocument,n.map(h,ra),m=0;i>m;m++)j=h[m],Z.test(j.type||"")&&!N.access(j,"globalEval")&&n.contains(k,j)&&(j.src?n._evalUrl&&n._evalUrl(j.src):n.globalEval(j.textContent.replace(oa,"")))}return a}function va(a,b,c){for(var d,e=b?n.filter(b,a):a,f=0;null!=(d=e[f]);f++)c||1!==d.nodeType||n.cleanData(_(d)),d.parentNode&&(c&&n.contains(d.ownerDocument,d)&&aa(_(d,"script")),d.parentNode.removeChild(d));return a}n.extend({htmlPrefilter:function(a){return a.replace(ka,"<$1>")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(l.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=_(h),f=_(a),d=0,e=f.length;e>d;d++)ta(f[d],g[d]);if(b)if(c)for(f=f||_(a),g=g||_(h),d=0,e=f.length;e>d;d++)sa(f[d],g[d]);else sa(a,h);return g=_(h,"script"),g.length>0&&aa(g,!i&&_(a,"script")),h},cleanData:function(a){for(var b,c,d,e=n.event.special,f=0;void 0!==(c=a[f]);f++)if(L(c)){if(b=c[N.expando]){if(b.events)for(d in b.events)e[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);c[N.expando]=void 0}c[O.expando]&&(c[O.expando]=void 0)}}}),n.fn.extend({domManip:ua,detach:function(a){return va(this,a,!0)},remove:function(a){return va(this,a)},text:function(a){return K(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=a)})},null,a,arguments.length)},append:function(){return ua(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=pa(this,a);b.appendChild(a)}})},prepend:function(){return ua(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=pa(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return ua(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return ua(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(_(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return K(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!la.test(a)&&!$[(Y.exec(a)||["",""])[1].toLowerCase()]){a=n.htmlPrefilter(a);try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(_(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=[];return ua(this,arguments,function(b){var c=this.parentNode;n.inArray(this,a)<0&&(n.cleanData(_(this)),c&&c.replaceChild(b,this))},a)}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),f=e.length-1,h=0;f>=h;h++)c=h===f?this:this.clone(!0),n(e[h])[b](c),g.apply(d,c.get());return this.pushStack(d)}});var wa,xa={HTML:"block",BODY:"block"};function ya(a,b){var c=n(b.createElement(a)).appendTo(b.body),d=n.css(c[0],"display");return c.detach(),d}function za(a){var b=d,c=xa[a];return c||(c=ya(a,b),"none"!==c&&c||(wa=(wa||n("