diff --git a/js/package.json b/js/package.json index 6f95dc9..b435e79 100644 --- a/js/package.json +++ b/js/package.json @@ -24,6 +24,7 @@ "browserify": "^13.0.0", "mocha": "^2.4.5", "expect.js": "^0.3.1", - "immutable": "^3.7.6" + "immutable": "^3.7.6", + "ohm-js": "cdglabs/ohm" } } diff --git a/js/src/actor.js b/js/src/actor.js new file mode 100644 index 0000000..6361193 --- /dev/null +++ b/js/src/actor.js @@ -0,0 +1,145 @@ +'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); + }; +} + +Actor.prototype.handleEvent = function(e) { + this.facets.forEach(function (f) { + f.handleEvent(e); + }); +}; + +Actor.prototype.addFacet = function(facet) { + this.facets = this.facets.add(facet); +}; + +//--------------------------------------------------------------------------- + +function createFacet() { + return new Facet(Network.activeBehavior()); +} + +function Facet(actor) { + this.actor = actor; + this.endpoints = Immutable.Map(); +} + +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; + return this.addEndpoint(new Endpoint(subscriptionFn, function(e) { + var proj = projectionFn.call(facet.actor.state); + var spec = Patch.prependAtMeta(proj.assertion, proj.metalevel); + + switch (e.type) { + case 'message': + if (eventType === 'message') { + var match = Route.matchPattern(e.message, spec); + // console.log(match); + if (match) { + if (isTerminal) { facet.terminate(); } + Util.kwApply(handlerFn, facet.actor.state, match); + } + } + break; + case 'stateChange': + { + var objects; + switch (eventType) { + case 'asserted': + objects = Route.projectObjects(e.patch.added, Route.compileProjection(spec)); + break; + case 'retracted': + objects = Route.projectObjects(e.patch.removed, Route.compileProjection(spec)); + break; + default: + break; + } + if (objects) { + if (isTerminal) { facet.terminate(); } + // console.log(objects.toArray()); + objects.forEach(function (o) { Util.kwApply(handlerFn, facet.actor.state, o); }); + } + } + } + + })); +}; + +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.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() { + this.actor.addFacet(this); +}; + +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(); +}; + +//--------------------------------------------------------------------------- + +function Endpoint(subscriptionFn, handlerFn) { + this.subscriptionFn = subscriptionFn; + this.handlerFn = handlerFn; +} + +//--------------------------------------------------------------------------- + +module.exports.spawnActor = spawnActor; +module.exports.createFacet = createFacet; diff --git a/js/src/compiler.js b/js/src/compiler.js new file mode 100644 index 0000000..c5698aa --- /dev/null +++ b/js/src/compiler.js @@ -0,0 +1,342 @@ +// Compile ES5+Syndicate to plain ES5. + +'use strict'; + +var fs = require('fs'); +var path = require('path'); + +var ohm = require('ohm-js'); +var ES5 = require('ohm-js/examples/ecmascript/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 + '))'; +} + +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, block) { + return 'new Syndicate.Ground(function () ' + block.asES5 + ').startStepping();'; + }, + 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, + 'null', + [], + '{}') + + '.completeBuild(); }'); + }, + + FacetStateTransition_withContinuation: function(_case, eventPattern, block) { + return buildOnEvent(true, + eventPattern.eventType, + eventPattern.subscription, + eventPattern.projection, + eventPattern.bindings, + block.asES5); + }, + FacetStateTransition_noContinuation: function(_case, eventPattern, _sc) { + return buildOnEvent(true, + eventPattern.eventType, + eventPattern.subscription, + eventPattern.projection, + eventPattern.bindings, + ''); + } +}; + +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'; } +}); + +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('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; }, + + 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); + }, + + 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 compileExtendedSource(inputSource) { + var parseResult = grammar.match(inputSource); + if (parseResult.failed()) console.error(parseResult.message); + return parseResult.succeeded() && semantics(parseResult).asES5; +} + +function compileAndPrint(inputSource) { + var translatedSource = compileExtendedSource(inputSource); + if (translatedSource) { + console.log('"use strict";'); + console.log(translatedSource); + } +} + +if (process.argv.length < 3 || process.argv[2] === '-') { + var inputSource = ''; + process.stdin.resume(); + process.stdin.setEncoding('utf8'); + process.stdin.on('data', function(buf) { inputSource += buf; }); + process.stdin.on('end', function() { compileAndPrint(inputSource); }); +} else { + var inputSource = fs.readFileSync(process.argv[2]).toString(); + compileAndPrint(inputSource); +} diff --git a/js/src/demo-bankaccount.js b/js/src/demo-bankaccount.js new file mode 100644 index 0000000..c92ffbb --- /dev/null +++ b/js/src/demo-bankaccount.js @@ -0,0 +1,36 @@ +// node src/compiler.js src/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 { + until { + case asserted Syndicate.observe(deposit(_)) { + :: deposit(+100); + :: deposit(-30); + } + } + } +} diff --git a/js/src/main.js b/js/src/main.js index 41f93db..8e383a1 100644 --- a/js/src/main.js +++ b/js/src/main.js @@ -42,7 +42,7 @@ copyKeys(['emptyPatch', module.exports.Patch); module.exports.Ground = require("./ground.js").Ground; -// module.exports.Actor = require("./actor.js").Actor; +module.exports.Actor = require("./actor.js"); // module.exports.Spy = require("./spy.js").Spy; // module.exports.WakeDetector = require("./wake-detector.js").WakeDetector; diff --git a/js/src/network.js b/js/src/network.js index 7e7b3cb..151e54d 100644 --- a/js/src/network.js +++ b/js/src/network.js @@ -52,6 +52,12 @@ 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; diff --git a/js/src/syndicate.ohm b/js/src/syndicate.ohm new file mode 100644 index 0000000..549c562 --- /dev/null +++ b/js/src/syndicate.ohm @@ -0,0 +1,82 @@ +// 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 Block -- ground + | network Block -- normal + + ActorFacetStatement + = state FacetBlock until FacetStateTransitionBlock -- state + | until FacetStateTransitionBlock -- until + | forever FacetBlock -- forever + + AssertionTypeDeclarationStatement + = assertion type identifier "(" FormalParameterList ")" ("=" stringLiteral)? #(sc) + + SendMessageStatement = "::" Expression #(sc) + + //--------------------------------------------------------------------------- + // Ongoing event handlers. + + FacetBlock = "{" FacetInitBlock? FacetSituation* FacetDoneBlock? "}" + FacetStateTransitionBlock = "{" FacetStateTransition* "}" + + FacetInitBlock = init Block + FacetDoneBlock = done Block + + FacetSituation + = assert FacetPattern #(sc) -- assert + | on FacetEventPattern Block -- event + | during FacetPattern FacetBlock -- during + + FacetEventPattern + = message FacetPattern -- messageEvent + | asserted FacetPattern -- assertedEvent + | retracted FacetPattern -- retractedEvent + + FacetStateTransition + = case FacetEventPattern Block -- withContinuation + | case FacetEventPattern #(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 +}