diff --git a/js/compiler/compiler.js b/js/compiler/compiler.js index 716794f..69bcf56 100644 --- a/js/compiler/compiler.js +++ b/js/compiler/compiler.js @@ -42,15 +42,14 @@ var forEachChild = (function () { return forEachChild; })(); -function buildActor(constructorES5, nameExpOpt, block) { +function buildActor(nameExpOpt, block) { var nameExpStr; if (nameExpOpt.numChildren === 1) { nameExpStr = ', ' + nameExpOpt.asES5; } else { nameExpStr = ''; } - return 'Syndicate.Actor.spawnActor(new '+constructorES5+', '+ - 'function() ' + block.asES5 + nameExpStr + ');'; + return 'Syndicate.Actor.spawnActor(function() ' + block.asES5 + nameExpStr + ');'; } function buildFacet(facetBlock, transitionBlock) { @@ -58,11 +57,11 @@ function buildFacet(facetBlock, transitionBlock) { '\nSyndicate.Actor.createFacet()' + (facetBlock ? facetBlock.asES5 : '') + (transitionBlock ? transitionBlock.asES5 : '') + - '.completeBuild(); })();'; + '.completeBuild(); }).call(this);'; } function buildOnEvent(isTerminal, eventType, subscription, projection, bindings, body) { - return '\n.onEvent(' + isTerminal + ', ' + JSON.stringify(eventType) + ', ' + + return '\n.onEvent(Syndicate.Actor.PRIORITY_NORMAL, ' + isTerminal + ', ' + JSON.stringify(eventType) + ', ' + subscription + ', ' + projection + ', (function(' + bindings.join(', ') + ') ' + body + '))'; } @@ -86,11 +85,8 @@ function buildCaseEvent(eventPattern, body) { } var modifiedSourceActions = { - ActorStatement_noConstructor: function(_actor, _namedOpt, nameExpOpt, block) { - return buildActor('Object()', nameExpOpt, block); - }, - ActorStatement_withConstructor: function(_actor, ctorExp, _namedOpt, nameExpOpt, block) { - return buildActor(ctorExp.asES5, nameExpOpt, block); + ActorStatement: function(_actor, _namedOpt, nameExpOpt, block) { + return buildActor(nameExpOpt, block); }, DataspaceStatement_ground: function(_ground, _dataspace, maybeId, block) { @@ -133,6 +129,22 @@ var modifiedSourceActions = { label + ', ' + JSON.stringify(formals) + ');'; }, + FieldDeclarationStatement: function(_field, memberExpr, _eq, initExpr, sc) { + return 'Syndicate.Actor.declareField(' + memberExpr.memberObjectExpr.asES5 + ', ' + + memberExpr.memberPropExpr.asES5 + ', ' + initExpr.asES5 + ')' + + sc.interval.contents; + }, + + MemberExpression_fieldRefExp: function (_field, memberExpr) { + return 'Syndicate.Actor.referenceField(' + memberExpr.memberObjectExpr.asES5 + ', ' + + memberExpr.memberPropExpr.asES5 + ')'; + }, + + UnaryExpression_fieldDelExp: function (_delete, _field, memberExpr) { + return 'Syndicate.Actor.deleteField(' + memberExpr.memberObjectExpr.asES5 + ', ' + + memberExpr.memberPropExpr.asES5 + ')'; + }, + SendMessageStatement: function(_colons, expr, sc) { return 'Syndicate.Dataspace.send(' + expr.asES5 + ')' + sc.interval.contents; }, @@ -165,6 +177,9 @@ var modifiedSourceActions = { FacetSituation_onEvent: function (_on, _event, id, block) { return '\n.addOnEventHandler((function(' + id.asES5 + ') ' + block.asES5 + '))'; }, + FacetSituation_dataflow: function (_dataflow, block) { + return '\n.addDataflow((function () ' + block.asES5 + '))'; + }, FacetSituation_during: function(_during, pattern, facetBlock) { var cachedAssertionVar = gensym('cachedAssertion'); return buildOnEvent(false, @@ -184,6 +199,28 @@ var modifiedSourceActions = { '{}') + '.completeBuild(); }'); }, + FacetSituation_duringActor: function(_during, pattern, _actor, _named, nameExpOpt, facetBlock) { + var cachedAssertionVar = gensym('cachedAssertion'); + var actorBlock = { + asES5: '{ ' + facetBlock.facetVarDecls + + '\nSyndicate.Actor.createFacet()' + + facetBlock.asES5 + + buildOnEvent(true, + 'retracted', + pattern.instantiatedSubscription(cachedAssertionVar), + pattern.instantiatedProjection(cachedAssertionVar), + [], + '{}') + + '.completeBuild(); }' + }; + return buildOnEvent(false, + 'asserted', + pattern.subscription, + pattern.projection, + pattern.bindings, + '{ var '+cachedAssertionVar+' = '+pattern.instantiatedAssertion+';'+ + '\n' + buildActor(nameExpOpt, actorBlock) + ' }'); + }, AssertWhenClause: function(_when, _lparen, expr, _rparen) { return expr.asES5; @@ -199,6 +236,24 @@ var modifiedSourceActions = { semantics.extendAttribute('modifiedSource', modifiedSourceActions); +semantics.addAttribute('memberObjectExpr', { + MemberExpression_propRefExp: function(objExpr, _dot, id) { + return objExpr; + }, + MemberExpression_arrayRefExp: function(objExpr, _lbrack, propExpr, _rbrack) { + return objExpr; + } +}); + +semantics.addAttribute('memberPropExpr', { + MemberExpression_propRefExp: function(objExpr, _dot, id) { + return { asES5: JSON.stringify(id.interval.contents) }; + }, + MemberExpression_arrayRefExp: function(objExpr, _lbrack, propExpr, _rbrack) { + return propExpr; + } +}); + semantics.addAttribute('facetVarDecls', { FacetBlock: function (_leftParen, varDecls, _init, _situations, _done, _rightParen) { return varDecls.asES5.join(' '); @@ -269,7 +324,7 @@ semantics.addAttribute('instantiatedAssertion', { var fragments = []; fragments.push('(function() { var _ = Syndicate.__; return '); children.forEach(function (c) { fragments.push(c.buildSubscription('instantiated')); }); - fragments.push('; })()'); + fragments.push('; }).call(this)'); return fragments.join(''); } }); @@ -345,6 +400,12 @@ semantics.addOperation('buildSubscription(mode)', { } }, + MemberExpression_fieldRefExp: function (_field, memberExpr) { + return 'Syndicate.Actor.referenceField(' + + memberExpr.memberObjectExpr.buildSubscription(this.args.mode) + ', ' + + memberExpr.memberPropExpr.buildSubscription(this.args.mode) + ')'; + }, + identifier: function(_name) { var i = this.interval.contents; if (i[0] === '$' && i.length > 1) { diff --git a/js/compiler/demo-bankaccount.js b/js/compiler/demo-bankaccount.js index 517142b..67ab2ff 100644 --- a/js/compiler/demo-bankaccount.js +++ b/js/compiler/demo-bankaccount.js @@ -7,10 +7,13 @@ message type deposit(amount); ground dataspace { actor { - this.balance = 0; + field this.balance = 0; react { assert account(this.balance); + dataflow { + console.log("Balance inside account is", this.balance); + } on message deposit($amount) { this.balance += amount; } diff --git a/js/compiler/demo-during-criterion-snapshotting.js b/js/compiler/demo-during-criterion-snapshotting.js index 032f9af..f071c60 100644 --- a/js/compiler/demo-during-criterion-snapshotting.js +++ b/js/compiler/demo-during-criterion-snapshotting.js @@ -20,19 +20,19 @@ assertion type foo(x, y); ground dataspace { actor { - var x = 123; + field this.x = 123; react { - assert foo(x, 999); + assert foo(this.x, 999); - during foo(x, $v) { + during foo(this.x, $v) { do { - console.log('x=', x, 'v=', v); - if (x === 123) { - x = 124; + console.log('x=', this.x, 'v=', v); + if (this.x === 123) { + this.x = 124; } } finally { - console.log('finally for x=', x, 'v=', v); + console.log('finally for x=', this.x, 'v=', v); } } } diff --git a/js/compiler/demo-filesystem.js b/js/compiler/demo-filesystem.js index 9e427ab..6dee48d 100644 --- a/js/compiler/demo-filesystem.js +++ b/js/compiler/demo-filesystem.js @@ -1,5 +1,17 @@ // bin/syndicatec compiler/demo-filesystem.js | node +// Good output: +// +// At least one reader exists for: hello.txt +// hello.txt has content undefined +// hello.txt has content "a" +// hello.txt has content undefined +// hello.txt has content "c" +// hello.txt has content "quit demo" +// The hello.txt file contained 'quit demo', so we will quit +// second observer sees that hello.txt content is "final contents" +// No remaining readers exist for: hello.txt + var Syndicate = require('./src/main.js'); assertion type file(name, content) = "file"; @@ -17,16 +29,16 @@ ground dataspace { do { console.log("At least one reader exists for:", name); } - assert file(name, this.files[name]); + assert file(name, field this.files[name]); finally { console.log("No remaining readers exist for:", name); } } on message saveFile($name, $newcontent) { - this.files[name] = newcontent; + field this.files[name] = newcontent; } on message deleteFile($name) { - delete this.files[name]; + delete field this.files[name]; } } } diff --git a/js/compiler/syndicate.ohm b/js/compiler/syndicate.ohm index eda6047..b91a487 100644 --- a/js/compiler/syndicate.ohm +++ b/js/compiler/syndicate.ohm @@ -11,13 +11,13 @@ Syndicate <: ES5 { | DataspaceStatement | ActorFacetStatement | AssertionTypeDeclarationStatement + | FieldDeclarationStatement | SendMessageStatement FunctionBodyBlock = "{" FunctionBody "}" // odd that this isn't in es5.ohm somewhere ActorStatement - = actor ~named CallExpression (named Expression)? FunctionBodyBlock -- withConstructor - | actor (named Expression)? FunctionBodyBlock -- noConstructor + = actor (named Expression)? FunctionBodyBlock DataspaceStatement = ground dataspace identifier? FunctionBodyBlock -- ground @@ -31,22 +31,33 @@ Syndicate <: ES5 { AssertionTypeDeclarationStatement = (assertion | message) type identifier "(" FormalParameterList ")" ("=" stringLiteral)? #(sc) + FieldDeclarationStatement = field MemberExpression "=" AssignmentExpression #(sc) + MemberExpression += field MemberExpression -- fieldRefExp + UnaryExpression += delete field MemberExpression -- fieldDelExp + SendMessageStatement = "::" Expression #(sc) //--------------------------------------------------------------------------- // Ongoing event handlers. - FacetBlock = "{" (VariableStatement | FunctionDeclaration)* FacetInitBlock? FacetSituation* FacetDoneBlock? "}" + FacetBlock = "{" + (VariableStatement | FieldDeclarationStatement | FunctionDeclaration)* + FacetInitBlock? + FacetSituation* + FacetDoneBlock? + "}" FacetStateTransitionBlock = "{" FacetStateTransition* "}" FacetInitBlock = do FunctionBodyBlock FacetDoneBlock = finally FunctionBodyBlock FacetSituation - = assert FacetPattern AssertWhenClause? #(sc) -- assert - | on FacetEventPattern FunctionBodyBlock -- event - | on event identifier FunctionBodyBlock -- onEvent - | during FacetPattern FacetBlock -- during + = assert FacetPattern AssertWhenClause? #(sc) -- assert + | on FacetEventPattern FunctionBodyBlock -- event + | on event identifier FunctionBodyBlock -- onEvent + | dataflow FunctionBodyBlock -- dataflow + | during FacetPattern FacetBlock -- during + | during FacetPattern actor (named Expression)? FacetBlock -- duringActor AssertWhenClause = when "(" Expression ")" @@ -76,9 +87,11 @@ Syndicate <: ES5 { assert = "assert" ~identifierPart asserted = "asserted" ~identifierPart assertion = "assertion" ~identifierPart + dataflow = "dataflow" ~identifierPart dataspace = "dataspace" ~identifierPart during = "during" ~identifierPart event = "event" ~identifierPart + field = "field" ~identifierPart ground = "ground" ~identifierPart message = "message" ~identifierPart metalevel = "metalevel" ~identifierPart diff --git a/js/examples/button/.gitignore b/js/examples/button/.gitignore new file mode 100644 index 0000000..62138e5 --- /dev/null +++ b/js/examples/button/.gitignore @@ -0,0 +1 @@ +*.expanded.js diff --git a/js/examples/button/Makefile b/js/examples/button/Makefile new file mode 100644 index 0000000..6b0a114 --- /dev/null +++ b/js/examples/button/Makefile @@ -0,0 +1,7 @@ +all: index.expanded.js + +%.expanded.js: %.js + ../../bin/syndicatec $< > $@ || (rm -f $@; false) + +clean: + rm -f *.expanded.js diff --git a/js/examples/button/index.expanded.js b/js/examples/button/index.expanded.js deleted file mode 100644 index 632fdc8..0000000 --- a/js/examples/button/index.expanded.js +++ /dev/null @@ -1,14 +0,0 @@ -"use strict"; -new Syndicate.Ground(function () { - Syndicate.UI.spawnUIDriver(); - - Syndicate.Actor.spawnActor(new Object(), function() { - var counter = 0; - var ui = new Syndicate.UI.Anchor(); - Syndicate.Actor.createFacet() -.addAssertion((function() { var _ = Syndicate.__; return Syndicate.Patch.assert(ui.html('#button-label',''+counter), 0); })) -.onEvent(false, "message", (function() { var _ = Syndicate.__; return Syndicate.Patch.sub(Syndicate.UI.globalEvent('#counter','click',_), 0); }), (function() { var _ = Syndicate.__; return { assertion: Syndicate.UI.globalEvent('#counter','click',_), metalevel: 0 }; }), (function() { - counter++; - })).completeBuild(); - }); -}).startStepping(); diff --git a/js/examples/button/index.js b/js/examples/button/index.js index bb660a8..4b433d8 100644 --- a/js/examples/button/index.js +++ b/js/examples/button/index.js @@ -2,12 +2,12 @@ ground dataspace { Syndicate.UI.spawnUIDriver(); actor { - var counter = 0; + field this.counter = 0; var ui = new Syndicate.UI.Anchor(); react { - assert ui.html('#button-label', '' + counter); + assert ui.html('#button-label', '' + this.counter); on message Syndicate.UI.globalEvent('#counter', 'click', _) { - counter++; + this.counter++; } } } diff --git a/js/examples/chat/index.js b/js/examples/chat/index.js index 2d4f608..887515a 100644 --- a/js/examples/chat/index.js +++ b/js/examples/chat/index.js @@ -17,6 +17,8 @@ function spawnChatApp() { actor { var ui = new Syndicate.UI.Anchor(); + field this.nym; + field this.status; react { on asserted inputValue('#nym', $v) { this.nym = v; } on asserted inputValue('#status', $v) { this.status = v; } @@ -83,17 +85,11 @@ assertion type inputValue(selector, value); function spawnInputChangeMonitor() { actor { react { - on asserted Syndicate.observe(inputValue($selector, _)) { - actor { - this.value = $(selector).val(); - react { - assert inputValue(selector, this.value); - on message Syndicate.UI.globalEvent(selector, 'change', $e) { - this.value = e.target.value; - } - } until { - case retracted Syndicate.observe(inputValue(selector, _)); - } + during Syndicate.observe(inputValue($selector, _)) actor { + field this.value = $(selector).val(); + assert inputValue(selector, this.value); + on message Syndicate.UI.globalEvent(selector, 'change', $e) { + this.value = e.target.value; } } } diff --git a/js/examples/iot/index.js b/js/examples/iot/index.js index 52c8e41..ff466c6 100644 --- a/js/examples/iot/index.js +++ b/js/examples/iot/index.js @@ -60,7 +60,7 @@ function spawnRemoteListener() { function spawnStoveSwitch() { actor { - this.powerOn = false; + field this.powerOn = false; this.ui = new Syndicate.UI.Anchor(); react { assert componentPresent('stove switch'); @@ -85,7 +85,7 @@ function spawnStoveSwitch() { function spawnPowerDrawMonitor() { actor { - this.watts = 0; + field this.watts = 0; this.ui = new Syndicate.UI.Anchor(); react { assert componentPresent('power draw monitor'); diff --git a/js/examples/location/index.js b/js/examples/location/index.js index b69b0e9..e10fb10 100644 --- a/js/examples/location/index.js +++ b/js/examples/location/index.js @@ -40,7 +40,7 @@ ground dataspace G { var geocoder = new google.maps.Geocoder(); var wsurl_base = 'wss://demo-broker.syndicate-lang.org:8443/location/'; - var wsurl = wsurl_base + group_element.value.trim(); + field this.wsurl = wsurl_base + group_element.value.trim(); var watchId = ('geolocation' in navigator) && navigator.geolocation.watchPosition(Syndicate.Dataspace.wrap(function (pos) { @@ -62,21 +62,21 @@ ground dataspace G { })); react { - var currentLocation = null; + field this.currentLocation = null; var selectedMarker = null; - assert brokerConnection(wsurl); - assert toBroker(wsurl, currentLocation) when (currentLocation); + assert brokerConnection(this.wsurl); + assert toBroker(this.wsurl, this.currentLocation) when (this.currentLocation); on message Syndicate.UI.globalEvent('#my_email', 'change', _) { var v = email_element.value.trim(); - if (currentLocation) currentLocation[1] = v; + if (this.currentLocation) this.currentLocation = this.currentLocation.set(1, v); localStorage.my_email = v; } on message Syndicate.UI.globalEvent('#group', 'change', _) { localStorage.group = group_element.value.trim(); - wsurl = wsurl_base + group_element.value.trim(); + this.wsurl = wsurl_base + group_element.value.trim(); } on message Syndicate.UI.globalEvent('#findMarker', 'click', $e) { @@ -87,10 +87,10 @@ ground dataspace G { } on message ($loc = locationRecord(_, _, _, _, _)) { - currentLocation = loc; + this.currentLocation = loc; } - during fromBroker(wsurl, locationRecord($id, $email, _, _, _)) { + during fromBroker(this.wsurl, locationRecord($id, $email, _, _, _)) { var ui = new Syndicate.UI.Anchor(); var marker = new google.maps.Marker({ map: map, @@ -131,7 +131,7 @@ ground dataspace G { selectMarker(); if (latestPosition) map.panTo(latestPosition); } - on asserted fromBroker(wsurl, locationRecord(id, email, $timestamp, $lat, $lng)) { + on asserted fromBroker(this.wsurl, locationRecord(id, email, $timestamp, $lat, $lng)) { latestTimestamp = new Date(timestamp); latestPosition = {lat: lat, lng: lng}; marker.setPosition(latestPosition); diff --git a/js/examples/motion/index.js b/js/examples/motion/index.js index a685d2e..7291ba5 100644 --- a/js/examples/motion/index.js +++ b/js/examples/motion/index.js @@ -14,8 +14,8 @@ ground dataspace G { var color = tinycolor('hsl ' + (Math.random() * 360 | 0) + ' 100% 50%').toHexString(); var x = 0; var y = 0; - var publishedX = x; - var publishedY = y; + field this.publishedX = x; + field this.publishedY = y; function clamp(v) { var limit = 9.8; @@ -28,10 +28,10 @@ ground dataspace G { assert Syndicate.UI.uiAttribute('rect#my_color', 'fill', color); - assert toBroker(wsurl, point(color, publishedX, publishedY)); + assert toBroker(wsurl, point(color, this.publishedX, this.publishedY)); on message Syndicate.Timer.periodicTick(100) { - publishedX = x; - publishedY = y; + this.publishedX = x; + this.publishedY = y; } on message Syndicate.UI.windowEvent('deviceorientation', $e) { diff --git a/js/examples/smoketest-dsl/index.js b/js/examples/smoketest-dsl/index.js index 23cfb88..56f0f72 100644 --- a/js/examples/smoketest-dsl/index.js +++ b/js/examples/smoketest-dsl/index.js @@ -6,16 +6,16 @@ ground dataspace { actor { react until { case asserted Syndicate.observe(beep(_)) { - var counter = 0; + field this.counter = 0; react { do { - :: beep(counter++); + :: beep(this.counter++); } on message beep(_) { - :: beep(counter++); + :: beep(this.counter++); } } until { - case (counter >= 10); + case (this.counter > 10); } } } diff --git a/js/examples/svg/index.js b/js/examples/svg/index.js index 5811235..cd6faa7 100644 --- a/js/examples/svg/index.js +++ b/js/examples/svg/index.js @@ -4,6 +4,10 @@ ground dataspace G { actor { var ui = new Syndicate.UI.Anchor(); + field this.angle; + field this.handX; + field this.handY; + react { assert ui.html('#clock', ''+ diff --git a/js/examples/table/index.js b/js/examples/table/index.js index 12215c1..d5cf9ed 100644 --- a/js/examples/table/index.js +++ b/js/examples/table/index.js @@ -20,7 +20,7 @@ function spawnModel() { function spawnView() { actor named 'view' { var ui = new Syndicate.UI.Anchor(); - var orderColumn = 2; + field this.orderColumn = 2; function cell(text) { // Should escape text in a real application. @@ -28,13 +28,13 @@ function spawnView() { } react { - on message setSortColumn($c) { orderColumn = c; } + on message setSortColumn($c) { this.orderColumn = c; } during person($id, $firstName, $lastName, $address, $age) { assert ui.context(id) .html('table#the-table tbody', '
' + [id, firstName, lastName, address, age].map(cell).join('') + '
', - [id, firstName, lastName, address, age][orderColumn]); + [id, firstName, lastName, address, age][this.orderColumn]); } } } diff --git a/js/examples/textfield-dsl/index.js b/js/examples/textfield-dsl/index.js index 45e47a5..53df1c6 100644 --- a/js/examples/textfield-dsl/index.js +++ b/js/examples/textfield-dsl/index.js @@ -26,24 +26,24 @@ function piece(text, pos, lo, hi, cls) { 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[0] : 0; - var hRight = highlight ? highlight[1] : 0; - document.getElementById("fieldContents").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"); - }; - react { + field this.text = ''; + field this.pos = 0; + field this.highlightState = false; + + dataflow { + var text = this.text; + var pos = this.pos; + var highlight = this.highlightState; + var hLeft = highlight ? highlight[0] : 0; + var hRight = highlight ? highlight[1] : 0; + document.getElementById("fieldContents").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"); + } + on message globalEvent("#inputRow", "+keydown", $event) { switch (event.keyCode) { case 37 /* left */: :: fieldCommand("cursorLeft"); break; @@ -67,12 +67,10 @@ function spawnGui() { on asserted fieldContents($text, $pos) { this.text = text; this.pos = pos; - this.updateDisplay(); } on asserted highlight($state) { this.highlightState = state; - this.updateDisplay(); } } } @@ -83,10 +81,10 @@ function spawnGui() { function spawnModel() { actor { - this.fieldValue = "initial"; - this.cursorPos = this.fieldValue.length; /* positions address gaps between characters */ - react { + field this.fieldValue = "initial"; + field this.cursorPos = this.fieldValue.length; /* positions address gaps between characters */ + assert fieldContents(this.fieldValue, this.cursorPos); on message fieldCommand("cursorLeft") { @@ -126,29 +124,28 @@ function spawnModel() { function spawnSearch() { actor { - this.fieldValue = ""; - this.highlight = false; - - this.search = function () { - var searchtext = document.getElementById("searchBox").value; - if (searchtext) { - var pos = this.fieldValue.indexOf(searchtext); - this.highlight = (pos !== -1) && [pos, pos + searchtext.length]; - } else { - this.highlight = false; - } - }; - react { + field this.searchtext = document.getElementById("searchBox").value; + field this.fieldValue = ""; + field this.highlight = false; + assert highlight(this.highlight); + dataflow { + if (this.searchtext) { + var pos = this.fieldValue.indexOf(this.searchtext); + this.highlight = (pos !== -1) && [pos, pos + this.searchtext.length]; + } else { + this.highlight = false; + } + } + on message globalEvent("#searchBox", "input", $event) { - this.search(); + this.searchtext = document.getElementById("searchBox").value; } on asserted fieldContents($text, _) { this.fieldValue = text; - this.search(); } } } diff --git a/js/examples/todo/index.js b/js/examples/todo/index.js index e8b1df7..69fbfa3 100644 --- a/js/examples/todo/index.js +++ b/js/examples/todo/index.js @@ -25,11 +25,11 @@ assertion type show(completed); function todoListItemModel(initialId, initialTitle, initialCompleted) { actor { - this.id = initialId; - this.title = initialTitle; - this.completed = initialCompleted; - react { + field this.id = initialId; + field this.title = initialTitle; + field this.completed = initialCompleted; + assert todo(this.id, this.title, this.completed); on message setCompleted(this.id, $v) { this.completed = v; } @@ -58,8 +58,9 @@ function getTemplate(id) { function todoListItemView(id) { actor { this.ui = new Syndicate.UI.Anchor(); - this.editing = false; react { + field this.editing = false; + during todo(id, $title, $completed) { during show(completed) { assert this.ui.html('.todo-list', @@ -158,15 +159,15 @@ ground dataspace G { } actor { - var completedCount = 0; - var activeCount = 0; react { - on asserted todo($id, _, $completed) { if (completed) completedCount++; else activeCount++; } - on retracted todo($id, _, $completed) { if (completed) completedCount--; else activeCount--; } - assert activeTodoCount(activeCount); - assert completedTodoCount(completedCount); - assert totalTodoCount(activeCount + completedCount); - assert allCompleted() when (completedCount > 0 && activeCount === 0); + field this.completedCount = 0; + field this.activeCount = 0; + on asserted todo($id, _, $c) { if (c) this.completedCount++; else this.activeCount++; } + on retracted todo($id, _, $c) { if (c) this.completedCount--; else this.activeCount--; } + assert activeTodoCount(this.activeCount); + assert completedTodoCount(this.completedCount); + assert totalTodoCount(this.activeCount + this.completedCount); + assert allCompleted() when (this.completedCount > 0 && this.activeCount === 0); } } diff --git a/js/examples/two-buyer-protocol/index.js b/js/examples/two-buyer-protocol/index.js index 2bb45fc..44a2a13 100644 --- a/js/examples/two-buyer-protocol/index.js +++ b/js/examples/two-buyer-protocol/index.js @@ -91,19 +91,27 @@ function seller() { /// We give our actor two state variables: a dictionary recording our /// inventory of books (mapping title to price), and a counter /// tracking the next order ID to be allocated. +/// +/// We mark each property (entry) in the `books` table as a *field* so +/// that dependency-tracking on a per-book basis is enabled. As a result, +/// when a book is sold, any client still interested in its price will +/// learn that the book is no longer available. +/// +/// We do not enable dependency-tracking for either the `books` table +/// itself or the `nextOrderId` field: nothing depends on tracking +/// changes in their values. - this.books = { - "The Wind in the Willows": 3.95, - "Catch 22": 2.22, - "Candide": 34.95 - }; + this.books = {}; + field this.books["The Wind in the Willows"] = 3.95; + field this.books["Catch 22"] = 2.22; + field this.books["Candide"] = 34.95; this.nextOrderId = 10001483; /// Looking up a price yields `false` if no such book is in our /// inventory. this.priceOf = function (title) { - return (title in this.books) && this.books[title]; + return (title in this.books) && field this.books[title]; }; /// The seller responds to interest in bookQuotes by asserting a @@ -134,7 +142,7 @@ function seller() { /// replying to the orderer. var orderId = this.nextOrderId++; - delete this.books[title]; + delete field this.books[title]; actor { whileRelevantAssert( diff --git a/js/src/actor.js b/js/src/actor.js index db0c53a..28efb3d 100644 --- a/js/src/actor.js +++ b/js/src/actor.js @@ -8,20 +8,22 @@ var Mux = require('./mux.js'); var Patch = require('./patch.js'); var Trie = require('./trie.js'); var Util = require('./util.js'); +var Dataflow = require('./dataflow.js'); //--------------------------------------------------------------------------- -function spawnActor(state, bootFn, optName) { - Dataspace.spawn(new Actor(state, bootFn, optName)); +function spawnActor(bootFn, optName) { + Dataspace.spawn(new Actor(bootFn, optName)); } -function Actor(state, bootFn, optName) { - this.state = state; +function Actor(bootFn, optName) { + this.fields = {}; this.facets = Immutable.Set(); this.mux = new Mux.Mux(); this.previousKnowledge = Trie.emptyTrie; this.knowledge = Trie.emptyTrie; this.pendingActions = []; + this.dataflowGraph = new Dataflow.Graph(); if (typeof optName !== 'undefined') { this.name = optName; @@ -29,36 +31,82 @@ function Actor(state, bootFn, optName) { this.boot = function() { var self = this; - withCurrentFacet(null, function () { - bootFn.call(self.state); + withCurrentFacet(self, null, function () { + bootFn.call(self.fields); }); - self.checkForTermination(); + this.quiesce(); }; } +Actor.current = null; + +(function () { + var priorities = ['PRIORITY_QUERY_HIGH', + 'PRIORITY_QUERY', + 'PRIORITY_QUERY_HANDLER', + 'PRIORITY_NORMAL']; + for (var i = 0; i < priorities.length; i++) { + Actor[priorities[i]] = i; + } +})(); + +Actor.prototype.nextPendingAction = function (probe) { + for (var i = 0; i < this.pendingActions.length; i++) { + var q = this.pendingActions[i]; + if (q.length > 0) { + return probe ? true : q.shift(); + } + } + return false; +}; + Actor.prototype.handleEvent = function(e) { + var actor = this; if (e.type === 'stateChange') { this.previousKnowledge = this.knowledge; this.knowledge = e.patch.updateInterests(this.knowledge); } - if (this.pendingActions.length > 0) { + if (this.nextPendingAction(true)) { throw new Error('Syndicate: pendingActions must not be nonempty at start of handleEvent'); } this.facets.forEach(function (f) { - withCurrentFacet(f, function () { f.handleEvent(e); }); + withCurrentFacet(actor, f, function () { f.handleEvent(e); }); }); - while (this.pendingActions.length) { - var entry = this.pendingActions.shift(); - withCurrentFacet(entry.facet, entry.action); + this.quiesce(); +}; + +Actor.prototype.quiesce = function() { + var actor = this; + + while (true) { + var entry = this.nextPendingAction(false); + if (!entry) break; + + withCurrentFacet(actor, entry.facet, entry.action); + + this.dataflowGraph.repairDamage(function (subjectId) { + var facet = subjectId[0]; + var endpoint = subjectId[1]; + withCurrentFacet(actor, facet, function () { + // TODO: coalesce patches within a single actor + var aggregate = Patch.emptyPatch; + var patch = Patch.retract(__).andThen(endpoint.subscriptionFn.call(facet.fields)); + var r = facet.actor.mux.updateStream(endpoint.eid, patch); + aggregate = aggregate.andThen(r.deltaAggregate); + Dataspace.stateChange(aggregate); + }); + }); } - this.facets.forEach(function (f) { - withCurrentFacet(f, function () { f.refresh(); }); - }); + this.checkForTermination(); }; -Actor.prototype.pushAction = function (a) { - this.pendingActions.push({facet: Facet.current, action: a}); +Actor.prototype.pushAction = function (a, priorityOpt) { + var priority = typeof priorityOpt === 'undefined' ? Actor.PRIORITY_NORMAL : priorityOpt; + while (this.pendingActions.length < priority + 1) { + this.pendingActions.push([]); + } + this.pendingActions[priority].push({facet: Facet.current, action: a}); }; Actor.prototype.addFacet = function(facet) { @@ -88,29 +136,34 @@ function Facet(actor) { this.doneBlocks = Immutable.List(); this.children = Immutable.Set(); this.parent = Facet.current; + this.fields = Dataflow.Graph.newScope((this.parent && this.parent.fields) || actor.fields); this.terminated = false; } Facet.current = null; -function withCurrentFacet(facet, f) { - var previous = Facet.current; +function withCurrentFacet(actor, facet, f) { + var previousActor = Actor.current; + var previousFacet = Facet.current; + Actor.current = actor; Facet.current = facet; var result; try { result = f(); } catch (e) { - Facet.current = previous; + Actor.current = previousActor; + Facet.current = previousFacet; throw e; } - Facet.current = previous; + Actor.current = previousActor; + Facet.current = previousFacet; return result; } Facet.prototype.handleEvent = function(e) { var facet = this; facet.endpoints.forEach(function(endpoint) { - endpoint.handlerFn.call(facet.actor.state, e); + endpoint.handlerFn.call(facet.fields, e); }); }; @@ -118,27 +171,47 @@ Facet.prototype.addAssertion = function(assertionFn) { return this.addEndpoint(new Endpoint(assertionFn, function(e) {})); }; -Facet.prototype.addOnEventHandler = function(handler) { +Facet.prototype.addOnEventHandler = function(handler, priorityOpt) { var facet = this; return this.addEndpoint(new Endpoint(function () { return Patch.emptyPatch; }, function (e) { - facet.actor.pushAction(function () { handler(e); }); + facet.actor.pushAction(function () { handler(e); }, priorityOpt); })); }; -Facet.prototype.onEvent = function(isTerminal, eventType, subscriptionFn, projectionFn, handlerFn) { +Facet.prototype.addDataflow = function(subjectFunction) { + var facet = this; + return this.addEndpoint(new Endpoint(function () { + var subjectId = facet.actor.dataflowGraph.currentSubjectId; + facet.actor.pushAction(function () { + facet.actor.dataflowGraph.withSubject(subjectId, function () { + subjectFunction.call(facet.fields); + }); + }); + return Patch.emptyPatch; + }, function (e) {})); +}; + +Facet.prototype.onEvent = function(priority, + 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 proj = projectionFn.call(facet.fields); var spec = Patch.prependAtMeta(proj.assertion, proj.metalevel); var match = Trie.matchPattern(e.message, spec); // console.log(match); if (match) { if (isTerminal) { facet.terminate(); } - facet.actor.pushAction(function () { Util.kwApply(handlerFn, facet.actor.state, match); }); + facet.actor.pushAction(function () { Util.kwApply(handlerFn, facet.fields, match); }, + priority); } } })); @@ -147,7 +220,7 @@ Facet.prototype.onEvent = function(isTerminal, eventType, subscriptionFn, projec case 'retracted': return this.addEndpoint(new Endpoint(subscriptionFn, function(e) { if (e.type === 'stateChange') { - var proj = projectionFn.call(facet.actor.state); + var proj = projectionFn.call(facet.fields); var spec = Patch.prependAtMeta(proj.assertion, proj.metalevel); var objects = Trie.projectObjects(eventType === 'asserted' ? e.patch.added @@ -163,8 +236,8 @@ Facet.prototype.onEvent = function(isTerminal, eventType, subscriptionFn, projec facet.terminate(); } facet.actor.pushAction(function () { - Util.kwApply(handlerFn, facet.actor.state, o); - }); + Util.kwApply(handlerFn, facet.fields, o); + }, priority); } }); } @@ -172,17 +245,17 @@ Facet.prototype.onEvent = function(isTerminal, eventType, subscriptionFn, projec })); 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(); } - facet.actor.pushAction(function () { - handlerFn.call(facet.actor.state); - }); - } - this.currentValue = newValue; - }); + var endpoint = new Endpoint(function() { + var newValue = subscriptionFn.call(facet.fields); + if (newValue && !this.currentValue) { + if (isTerminal) { facet.terminate(); } + facet.actor.pushAction(function () { + handlerFn.call(facet.fields); + }, priority); + } + this.currentValue = newValue; + return Patch.emptyPatch; + }, function(e) {}); endpoint.currentValue = false; return this.addEndpoint(endpoint); @@ -206,11 +279,15 @@ Facet.prototype.interestWas = function(assertedOrRetracted, pat) { }; 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); + var facet = this; + var patch = facet.actor.dataflowGraph.withSubject([facet, endpoint], function () { + return endpoint.subscriptionFn.call(facet.fields); + }); + var r = facet.actor.mux.addStream(patch); + endpoint.eid = r.pid; + facet.endpoints = facet.endpoints.set(endpoint.eid, endpoint); Dataspace.stateChange(r.deltaAggregate); - return this; // for chaining + return facet; // for chaining }; Facet.prototype.addInitBlock = function(thunk) { @@ -223,28 +300,17 @@ Facet.prototype.addDoneBlock = function(thunk) { return this; }; -Facet.prototype.refresh = function() { - var facet = this; - var aggregate = Patch.emptyPatch; - this.endpoints.forEach(function(endpoint, eid) { - var patch = Patch.retract(__).andThen(endpoint.subscriptionFn.call(facet.actor.state)); - var r = facet.actor.mux.updateStream(eid, patch); - aggregate = aggregate.andThen(r.deltaAggregate); - }); - Dataspace.stateChange(aggregate); -}; - Facet.prototype.completeBuild = function() { var facet = this; this.actor.addFacet(this); if (this.parent) { this.parent.children = this.parent.children.add(this); } - withCurrentFacet(facet, function () { - facet.initBlocks.forEach(function(b) { b.call(facet.actor.state); }); + withCurrentFacet(this.actor, facet, function () { + facet.initBlocks.forEach(function(b) { b.call(facet.fields); }); }); var initialEvent = _Dataspace.stateChange(new Patch.Patch(facet.actor.knowledge, Trie.emptyTrie)); - withCurrentFacet(facet, function () { facet.handleEvent(initialEvent); }); + withCurrentFacet(this.actor, facet, function () { facet.handleEvent(initialEvent); }); }; Facet.prototype.terminate = function() { @@ -267,13 +333,13 @@ Facet.prototype.terminate = function() { this.actor.removeFacet(this); - withCurrentFacet(facet, function () { - facet.doneBlocks.forEach(function(b) { b.call(facet.actor.state); }); - }); - this.children.forEach(function (child) { child.terminate(); }); + + withCurrentFacet(this.actor, facet, function () { + facet.doneBlocks.forEach(function(b) { b.call(facet.fields); }); + }); }; //--------------------------------------------------------------------------- @@ -281,9 +347,37 @@ Facet.prototype.terminate = function() { function Endpoint(subscriptionFn, handlerFn) { this.subscriptionFn = subscriptionFn; this.handlerFn = handlerFn; + this.eid = 'uninitialized_eid'; // initialized later +} + +//--------------------------------------------------------------------------- + +function referenceField(obj, prop) { + if (!(prop in obj)) { + Actor.current.dataflowGraph.recordObservation(Immutable.List.of(obj, prop)); + } + return obj[prop]; +} + +function declareField(obj, prop, init) { + if (prop in obj) { + obj[prop] = init; + } else { + Actor.current.dataflowGraph.defineObservableProperty(obj, prop, init, { + objectId: Immutable.List.of(obj, prop) + }); + } +} + +function deleteField(obj, prop) { + Actor.current.dataflowGraph.recordDamage(Immutable.List.of(obj, prop)); + return delete obj[prop]; } //--------------------------------------------------------------------------- module.exports.spawnActor = spawnActor; module.exports.createFacet = createFacet; +module.exports.referenceField = referenceField; +module.exports.declareField = declareField; +module.exports.deleteField = deleteField; diff --git a/js/src/dataflow.js b/js/src/dataflow.js index 4088408..3adbf1a 100644 --- a/js/src/dataflow.js +++ b/js/src/dataflow.js @@ -9,7 +9,6 @@ function Graph() { this.edgesReverse = Immutable.Map(); this.damagedNodes = Immutable.Set(); this.currentSubjectId = null; - this.enforceSubjectPresence = true; } Graph.prototype.withSubject = function (subjectId, f) { @@ -30,8 +29,6 @@ Graph.prototype.recordObservation = function (objectId) { if (this.currentSubjectId) { this.edgesForward = MapSet.add(this.edgesForward, objectId, this.currentSubjectId); this.edgesReverse = MapSet.add(this.edgesReverse, this.currentSubjectId, objectId); - } else if (this.enforceSubjectPresence) { - throw new Error('Attempt to observe ' + objectId + ' with no currentSubjectId'); } }; @@ -80,7 +77,7 @@ Graph.prototype.repairDamage = function (repairNode) { Graph.prototype.defineObservableProperty = function (obj, prop, value, maybeOptions) { var graph = this; var options = typeof maybeOptions === 'undefined' ? {} : maybeOptions; - var objectId = '__' + (options.baseId || prop); + var objectId = options.objectId || '__' + prop; Object.defineProperty(obj, prop, { configurable: true, enumerable: true, diff --git a/js/src/main.js b/js/src/main.js index 53feb8c..df6e1e8 100644 --- a/js/src/main.js +++ b/js/src/main.js @@ -33,6 +33,7 @@ module.exports.Reflect = require("./reflect.js"); module.exports.WakeDetector = require("./wake-detector-driver.js"); module.exports.Codec = require("./codec.js"); module.exports.Broker = require("./broker.js"); +module.exports.Dataflow = require("./dataflow.js"); module.exports.Patch = require("./patch.js"); copyKeys(['emptyPatch', diff --git a/js/src/struct.js b/js/src/struct.js index 935dfce..74dd5de 100644 --- a/js/src/struct.js +++ b/js/src/struct.js @@ -54,6 +54,19 @@ function Structure(meta, fields) { } } +Structure.prototype.clone = function () { + return new Structure(this.meta, this.fields); +}; + +Structure.prototype.get = function (index) { + return this[index]; +}; + +Structure.prototype.set = function (index, value) { + var s = this.clone(); + s[index] = s.fields[index] = value; +}; + function reviveStructs(j) { if (Array.isArray(j)) { return j.map(reviveStructs); diff --git a/js/test/test-dataflow.js b/js/test/test-dataflow.js index 82c2806..fb12cc1 100644 --- a/js/test/test-dataflow.js +++ b/js/test/test-dataflow.js @@ -7,7 +7,7 @@ var Dataflow = require('../src/dataflow.js'); function Cell(graph, initialValue, name) { this.objectId = graph.defineObservableProperty(this, 'value', initialValue, { - baseId: name, + objectId: name, noopGuard: function (a, b) { return a === b; } @@ -50,7 +50,6 @@ describe('dataflow edges, damage and subjects', function () { describe('DerivedCell', function () { describe('simple case', function () { var g = new Dataflow.Graph(); - g.enforceSubjectPresence = false; var c = DerivedCell(g, 'c', function () { return 123; }); var d = DerivedCell(g, 'd', function () { return c.value * 2; }); it('should be properly initialized', function () { @@ -79,7 +78,6 @@ describe('DerivedCell', function () { describe('a more complex case', function () { var g = new Dataflow.Graph(); - g.enforceSubjectPresence = false; function add(a, b) { return a + b; } var xs = new Cell(g, Immutable.List.of(1, 2, 3, 4), 'xs'); @@ -132,7 +130,6 @@ describe('DerivedCell', function () { describe('scopes', function () { var g = new Dataflow.Graph(); - g.enforceSubjectPresence = false; function buildScopes() { var rootScope = {}; @@ -147,6 +144,9 @@ describe('scopes', function () { expect(ss.root.p).to.be(123); expect(ss.mid.p).to.be(123); expect(ss.outer.p).to.be(123); + expect('p' in ss.root).to.be(true); + expect('p' in ss.mid).to.be(true); + expect('p' in ss.outer).to.be(true); }); it('should make changes at root visible at leaves', function () { @@ -173,6 +173,9 @@ describe('scopes', function () { expect(ss.outer.p).to.be(123); expect(ss.mid.p).to.be(undefined); expect(ss.root.p).to.be(undefined); + expect('p' in ss.root).to.be(false); + expect('p' in ss.mid).to.be(false); + expect('p' in ss.outer).to.be(true); }); it('should hide middle definitions from roots but show to leaves', function () { @@ -181,5 +184,8 @@ describe('scopes', function () { expect(ss.outer.p).to.be(123); expect(ss.mid.p).to.be(123); expect(ss.root.p).to.be(undefined); + expect('p' in ss.root).to.be(false); + expect('p' in ss.mid).to.be(true); + expect('p' in ss.outer).to.be(true); }); });