Merge branch 'syndicate-js-hs' into 'master'
This commit is contained in:
commit
a3577edb00
|
@ -0,0 +1 @@
|
|||
dist/
|
|
@ -0,0 +1,165 @@
|
|||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
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.
|
|
@ -0,0 +1,3 @@
|
|||
#!/usr/bin/env runhaskell
|
||||
import Distribution.Simple
|
||||
main = defaultMain
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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 "<xyz>", 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
|
|
@ -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
|
|
@ -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
|
||||
]
|
||||
]
|
|
@ -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 "<a>" (ok [2])),
|
||||
"route union wild1" ~: Set.fromList [1,2] ~=?
|
||||
route [Open, Ch 'a', Close] (union
|
||||
(seqs "*" (ok [1]))
|
||||
(seqs "<a>" (ok [2]))) Set.empty,
|
||||
"route union wild2" ~: Set.fromList [1] ~=?
|
||||
route [Open, Ch 'b', Close] (union
|
||||
(seqs "*" (ok [1]))
|
||||
(seqs "<a>" (ok [2]))) Set.empty,
|
||||
"route union wild3" ~: Set.fromList [1] ~=?
|
||||
route [Open, Close] (union
|
||||
(seqs "*" (ok [1]))
|
||||
(seqs "<a>" (ok [2]))) Set.empty,
|
||||
"route union wild4" ~: Set.fromList [1] ~=?
|
||||
route [Open, Ch 'a', Ch 'a', Close] (union
|
||||
(seqs "*" (ok [1]))
|
||||
(seqs "<a>" (ok [2]))) Set.empty
|
||||
]
|
|
@ -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 "<a>" (ok [2])),
|
||||
"route union wild1" ~: Set.fromList [1,2] ~=?
|
||||
route [Open, Ch 'a', Close] (union
|
||||
(seqs "*" (ok [1]))
|
||||
(seqs "<a>" (ok [2]))) Set.empty,
|
||||
"route union wild2" ~: Set.fromList [1] ~=?
|
||||
route [Open, Ch 'b', Close] (union
|
||||
(seqs "*" (ok [1]))
|
||||
(seqs "<a>" (ok [2]))) Set.empty,
|
||||
"route union wild3" ~: Set.fromList [1] ~=?
|
||||
route [Open, Close] (union
|
||||
(seqs "*" (ok [1]))
|
||||
(seqs "<a>" (ok [2]))) Set.empty,
|
||||
"route union wild4" ~: Set.fromList [1] ~=?
|
||||
route [Open, Ch 'a', Ch 'a', Close] (union
|
||||
(seqs "*" (ok [1]))
|
||||
(seqs "<a>" (ok [2]))) Set.empty
|
||||
]
|
|
@ -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 "<a>" (ok [2])),
|
||||
"route union wild1" ~: Set.fromList [1,2] ~=?
|
||||
route "<a>" (union
|
||||
(seqs "*" (ok [1]))
|
||||
(seqs "<a>" (ok [2]))) Set.empty,
|
||||
"route union wild2" ~: Set.fromList [1] ~=?
|
||||
route "<b>" (union
|
||||
(seqs "*" (ok [1]))
|
||||
(seqs "<a>" (ok [2]))) Set.empty,
|
||||
"route union wild3" ~: Set.fromList [1] ~=?
|
||||
route "<>" (union
|
||||
(seqs "*" (ok [1]))
|
||||
(seqs "<a>" (ok [2]))) Set.empty,
|
||||
"route union wild4" ~: Set.fromList [1] ~=?
|
||||
route "<aa>" (union
|
||||
(seqs "*" (ok [1]))
|
||||
(seqs "<a>" (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]))
|
||||
]
|
|
@ -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 "<xyz>", 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
|
||||
]
|
|
@ -0,0 +1,2 @@
|
|||
scratch/
|
||||
node_modules/
|
|
@ -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/
|
|
@ -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.
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
# Syndicate/js compiler
|
||||
|
||||
Translates ES5 + Syndicate extensions to plain ES5 using
|
||||
[Ohm](https://github.com/cdglabs/ohm#readme).
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
};
|
|
@ -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 <tonygarnockjones@gmail.com> 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<withIn> ")" -- parenExpr
|
||||
|
||||
ArrayLiteral = "[" ListOf<AssignmentExpressionOrElision, ","> "]"
|
||||
AssignmentExpressionOrElision = AssignmentExpression<withIn>
|
||||
| -- elision
|
||||
|
||||
ObjectLiteral = "{" ListOf<PropertyAssignment, ","> "}" -- noTrailingComma
|
||||
| "{" NonemptyListOf<PropertyAssignment, ","> "," "}" -- trailingComma
|
||||
|
||||
PropertyAssignment = get PropertyName "(" ")" "{" FunctionBody "}" -- getter
|
||||
| set PropertyName "(" FormalParameter ")" "{" FunctionBody "}" -- setter
|
||||
| PropertyName ":" AssignmentExpression<withIn> -- simple
|
||||
|
||||
PropertyName = identifierName
|
||||
| stringLiteral
|
||||
| numericLiteral
|
||||
|
||||
MemberExpression = MemberExpression "[" Expression<withIn> "]" -- arrayRefExp
|
||||
| MemberExpression "." identifierName -- propRefExp
|
||||
| new MemberExpression Arguments -- newExp
|
||||
| FunctionExpression
|
||||
| PrimaryExpression
|
||||
|
||||
NewExpression = MemberExpression
|
||||
| new NewExpression -- newExp
|
||||
|
||||
CallExpression = CallExpression "[" Expression<withIn> "]" -- arrayRefExp
|
||||
| CallExpression "." identifierName -- propRefExp
|
||||
| CallExpression Arguments -- callExpExp
|
||||
| MemberExpression Arguments -- memberExpExp
|
||||
|
||||
Arguments = "(" ListOf<AssignmentExpression<withIn>, ","> ")"
|
||||
|
||||
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<guardIn>
|
||||
= RelationalExpression<guardIn> "<" ShiftExpression -- lt
|
||||
| RelationalExpression<guardIn> ">" ShiftExpression -- gt
|
||||
| RelationalExpression<guardIn> "<=" ShiftExpression -- le
|
||||
| RelationalExpression<guardIn> ">=" ShiftExpression -- ge
|
||||
| RelationalExpression<guardIn> "instanceof" ShiftExpression -- instanceOfExp
|
||||
| RelationalExpression<guardIn> guardIn "in" ShiftExpression -- inExp
|
||||
| ShiftExpression
|
||||
|
||||
EqualityExpression<guardIn>
|
||||
= EqualityExpression<guardIn> "==" RelationalExpression<guardIn> -- equal
|
||||
| EqualityExpression<guardIn> "!=" RelationalExpression<guardIn> -- notEqual
|
||||
| EqualityExpression<guardIn> "===" RelationalExpression<guardIn> -- eq
|
||||
| EqualityExpression<guardIn> "!==" RelationalExpression<guardIn> -- notEq
|
||||
| RelationalExpression<guardIn>
|
||||
|
||||
BitwiseANDExpression<guardIn>
|
||||
= BitwiseANDExpression<guardIn> "&" EqualityExpression<guardIn> -- band
|
||||
| EqualityExpression<guardIn>
|
||||
|
||||
BitwiseXORExpression<guardIn>
|
||||
= BitwiseXORExpression<guardIn> "^" BitwiseANDExpression<guardIn> -- bxor
|
||||
| BitwiseANDExpression<guardIn>
|
||||
|
||||
BitwiseORExpression<guardIn>
|
||||
= BitwiseORExpression<guardIn> "|" BitwiseXORExpression<guardIn> -- bor
|
||||
| BitwiseXORExpression<guardIn>
|
||||
|
||||
LogicalANDExpression<guardIn>
|
||||
= LogicalANDExpression<guardIn> "&&" BitwiseORExpression<guardIn> -- land
|
||||
| BitwiseORExpression<guardIn>
|
||||
|
||||
LogicalORExpression<guardIn>
|
||||
= LogicalORExpression<guardIn> "||" LogicalANDExpression<guardIn> -- lor
|
||||
| LogicalANDExpression<guardIn>
|
||||
|
||||
ConditionalExpression<guardIn>
|
||||
= LogicalORExpression<guardIn> "?" AssignmentExpression<withIn> ":" AssignmentExpression<guardIn> -- conditional
|
||||
| LogicalORExpression<guardIn>
|
||||
|
||||
AssignmentExpression<guardIn>
|
||||
= LeftHandSideExpression AssignmentOperator AssignmentExpression<guardIn> -- assignment
|
||||
| ConditionalExpression<guardIn>
|
||||
|
||||
Expression<guardIn> (an expression)
|
||||
= Expression<guardIn> "," AssignmentExpression<guardIn> -- commaExp
|
||||
| AssignmentExpression<guardIn>
|
||||
|
||||
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<withIn> #(sc)
|
||||
|
||||
VariableDeclarationList<guardIn> = NonemptyListOf<VariableDeclaration<guardIn>, ",">
|
||||
|
||||
VariableDeclaration<guardIn> = identifier Initialiser<guardIn>?
|
||||
|
||||
Initialiser<guardIn> = "=" AssignmentExpression<guardIn>
|
||||
|
||||
EmptyStatement = ";" // note: this semicolon eats newlines
|
||||
|
||||
ExpressionStatement = ~("{" | function) Expression<withIn> #(sc)
|
||||
|
||||
IfStatement = if "(" Expression<withIn> ")" Statement (else Statement)?
|
||||
|
||||
IterationStatement = do Statement while "(" Expression<withIn> ")" #(sc) -- doWhile
|
||||
| while "(" Expression<withIn> ")" Statement -- whileDo
|
||||
| for "(" Expression<noIn>? ";"
|
||||
Expression<withIn>? ";"
|
||||
Expression<withIn>? ")" Statement -- for3
|
||||
| for "(" var VariableDeclarationList<noIn> ";"
|
||||
Expression<withIn>? ";"
|
||||
Expression<withIn>? ")" Statement -- for3var
|
||||
| for "(" LeftHandSideExpression in
|
||||
Expression<withIn> ")" Statement -- forIn
|
||||
| for "(" var VariableDeclaration<noIn> in
|
||||
Expression<withIn> ")" Statement -- forInVar
|
||||
|
||||
ContinueStatement = continue #((spacesNoNL identifier)? sc)
|
||||
|
||||
BreakStatement = break #((spacesNoNL identifier)? sc)
|
||||
|
||||
ReturnStatement = return (#(spacesNoNL ~space) Expression<withIn>)? #(sc)
|
||||
|
||||
WithStatement = with "(" Expression<withIn> ")" Statement
|
||||
|
||||
SwitchStatement = switch "(" Expression<withIn> ")" CaseBlock
|
||||
|
||||
CaseBlock = "{" CaseClause* DefaultClause CaseClause* "}" -- withDefault
|
||||
| "{" CaseClause* "}" -- withoutDefault
|
||||
|
||||
CaseClause = case Expression<withIn> ":" Statement*
|
||||
|
||||
DefaultClause = default ":" Statement*
|
||||
|
||||
LabelledStatement = identifier ":" Statement
|
||||
|
||||
ThrowStatement = throw Expression<withIn> #(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, ",">
|
||||
|
||||
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
|
||||
}
|
|
@ -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;
|
|
@ -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<withIn> #(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<withIn> ")" -- 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
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
Directory for build products, checked in to the repo for ease-of-use.
|
|
@ -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();
|
||||
});
|
|
@ -0,0 +1,22 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Syndicate: Button Example</title>
|
||||
<meta charset="utf-8">
|
||||
<link href="style.css" rel="stylesheet">
|
||||
<script src="../../third-party/jquery-2.2.0.min.js"></script>
|
||||
<script src="../../dist/syndicatecompiler.js"></script>
|
||||
<script src="../../dist/syndicate.js"></script>
|
||||
<script type="text/syndicate-js" src="index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Button Example</h1>
|
||||
<button id="counter"><span id="button-label"></span></button>
|
||||
<p>
|
||||
Source code: <a href="index.js">index.js</a>
|
||||
</p>
|
||||
<p>
|
||||
Expanded source code: <a href="index.expanded.js">index.expanded.js</a>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
|
@ -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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Syndicate: DOM Example</title>
|
||||
<meta charset="utf-8">
|
||||
<script src="../../third-party/jquery-2.2.0.min.js"></script>
|
||||
<script src="../../dist/syndicate.js"></script>
|
||||
<script src="index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>DOM example</h1>
|
||||
<div id="counter-holder"></div>
|
||||
<div id="clicker-holder"></div>
|
||||
<pre id="spy-holder"></pre>
|
||||
</body>
|
||||
</html>
|
|
@ -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();
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Syndicate: jQuery Example</title>
|
||||
<meta charset="utf-8">
|
||||
<script src="../../third-party/jquery-2.2.0.min.js"></script>
|
||||
<script src="../../dist/syndicate.js"></script>
|
||||
<script src="index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>jQuery example</h1>
|
||||
<button id="clicker">Click me</button>
|
||||
<div id="result">0</div>
|
||||
<pre id="spy-holder"></pre>
|
||||
</body>
|
||||
</html>
|
|
@ -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();
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Syndicate: Smoketest with DSL</title>
|
||||
<meta charset="utf-8">
|
||||
<script src="../../third-party/jquery-2.2.0.min.js"></script>
|
||||
<script src="../../dist/syndicatecompiler.js"></script>
|
||||
<script src="../../dist/syndicate.js"></script>
|
||||
<script type="text/syndicate-js" src="index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Smoketest</h1>
|
||||
</body>
|
||||
</html>
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Syndicate: Smoketest</title>
|
||||
<meta charset="utf-8">
|
||||
<script src="../../third-party/jquery-2.2.0.min.js"></script>
|
||||
<script src="../../dist/syndicate.js"></script>
|
||||
<script src="index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Smoketest</h1>
|
||||
</body>
|
||||
</html>
|
|
@ -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();
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Syndicate: Textfield Example (DSL variation)</title>
|
||||
<meta charset="utf-8">
|
||||
<link href="style.css" rel="stylesheet">
|
||||
<script src="../../third-party/jquery-2.2.0.min.js"></script>
|
||||
<script src="../../dist/syndicatecompiler.js"></script>
|
||||
<script src="../../dist/syndicate.js"></script>
|
||||
<script type="text/syndicate-js" src="index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Textfield Example (DSL variation)</h1>
|
||||
<p>
|
||||
After <a href="http://www.hesam.us/cooplangs/textfield.pdf">Hesam
|
||||
Samimi's paper</a>.
|
||||
</p>
|
||||
<p id="inputRow" tabindex="0">Field contents: <span id="fieldContents"></span></p>
|
||||
<h2>Search</h2>
|
||||
<input type="text" id="searchBox" value="iti">
|
||||
<pre id="spy-holder"></pre>
|
||||
</body>
|
||||
</html>
|
|
@ -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, '>');
|
||||
text = text.replace(/ /g, ' ');
|
||||
return text;
|
||||
}
|
||||
|
||||
function piece(text, pos, lo, hi, cls) {
|
||||
return "<span class='"+cls+"'>"+
|
||||
((pos >= lo && pos < hi)
|
||||
? (escapeText(text.substring(lo, pos)) +
|
||||
"<span class='cursor'></span>" +
|
||||
escapeText(text.substring(pos, hi)))
|
||||
: escapeText(text.substring(lo, hi)))
|
||||
+ "</span>";
|
||||
}
|
||||
|
||||
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));
|
||||
};
|
||||
});
|
|
@ -0,0 +1,12 @@
|
|||
#fieldContents {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.cursor {
|
||||
border-left: solid red 1px;
|
||||
border-right: solid red 1px;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background-color: yellow;
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Syndicate: Textfield Example</title>
|
||||
<meta charset="utf-8">
|
||||
<link href="style.css" rel="stylesheet">
|
||||
<script src="../../third-party/jquery-2.2.0.min.js"></script>
|
||||
<script src="../../dist/syndicate.js"></script>
|
||||
<script src="index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Textfield Example</h1>
|
||||
<p>
|
||||
After <a href="http://www.hesam.us/cooplangs/textfield.pdf">Hesam
|
||||
Samimi's paper</a>.
|
||||
</p>
|
||||
<p id="inputRow" tabindex="0">Field contents: <span id="fieldContents"></span></p>
|
||||
<h2>Search</h2>
|
||||
<input type="text" id="searchBox" value="iti">
|
||||
<pre id="spy-holder"></pre>
|
||||
</body>
|
||||
</html>
|
|
@ -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, '>');
|
||||
text = text.replace(/ /g, ' ');
|
||||
return text;
|
||||
}
|
||||
|
||||
function piece(text, pos, lo, hi, cls) {
|
||||
return "<span class='"+cls+"'>"+
|
||||
((pos >= lo && pos < hi)
|
||||
? (escapeText(text.substring(lo, pos)) +
|
||||
"<span class='cursor'></span>" +
|
||||
escapeText(text.substring(pos, hi)))
|
||||
: escapeText(text.substring(lo, hi)))
|
||||
+ "</span>";
|
||||
}
|
||||
|
||||
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();
|
||||
});
|
|
@ -0,0 +1,12 @@
|
|||
#fieldContents {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.cursor {
|
||||
border-left: solid red 1px;
|
||||
border-right: solid red 1px;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
background-color: yellow;
|
||||
}
|
|
@ -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 <tonyg@ccs.neu.edu>",
|
||||
"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"
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
File diff suppressed because it is too large
Load Diff
|
@ -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;
|
|
@ -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);
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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']);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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]]);
|
||||
// });
|
||||
// });
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue