Merge branch 'syndicate-js-hs' into 'master'

This commit is contained in:
Tony Garnock-Jones 2016-04-01 19:12:21 -04:00
commit a3577edb00
64 changed files with 6738 additions and 0 deletions

1
hs/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
dist/

165
hs/LICENSE Normal file
View File

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

3
hs/Setup.hs Executable file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env runhaskell
import Distribution.Simple
main = defaultMain

View File

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

View File

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

View File

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

View File

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

37
hs/syndicate.cabal Normal file
View File

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

27
hs/test/Main.hs Normal file
View File

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

View File

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

View File

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

View File

@ -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]))
]

View File

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

2
js/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
scratch/
node_modules/

23
js/Makefile Normal file
View File

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

24
js/README.md Normal file
View File

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

23
js/bin/syndicatec Executable file
View File

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

4
js/compiler/README.md Normal file
View File

@ -0,0 +1,4 @@
# Syndicate/js compiler
Translates ES5 + Syndicate extensions to plain ES5 using
[Ohm](https://github.com/cdglabs/ohm#readme).

365
js/compiler/compiler.js Normal file
View File

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

View File

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

View File

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

94
js/compiler/es5.js Normal file
View File

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

502
js/compiler/es5.ohm Normal file
View File

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

38
js/compiler/inbrowser.js Normal file
View File

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

87
js/compiler/syndicate.ohm Normal file
View File

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

1
js/dist/README.md vendored Normal file
View File

@ -0,0 +1 @@
Directory for build products, checked in to the repo for ease-of-use.

View File

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

View File

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

View File

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

View File

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

56
js/examples/dom/index.js Normal file
View File

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

View File

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

31
js/examples/jquery/index.js vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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, '&amp;');
text = text.replace(/</g, '&lt;');
text = text.replace(/>/g, '&gt;');
text = text.replace(/ /g, '&nbsp;');
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));
};
});

View File

@ -0,0 +1,12 @@
#fieldContents {
font-family: monospace;
}
.cursor {
border-left: solid red 1px;
border-right: solid red 1px;
}
.highlight {
background-color: yellow;
}

View File

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

View File

@ -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, '&amp;');
text = text.replace(/</g, '&lt;');
text = text.replace(/>/g, '&gt;');
text = text.replace(/ /g, '&nbsp;');
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();
});

View File

@ -0,0 +1,12 @@
#fieldContents {
font-family: monospace;
}
.cursor {
border-left: solid red 1px;
border-right: solid red 1px;
}
.highlight {
background-color: yellow;
}

36
js/package.json Normal file
View File

@ -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"
}
}

41
js/src/ack.js Normal file
View File

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

187
js/src/actor.js Normal file
View File

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

79
js/src/demand-matcher.js Normal file
View File

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

141
js/src/dom-driver.js Normal file
View File

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

79
js/src/ground.js Normal file
View File

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

91
js/src/jquery-driver.js vendored Normal file
View File

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

51
js/src/main.js Normal file
View File

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

123
js/src/mux.js Normal file
View File

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

299
js/src/network.js Normal file
View File

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

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

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

26
js/src/randomid.js Normal file
View File

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

28
js/src/reflect.js Normal file
View File

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

1117
js/src/route.js Normal file

File diff suppressed because it is too large Load Diff

18
js/src/seal.js Normal file
View File

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

25
js/src/util.js Normal file
View File

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

275
js/test/test-mux.js Normal file
View File

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

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

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

527
js/test/test-route.js Normal file
View File

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

152
js/test/test-syndicate.js Normal file
View File

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

4
js/third-party/jquery-2.2.0.min.js vendored Normal file

File diff suppressed because one or more lines are too long