LXSC stands for "Lua XML StateCharts", and is pronounced "Lexie". The LXSC library allows you to run SCXML state machines in Lua. The Data Model for interpretation is all evaluated Lua, allowing you to write conditionals and data expressions in one of the best scripting languages in the world for embedded integration.
require"lxsc-min-0.8.1" -- or dofile"lxsc-bin-0.8.1.luac"
local scxml = io.read('my.scxml'):read('*all')
local machine = LXSC:parse(scxml)
machine:start() -- initiate the interpreter and run until stable
machine:fireEvent("my.event") -- add events to the event queue to be processed
machine:fireEvent("another.event.name") -- as many as you like; they won't have any effect until you
machine:step() -- call step() to process all events and run until stable
print("Is the machine still running?",machine.running)
print("Is a state in the configuration?",machine:isActive('some-state-id'))
-- Keep firing events and calling step() to process them
The data model used by the interpreter is a Lua table. This table is used to store and retrieve the values created via <data>
or <assign>
. This table is also used as the environment under which the <script>
blocks run and the code="…"
attributes of <transition>
elements are evaluated.
Providing your own data model table allows you to:
- supply an initial set of data values—useful for initial conditional transitions
- expose functions, either utilities like
print()
or custom defined functions that provide the meat for a simple<script>doTheThing()</script>
semantic callbacks - create a custom datatable that performs metamagic when new keys are accessed or modified by the state machine
You supply a custom data model table by passing a named data
parameter to the start()
method:
local mydata = { reloading=true, userName="Gavin" } -- populate initial data values
local funcs = { print=print, doTheThing=utils.doIt } -- create 'global' functions
setmetatable( mydata, {__index=funcs} )
machine:start{ data=mydata }
There are four special machine keys that you may set to a function value to keep track of what the machine is doing: onBeforeExit
, onAfterEnter
, onDataSet
, and onTransition
.
machine.onBeforeExit = function(stateId,stateKind,isAtomic) ... end
machine.onAfterEnter = function(stateId,stateKind,isAtomic) ... end
The state change callbacks are passed three parameters:
- The string id of the state being exited or entered.
- The string kind of the state:
"state"
,"parallel"
, or"final"
.- The callbacks are not invoked for
history
orinitial
pseudo-states.
- The callbacks are not invoked for
- A boolean indicating whether the state is atomic or not.
As implied by the names the onBeforeExit
callback is invoked right before leaving a state, whilte the onAfterEnter
callback is invoked right after entering a state.
machine.onDataSet = function(dataid,newvalue) ... end
If supplied, this callback will be invoked any time the data model is changed.
Warning: using this callback may slow down the interpreter appreciably, as many internal modifications take place during normal operation (most notably setting the _event
system variable).
machine.onTransition = function(transitionTable) ... end
The onTransition
callback is invoked right before the executable content of a transition (if any) is run.
Warning: the table supplied by this callback is an internal representation whose implementation is not guaranteed to remain unchanged. Currently you can access the following keys for information about the transition:
type
- the string"internal"
or"external"
.cond
- the string value of thecond="…"
attribute, if any, ornil
._event
- the string value of theevent="…"
attribute, if any, ornil
._target
- the string of thetarget="…"
attribute, if any, ornil
.events
- an array of internalLXSC.Event
tables, one for each event, ornil
.targets
- an array of internalLXSC.State
tables, one for each target, ornil
.- Any custom attributes supplied on the transition appear as direct attributes (with no namespace information or protection).
While the machine is running (after you have called start()
) you can peek at the data for a specific location via:
local theValue = machine:get("dataId")
…and you can set the value for a particular location via:
machine:set("dataId",someValue)
You can evaluate code in the data model (just like a cond="…"
or expr="…"
attribute does) by:
local theResult = machine:eval("mycodestring")
…and you can run arbitrary code against the data model (just like a <script>
block does) by:
machine:run("mycodestring")
You can ask a running machine if a particular state id is active (in the current configuration):
print("Is the foo-bar state active?", machine:isActive('foo-bar'))
…or you can ask for the set of all states that are active:
for stateId,_ in pairs(machine:activeStateIds()) do
print("This state is currently active:",stateId)
end
…or you can ask just for the set of atomic (no sub-state) states:
for stateId,_ in pairs(machine:activeAtomicIds()) do
print("This atomic state is currently active:",stateId)
end
You can also ask for a list of all state IDs in the machine, including those autogenerated for states that have no id="…"
attribute:
for stateId,_ in pairs(machine:allStateIds()) do
print("One of the states has this id:",stateId)
end
You can ask a machine for the set of all events that trigger transitions:
for eventDescriptor,_ in pairs(machine:allEvents()) do
-- eventDescriptor is a simple dotted string, e.g. "foo.bar"
print("There's at least one transition triggered by:",eventDescriptor)
end
…or you can ask just for the events that may trigger a transition in the current configuration:
for eventDescriptor,_ in pairs(machine:availableEvents()) do
print("There's at least one active transition triggered by:",eventDescriptor)
end
Anywhere that executable content is permitted—in <onentry>
, <onexit>
, and <transition>
—a state chart may specify custom elements via a custom XML namespace. For example:
<state xmlns:my="goodstuff">
<onentry><my:explode amount="10"/></onentry>
</state>
With no modifications, when LXSC encounters such an executable it fires an error.execution.unhandled
event internally with the _event.data
set to the string "unhandled executable type explode"
.
Internal error events do not halt execution of the intepreter (unless the state machine reacts to that event in a violent manner, such as transitioning to a <final>
state). However, if you want such elements to actually do something, you must extend LXSC to handle the executable type like so:
require'lxsc-min-0.8.1'
function LXSC.Exec:explode(machine)
print("The state machine wants to explode with an amount of",self.amount)
end
The current machine is passed to your function so that you may call :fireEvent()
, :eval()
, etc. as needed. Attributes on the element are set as named keys on the self
table supplied to your function (e.g. amount
above).
Note: executable elements with conflicting names in different namespaces will use the same callback function. The only way to disambiguate them currently is via a _nsURI
property set on the table. For example, to handle this document:
<state xmlns:my="goodstuff" xmlns:their="badstuff">
<onentry>
<my:explode amount="10"/>
<their:explode chunkiness="very"/>
</onentry>
</state>
you would need to do something like:
function LXSC.Exec:explode(machine)
if self._nsURI=='goodstuff' then
print("The state machine wants to explode with an amount of",self.amount)
else
machine:fireEvent(
"error.execution.unhandled",
"Dunno how to handle 'explode' in the "..self._nsURI.." namespace"
)
end
end
You can also use this to re-implement or augment existing executables like <log>
:
-- Augmenting the <log> to use a logger with a custom logging level, e.g.
-- <transition event="error.*">
-- <log label="An error occurred" expr="_event.data" my:log-level="error" />
-- </transition>
function LXSC.Exec:log(machine)
local result = {self.label}
if self.expr then table.insert(result,machine:eval(self.expr)) end
local level = self['log-level'] or 'info'
my_global_logger[level]( my_global_logger, table.concat(result,": ") )
end
LXSC aims to be almost 100% compliant with the SCXML Interpretation Algorithm. However, there are a couple of minor variations (compared to the Working Draft as of 2013-Feb-14):
- Manual Event Processing: Where the W3C implementation calls for the interpreter to run in a separate thread with a blocking queue feeding in the events, LXSC is designed to be frame-based. You feed events into the machine and then manually call
my_lxsc:step()
to crank the machine in the same thread. This will cause the event queues to be fully processed and the machine to run until it is stable, and then return. Rinse/repeat the process of event population followed by callingstep()
each frame.- This single-threaded, on-demand approach affects a delayed
<send>
the most. While a<send event="e" delay="1s"/>
command will not inject the event at least one second has passed, it could be substantially longer than that if your script only callsstep()
every 30 seconds, or (worse) waits until some user interaction occurs to callstep()
again.
- This single-threaded, on-demand approach affects a delayed
- Configuration Clearing: The W3C algorithm calls for the state machine configuration to be cleared when the interpreter is exited. LXSC will instead leave the configuration (and data model) intact for you to inspect the final state of the machine.
- The
src="…"
attribute is unsupported for<data>
or<script>
elements. - The
_event
system variable supports.name
and.data
but none of the other properties (.type
,.sendid
,.origin
,.origintype
,.invokeid
). <assign>
elements do not support executable content instead ofexpr="…"
<send>
selements do not support thetype
/typeexpr
/target
/targetexpr
attributes.- No support for executable elements
<if>
/<elseif>
/<else>
/<foreach>
. - No support for inter-machine communication.
- No support for
<invoke>
. - No support for
<param>
in<donedata>
, nor has there been extensive testing of donedata. - Data model locations like
foo.bar
get and set a single key instead of nested tables.
LXSC is copyright ©2013 by Gavin Kistner and is licensed under the MIT License. See the LICENSE.txt file for more details.
For bugs or feature requests please open issues on GitHub. For other communication you can email the author directly.