From 26aba9727a22c7d1c39e264b75c78f53ac624dd1 Mon Sep 17 00:00:00 2001 From: Jacob Beard Date: Thu, 29 Mar 2012 16:49:45 -0400 Subject: [PATCH] Rewrote core SCXML interpreter in JavaScript. --- lib/scxml/SCXML.js | 636 ++++++++++++++++++ lib/scxml/default-transition-selector.js | 21 + lib/scxml/json2model.js | 133 ++++ lib/scxml/model.js | 100 +++ ...-dynamic-name-match-transition-selector.js | 44 ++ lib/scxml/set/ArraySet.js | 81 +++ lib/scxml/setup-default-opts.js | 28 + lib/scxml/state-kinds-enum.js | 22 + 8 files changed, 1065 insertions(+) create mode 100644 lib/scxml/SCXML.js create mode 100644 lib/scxml/default-transition-selector.js create mode 100644 lib/scxml/json2model.js create mode 100644 lib/scxml/model.js create mode 100644 lib/scxml/scxml-dynamic-name-match-transition-selector.js create mode 100644 lib/scxml/set/ArraySet.js create mode 100644 lib/scxml/setup-default-opts.js create mode 100644 lib/scxml/state-kinds-enum.js diff --git a/lib/scxml/SCXML.js b/lib/scxml/SCXML.js new file mode 100644 index 00000000..adff26de --- /dev/null +++ b/lib/scxml/SCXML.js @@ -0,0 +1,636 @@ +// Copyright 2011-2012 Jacob Beard, INFICON, and other SCION contributors +// +// 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. + +var _ = require('underscore'); + +var ArraySet = require('./set/ArraySet'), + stateKinds = require('./state-kinds-enum'), + setupDefaultOpts = require('./setup-default-opts'), + scxmlPrefixTransitionSelector = require('./scxml-dynamic-name-match-transition-selector'); + +function create(o) { + var F; + if (Object.create) { + return Object.create(o); + } else { + F = function() {}; + F.prototype = o; + return new F(); + } +} + +function getTransitionWithHigherSourceChildPriority(model) { + return function(_arg) { + var t1 = _arg[0], t2 = _arg[1]; + //compare transitions based first on depth, then based on document order + if (model.getDepth(t1.source) < model.getDepth(t2.source)) { + return t2; + } else if (model.getDepth(t2.source) < model.getDepth(t1.source)) { + return t1; + } else { + if (t1.documentOrder < t2.documentOrder) { + return t1; + } else { + return t2; + } + } + }; +} + +function SCXMLInterpreter(model, opts){ + if(model && opts){ + this.model = model; + this.opts = opts; + + //this.opts.printTrace = true; + + this.opts.StateIdSet = this.opts.StateIdSet || ArraySet; + this.opts.EventSet = this.opts.EventSet || ArraySet; + this.opts.TransitionPairSet = this.opts.TransitionPairSet || ArraySet; + this.opts.priorityComparisonFn = this.opts.priorityComparisonFn || getTransitionWithHigherSourceChildPriority(this.opts.model); + this.opts.globalEval = this.opts.globalEval || eval; + + this._configuration = new this.opts.BasicStateSet(); + this._historyValue = {}; + this._innerEventQueue = []; + this._isInFinalState = false; + this._datamodel = create(this.model.datamodel); + this._timeoutMap = {}; + } +} + +SCXMLInterpreter.prototype = { + + start : function() { + //perform big step without events to take all default transitions and reach stable initial state + if (this.opts.printTrace) console.log("performing initial big step"); + this._configuration.add(this.model.root.initial); + + //eval top-level scripts + //we treat these differently than other scripts. they get evaled in global scope, and without explicit scripting interface + //this is necessary in order to, e.g., allow js function declarations that are visible to scxml script tags later. + _(this.model.scripts).forEach(function(script){ + /*jsl:ignore*/ + with(this._datamodel){ this.opts.globalEval.call(null,script) }; + /*jsl:end*/ + }); + + //initialize top-level datamodel expressions. simple eval + for(var k in this._datamodel){ + var v = this._datamodel[k]; + if (v) this._datamodel[k] = eval(v); + } + this._performBigStep(); + return this.getConfiguration(); + }, + + getConfiguration : function() { + return new _.map(this._configuration.iter(),function(s){return s.id;}); + }, + + getFullConfiguration : function() { + return _.chain(this._configuration.iter()). + map(_.bind(function(s){ return [s].concat(this.opts.model.getAncestors(s));},this)). + flatten(). + map(function(s){return s.id;}). + uniq(). + value(); + }, + + isIn : function(stateName) { + return _.contains(this.getFullConfiguration(),stateName); + }, + + _performBigStep : function(e) { + if (e) this._innerEventQueue.push(new this.opts.EventSet([e])); + var keepGoing = true; + while (keepGoing) { + var eventSet = this._innerEventQueue.length ? this._innerEventQueue.shift() : new this.opts.EventSet(); + + //create new datamodel cache for the next small step + var datamodelForNextStep = {}; + var selectedTransitions = this._performSmallStep(eventSet, datamodelForNextStep); + keepGoing = !selectedTransitions.isEmpty(); + } + this._isInFinalState = _.every(this._configuration.iter(),function(s){ return s.kind === stateKinds.FINAL; }); + }, + + _performSmallStep : function(eventSet, datamodelForNextStep) { + + if (this.opts.printTrace) console.log("selecting transitions with eventSet: ", eventSet); + + var selectedTransitions = this._selectTransitions(eventSet, datamodelForNextStep); + + if (this.opts.printTrace) console.log("selected transitions: ", selectedTransitions); + + if (!selectedTransitions.isEmpty()) { + + if (this.opts.printTrace) console.log("sorted transitions: ", selectedTransitions); + + //we only want to enter and exit states from transitions with targets + //filter out targetless transitions here - we will only use these to execute transition actions + var selectedTransitionsWithTargets = new this.opts.TransitionSet(_.filter(selectedTransitions.iter(),function(t){return t.targets;})); + + var exitedTuple = this._getStatesExited(selectedTransitionsWithTargets), + basicStatesExited = exitedTuple[0], + statesExited = exitedTuple[1]; + + var enteredTuple = this._getStatesEntered(selectedTransitionsWithTargets), + basicStatesEntered = enteredTuple[0], + statesEntered = enteredTuple[1]; + + if (this.opts.printTrace) console.log("basicStatesExited ", basicStatesExited); + if (this.opts.printTrace) console.log("basicStatesEntered ", basicStatesEntered); + if (this.opts.printTrace) console.log("statesExited ", statesExited); + if (this.opts.printTrace) console.log("statesEntered ", statesEntered); + + var eventsToAddToInnerQueue = new this.opts.EventSet(); + + //update history states + if (this.opts.printTrace) console.log("executing state exit actions"); + + _.forEach(statesExited,_.bind(function(state){ + + if (this.opts.printTrace) console.log("exiting ", state); + + _.forEach(state.onexit,_.bind(function(action){ + this._evaluateAction(action, eventSet, datamodelForNextStep, eventsToAddToInnerQueue); + },this)); + + var f; + if (state.history) { + if (state.history.isDeep) { + f = _.bind(function(s0) { + return s0.kind === stateKinds.BASIC && _.contains(this.opts.model.getDescendants(state), s0); + },this); + } else { + f = function(s0) { + return s0.parent === state; + }; + } + //update history + this._historyValue[state.history.id] = _.filter(statesExited,f); + } + },this)); + + + // -> Concurrency: Number of transitions: Multiple + // -> Concurrency: Order of transitions: Explicitly defined + var sortedTransitions = selectedTransitions.iter().sort(function(t1, t2) { + return t1.documentOrder - t2.documentOrder; + }); + + if (this.opts.printTrace) console.log("executing transitition actions"); + + + _.forEach(sortedTransitions,_.bind(function(transition){ + _.forEach(transition.actions,_.bind(function(action){ + this._evaluateAction(action, eventSet, datamodelForNextStep, eventsToAddToInnerQueue); + },this)); + },this)); + + if (this.opts.printTrace) console.log("executing state enter actions"); + + _.forEach(statesEntered,_.bind(function(state){ + _.forEach(state.onentry,_.bind(function(action){ + this._evaluateAction(action, eventSet, datamodelForNextStep, eventsToAddToInnerQueue); + },this)); + },this)); + + if (this.opts.printTrace) console.log("updating configuration "); + if (this.opts.printTrace) console.log("old configuration ", this._configuration); + + //update configuration by removing basic states exited, and adding basic states entered + this._configuration.difference(basicStatesExited); + this._configuration.union(basicStatesEntered); + + if (this.opts.printTrace) console.log("new configuration ", this._configuration); + + //add set of generated events to the innerEventQueue -> Event Lifelines: Next small-step + if (!eventsToAddToInnerQueue.isEmpty()) { + if (this.opts.printTrace) console.log("adding triggered events to inner queue ", eventsToAddToInnerQueue); + this._innerEventQueue.push(eventsToAddToInnerQueue); + } + + if (this.opts.printTrace) console.log("updating datamodel for next small step :"); + + //update the datamodel + for (var key in datamodelForNextStep) { + if (this.opts.printTrace) console.log("key ", key); + + if (key in this._datamodel) { + if (this.opts.printTrace) console.log("old value ", this._datamodel[key]); + } else { + if (this.opts.printTrace) console.log("old value is null"); + } + if (this.opts.printTrace) console.log("new value ", datamodelForNextStep[key]); + this._datamodel[key] = datamodelForNextStep[key]; + } + } + + //if selectedTransitions is empty, we have reached a stable state, and the big-step will stop, otherwise will continue -> Maximality: Take-Many + return selectedTransitions; + }, + + _evaluateAction : function(action, eventSet, datamodelForNextStep, eventsToAddToInnerQueue) { + var _constructEventData = _.bind(function(){ + var data = {}; + + if(action.content){ + //content + data = action.content; + }else{ + //namelist + if (action.namelist) { + _.forEach(action.namelist,_.bind(function(name){ + data[name] = this._datamodel[name]; + },this)); + } + + //params + _.forEach(action.params,_.bind(function(param){ + if(param.expr){ + data[param.name] = this._eval(param.expr, datamodelForNextStep, eventSet); + }else if(param.location){ + data[param.name] = this._datamodel[param.location]; + } + },this)); + } + + return data; + },this); + + switch (action.type) { + case "raise": + if (this.opts.printTrace) console.log("sending event", action.event, "with content", action.contentexpr); + + eventsToAddToInnerQueue.add({ name: action.event }); + break; + case "assign": + this._datamodel[action.location] = this._eval(action, datamodelForNextStep, eventSet); + break; + case "script": + this._eval(action, datamodelForNextStep, eventSet, true); + break; + case "log": + console.log(this._eval(action, datamodelForNextStep, eventSet)); + break; + case "send": + debugger; + if (this._send) { + this._send({ + target: action.targetexpr ? this._eval(action.targetexpr, datamodelForNextStep, eventSet) : action.target, + name: action.eventexpr ? this._eval(action.eventexpr, datamodelForNextStep, eventSet) : action.event, + data: _constructEventData(), + origin: this.opts.origin, + type: action.typeexpr ? this._eval(action.typeexpr, datamodelForNextStep, eventSet) : action.sendType + }, { + delay: action.delayexpr ? this._eval(action.delayexpr, datamodelForNextStep, eventSet) : action.delay, + sendId: action.idlocation ? this._datamodel[action.idlocation] : action.id + }); + } + break; + case "cancel": + if (this._cancel) this._cancel(action.sendid); + break; + default : break; + } + }, + + _eval : function(action, datamodelForNextStep, eventSet, allowWrite) { + var n = this._getScriptingInterface(datamodelForNextStep, eventSet, allowWrite); + return action.evaluate.call(this.opts.evaluationContext, n.getData, n.setData, n.In, n.events, this._datamodel); + }, + + _getScriptingInterface : function(datamodelForNextStep, eventSet, allowWrite) { + return { + setData: allowWrite ? function(name, value) { + return datamodelForNextStep[name] = value; + } : function() {}, + getData: _.bind(function(name) { + return this._datamodel[name]; + },this), + In: _.bind(function(s) { + return this.isIn(s); + },this), + events: eventSet.iter() + }; + }, + + _getStatesExited : function(transitions) { + var statesExited = new this.opts.StateSet(); + var basicStatesExited = new this.opts.BasicStateSet(); + + _.forEach(transitions.iter(),_.bind(function(transition){ + var lca = this.opts.model.getLCA(transition); + var desc = this.opts.model.getDescendants(lca); + + _.forEach(this._configuration.iter(),_.bind(function(state){ + if(_.contains(desc,state)){ + basicStatesExited.add(state); + statesExited.add(state); + _.forEach(this.opts.model.getAncestors(state, lca),function(anc){ + statesExited.add(anc); + }); + } + },this)); + },this)); + + var sortedStatesExited = statesExited.iter().sort(_.bind(function(s1, s2) { + return this.opts.model.getDepth(s2) - this.opts.model.getDepth(s1); + },this)); + return [basicStatesExited, sortedStatesExited]; + }, + + _getStatesEntered : function(transitions) { + var statesToRecursivelyAdd = + _.chain(transitions.iter()). + map(function(transition){ + return transition.targets; + }). + flatten(). + value(); + + if (this.opts.printTrace) console.log("statesToRecursivelyAdd :", statesToRecursivelyAdd); + + var statesToEnter = new this.opts.StateSet(); + var basicStatesToEnter = new this.opts.BasicStateSet(); + while (statesToRecursivelyAdd.length) { + _.forEach(statesToRecursivelyAdd,_.bind(function(state){ + this._recursiveAddStatesToEnter(state, statesToEnter, basicStatesToEnter); + },this)); + + //add children of parallel states that are not already in statesToEnter to statesToRecursivelyAdd + var childrenOfParallelStatesInStatesToEnter = + _.chain(statesToEnter.iter()). + filter(function(s){return s.kind === stateKinds.PARALLEL;}). + map(function(s){return s.children;}). + flatten(). + value(); + + statesToRecursivelyAdd = _.filter(childrenOfParallelStatesInStatesToEnter,function(s){ + return s.kind !== stateKinds.HISTORY && !statesToEnter.contains(s); + }); + } + var sortedStatesEntered = statesToEnter.iter().sort(_.bind(function(s1, s2) { + return this.opts.model.getDepth(s1) - this.opts.model.getDepth(s2); + },this)); + return [basicStatesToEnter, sortedStatesEntered]; + }, + + _recursiveAddStatesToEnter : function(s, statesToEnter, basicStatesToEnter) { + if (s.kind === stateKinds.HISTORY) { + if (s.id in this._historyValue) { + _.forEach(this._historyValue[s.id],_.bind(function(historyState){ + this._recursiveAddStatesToEnter(historyState, statesToEnter, basicStatesToEnter); + },this)); + } else { + statesToEnter.add(s); + basicStatesToEnter.add(s); + } + } else { + statesToEnter.add(s); + + if (s.kind === stateKinds.PARALLEL) { + _.forEach(s.children,_.bind(function(child){ + if(child.kind !== stateKinds.HISTORY){ //don't enter history by default + this._recursiveAddStatesToEnter(child, statesToEnter, basicStatesToEnter); + } + },this)); + } else if (s.kind === stateKinds.COMPOSITE) { + this._recursiveAddStatesToEnter(s.initial, statesToEnter, basicStatesToEnter); + } else if (s.kind === stateKinds.INITIAL || s.kind === stateKinds.BASIC || s.kind === stateKinds.FINAL) { + basicStatesToEnter.add(s); + } + } + }, + + _selectTransitions : function(eventSet, datamodelForNextStep) { + if (this.opts.onlySelectFromBasicStates) { + var states = this._configuration.iter(); + } else { + var statesAndParents = new this.opts.StateSet; + + //get full configuration, unordered + //this means we may select transitions from parents before children + + _.forEach(this._configuration.iter(),_.bind(function(basicState){ + statesAndParents.add(basicState); + _.forEach(this.opts.model.getAncestors(basicState),function(ancestor){ + statesAndParents.add(ancestor); + }); + },this)); + + states = statesAndParents.iter(); + } + var n = this._getScriptingInterface(datamodelForNextStep, eventSet); + var e = _.bind(function(t) { + return t.evaluateCondition.call(this.opts.evaluationContext, n.getData, n.setData, n.In, n.events, this._datamodel); + },this); + + var eventNames = _.map(eventSet.iter(),function(event){return event.name;}); + + var usePrefixMatchingAlgorithm = _(eventNames).filter(function(name){return name.search(".");}).length; + + var transitionSelector = usePrefixMatchingAlgorithm ? scxmlPrefixTransitionSelector : this.opts.transitionSelector; + var enabledTransitions = new this.opts.TransitionSet(); + + _.forEach(states,function(state){ + _.forEach(transitionSelector(state,eventNames,e),function(t){ + enabledTransitions.add(t); + }); + }); + + var priorityEnabledTransitions = this._selectPriorityEnabledTransitions(enabledTransitions); + + if (this.opts.printTrace) console.log("priorityEnabledTransitions", priorityEnabledTransitions); + + return priorityEnabledTransitions; + }, + + _selectPriorityEnabledTransitions : function(enabledTransitions) { + var priorityEnabledTransitions = new this.opts.TransitionSet(); + + var tuple = this._getInconsistentTransitions(enabledTransitions), + consistentTransitions = tuple[0], + inconsistentTransitionsPairs = tuple[1]; + + priorityEnabledTransitions.union(consistentTransitions); + + if (this.opts.printTrace) console.log("enabledTransitions", enabledTransitions); + if (this.opts.printTrace) console.log("consistentTransitions", consistentTransitions); + if (this.opts.printTrace) console.log("inconsistentTransitionsPairs", inconsistentTransitionsPairs); + if (this.opts.printTrace) console.log("priorityEnabledTransitions", priorityEnabledTransitions); + + while (!inconsistentTransitionsPairs.isEmpty()) { + enabledTransitions = new this.opts.TransitionSet( + _.map( + inconsistentTransitionsPairs.iter(), + _.bind(function(t){return this.opts.priorityComparisonFn(t);},this))); + + tuple = this._getInconsistentTransitions(enabledTransitions); + consistentTransitions = tuple[0]; + inconsistentTransitionsPairs = tuple[1]; + + priorityEnabledTransitions.union(consistentTransitions); + + if (this.opts.printTrace) console.log("enabledTransitions", enabledTransitions); + if (this.opts.printTrace) console.log("consistentTransitions", consistentTransitions); + if (this.opts.printTrace) console.log("inconsistentTransitionsPairs", inconsistentTransitionsPairs); + if (this.opts.printTrace) console.log("priorityEnabledTransitions", priorityEnabledTransitions); + + } + return priorityEnabledTransitions; + }, + + _getInconsistentTransitions : function(transitions) { + var allInconsistentTransitions = new this.opts.TransitionSet(); + var inconsistentTransitionsPairs = new this.opts.TransitionPairSet(); + var transitionList = transitions.iter(); + + if (this.opts.printTrace) console.log("transitions", transitionList); + + for(var i = 0; i < transitionList.length; i++){ + for(var j = i+1; j < transitionList.length; j++){ + var t1 = transitionList[i]; + var t2 = transitionList[j]; + if (this._conflicts(t1, t2)) { + allInconsistentTransitions.add(t1); + allInconsistentTransitions.add(t2); + inconsistentTransitionsPairs.add([t1, t2]); + } + } + } + + var consistentTransitions = transitions.difference(allInconsistentTransitions); + return [consistentTransitions, inconsistentTransitionsPairs]; + }, + + _conflicts : function(t1, t2) { + return !this._isArenaOrthogonal(t1, t2); + }, + + _isArenaOrthogonal : function(t1, t2) { + var t1LCA = t1.targets ? this.opts.model.getLCA(t1) : t1.source; + var t2LCA = t2.targets ? this.opts.model.getLCA(t2) : t2.source; + var isOrthogonal = this.opts.model.isOrthogonalTo(t1LCA, t2LCA); + + if (this.opts.printTrace) { + console.log("transition LCAs", t1LCA.id, t2LCA.id); + console.log("transition LCAs are orthogonal?", isOrthogonal); + } + + return isOrthogonal; + } + +}; + + +//TODO: figure out how to do the kind of simple object-orientation we need +function SimpleInterpreter(model, opts) { + + if(model && opts){ + + this._isStepping = false; + + this._send = opts.send || this._send; + + this._cancel = opts.cancel || this._cancel; + + SCXMLInterpreter.call(this,model,opts); //call super constructor + } +} +SimpleInterpreter.prototype = new SCXMLInterpreter(); + +_.extend(SimpleInterpreter.prototype,{ + + gen : function(e) { + if (e === undefined) { + throw new Error("gen must be passed an event object."); + } + if (this._isStepping) { + throw new Error("gen called before previous call to gen could complete. if executed in single-threaded environment, this means it was called recursively, which is illegal, as it would break SCION step semantics."); + } + this._isStepping = true; + this._performBigStep(e); + this._isStepping = false; + return this.getConfiguration(); + }, + + _send : function(event, options) { + var callback, timeoutId, + _this = this; + if (this.opts.setTimeout) { + if (this.opts.printTrace) { + console.log("sending event", event.name, "with content", event.data, "after delay", options.delay); + } + callback = function() { + return _this.gen(event); + }; + timeoutId = this.opts.setTimeout(callback, options.delay); + if (options.sendid) return this._timeoutMap[options.sendid] = timeoutId; + } else { + throw new Error("setTimeout function not set"); + } + }, + + _cancel : function(sendid){ + if (this.opts.clearTimeout) { + if (sendid in this._timeoutMap) { + if (this.opts.printTrace) { + console.log("cancelling ", sendid, " with timeout id ", this._timeoutMap[sendid]); + } + return this.opts.clearTimeout(this._timeoutMap[sendid]); + } + } else { + throw new Error("clearTimeout function not set"); + } + } + +}); + +function BrowserInterpreter(model, opts) { + opts = opts || {}; + setupDefaultOpts(opts); + if (!opts.setTimeout) { + opts.setTimeout = function(callback, timeout) { + return window.setTimeout(callback, timeout); + }; + } + if (!opts.clearTimeout) { + opts.clearTimeout = function(timeoutId) { + return window.clearTimeout(timeoutId); + }; + } + + SimpleInterpreter.call(this,model,opts); //call super constructor +} +BrowserInterpreter.prototype = new SimpleInterpreter(); + +function NodeInterpreter(model, opts) { + opts = opts || {}; + setupDefaultOpts(opts); + opts.setTimeout = setTimeout; + opts.clearTimeout = clearTimeout; + + SimpleInterpreter.call(this,model,opts); //call super constructor +} +NodeInterpreter.prototype = new SimpleInterpreter(); + +module.exports = { + SCXMLInterpreter: SCXMLInterpreter, + SimpleInterpreter: SimpleInterpreter, + BrowserInterpreter: BrowserInterpreter, + NodeInterpreter: NodeInterpreter +}; diff --git a/lib/scxml/default-transition-selector.js b/lib/scxml/default-transition-selector.js new file mode 100644 index 00000000..15f8686d --- /dev/null +++ b/lib/scxml/default-transition-selector.js @@ -0,0 +1,21 @@ +// Copyright 2011-2012 Jacob Beard, INFICON, and other SCION contributors +// +// 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. + +var _ = require('underscore'); + +module.exports = function(state,eventNames,evaluator){ + return _.filter(state.transitions,function(t){ + return !t.event || ( _.contains(eventNames,t.event) && (!t.cond || evaluator(t)) ); + }); +}; diff --git a/lib/scxml/json2model.js b/lib/scxml/json2model.js new file mode 100644 index 00000000..da61c5ba --- /dev/null +++ b/lib/scxml/json2model.js @@ -0,0 +1,133 @@ +// Copyright 2011-2012 Jacob Beard, INFICON, and other SCION contributors +// +// 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. + +var _ = require('underscore'); + +function getDelayInMs(delayString){ + if (!delayString) { + return 0; + } else { + if (delayString.slice(-2) === "ms") { + return parseFloat(delayString.slice(0, -2)); + } else if (delayString.slice(-1) === "s") { + return parseFloat(delayString.slice(0, -1)) * 1000; + } else { + return parseFloat(delayString); + } + } +} + +function makeEvaluationFn(s, isExpression) { + return new Function( + "getData", + "setData", + "In", + "_events", + "datamodel", + "var _event = _events[0]; with(datamodel){" + (isExpression ? "return" : "") + " " + s + "}"); +} + +module.exports = function(json) { + + function stateIdToReference(stateId){ + return idToStateMap[stateId]; + } + + var idToStateMap = {}; + _.forEach(json.states,function(state){ + idToStateMap[state.id] = state; + }); + + _.forEach(json.transitions,function(transition){ + transition.evaluateCondition = makeEvaluationFn(transition.cond,true); + }); + + _.forEach(json.states,function(state){ + state.transitions = _.map(state.transitions,function(transitionNum){ return json.transitions[transitionNum];}); + + var actions = state.onentry.concat(state.onexit); + + _.forEach(state.transitions,function(transition){ + _.forEach(transition.actions,function(action){ + actions.push(action); + }); + + if(transition.lca){ + transition.lca = idToStateMap[transition.lca]; + } + }); + + _.forEach(actions,function(action){ + switch (action.type) { + case "script": + action.evaluate = makeEvaluationFn(action.script); + break; + case "assign": + action.evaluate = makeEvaluationFn(action.expr, true); + break; + case "send": + _.forEach(['contentexpr', 'eventexpr', 'targetexpr', 'typeexpr', 'delayexpr'],function(attributeName){ + if (action[attributeName]) { + action[attributeName] = { + evaluate: makeEvaluationFn(action[attributeName], true) + }; + } + }); + + _.forEach(action.params,function(param){ + if (param.expr) { + param.expr = { + evaluate: makeEvaluationFn(param.expr, true) + }; + } + }); + break; + case "log": + action.evaluate = makeEvaluationFn(action.expr, true); + break; + default : break; + } + + if (action.type === "send" && action.delay) { + action.delay = getDelayInMs(action.delay); + } + + }); + + state.initial = idToStateMap[state.initial]; + state.history = idToStateMap[state.history]; + + state.children = _.map(state.children,stateIdToReference); + + state.parent = idToStateMap[state.parent]; + + if (state.ancestors) { + state.ancestors = _.map(state.ancestors,stateIdToReference); + } + + if (state.descendants) { + state.descendants = _.map(state.descendants,stateIdToReference); + } + + _.forEach(state.transitions,function(t){ + t.source = idToStateMap[t.source]; + t.targets = t.targets && _.map(t.targets,stateIdToReference); + }); + }); + + json.root = idToStateMap[json.root]; + + return json; +}; + diff --git a/lib/scxml/model.js b/lib/scxml/model.js new file mode 100644 index 00000000..f7b4fe61 --- /dev/null +++ b/lib/scxml/model.js @@ -0,0 +1,100 @@ +// Copyright 2011-2012 Jacob Beard, INFICON, and other SCION contributors +// +// 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. + +var _ = require('underscore'); + +var stateKinds = require('./state-kinds-enum'); + +module.exports = { + getDepth: function(s) { + var count, state; + if (s.depth !== undefined) { + return s.depth; + } else { + count = 0; + state = s.parent; + while (state) { + count = count + 1; + state = state.parent; + } + return count; + } + }, + getAncestors: function(s, root) { + var ancestors, index, state; + if (s.ancestors) { + if (_.contains(s.ancestors,root)) { + return s.ancestors.slice(0, index); + } else { + return s.ancestors; + } + } else { + ancestors = []; + state = s.parent; + while (state && !(state === root)) { + ancestors.push(state); + state = state.parent; + } + return ancestors; + } + }, + getAncestorsOrSelf: function(s, root) { + return [s].concat(this.getAncestors(s, root)); + }, + getDescendants: function(s) { + var child, descendants, queue, state, _i, _len, _ref; + if (s.descendants) { + return s.descendants; + } else { + descendants = []; + queue = s.children.slice(); + while (queue.length) { + state = queue.shift(); + descendants.push(state); + queue.push.apply(queue,state.children); + } + return descendants; + } + }, + getDescendantsOrSelf: function(s) { + return [s].concat(this.getDescendants(s)); + }, + isOrthogonalTo: function(s1, s2) { + //Two control states are orthogonal if they are not ancestrally + //related, and their smallest, mutual parent is a Concurrent-state. + return !this.isAncestrallyRelatedTo(s1, s2) && this.getLCA(s1, s2).kind === stateKinds.PARALLEL; + }, + isAncestrallyRelatedTo: function(s1, s2) { + //Two control states are ancestrally related if one is child/grandchild of another. + return _.contains(this.getAncestorsOrSelf(s2), s1) || _.contains(this.getAncestorsOrSelf(s1), s2); + }, + getLCA: function(tOrS1, s2) { + if (tOrS1.lca) { + return tOrS1.lca; + } else { + //can take one or two arguments: either 1 transition, or two states + if (arguments.length === 1) { + var transition = tOrS1; + return this.getLCA(transition.source, transition.targets[0]); + } else { + var s1 = tOrS1; + var commonAncestors = _.filter(this.getAncestors(s1),_.bind(function(a){ + return _.contains(this.getDescendants(a),s2); + },this)); + return commonAncestors[0]; + } + } + } +}; + diff --git a/lib/scxml/scxml-dynamic-name-match-transition-selector.js b/lib/scxml/scxml-dynamic-name-match-transition-selector.js new file mode 100644 index 00000000..68822a1a --- /dev/null +++ b/lib/scxml/scxml-dynamic-name-match-transition-selector.js @@ -0,0 +1,44 @@ +// Copyright 2011-2012 Jacob Beard, INFICON, and other SCION contributors +// +// 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. + +var _ = require('underscore'); + +var eventNameReCache = {}; + +function eventNameToRe(name) { + return new RegExp("^" + (name.replace(/\./g, "\\.")) + "(\\.[0-9a-zA-Z]+)*$"); +} + +function retrieveEventRe(name) { + return eventNameReCache[name] ? eventNameReCache[name] : eventNameReCache[name] = eventNameToRe(name); +} + +function nameMatch(t, eventNames) { + var tEvents = t.events; + var f = + _.contains(tEvents, "*") ? + function() { return true; } : + function(name) { + return _(tEvents).filter(function(tEvent){ + return retrieveEventRe(tEvent).test(name); + }).length; + }; + return _(eventNames).filter(f).length; +} + +module.exports = function(state, eventNames, evaluator) { + return _(state.transitions).filter(function(t){ + return (!t.events || nameMatch(t,eventNames)) && (!t.cond || evaluator(t)); + }); +}; diff --git a/lib/scxml/set/ArraySet.js b/lib/scxml/set/ArraySet.js new file mode 100644 index 00000000..2016195c --- /dev/null +++ b/lib/scxml/set/ArraySet.js @@ -0,0 +1,81 @@ +// Copyright 2011-2012 Jacob Beard, INFICON, and other SCION contributors +// +// 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. + +var _ = require('underscore'); + +module.exports = function(l) { + l = l || []; + this.o = []; + + _.forEach(l,_.bind(function(x){ + this.add(x); + },this)); +}; + +//TODO: delegate to underscore's union and difference +module.exports.prototype = { + + add : function(x) { + if (!this.contains(x)) return this.o.push(x); + }, + + remove : function(x) { + var i = _.indexOf(this.o,x); + if(i === -1){ + return false; + }else{ + this.o.splice(i, 1); + } + return true; + }, + + union : function(l) { + l = l.iter ? l.iter() : l; + _.forEach(l,_.bind(function(x){ + this.add(x); + },this)); + return this; + }, + + difference : function(l) { + l = l.iter ? l.iter() : l; + + _.forEach(l,_.bind(function(x){ + this.remove(x); + },this)); + return this; + }, + + contains : function(x) { + return _.indexOf(this.o, x) >= 0; + }, + + iter : function() { + return this.o; + }, + + isEmpty : function() { + return !this.o.length; + }, + + equals : function(s2) { + var l2 = s2.iter(); + + return _.difference(this.o,l2).length === 0; + }, + + toString : function() { + return "Set(" + this.o.toString() + ")"; + } +}; diff --git a/lib/scxml/setup-default-opts.js b/lib/scxml/setup-default-opts.js new file mode 100644 index 00000000..f08176fa --- /dev/null +++ b/lib/scxml/setup-default-opts.js @@ -0,0 +1,28 @@ +// Copyright 2011-2012 Jacob Beard, INFICON, and other SCION contributors +// +// 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. + +var selector = require('./scxml-dynamic-name-match-transition-selector'), + ArraySet = require('./set/ArraySet'), + m = require('./model'); + +module.exports = function(opts) { + opts = opts || {}; + opts.TransitionSet = opts.TransitionSet || ArraySet; + opts.StateSet = opts.StateSet || ArraySet; + opts.BasicStateSet = opts.BasicStateSet || ArraySet; + opts.transitionSelector = opts.transitionSelector || selector; + opts.model = opts.model || m; + return opts; +}; + diff --git a/lib/scxml/state-kinds-enum.js b/lib/scxml/state-kinds-enum.js new file mode 100644 index 00000000..0a49930a --- /dev/null +++ b/lib/scxml/state-kinds-enum.js @@ -0,0 +1,22 @@ +// Copyright 2011-2012 Jacob Beard, INFICON, and other SCION contributors +// +// 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. + +module.exports = { + BASIC: 0, + COMPOSITE: 1, + PARALLEL: 2, + HISTORY: 3, + INITIAL: 4, + FINAL: 5 +};