├── test ├── scxml-suite │ ├── luacov │ │ ├── tick.lua │ │ ├── defaults.lua │ │ ├── stats.lua │ │ ├── runner.lua │ │ └── reporter.lua │ ├── luacov.lua │ ├── luacov.sh │ ├── autotest.lua │ ├── manifest-mod.xml │ ├── scxml10-ir-results-lxsc.xml │ └── run_suite.rb ├── testcases │ ├── final2.scxml │ ├── script.scxml │ ├── parallel3.scxml │ ├── datamodel.scxml │ ├── parallel3-internal.scxml │ ├── internal_transition.scxml │ ├── history2.scxml │ ├── parallel4.scxml │ ├── counting.scxml │ ├── final1.scxml │ ├── testReenterChild.scxml │ ├── testSiblingTransition.scxml │ ├── events-unhandled.scxml │ ├── broken.scxml │ ├── history.scxml │ ├── eventname-matching.scxml │ ├── microwave.scxml │ └── testPreemption.scxml ├── benchmark │ ├── results-0.1.txt │ ├── results-0.10.txt │ ├── results-0.2.txt │ ├── results-0.3.txt │ ├── results-0.4.txt │ ├── results-0.5.txt │ ├── results-0.6.txt │ ├── results-0.7.txt │ ├── results-0.8.txt │ ├── results-0.3.1.txt │ ├── benchmark.lua │ └── Dashboard.scxml ├── tracetests.lua ├── test.lua └── lunity.lua ├── lxsc.lua ├── lib ├── generic.lua ├── lxsc.lua ├── script.lua ├── event.lua ├── parse.lua ├── serialize.lua ├── scxml.lua ├── datatypes.lua ├── transition.lua ├── datamodel.lua ├── state.lua ├── executable.lua ├── slaxml.lua └── runtime.lua ├── LICENSE.txt ├── compiled └── compress.lua └── README.md /test/scxml-suite/luacov/tick.lua: -------------------------------------------------------------------------------- 1 | 2 | --- Load luacov using this if you want it to periodically 3 | -- save the stats file. This is useful if your script is 4 | -- a daemon (ie, does not properly terminate.) 5 | -- @class module 6 | -- @name luacov.tick 7 | require("luacov") 8 | return {} 9 | -------------------------------------------------------------------------------- /test/scxml-suite/luacov.lua: -------------------------------------------------------------------------------- 1 | --- Loads luacov.runner and immediately starts it. 2 | -- Useful for launching scripts from the command-line. 3 | -- @class module 4 | -- @name luacov 5 | -- @example lua -lluacov sometest.lua 6 | local runner = require("luacov.runner") 7 | runner.init() 8 | -------------------------------------------------------------------------------- /test/testcases/final2.scxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lxsc.lua: -------------------------------------------------------------------------------- 1 | require 'lib/state' 2 | require 'lib/scxml' 3 | require 'lib/transition' 4 | require 'lib/datamodel' 5 | require 'lib/event' 6 | require 'lib/generic' 7 | require 'lib/executable' 8 | require 'lib/script' 9 | require 'lib/datatypes' 10 | require 'lib/runtime' 11 | require 'lib/parse' 12 | require 'lib/serialize' 13 | return require 'lib/lxsc' -------------------------------------------------------------------------------- /test/testcases/script.scxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /lib/generic.lua: -------------------------------------------------------------------------------- 1 | local LXSC = require 'lib/lxsc' 2 | local generic = {} 3 | local genericMeta = {__index=generic } 4 | 5 | function LXSC:_generic(kind,nsURI) 6 | return setmetatable({_kind=kind,_kids={},_nsURI=nsURI},genericMeta) 7 | end 8 | 9 | function generic:addChild(item) 10 | table.insert(self._kids,item) 11 | end 12 | 13 | function generic:attr(name,value) 14 | self[name] = value 15 | end 16 | 17 | setmetatable(LXSC,{__index=function() return LXSC._generic end}) 18 | -------------------------------------------------------------------------------- /test/benchmark/results-0.1.txt: -------------------------------------------------------------------------------- 1 | Parse XML: 6.61ms 2 | Start Machine: 0.69ms 3 | Inject 10 Useless Events: 0.02ms 4 | Process 10 Useless Events: 0.04ms 5 | Fire 4 Events and Process: 0.05ms 6 | Fire 4 Events and Process: 0.05ms 7 | Fire 0 Events and Process: 0.04ms 8 | Fire 0 Events and Process: 0.04ms 9 | Fire 3 Events and Process: 0.05ms 10 | Fire 5 Events and Process: 0.06ms 11 | ---------------------------------- 12 | Total time: 7.67ms ± 20% 13 | -------------------------------------------------------------------------------- /test/benchmark/results-0.10.txt: -------------------------------------------------------------------------------- 1 | Parse XML: 4.86ms 2 | Start Machine: 0.87ms 3 | Inject 10 Useless Events: 0.03ms 4 | Process 10 Useless Events: 0.45ms 5 | Fire 4 Events and Process: 0.36ms 6 | Fire 4 Events and Process: 0.25ms 7 | Fire 0 Events and Process: 0.02ms 8 | Fire 0 Events and Process: 0.01ms 9 | Fire 3 Events and Process: 0.22ms 10 | Fire 5 Events and Process: 0.25ms 11 | ---------------------------------- 12 | Total time: 7.33ms ± 20% 13 | -------------------------------------------------------------------------------- /test/benchmark/results-0.2.txt: -------------------------------------------------------------------------------- 1 | Parse XML: 6.59ms 2 | Start Machine: 0.80ms 3 | Inject 10 Useless Events: 0.08ms 4 | Process 10 Useless Events: 0.06ms 5 | Fire 4 Events and Process: 0.09ms 6 | Fire 4 Events and Process: 0.11ms 7 | Fire 0 Events and Process: 0.05ms 8 | Fire 0 Events and Process: 0.06ms 9 | Fire 3 Events and Process: 0.07ms 10 | Fire 5 Events and Process: 0.08ms 11 | ---------------------------------- 12 | Total time: 7.98ms ± 20% 13 | -------------------------------------------------------------------------------- /test/benchmark/results-0.3.txt: -------------------------------------------------------------------------------- 1 | Parse XML: 6.94ms 2 | Start Machine: 1.77ms 3 | Inject 10 Useless Events: 0.43ms 4 | Process 10 Useless Events: 0.54ms 5 | Fire 4 Events and Process: 0.37ms 6 | Fire 4 Events and Process: 0.27ms 7 | Fire 0 Events and Process: 0.02ms 8 | Fire 0 Events and Process: 0.02ms 9 | Fire 3 Events and Process: 0.23ms 10 | Fire 5 Events and Process: 0.27ms 11 | ---------------------------------- 12 | Total time: 10.86ms ± 20% 13 | -------------------------------------------------------------------------------- /test/benchmark/results-0.4.txt: -------------------------------------------------------------------------------- 1 | Parse XML: 6.51ms 2 | Start Machine: 0.64ms 3 | Inject 10 Useless Events: 0.02ms 4 | Process 10 Useless Events: 0.39ms 5 | Fire 4 Events and Process: 0.38ms 6 | Fire 4 Events and Process: 0.29ms 7 | Fire 0 Events and Process: 0.02ms 8 | Fire 0 Events and Process: 0.02ms 9 | Fire 3 Events and Process: 0.25ms 10 | Fire 5 Events and Process: 0.30ms 11 | ---------------------------------- 12 | Total time: 8.82ms ± 20% 13 | -------------------------------------------------------------------------------- /test/benchmark/results-0.5.txt: -------------------------------------------------------------------------------- 1 | Parse XML: 6.58ms 2 | Start Machine: 0.71ms 3 | Inject 10 Useless Events: 0.02ms 4 | Process 10 Useless Events: 0.42ms 5 | Fire 4 Events and Process: 0.38ms 6 | Fire 4 Events and Process: 0.28ms 7 | Fire 0 Events and Process: 0.02ms 8 | Fire 0 Events and Process: 0.02ms 9 | Fire 3 Events and Process: 0.24ms 10 | Fire 5 Events and Process: 0.28ms 11 | ---------------------------------- 12 | Total time: 8.94ms ± 20% 13 | -------------------------------------------------------------------------------- /test/benchmark/results-0.6.txt: -------------------------------------------------------------------------------- 1 | Parse XML: 6.30ms 2 | Start Machine: 1.13ms 3 | Inject 10 Useless Events: 0.03ms 4 | Process 10 Useless Events: 0.71ms 5 | Fire 4 Events and Process: 0.41ms 6 | Fire 4 Events and Process: 0.30ms 7 | Fire 0 Events and Process: 0.02ms 8 | Fire 0 Events and Process: 0.02ms 9 | Fire 3 Events and Process: 0.26ms 10 | Fire 5 Events and Process: 0.30ms 11 | ---------------------------------- 12 | Total time: 9.48ms ± 20% 13 | -------------------------------------------------------------------------------- /test/benchmark/results-0.7.txt: -------------------------------------------------------------------------------- 1 | Parse XML: 6.69ms 2 | Start Machine: 0.66ms 3 | Inject 10 Useless Events: 0.02ms 4 | Process 10 Useless Events: 0.39ms 5 | Fire 4 Events and Process: 0.40ms 6 | Fire 4 Events and Process: 0.27ms 7 | Fire 0 Events and Process: 0.02ms 8 | Fire 0 Events and Process: 0.02ms 9 | Fire 3 Events and Process: 0.24ms 10 | Fire 5 Events and Process: 0.28ms 11 | ---------------------------------- 12 | Total time: 8.99ms ± 20% 13 | -------------------------------------------------------------------------------- /test/benchmark/results-0.8.txt: -------------------------------------------------------------------------------- 1 | Parse XML: 5.75ms 2 | Start Machine: 0.76ms 3 | Inject 10 Useless Events: 0.02ms 4 | Process 10 Useless Events: 0.39ms 5 | Fire 4 Events and Process: 0.39ms 6 | Fire 4 Events and Process: 0.28ms 7 | Fire 0 Events and Process: 0.02ms 8 | Fire 0 Events and Process: 0.02ms 9 | Fire 3 Events and Process: 0.27ms 10 | Fire 5 Events and Process: 0.28ms 11 | ---------------------------------- 12 | Total time: 8.18ms ± 20% 13 | -------------------------------------------------------------------------------- /test/benchmark/results-0.3.1.txt: -------------------------------------------------------------------------------- 1 | Parse XML: 6.66ms 2 | Start Machine: 0.82ms 3 | Inject 10 Useless Events: 0.03ms 4 | Process 10 Useless Events: 0.44ms 5 | Fire 4 Events and Process: 0.44ms 6 | Fire 4 Events and Process: 0.35ms 7 | Fire 0 Events and Process: 0.03ms 8 | Fire 0 Events and Process: 0.03ms 9 | Fire 3 Events and Process: 0.33ms 10 | Fire 5 Events and Process: 0.30ms 11 | ---------------------------------- 12 | Total time: 9.42ms ± 20% 13 | -------------------------------------------------------------------------------- /test/testcases/parallel3.scxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/testcases/datamodel.scxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 7 * 6 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/testcases/parallel3-internal.scxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/testcases/internal_transition.scxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /lib/lxsc.lua: -------------------------------------------------------------------------------- 1 | local LXSC = { 2 | VERSION="0.14", 3 | scxmlNS="http://www.w3.org/2005/07/scxml" 4 | } 5 | 6 | -- Horribly simple xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx 7 | function LXSC.uuid4() 8 | return table.concat({ 9 | string.format('%04x', math.random(0, 0xffff))..string.format('%04x',math.random(0, 0xffff)), 10 | string.format('%04x', math.random(0, 0xffff)), 11 | string.format('4%03x',math.random(0, 0xfff)), 12 | string.format('a%03x',math.random(0, 0xfff)), 13 | string.format('%06x', math.random(0, 0xffffff))..string.format('%06x',math.random(0, 0xffffff)) 14 | },'-') 15 | end 16 | 17 | return LXSC -------------------------------------------------------------------------------- /test/testcases/history2.scxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/testcases/parallel4.scxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/testcases/counting.scxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/testcases/final1.scxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/testcases/testReenterChild.scxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/testcases/testSiblingTransition.scxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /lib/script.lua: -------------------------------------------------------------------------------- 1 | local LXSC = require 'lib/lxsc' 2 | LXSC.Script={}; LXSC.Script.__meta = {__index=LXSC.Script,} 3 | function LXSC:script() 4 | local t = { _kind = 'script' } 5 | return setmetatable(t,self.Script.__meta) 6 | end 7 | 8 | function LXSC.Script:attr(name,value) 9 | if name=="src" then 10 | local scheme, hierarchy 11 | local colon = value:find(':') 12 | if colon then scheme,hierarchy = value:sub(1,colon-1), value:sub(colon+1) end 13 | if scheme=='file' then 14 | local f = assert(io.open(hierarchy,"r")) 15 | self._text = f:read("*all") 16 | f:close() 17 | else 18 | error("Cannot load 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | unhandled executable 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /test/scxml-suite/luacov/defaults.lua: -------------------------------------------------------------------------------- 1 | --- Global configuration file. Copy, customize and store in your 2 | -- project folder as '.luacov' for project specific configuration 3 | -- @class module 4 | -- @name luacov.defaults 5 | return { 6 | 7 | -- default filename to load for config options if not provided 8 | -- only has effect in 'luacov.defaults.lua' 9 | ["configfile"] = ".luacov", 10 | 11 | -- filename to store stats collected 12 | ["statsfile"] = "luacov.stats.out", 13 | 14 | -- filename to store report 15 | ["reportfile"] = "luacov.report.out", 16 | 17 | -- Run reporter on completion? (won't work for ticks) 18 | runreport = false, 19 | 20 | -- Delete stats file after reporting? 21 | deletestats = false, 22 | 23 | -- Patterns for files to include when reporting 24 | -- all will be included if nothing is listed 25 | -- (exclude overrules include, do not include 26 | -- the .lua extension) 27 | ["include"] = { 28 | "runtime" 29 | }, 30 | 31 | -- Patterns for files to exclude when reporting 32 | -- all will be included if nothing is listed 33 | -- (exclude overrules include, do not include 34 | -- the .lua extension) 35 | ["exclude"] = { 36 | "luacov$", 37 | "luacov%.reporter$", 38 | "luacov%.defaults$", 39 | "luacov%.runner$", 40 | "luacov%.stats$", 41 | "luacov%.tick$", 42 | }, 43 | 44 | 45 | } 46 | -------------------------------------------------------------------------------- /test/testcases/history.scxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /lib/event.lua: -------------------------------------------------------------------------------- 1 | local LXSC = require 'lib/lxsc' 2 | LXSC.Event={ 3 | origintype="http://www.w3.org/TR/scxml/#SCXMLEventProcessor", 4 | type ="platform", 5 | sendid ="", 6 | origin ="", 7 | invokeid ="", 8 | } 9 | local EventMeta; EventMeta = { __index=LXSC.Event, __tostring=function(e) return e:inspect() end } 10 | setmetatable(LXSC.Event,{__call=function(_,name,data,fields) 11 | local e = {name=name,data=data,_tokens={}} 12 | setmetatable(e,EventMeta) 13 | for k,v in pairs(fields) do e[k] = v end 14 | for token in string.gmatch(name,'[^.*]+') do table.insert(e._tokens,token) end 15 | return e 16 | end}) 17 | 18 | 19 | 20 | function LXSC.Event:triggersDescriptor(descriptor) 21 | if self.name==descriptor or descriptor=="*" then 22 | return true 23 | else 24 | local i=1 25 | for token in string.gmatch(descriptor,'[^.*]+') do 26 | if self._tokens[i]~=token then return false end 27 | i=i+1 28 | end 29 | return true 30 | end 31 | return false 32 | end 33 | 34 | function LXSC.Event:triggersTransition(t) return t:matchesEvent(self) end 35 | 36 | function LXSC.Event:inspect(detailed) 37 | if detailed then 38 | return ""..LXSC.serializeLua( self, {sort=self.__sorter, nokey={_tokens=1}} ) 39 | else 40 | return string.format("",self.name,self.type) 41 | end 42 | end 43 | 44 | function LXSC.Event.__sorter(a,b) 45 | local keyorder = {name='_____________',type='___',data='~~~~~~~~~~~~'} 46 | a = keyorder[a[1]] or a[1] 47 | b = keyorder[b[1]] or b[1] 48 | return a 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /test/testcases/microwave.scxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /compiled/compress.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | -- Merges all the files into one -flat file 3 | -- Creates a simplified -min version (if lstrip is available) 4 | -- Creates a compiled bytecode -bin version (NOPE) 5 | package.path = '../lib/?.lua;../?.lua;' .. package.path 6 | local LXSC = require 'lxsc' 7 | require 'io' 8 | require 'os' 9 | 10 | DIR = "../" 11 | 12 | function compress() 13 | local flatName = "lxsc-flat-"..LXSC.VERSION..".lua" 14 | local minName = "lxsc-min-"..LXSC.VERSION..".lua" 15 | -- local binName = "lxsc-bin-"..LXSC.VERSION..".luac" 16 | 17 | local flat = io.open(flatName,"w") 18 | flat:write(getFlatContent().."\n") 19 | flat:close() 20 | 21 | -- os.execute(string.format("luac -s -o %s %s",binName,flatName)) 22 | 23 | -- http://www.tecgraf.puc-rio.br/~lhf/ftp/lua/#lstrip 24 | os.execute(string.format("lstrip %s > %s",flatName,minName)) 25 | end 26 | 27 | -- Merge all file content into a single string, 28 | -- removing requires. 29 | function getFlatContent() 30 | local lines = {} 31 | for line in io.lines(DIR..'lib/lxsc.lua') do table.insert(lines,line) end 32 | table.remove(lines) -- Pop off the final "return" statement; we'll add it later. 33 | 34 | for line in io.lines(DIR..'lxsc.lua') do 35 | for i=1,10 do table.insert(lines,'') end 36 | local target = string.match(line,[[^require%s?["']([^"']+)]]) 37 | if target then table.insert(lines,unwrapRequire(target..".lua")) end 38 | end 39 | 40 | for i=1,10 do table.insert(lines,'') end 41 | 42 | table.insert(lines,"return LXSC") 43 | return table.concat(lines,"\n") 44 | end 45 | 46 | -- Gather the lines from file, 47 | -- recursively expanding requires into the required file content. 48 | function unwrapRequire(file) 49 | local lines = {} 50 | 51 | for line in io.lines(DIR..file) do 52 | local preamble,target = string.match(line,[[^(.-)require ["']([^"']+)]]) 53 | if target~='lib/lxsc' then -- Skip lib/lxsc requires; it's already at the top of the file. 54 | if target then 55 | line = unwrapRequire(target..".lua") 56 | if preamble~="" then line = preamble.."(function()\n"..line.."\nend)()" end 57 | end 58 | table.insert(lines,line) 59 | end 60 | end 61 | return table.concat(lines,"\n") 62 | end 63 | 64 | compress() -------------------------------------------------------------------------------- /test/testcases/testPreemption.scxml: -------------------------------------------------------------------------------- 1 | 9 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /lib/serialize.lua: -------------------------------------------------------------------------------- 1 | local function serialize(v,opts) 2 | if not opts then opts = {} end 3 | if not opts.notype then opts.notype = {} end 4 | if not opts.nokey then opts.nokey = {} end 5 | if not opts.lv then opts.lv=0 end 6 | if opts.sort and type(opts.sort)~='function' then opts.sort = function(a,b) if type(a[1])==type(b[1]) then return a[1]stats.statsfile 4 | -- has been set to the filename of the statsfile to create, load, etc. 5 | -- @class module 6 | -- @name luacov.stats 7 | local M = {} 8 | 9 | ----------------------------------------------------- 10 | -- Loads the stats file. 11 | -- @return table with data 12 | -- @return hitcount of the line with the most hits (to provide the widest number format for reporting) 13 | function M.load() 14 | local data, most_hits = {}, 0 15 | local stats = io.open(M.statsfile, "r") 16 | if not stats then 17 | return nil 18 | end 19 | while true do 20 | local nlines = stats:read("*n") 21 | if not nlines then 22 | break 23 | end 24 | local skip = stats:read(1) 25 | if skip ~= ":" then 26 | break 27 | end 28 | local filename = stats:read("*l") 29 | if not filename then 30 | break 31 | end 32 | data[filename] = { 33 | max=nlines 34 | } 35 | for i = 1, nlines do 36 | local hits = stats:read("*n") 37 | if not hits then 38 | break 39 | end 40 | local skip = stats:read(1) 41 | if skip ~= " " then 42 | break 43 | end 44 | if hits > 0 then 45 | data[filename][i] = hits 46 | most_hits = math.max(most_hits, hits) 47 | end 48 | end 49 | end 50 | stats:close() 51 | return data, most_hits 52 | end 53 | 54 | -------------------------------- 55 | -- Opens the statfile 56 | -- @return filehandle 57 | function M.start() 58 | return io.open(M.statsfile, "w") 59 | end 60 | 61 | -------------------------------- 62 | -- Closes the statfile 63 | -- @param stats filehandle to the statsfile 64 | function M.stop(stats) 65 | stats:close() 66 | end 67 | 68 | -------------------------------- 69 | -- Saves data to the statfile 70 | -- @param data data to store 71 | -- @param stats filehandle where to store 72 | function M.save(data, stats) 73 | stats:seek("set") 74 | for filename, filedata in pairs(data) do 75 | local max = filedata.max 76 | stats:write(max, ":", filename, "\n") 77 | for i = 1, max do 78 | local hits = filedata[i] 79 | if not hits then 80 | hits = 0 81 | end 82 | stats:write(hits, " ") 83 | end 84 | stats:write("\n") 85 | end 86 | stats:flush() 87 | end 88 | 89 | return M 90 | -------------------------------------------------------------------------------- /lib/scxml.lua: -------------------------------------------------------------------------------- 1 | local LXSC = require 'lib/lxsc' 2 | LXSC.SCXML={}; LXSC.SCXML.__meta = {__index=LXSC.SCXML} 3 | setmetatable(LXSC.SCXML,{__index=LXSC.State}) 4 | 5 | function LXSC:scxml() 6 | local t = LXSC:state('scxml') 7 | t.name = "(lxsc)" 8 | t.binding = "early" 9 | t.datamodel = "lua" 10 | -- t.id = nil 11 | 12 | t.running = false 13 | t._configuration = LXSC.OrderedSet() 14 | 15 | return setmetatable(t,LXSC.SCXML.__meta) 16 | end 17 | 18 | -- Fetch a single named value from the data model 19 | function LXSC.SCXML:get(location) 20 | return self._data:get(location) 21 | end 22 | 23 | -- Set a single named value in the data model 24 | function LXSC.SCXML:set(location,value) 25 | return self._data:set(location,value) 26 | end 27 | 28 | -- Evaluate a single Lua expression and return the value 29 | function LXSC.SCXML:eval(expression) 30 | return self._data:eval(expression) 31 | end 32 | 33 | -- Run arbitrary script code (multiple lines) with no return value 34 | function LXSC.SCXML:run(code) 35 | return self._data:run(code) 36 | end 37 | 38 | function LXSC.SCXML:isActive(stateId) 39 | if not rawget(self,'_stateById') then self:expandScxmlSource() end 40 | return self._configuration[self._stateById[stateId]] 41 | end 42 | 43 | function LXSC.SCXML:activeStateIds() 44 | local a = LXSC.OrderedSet() 45 | for _,s in ipairs(self._configuration) do a:add(rawget(s,'id') or rawget(s,'name')) end 46 | return a 47 | end 48 | 49 | function LXSC.SCXML:activeAtomicIds() 50 | local a = LXSC.OrderedSet() 51 | for _,s in ipairs(self._configuration) do 52 | if s.isAtomic then a:add(s.id) end 53 | end 54 | return a 55 | end 56 | 57 | function LXSC.SCXML:allEvents() 58 | local all = {} 59 | local function crawl(state) 60 | for _,s in ipairs(state.states) do 61 | for _,t in ipairs(s._eventedTransitions) do 62 | for _,e in ipairs(t.events) do 63 | all[e.name] = true 64 | end 65 | end 66 | crawl(s) 67 | end 68 | end 69 | crawl(self) 70 | return all 71 | end 72 | 73 | function LXSC.SCXML:availableEvents() 74 | local all = {} 75 | for _,s in ipairs(self._configuration) do 76 | for _,t in ipairs(s._eventedTransitions) do 77 | for _,e in ipairs(t.events) do 78 | all[e.name] = true 79 | end 80 | end 81 | end 82 | return all 83 | end 84 | 85 | function LXSC.SCXML:allStateIds() 86 | if not rawget(self,'_stateById') then self:expandScxmlSource() end 87 | local stateById = {} 88 | for id,s in pairs(self._stateById) do 89 | if s._kind~="initial" then stateById[id]=s end 90 | end 91 | return stateById 92 | end 93 | 94 | function LXSC.SCXML:atomicStateIds() 95 | if not rawget(self,'_stateById') then self:expandScxmlSource() end 96 | local stateById = {} 97 | for id,s in pairs(self._stateById) do 98 | if s.isAtomic and s._kind~="initial" then stateById[id]=s end 99 | end 100 | return stateById 101 | end 102 | 103 | function LXSC.SCXML:addChild(item) 104 | if item._kind=='script' then 105 | self._script = item 106 | else 107 | LXSC.State.addChild(self,item) 108 | end 109 | end 110 | 111 | -- Wrap os.clock() as SCXML:elapsed() so that clients can override with own implementation if desired 112 | LXSC.SCXML.elapsed = os.clock -------------------------------------------------------------------------------- /lib/datatypes.lua: -------------------------------------------------------------------------------- 1 | local LXSC = require 'lib/lxsc' 2 | LXSC.OrderedSet = {_kind='OrderedSet'}; LXSC.OrderedSet.__meta = {__index=LXSC.OrderedSet} 3 | 4 | setmetatable(LXSC.OrderedSet,{__call=function(o) 5 | return setmetatable({},o.__meta) 6 | end}) 7 | 8 | function LXSC.OrderedSet:add(e) 9 | if not self[e] then 10 | local idx = #self+1 11 | self[idx] = e 12 | self[e] = idx 13 | end 14 | end 15 | 16 | function LXSC.OrderedSet:delete(e) 17 | local index = self[e] 18 | if index then 19 | table.remove(self,index) 20 | self[e] = nil 21 | for i,o in ipairs(self) do self[o]=i end -- Store new indexes 22 | end 23 | end 24 | 25 | function LXSC.OrderedSet:union(set2) 26 | local i=#self 27 | for _,e in ipairs(set2) do 28 | if not self[e] then 29 | i = i+1 30 | self[i] = e 31 | self[e] = i 32 | end 33 | end 34 | end 35 | 36 | function LXSC.OrderedSet:isMember(e) 37 | return self[e] 38 | end 39 | 40 | function LXSC.OrderedSet:some(f) 41 | for _,o in ipairs(self) do 42 | if f(o) then return true end 43 | end 44 | end 45 | 46 | function LXSC.OrderedSet:every(f) 47 | for _,v in ipairs(self) do 48 | if not f(v) then return false end 49 | end 50 | return true 51 | end 52 | 53 | function LXSC.OrderedSet:isEmpty() 54 | return not self[1] 55 | end 56 | 57 | function LXSC.OrderedSet:clear() 58 | for k,v in pairs(self) do self[k]=nil end 59 | end 60 | 61 | function LXSC.OrderedSet:toList() 62 | return LXSC.List(table.unpack(self)) 63 | end 64 | 65 | function LXSC.OrderedSet:hasIntersection(set2) 66 | if #self<#set2 then 67 | for _,e in ipairs(self) do if set2[e] then return true end end 68 | else 69 | for _,e in ipairs(set2) do if self[e] then return true end end 70 | end 71 | return false 72 | end 73 | 74 | function LXSC.OrderedSet:inspect() 75 | local t = {} 76 | for i,v in ipairs(self) do t[i] = v.inspect and v:inspect() or tostring(v) end 77 | return t[1] and "{ "..table.concat(t,', ').." }" or '{}' 78 | end 79 | 80 | -- ******************************************************************* 81 | 82 | LXSC.List = {_kind='List'}; LXSC.List.__meta = {__index=LXSC.List} 83 | setmetatable(LXSC.List,{__call=function(o,...) 84 | return setmetatable({...},o.__meta) 85 | end}) 86 | 87 | function LXSC.List:head() 88 | return self[1] 89 | end 90 | 91 | function LXSC.List:tail() 92 | local l = LXSC.List(table.unpack(self)) 93 | table.remove(l,1) 94 | return l 95 | end 96 | 97 | function LXSC.List:append(...) 98 | local len=#self 99 | for i,v in ipairs{...} do self[len+i] = v end 100 | return self 101 | end 102 | 103 | function LXSC.List:filter(f) 104 | local t={} 105 | local i=1 106 | for _,v in ipairs(self) do 107 | if f(v) then 108 | t[i]=v; i=i+1 109 | end 110 | end 111 | return LXSC.List(table.unpack(t)) 112 | end 113 | 114 | LXSC.List.some = LXSC.OrderedSet.some 115 | LXSC.List.every = LXSC.OrderedSet.every 116 | LXSC.List.inspect = LXSC.OrderedSet.inspect 117 | 118 | function LXSC.List:sort(f) 119 | table.sort(self,f) 120 | return self 121 | end 122 | 123 | 124 | -- ******************************************************************* 125 | 126 | LXSC.Queue = {_kind='Queue'}; LXSC.Queue.__meta = {__index=LXSC.Queue} 127 | setmetatable(LXSC.Queue,{__call=function(o) 128 | return setmetatable({},o.__meta) 129 | end}) 130 | 131 | function LXSC.Queue:enqueue(e) 132 | self[#self+1] = e 133 | end 134 | 135 | function LXSC.Queue:dequeue() 136 | return table.remove(self,1) 137 | end 138 | 139 | function LXSC.Queue:isEmpty() 140 | return not self[1] 141 | end 142 | 143 | LXSC.Queue.inspect = LXSC.OrderedSet.inspect 144 | -------------------------------------------------------------------------------- /test/scxml-suite/manifest-mod.xml: -------------------------------------------------------------------------------- 1 | 2 | No support for 'invoke' 3 | No support for 'invoke' 4 | No support for 'invoke' 5 | No support for 'invoke' 6 | No support for 'invoke' 7 | No support for 'invoke' 8 | No support for 'invoke' 9 | No support for 'invoke' 10 | No support for 'invoke' 11 | No support for 'invoke' 12 | No support for 'invoke' 13 | No support for 'invoke' 14 | No support for 'invoke' 15 | No support for 'invoke' 16 | No support for 'invoke' 17 | No support for 'invoke' 18 | No support for 'invoke' 19 | No support for 'invoke' 20 | No support for 'invoke' 21 | No support for 'invoke' 22 | No support for 'invoke' 23 | No support for 'invoke' 24 | No support for 'invoke' 25 | No support for 'invoke' 26 | No support for 'invoke' 27 | No support for 'invoke' 28 | No support for 'invoke' 29 | No support for 'invoke' 30 | No support for 'invoke' 31 | No support for 'invoke' 32 | No support for 'invoke' 33 | No support for 'invoke' 34 | No support for 'invoke' 35 | No support for 'invoke' 36 | No support for 'send' to external targets 37 | No support for IO Processors 38 | No support for 'send' to external targets 39 | No support for 'invoke' 40 | No support for 'invoke' 41 | 42 | 43 | The LXSC data model cannot support multiple key/value pairs with the same key name. 44 | No support for 'invoke' 45 | No support for 'invoke' 46 | 47 | The late binding works, but an error is only raised in s1, since in s0 it is valid in the LXSC Lua data model to access a variable that has not yet been set. 48 | The document is not rejected, but does raise an internal error during execution. 49 | The document is not rejected, but does raise an internal error during execution when s03 is entered. 50 | The event is raised, but remains on the internal queue after the interpreter is stopped. 51 | 52 | -------------------------------------------------------------------------------- /lib/transition.lua: -------------------------------------------------------------------------------- 1 | local LXSC = require 'lib/lxsc' 2 | LXSC.Transition={} 3 | LXSC.Transition.__meta = { 4 | __index=LXSC.Transition, 5 | __tostring=function(t) return t:inspect() end 6 | } 7 | local validTransitionFields = {targets=1,cond=1,_target=1} 8 | setmetatable(LXSC.Transition,{__index=function(s,k) if not validTransitionFields[k] then error("Attempt to access '"..tostring(k).."' on transition "..tostring(s)) end end}) 9 | 10 | function LXSC:transition() 11 | local t = { _kind='transition', _exec={}, type="external" } 12 | setmetatable(t,self.Transition.__meta) 13 | return t 14 | end 15 | 16 | function LXSC.Transition:attr(name,value) 17 | if name=='event' then 18 | self.events = {} 19 | self._event = value 20 | for event in string.gmatch(value,'[^%s]+') do 21 | local tokens = {} 22 | for token in string.gmatch(event,'[^.*]+') do table.insert(tokens,token) end 23 | tokens.name = table.concat(tokens,'.') 24 | table.insert(self.events,tokens) 25 | end 26 | 27 | elseif name=='target' then 28 | self.targets = nil 29 | self._target = value 30 | for target in string.gmatch(value,'[^%s]+') do self:addTarget(target) end 31 | 32 | elseif name=='cond' or name=='type' then 33 | self[name] = value 34 | 35 | else 36 | -- local was = rawget(self,name) 37 | -- if was~=nil and was~=value then print(string.format("Warning: updating transition %s=%s with %s=%s",name,tostring(self[name]),name,tostring(value))) end 38 | self[name] = value 39 | 40 | end 41 | end 42 | 43 | function LXSC.Transition:addChild(item) 44 | table.insert(self._exec,item) 45 | end 46 | 47 | function LXSC.Transition:addTarget(stateOrId) 48 | if not self.targets then self.targets = LXSC.List() end 49 | if type(stateOrId)=='string' then 50 | for id in string.gmatch(stateOrId,'[^%s]+') do 51 | table.insert(self.targets,id) 52 | end 53 | else 54 | table.insert(self.targets,stateOrId) 55 | end 56 | end 57 | 58 | function LXSC.Transition:conditionMatched(datamodel) 59 | if self.cond then 60 | local result = datamodel:eval(self.cond) 61 | return result and (result ~= LXSC.Datamodel.EVALERROR) 62 | end 63 | return true 64 | end 65 | 66 | function LXSC.Transition:matchesEvent(event) 67 | for _,tokens in ipairs(self.events) do 68 | if event.name==tokens.name or tokens.name=="*" then 69 | return true 70 | elseif #tokens <= #event._tokens then 71 | local matched = true 72 | for i,token in ipairs(tokens) do 73 | if event._tokens[i]~=token then 74 | matched = false 75 | break 76 | end 77 | end 78 | if matched then 79 | -- print("Transition",self._event,"matched",event.name) 80 | return true 81 | end 82 | end 83 | end 84 | -- print("Transition",self._event,"does not match",event.name) 85 | end 86 | 87 | function LXSC.Transition:inspect(detailed) 88 | local targets 89 | if self.targets then 90 | targets = {} 91 | for i,s in ipairs(self.targets) do targets[i] = s.id end 92 | end 93 | if detailed then 94 | return string.format( 95 | "", 96 | self.source.id or self.source.name, 97 | rawget(self,'_event') and (" on '"..self._event.."'") or "", 98 | rawget(self,'cond') and (" if '"..self.cond.."'") or "", 99 | targets and (" target='"..table.concat(targets,' ').."'") or " TARGETLESS", 100 | self.type 101 | ) 102 | else 103 | return string.format( 104 | "", 105 | rawget(self,'_event') and (" event='"..self._event.."'") or "", 106 | rawget(self,'cond') and (" cond='"..self.cond.."'") or "", 107 | targets and (" target='"..table.concat(targets,' ').."'") or " TARGETLESS", 108 | self.type 109 | ) 110 | end 111 | end 112 | -------------------------------------------------------------------------------- /lib/datamodel.lua: -------------------------------------------------------------------------------- 1 | local LXSC = require 'lib/lxsc' 2 | LXSC.Datamodel = {}; LXSC.Datamodel.__meta = {__index=LXSC.Datamodel} 3 | 4 | setmetatable(LXSC.Datamodel,{__call=function(dm,scxml,scope) 5 | if not scope then scope = {} end 6 | function scope.In(id) return scxml:isActive(id) end 7 | return setmetatable({ statesInited={}, scxml=scxml, scope=scope, cache={} },dm.__meta) 8 | end}) 9 | 10 | function LXSC.Datamodel:initAll() 11 | local function recurse(state) 12 | self:initState(state) 13 | for _,s in ipairs(state.reals) do recurse(s) end 14 | end 15 | recurse(self.scxml) 16 | end 17 | 18 | function LXSC.Datamodel:initState(state) 19 | if not self.statesInited[state] then 20 | for _,data in ipairs(state._datamodels) do 21 | local value, err 22 | if data.src then 23 | local colon = data.src:find(':') 24 | local scheme,hierarchy = data.src:sub(1,colon-1), data.src:sub(colon+1) 25 | if scheme=='file' then 26 | local f,msg = io.open(hierarchy,"r") 27 | if not f then 28 | self.scxml:fireEvent("error.execution.invalid-file",msg) 29 | else 30 | value = self:eval(f:read("*all")) 31 | f:close() 32 | end 33 | else 34 | self.scxml:fireEvent("error.execution.invalid-data-scheme","LXSC does not support ") 35 | end 36 | else 37 | value = self:eval(data.expr or tostring(data._text)) 38 | end 39 | 40 | if value~=LXSC.Datamodel.EVALERROR then 41 | self:set( data.id, value ) 42 | else 43 | self:set( data.id, nil ) 44 | end 45 | end 46 | self.statesInited[state] = true 47 | end 48 | end 49 | 50 | function LXSC.Datamodel:eval(expression) 51 | return self:run('return '..expression) 52 | end 53 | 54 | function LXSC.Datamodel:run(code) 55 | local func,err = self.cache[code] 56 | if not func then 57 | func,err = load(code, nil, 't', self.scope) 58 | if func then 59 | self.cache[code] = func 60 | else 61 | self.scxml:fireEvent("error.execution.syntax",err) 62 | return LXSC.Datamodel.EVALERROR 63 | end 64 | end 65 | if func then 66 | local ok,result = pcall(func) 67 | if not ok then 68 | self.scxml:fireEvent("error.execution.evaluation",result) 69 | return LXSC.Datamodel.EVALERROR 70 | else 71 | return result 72 | end 73 | end 74 | end 75 | 76 | -- Reserved for internal use; should not be used by user scripts 77 | function LXSC.Datamodel:_setSystem(location,value) 78 | self.scope[location] = value 79 | if rawget(self.scxml,'onDataSet') then self.scxml.onDataSet(location,value) end 80 | end 81 | 82 | function LXSC.Datamodel:set(location,value) 83 | -- TODO: support foo.bar location dereferencing 84 | if location~=nil then 85 | if type(location)=='string' and string.sub(location,1,1)=='_' then 86 | self.scxml:fireEvent("error.execution.invalid-set","Cannot set system variables") 87 | else 88 | self.scope[location] = value 89 | if rawget(self.scxml,'onDataSet') then self.scxml.onDataSet(location,value) end 90 | return true 91 | end 92 | else 93 | self.scxml:fireEvent("error.execution.invalid-set","Location must not be nil") 94 | end 95 | end 96 | 97 | function LXSC.Datamodel:get(id) 98 | if id==nil or id=='' then 99 | return LXSC.Datamodel.INVALIDLOCATION 100 | else 101 | return self.scope[id] 102 | end 103 | end 104 | 105 | function LXSC.Datamodel:serialize(pretty) 106 | if pretty then 107 | return LXSC.serializeLua(self.scope,{sort=self.__sorter,indent=' '}) 108 | else 109 | return LXSC.serializeLua(self.scope) 110 | end 111 | end 112 | 113 | function LXSC.Datamodel.__sorter(a,b) 114 | local ak,av,bk,bv = a[1],a[2],b[1],b[2] 115 | local tak,tav,tbk,tbv = type(a[1]),type(a[2]),type(b[1]),type(b[2]) 116 | a,b = ak,bk 117 | if tav=='function' then a='~~~'..ak end 118 | if tak=='function' then a='~~~~' end 119 | if tbv=='function' then b='~~~'..bk end 120 | if tbk=='function' then b='~~~~' end 121 | if tak=='string' and ak:find('_')==1 then a='~~'..ak end 122 | if tbk=='string' and bk:find('_')==1 then b='~~'..bk end 123 | if type(a)==type(b) then return a file.max then 53 | file.max = line_nr 54 | end 55 | file[line_nr] = (file[line_nr] or 0) + 1 56 | end 57 | 58 | local function run_report() 59 | local success, error = pcall(function() require("luacov.reporter").report() end) 60 | if not success then 61 | print ("LuaCov reporting error; "..tostring(error)) 62 | end 63 | end 64 | 65 | local function on_exit() 66 | os.remove(luacovlock) 67 | stats.save(data, statsfile) 68 | stats.stop(statsfile) 69 | 70 | if M.configuration.runreport then run_report() end 71 | end 72 | 73 | ------------------------------------------------------ 74 | -- Loads a valid configuration 75 | -- @param configuration user provided config (config-table or filename) 76 | -- @return existing configuration if already set, otherwise loads a new 77 | -- config from the provided data or the defaults 78 | function M.load_config(configuration) 79 | if not M.configuration then 80 | if not configuration then 81 | -- nothing provided, try and load from defaults 82 | local success 83 | success, configuration = pcall(dofile, M.defaults.configfile) 84 | if not success then 85 | configuration = M.defaults 86 | end 87 | elseif type(configuration) == "string" then 88 | configuration = dofile(configuration) 89 | elseif type(configuration) == "table" then 90 | -- do nothing 91 | else 92 | error("Expected filename, config table or nil. Got " .. type(configuration)) 93 | end 94 | M.configuration = configuration 95 | end 96 | return M.configuration 97 | end 98 | 99 | -------------------------------------------------- 100 | -- Initializes LuaCov runner to start collecting data 101 | -- @param configuration if string, filename of config file (used to call load_config). 102 | -- If table then config table (see file luacov.default.lua for an example) 103 | function M.init(configuration) 104 | M.configuration = M.load_config(configuration) 105 | 106 | stats.statsfile = M.configuration.statsfile 107 | 108 | data = stats.load() or {} 109 | statsfile = stats.start() 110 | M.statsfile = statsfile 111 | tick = package.loaded["luacov.tick"] 112 | 113 | if not tick then 114 | M.on_exit_trick = io.open(luacovlock, "w") 115 | debug.setmetatable(M.on_exit_trick, { __gc = on_exit } ) 116 | end 117 | -- metatable trick on filehandle won't work if Lua exits through 118 | -- os.exit() hence wrap that with exit code as well 119 | local rawexit = os.exit 120 | os.exit = function(...) 121 | on_exit() 122 | rawexit(...) 123 | end 124 | 125 | debug.sethook(on_line, "l") 126 | 127 | -- debug must be set for each coroutine separately 128 | -- hence wrap coroutine function to set the hook there 129 | -- as well 130 | local rawcoroutinecreate = coroutine.create 131 | coroutine.create = function(...) 132 | local co = rawcoroutinecreate(...) 133 | debug.sethook(co, on_line, "l") 134 | return co 135 | end 136 | coroutine.wrap = function(...) 137 | local co = rawcoroutinecreate(...) 138 | debug.sethook(co, on_line, "l") 139 | return function() 140 | local r = { coroutine.resume(co) } 141 | if not r[1] then 142 | error(r[2]) 143 | end 144 | return unpack(r, 2) 145 | end 146 | end 147 | 148 | end 149 | 150 | return setmetatable(M, { ["__call"] = function(self, configfile) M.init(configfile) end }) 151 | -------------------------------------------------------------------------------- /lib/state.lua: -------------------------------------------------------------------------------- 1 | local LXSC = require 'lib/lxsc' 2 | LXSC.State={}; LXSC.State.__meta = {__index=LXSC.State} 3 | 4 | setmetatable(LXSC.State,{__index=function(s,k) error("Attempt to access "..tostring(k).." on state") end}) 5 | 6 | LXSC.State.stateKinds = {state=1,parallel=1,final=1,history=1,initial=1} 7 | LXSC.State.realKinds = {state=1,parallel=1,final=1} 8 | LXSC.State.aggregates = {datamodel=1,donedata=1} 9 | LXSC.State.executes = {onentry='_onentrys',onexit='_onexits'} 10 | 11 | function LXSC:state(kind) 12 | local t = { 13 | _kind = kind or 'state', 14 | id = kind.."-"..self.uuid4(), 15 | isAtomic = true, 16 | isCompound = false, 17 | isParallel = kind=='parallel', 18 | isHistory = kind=='history', 19 | isFinal = kind=='final', 20 | ancestors = {}, 21 | 22 | states = {}, 23 | reals = LXSC.List(), -- , , and children only 24 | transitions = LXSC.List(), 25 | _eventlessTransitions = {}, 26 | _eventedTransitions = {}, 27 | 28 | _onentrys = {}, 29 | _onexits = {}, 30 | _datamodels = {}, 31 | _donedatas = {}, 32 | _invokes = {} 33 | } 34 | if kind=='history' then t.type='shallow' end -- default value 35 | t.selfAndAncestors={t} 36 | return setmetatable(t,self.State.__meta) 37 | end 38 | 39 | function LXSC.State:attr(name,value) 40 | if name=="name" or name=="id" or name=="initial" then 41 | self[name] = value 42 | else 43 | -- local was = rawget(self,name) 44 | -- if was~=nil and was~=value then print(string.format("Warning: updating state %s=%s with %s=%s",name,tostring(self[name]),name,tostring(value))) end 45 | self[name] = value 46 | end 47 | end 48 | 49 | function LXSC.State:addChild(item) 50 | if item._kind=='transition' then 51 | item.source = self 52 | table.insert( self.transitions, item ) 53 | 54 | elseif self.aggregates[item._kind] then 55 | item.state = self 56 | 57 | elseif self.executes[item._kind] then 58 | item.state = self 59 | table.insert( self[self.executes[item._kind]], item ) 60 | 61 | elseif self.stateKinds[item._kind] then 62 | table.insert(self.states,item) 63 | item.parent = self 64 | item.ancestors[1] = self 65 | item.ancestors[self] = true 66 | item.selfAndAncestors[2] = self 67 | for i,anc in ipairs(self.ancestors) do 68 | item.ancestors[i+1] = anc 69 | item.ancestors[anc] = true 70 | item.selfAndAncestors[i+2] = anc 71 | end 72 | if self.realKinds[item._kind] then 73 | table.insert(self.reals,item) 74 | self.isCompound = self._kind~='parallel' 75 | self.isAtomic = false 76 | end 77 | 78 | elseif item._kind=='invoke' then 79 | item.state = self 80 | table.insert(self._invokes,item) 81 | 82 | else 83 | -- print("Warning: unhandled child of state: "..item._kind ) 84 | end 85 | end 86 | 87 | function LXSC.State:ancestorsUntil(stopNode) 88 | local i=0 89 | return function() 90 | i=i+1 91 | if self.ancestors[i] ~= stopNode then 92 | return self.ancestors[i] 93 | end 94 | end 95 | end 96 | 97 | function LXSC.State:createInitialTo(stateOrId) 98 | local initial = LXSC:state('initial') 99 | self:addChild(initial) 100 | local transition = LXSC:transition() 101 | initial:addChild(transition) 102 | transition:addTarget(stateOrId) 103 | transition._target = type(stateOrId)=='string' and stateOrId or stateOrId.id 104 | self.initial = initial 105 | end 106 | 107 | function LXSC.State:convertInitials() 108 | local init = rawget(self,'initial') 109 | if type(init)=='string' then 110 | -- Convert initial="..." attribute to state 111 | self:createInitialTo(self.initial) 112 | elseif not init then 113 | local initialElement 114 | for _,s in ipairs(self.states) do 115 | if s._kind=='initial' then initialElement=s; break end 116 | end 117 | 118 | if initialElement then 119 | self.initial = initialElement 120 | elseif self.states[1] then 121 | self:createInitialTo(self.states[1]) 122 | end 123 | end 124 | for _,s in ipairs(self.reals) do s:convertInitials() end 125 | end 126 | 127 | function LXSC.State:cacheReference(lookup) 128 | lookup[self.id] = self 129 | for _,s in ipairs(self.states) do s:cacheReference(lookup) end 130 | end 131 | 132 | function LXSC.State:resolveReferences(lookup) 133 | for _,t in ipairs(self.transitions) do 134 | if t.targets then 135 | for i,target in ipairs(t.targets) do 136 | if type(target)=="string" then 137 | if lookup[target] then 138 | t.targets[i] = lookup[target] 139 | else 140 | error(string.format("Cannot find start with id '%s' for target",tostring(target))) 141 | end 142 | end 143 | end 144 | end 145 | end 146 | for _,s in ipairs(self.states) do s:resolveReferences(lookup) end 147 | end 148 | 149 | function LXSC.State:descendantOf(possibleAncestor) 150 | return self.ancestors[possibleAncestor] 151 | end 152 | 153 | function LXSC.State:inspect() 154 | return string.format("<%s id=%s>",tostring(rawget(self,'_kind')),tostring(rawget(self,'id'))) 155 | end 156 | 157 | -- ******************************************************** 158 | 159 | -- These elements pass their children through to the appropriate collection on the state 160 | for kind,collection in pairs{ datamodel='_datamodels', donedata='_donedatas' } do 161 | LXSC[kind] = function() 162 | local t = {_kind=kind} 163 | function t:addChild(item) 164 | table.insert(self.state[collection],item) 165 | end 166 | return t 167 | end 168 | end -------------------------------------------------------------------------------- /test/scxml-suite/scxml10-ir-results-lxsc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | No support for 'invoke' 26 | 27 | 28 | No support for 'invoke' 29 | No support for 'invoke' 30 | 31 | 32 | 33 | 34 | 35 | No support for 'invoke' 36 | 37 | 38 | No support for 'invoke' 39 | No support for 'invoke' 40 | No support for 'invoke' 41 | No support for 'invoke' 42 | No support for 'invoke' 43 | No support for 'invoke' 44 | No support for 'invoke' 45 | No support for 'invoke' 46 | No support for 'invoke' 47 | No support for 'invoke' 48 | No support for 'invoke' 49 | No support for 'invoke' 50 | No support for 'invoke' 51 | No support for 'invoke' 52 | No support for 'invoke' 53 | No support for 'invoke' 54 | No support for 'invoke' 55 | No support for 'invoke' 56 | No support for 'invoke' 57 | No support for 'invoke' 58 | No support for 'invoke' 59 | No support for 'invoke' 60 | No support for 'invoke' 61 | No support for 'invoke' 62 | No support for 'invoke' 63 | No support for 'invoke' 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | No support for 'invoke' 95 | 96 | 97 | 98 | 99 | 100 | No support for 'invoke' 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | No support for 'invoke' 136 | 137 | 138 | 139 | 140 | 141 | No support for 'send' to external targets 142 | No support for IO Processors 143 | 144 | 145 | 146 | 147 | 148 | No support for 'send' to external targets 149 | 150 | 151 | 152 | 153 | No support for 'invoke' 154 | 155 | 156 | 157 | 158 | 159 | No support for 'invoke' 160 | 161 | 162 | 163 | 164 | The LXSC data model cannot support multiple key/value pairs with the same key name. 165 | No support for 'invoke' 166 | No support for 'invoke' 167 | 168 | The late binding works, but an error is only raised in s1, since in s0 it is valid in the LXSC Lua data model to access a variable that has not yet been set. 169 | The document is not rejected, but does raise an internal error during execution. 170 | The document is not rejected, but does raise an internal error during execution when s03 is entered. 171 | The event is raised, but remains on the internal queue after the interpreter is stopped. 172 | 173 | -------------------------------------------------------------------------------- /lib/executable.lua: -------------------------------------------------------------------------------- 1 | local LXSC = require 'lib/lxsc' 2 | LXSC.Exec = {} 3 | 4 | function LXSC.Exec:log(scxml) 5 | local message = {self.label} 6 | if self.expr and self.expr~="" then 7 | local value = scxml:eval(self.expr) 8 | if value==LXSC.Datamodel.EVALERROR then return end 9 | table.insert(message,tostring(value)) 10 | end 11 | print(table.concat(message,": ")) 12 | return true 13 | end 14 | 15 | function LXSC.Exec:assign(scxml) 16 | -- TODO: support child executable content in place of expr 17 | if self.location=="" then 18 | scxml:fireEvent("error.execution.invalid-location","Unsupported location '"..tostring(self.location).."'") 19 | else 20 | local value = scxml:eval(self.expr) 21 | if value~=LXSC.Datamodel.EVALERROR then 22 | scxml:set( self.location, value ) 23 | return true 24 | end 25 | end 26 | end 27 | 28 | function LXSC.Exec:raise(scxml) 29 | scxml:fireEvent(self.event,nil,{type='internal',origintype=''}) 30 | return true 31 | end 32 | 33 | function LXSC.Exec:script(scxml) 34 | local result = scxml:run(self._text) 35 | return result ~= LXSC.Datamodel.EVALERROR 36 | end 37 | 38 | function LXSC.Exec:send(scxml) 39 | -- TODO: support type/typeexpr/target/targetexpr 40 | local type = self.type or self.typeexpr and scxml:eval(self.typeexpr) 41 | if type == LXSC.Datamodel.EVALERROR then return end 42 | 43 | local id = self.id 44 | if self.idlocation and not id then 45 | local loc = scxml:eval(self.idlocation) 46 | if loc == LXSC.Datamodel.EVALERROR then return end 47 | id = LXSC.uuid4() 48 | scxml:set( loc, id ) 49 | end 50 | 51 | if not type then type = 'http://www.w3.org/TR/scxml/#SCXMLEventProcessor' end 52 | if type ~= 'http://www.w3.org/TR/scxml/#SCXMLEventProcessor' then 53 | scxml:fireEvent("error.execution.invalid-send-type","Unsupported type '"..tostring(type).."'",{sendid=id}) 54 | return 55 | end 56 | 57 | local target = self.target or self.targetexpr and scxml:eval(self.targetexpr) 58 | if target == LXSC.Datamodel.EVALERROR then return end 59 | if target and target ~= '#_internal' and target ~= '#_scxml_' .. scxml:get('_sessionid') then 60 | scxml:fireEvent("error.execution.invalid-send-target","Unsupported target '"..tostring(target).."'",{sendid=id}) 61 | return 62 | end 63 | 64 | local name = self.event or scxml:eval(self.eventexpr) 65 | if name == LXSC.Datamodel.EVALERROR then return end 66 | local data 67 | if self.namelist then 68 | data = {} 69 | for name in string.gmatch(self.namelist,'[^%s]+') do data[name] = scxml:get(name) end 70 | if not next(data) then 71 | scxml:fireEvent("error.execution.invalid-send-namelist"," namelist must include one or more locations",{sendid=id}) 72 | return 73 | end 74 | end 75 | for _,child in ipairs(self._kids) do 76 | if child._kind=='param' then 77 | if not data then data = {} end 78 | if not scxml:executeSingle(child,data) then return end 79 | elseif child._kind=='content' then 80 | if data then error(" may not have both and child elements.") end 81 | data = {} 82 | if not scxml:executeSingle(child,data) then return end 83 | data = data.content -- unwrap the content 84 | end 85 | end 86 | 87 | if self.delay or self.delayexpr then 88 | local delay = self.delay or scxml:eval(self.delayexpr) 89 | if delay == LXSC.Datamodel.EVALERROR then return end 90 | local delaySeconds, units = string.match(delay,'^(.-)(m?s)') 91 | delaySeconds = tonumber(delaySeconds) 92 | if units=="ms" then delaySeconds = delaySeconds/1000 end 93 | local delayedEvent = { expires=scxml:elapsed()+delaySeconds, name=name, data=data, sendid=id } 94 | local i=1 95 | for _,delayed2 in ipairs(scxml._delayedSend) do 96 | if delayed2.expires>delayedEvent.expires then break else i=i+1 end 97 | end 98 | table.insert(scxml._delayedSend,i,delayedEvent) 99 | else 100 | local fields = {type=target=='#_internal' and 'internal' or 'external'} 101 | if fields.type=='external' then 102 | fields.origin = '#_scxml_' .. scxml:get('_sessionid') 103 | else 104 | fields.origintype = '' 105 | end 106 | fields.sendid = self.id 107 | scxml:fireEvent(name,data,fields) 108 | end 109 | return true 110 | end 111 | 112 | function LXSC.Exec:param(scxml,context) 113 | if not context then error(" only supported as child of ") end 114 | if not self.name then error(" element missing 'name' attribute") end 115 | if not (self.location or self.expr) then error(" element requires either 'expr' or 'location' attribute") end 116 | local val 117 | if self.location then 118 | val = scxml:get(self.location) 119 | if val == LXSC.Datamodel.INVALIDLOCATION then return end 120 | elseif self.expr then 121 | val = scxml:eval(self.expr) 122 | if val == LXSC.Datamodel.EVALERROR then return end 123 | end 124 | context[self.name] = val 125 | return true 126 | end 127 | 128 | function LXSC.Exec:content(scxml,context) 129 | if not context then error(" only supported as child of or ") end 130 | if self.expr and self._text then error(" element must have either 'expr' attribute or child content, but not both") end 131 | if not (self.expr or self._text) then error(" element requires either 'expr' attribute or child content") end 132 | local val = scxml:eval(self.expr or self._text) 133 | if val == LXSC.Datamodel.EVALERROR then return end 134 | context.content = val 135 | return true 136 | end 137 | 138 | function LXSC.Exec:cancel(scxml) 139 | local sendid = self.sendid or scxml:eval(self.sendidexpr) 140 | if sendid == LXSC.Datamodel.EVALERROR then return end 141 | scxml:cancelDelayedSend(sendid) 142 | return true 143 | end 144 | 145 | LXSC.Exec['if'] = function (self,scxml) 146 | local result = scxml:eval(self.cond) 147 | if result == LXSC.Datamodel.EVALERROR then return end 148 | if result then 149 | for _,child in ipairs(self._kids) do 150 | if child._kind=='else' or child._kind=='elseif' then 151 | break 152 | else 153 | if not scxml:executeSingle(child) then return end 154 | end 155 | end 156 | else 157 | local executeFlag = false 158 | for _,child in ipairs(self._kids) do 159 | if child._kind=='else' then 160 | if executeFlag then break else executeFlag = true end 161 | elseif child._kind=='elseif' then 162 | if executeFlag then 163 | break 164 | else 165 | result = scxml:eval(child.cond) 166 | if result == LXSC.Datamodel.EVALERROR then return end 167 | if result then executeFlag = true end 168 | end 169 | elseif executeFlag then 170 | if not scxml:executeSingle(child) then return end 171 | end 172 | end 173 | end 174 | return true 175 | end 176 | 177 | function LXSC.Exec:foreach(scxml) 178 | local array = scxml:get(self.array) 179 | if type(array) ~= 'table' then 180 | scxml:fireEvent('error.execution',"foreach array '"..self.array.."' is not a table") 181 | else 182 | local list = {} 183 | for i,v in ipairs(array) do list[i]=v end 184 | for i,v in ipairs(list) do 185 | if not scxml:set(self.item,v) then return end 186 | if self.index and not scxml:set(self.index,i) then return end 187 | for _,child in ipairs(self._kids) do 188 | if not scxml:executeSingle(child) then return end 189 | end 190 | end 191 | return true 192 | end 193 | end 194 | 195 | function LXSC.SCXML:processDelayedSends() -- automatically called by :step() 196 | local i,last=1,#self._delayedSend 197 | while i<=last do 198 | local delayedEvent = self._delayedSend[i] 199 | if delayedEvent.expires <= self:elapsed() then 200 | table.remove(self._delayedSend,i) 201 | self:fireEvent(delayedEvent.name,delayedEvent.data,{type='external'}) 202 | last = last-1 203 | else 204 | i=i+1 205 | end 206 | end 207 | end 208 | 209 | function LXSC.SCXML:cancelDelayedSend(sendId) 210 | for i=#self._delayedSend,1,-1 do 211 | if self._delayedSend[i].sendid==sendId then table.remove(self._delayedSend,i) end 212 | end 213 | end 214 | 215 | -- ****************************************************************** 216 | 217 | function LXSC.SCXML:executeContent(parent) 218 | for _,executable in ipairs(parent._kids) do 219 | if not self:executeSingle(executable) then break end 220 | end 221 | end 222 | 223 | function LXSC.SCXML:executeSingle(item,...) 224 | local handler = LXSC.Exec[item._kind] 225 | if handler then 226 | return handler(item,self,...) 227 | else 228 | self:fireEvent('error.execution.unhandled',"unhandled executable type "..item._kind) 229 | return true -- Just because we didn't understand it doesn't mean we should stop processing executable 230 | end 231 | end 232 | 233 | -------------------------------------------------------------------------------- /lib/slaxml.lua: -------------------------------------------------------------------------------- 1 | --[=====================================================================[ 2 | v0.8 Copyright © 2013-2018 Gavin Kistner ; MIT Licensed 3 | See http://github.com/Phrogz/SLAXML for details. 4 | --]=====================================================================] 5 | local SLAXML = { 6 | VERSION = "0.8", 7 | _call = { 8 | pi = function(target,content) 9 | print(string.format("",target,content)) 10 | end, 11 | comment = function(content) 12 | print(string.format("",content)) 13 | end, 14 | startElement = function(name,nsURI,nsPrefix) 15 | io.write("<") 16 | if nsPrefix then io.write(nsPrefix,":") end 17 | io.write(name) 18 | if nsURI then io.write(" (ns='",nsURI,"')") end 19 | print(">") 20 | end, 21 | attribute = function(name,value,nsURI,nsPrefix) 22 | io.write(' ') 23 | if nsPrefix then io.write(nsPrefix,":") end 24 | io.write(name,'=',string.format('%q',value)) 25 | if nsURI then io.write(" (ns='",nsURI,"')") end 26 | io.write("\n") 27 | end, 28 | text = function(text,cdata) 29 | print(string.format(" %s: %q",cdata and 'cdata' or 'text',text)) 30 | end, 31 | closeElement = function(name,nsURI,nsPrefix) 32 | io.write("") 35 | end, 36 | } 37 | } 38 | 39 | function SLAXML:parser(callbacks) 40 | return { _call=callbacks or self._call, parse=SLAXML.parse } 41 | end 42 | 43 | function SLAXML:parse(xml,options) 44 | if not options then options = { stripWhitespace=false } end 45 | 46 | -- Cache references for maximum speed 47 | local find, sub, gsub, char, push, pop, concat = string.find, string.sub, string.gsub, string.char, table.insert, table.remove, table.concat 48 | local first, last, match1, match2, match3, pos2, nsURI 49 | local unpack = unpack or table.unpack 50 | local pos = 1 51 | local state = "text" 52 | local textStart = 1 53 | local currentElement={} 54 | local currentAttributes={} 55 | local currentAttributeCt -- manually track length since the table is re-used 56 | local nsStack = {} 57 | local anyElement = false 58 | 59 | local utf8markers = { {0x7FF,192}, {0xFFFF,224}, {0x1FFFFF,240} } 60 | local function utf8(decimal) -- convert unicode code point to utf-8 encoded character string 61 | if decimal<128 then return char(decimal) end 62 | local charbytes = {} 63 | for bytes,vals in ipairs(utf8markers) do 64 | if decimal<=vals[1] then 65 | for b=bytes+1,2,-1 do 66 | local mod = decimal%64 67 | decimal = (decimal-mod)/64 68 | charbytes[b] = char(128+mod) 69 | end 70 | charbytes[1] = char(vals[2]+decimal) 71 | return concat(charbytes) 72 | end 73 | end 74 | end 75 | local entityMap = { ["lt"]="<", ["gt"]=">", ["amp"]="&", ["quot"]='"', ["apos"]="'" } 76 | local entitySwap = function(orig,n,s) return entityMap[s] or n=="#" and utf8(tonumber('0'..s)) or orig end 77 | local function unescape(str) return gsub( str, '(&(#?)([%d%a]+);)', entitySwap ) end 78 | 79 | local function finishText() 80 | if first>textStart and self._call.text then 81 | local text = sub(xml,textStart,first-1) 82 | if options.stripWhitespace then 83 | text = gsub(text,'^%s+','') 84 | text = gsub(text,'%s+$','') 85 | if #text==0 then text=nil end 86 | end 87 | if text then self._call.text(unescape(text),false) end 88 | end 89 | end 90 | 91 | local function findPI() 92 | first, last, match1, match2 = find( xml, '^<%?([:%a_][:%w_.-]*) ?(.-)%?>', pos ) 93 | if first then 94 | finishText() 95 | if self._call.pi then self._call.pi(match1,match2) end 96 | pos = last+1 97 | textStart = pos 98 | return true 99 | end 100 | end 101 | 102 | local function findComment() 103 | first, last, match1 = find( xml, '^', pos ) 104 | if first then 105 | finishText() 106 | if self._call.comment then self._call.comment(match1) end 107 | pos = last+1 108 | textStart = pos 109 | return true 110 | end 111 | end 112 | 113 | local function nsForPrefix(prefix) 114 | if prefix=='xml' then return 'http://www.w3.org/XML/1998/namespace' end -- http://www.w3.org/TR/xml-names/#ns-decl 115 | for i=#nsStack,1,-1 do if nsStack[i][prefix] then return nsStack[i][prefix] end end 116 | error(("Cannot find namespace for prefix %s"):format(prefix)) 117 | end 118 | 119 | local function startElement() 120 | anyElement = true 121 | first, last, match1 = find( xml, '^<([%a_][%w_.-]*)', pos ) 122 | if first then 123 | currentElement[2] = nil -- reset the nsURI, since this table is re-used 124 | currentElement[3] = nil -- reset the nsPrefix, since this table is re-used 125 | finishText() 126 | pos = last+1 127 | first,last,match2 = find(xml, '^:([%a_][%w_.-]*)', pos ) 128 | if first then 129 | currentElement[1] = match2 130 | currentElement[3] = match1 -- Save the prefix for later resolution 131 | match1 = match2 132 | pos = last+1 133 | else 134 | currentElement[1] = match1 135 | for i=#nsStack,1,-1 do if nsStack[i]['!'] then currentElement[2] = nsStack[i]['!']; break end end 136 | end 137 | currentAttributeCt = 0 138 | push(nsStack,{}) 139 | return true 140 | end 141 | end 142 | 143 | local function findAttribute() 144 | first, last, match1 = find( xml, '^%s+([:%a_][:%w_.-]*)%s*=%s*', pos ) 145 | if first then 146 | pos2 = last+1 147 | first, last, match2 = find( xml, '^"([^<"]*)"', pos2 ) -- FIXME: disallow non-entity ampersands 148 | if first then 149 | pos = last+1 150 | match2 = unescape(match2) 151 | else 152 | first, last, match2 = find( xml, "^'([^<']*)'", pos2 ) -- FIXME: disallow non-entity ampersands 153 | if first then 154 | pos = last+1 155 | match2 = unescape(match2) 156 | end 157 | end 158 | end 159 | if match1 and match2 then 160 | local currentAttribute = {match1,match2} 161 | local prefix,name = string.match(match1,'^([^:]+):([^:]+)$') 162 | if prefix then 163 | if prefix=='xmlns' then 164 | nsStack[#nsStack][name] = match2 165 | else 166 | currentAttribute[1] = name 167 | currentAttribute[4] = prefix 168 | end 169 | else 170 | if match1=='xmlns' then 171 | nsStack[#nsStack]['!'] = match2 172 | currentElement[2] = match2 173 | end 174 | end 175 | currentAttributeCt = currentAttributeCt + 1 176 | currentAttributes[currentAttributeCt] = currentAttribute 177 | return true 178 | end 179 | end 180 | 181 | local function findCDATA() 182 | first, last, match1 = find( xml, '^', pos ) 183 | if first then 184 | finishText() 185 | if self._call.text then self._call.text(match1,true) end 186 | pos = last+1 187 | textStart = pos 188 | return true 189 | end 190 | end 191 | 192 | local function closeElement() 193 | first, last, match1 = find( xml, '^%s*(/?)>', pos ) 194 | if first then 195 | state = "text" 196 | pos = last+1 197 | textStart = pos 198 | 199 | -- Resolve namespace prefixes AFTER all new/redefined prefixes have been parsed 200 | if currentElement[3] then currentElement[2] = nsForPrefix(currentElement[3]) end 201 | if self._call.startElement then self._call.startElement(unpack(currentElement)) end 202 | if self._call.attribute then 203 | for i=1,currentAttributeCt do 204 | if currentAttributes[i][4] then currentAttributes[i][3] = nsForPrefix(currentAttributes[i][4]) end 205 | self._call.attribute(unpack(currentAttributes[i])) 206 | end 207 | end 208 | 209 | if match1=="/" then 210 | pop(nsStack) 211 | if self._call.closeElement then self._call.closeElement(unpack(currentElement)) end 212 | end 213 | return true 214 | end 215 | end 216 | 217 | local function findElementClose() 218 | first, last, match1, match2 = find( xml, '^', pos ) 219 | if first then 220 | nsURI = nil 221 | for i=#nsStack,1,-1 do if nsStack[i]['!'] then nsURI = nsStack[i]['!']; break end end 222 | else 223 | first, last, match2, match1 = find( xml, '^', pos ) 224 | if first then nsURI = nsForPrefix(match2) end 225 | end 226 | if first then 227 | finishText() 228 | if self._call.closeElement then self._call.closeElement(match1,nsURI) end 229 | pos = last+1 230 | textStart = pos 231 | pop(nsStack) 232 | return true 233 | end 234 | end 235 | 236 | while pos<#xml do 237 | if state=="text" then 238 | if not (findPI() or findComment() or findCDATA() or findElementClose()) then 239 | if startElement() then 240 | state = "attributes" 241 | else 242 | first, last = find( xml, '^[^<]+', pos ) 243 | pos = (first and last or pos) + 1 244 | end 245 | end 246 | elseif state=="attributes" then 247 | if not findAttribute() then 248 | if not closeElement() then 249 | error("Was in an element and couldn't find attributes or the close.") 250 | end 251 | end 252 | end 253 | end 254 | 255 | if not anyElement then error("Parsing did not discover any elements") end 256 | if #nsStack > 0 then error("Parsing ended with unclosed elements") end 257 | end 258 | 259 | return SLAXML -------------------------------------------------------------------------------- /test/scxml-suite/luacov/reporter.lua: -------------------------------------------------------------------------------- 1 | ------------------------ 2 | -- Report module, will transform statistics file into a report. 3 | -- @class module 4 | -- @name luacov.reporter 5 | local M = {} 6 | 7 | --- Utility function to make patterns more readable 8 | local function fixup(pat) 9 | return pat:gsub(" ", " +") -- ' ' represents "at least one space" 10 | :gsub("=", " *= *") -- '=' may be surrounded by spaces 11 | :gsub("%(", " *%%( *") -- '(' may be surrounded by spaces 12 | :gsub("%)", " *%%) *") -- ')' may be surrounded by spaces 13 | :gsub("", " *[%%w_]+ *") -- identifier 14 | :gsub("", " *[%%w._]+ *") -- identifier 15 | :gsub("", "%%[(=*)%%[[^]]* *") 16 | :gsub("", "[%%w_, ]+") -- comma-separated identifiers 17 | :gsub("", "[%%w_, \"'%%.]*") -- comma-separated arguments 18 | :gsub("", "%%[? *[\"'%%w_]+ *%%]?") -- field, possibly like ["this"] 19 | :gsub(" %* ", " ") -- collapse consecutive spacing rules 20 | :gsub(" %+ %*", " +") -- collapse consecutive spacing rules 21 | end 22 | 23 | local long_string_1 = "^() *" .. fixup"=$" 24 | local long_string_2 = "^() *" .. fixup"local =$" 25 | 26 | local function check_long_string(line, in_long_string, ls_equals, linecount) 27 | local long_string 28 | if not linecount then 29 | if line:match("%[=*%[") then 30 | long_string, ls_equals = line:match(long_string_1) 31 | if not long_string then 32 | long_string, ls_equals = line:match(long_string_2) 33 | end 34 | end 35 | end 36 | ls_equals = ls_equals or "" 37 | if long_string then 38 | in_long_string = true 39 | elseif in_long_string and line:match("%]"..ls_equals.."%]") then 40 | in_long_string = false 41 | end 42 | return in_long_string, ls_equals or "" 43 | end 44 | 45 | --- Lines that are always excluded from accounting 46 | local exclusions = { 47 | { false, "^#!" }, -- Unix hash-bang magic line 48 | { true, "" }, -- Empty line 49 | { true, fixup "end,?" }, -- Single "end" 50 | { true, fixup "else" }, -- Single "else" 51 | { true, fixup "repeat" }, -- Single "repeat" 52 | { true, fixup "do" }, -- Single "do" 53 | { true, fixup "while true do" }, -- "while true do" generates no code 54 | { true, fixup "if true then" }, -- "if true then" generates no code 55 | { true, fixup "local " }, -- "local var1, ..., varN" 56 | { true, fixup "local =" }, -- "local var1, ..., varN =" 57 | { true, fixup "local function()" }, -- "local function(arg1, ..., argN)" 58 | { true, fixup "local function ()" }, -- "local function f (arg1, ..., argN)" 59 | } 60 | 61 | --- Lines that are only excluded from accounting when they have 0 hits 62 | local hit0_exclusions = { 63 | { true, "[%w_,='\" ]+," }, -- "var1 var2," multi columns table stuff 64 | { true, fixup "=.+," }, -- "[123] = 23," "['foo'] = "asd"," 65 | { true, fixup "*function()" }, -- "1,2,function(...)" 66 | { true, fixup "function()" }, -- "local a = function(arg1, ..., argN)" 67 | { true, fixup "local =function()" }, -- "local a = function(arg1, ..., argN)" 68 | { true, fixup "=function()" }, -- "a = function(arg1, ..., argN)" 69 | { true, fixup "break" }, -- "break" generates no trace in Lua 5.2 70 | { true, "{" }, -- "{" opening table 71 | { true, "}" }, -- "{" closing table 72 | { true, fixup "})" }, -- function closer 73 | { true, fixup ")" }, -- function closer 74 | } 75 | 76 | ------------------------ 77 | -- Starts the report generator 78 | -- To load a config, use luacov.runner to load 79 | -- settings and then start the report. 80 | -- @example# local runner = require("luacov.runner") 81 | -- local reporter = require("luacov.reporter") 82 | -- runner.load_config() 83 | -- table.insert(luacov.configuration.include, "thisfile") 84 | -- reporter.report() 85 | function M.report() 86 | local luacov = require("luacov.runner") 87 | local stats = require("luacov.stats") 88 | 89 | local configuration = luacov.load_config() 90 | stats.statsfile = configuration.statsfile 91 | 92 | local data, most_hits = stats.load() 93 | 94 | if not data then 95 | print("Could not load stats file "..configuration.statsfile..".") 96 | print("Run your Lua program with -lluacov and then rerun luacov.") 97 | os.exit(1) 98 | end 99 | 100 | local report = io.open(configuration.reportfile, "w") 101 | 102 | local names = {} 103 | for filename, _ in pairs(data) do 104 | local include = false 105 | -- normalize paths in patterns 106 | local path = filename:gsub("/", "."):gsub("\\", "."):gsub("%.lua$", "") 107 | if not configuration.include[1] then 108 | include = true 109 | else 110 | include = false 111 | for _, p in ipairs(configuration.include) do 112 | if path:match(p) then 113 | include = true 114 | break 115 | end 116 | end 117 | end 118 | if include and configuration.exclude[1] then 119 | for _, p in ipairs(configuration.exclude) do 120 | if path:match(p) then 121 | include = false 122 | break 123 | end 124 | end 125 | end 126 | if include then 127 | table.insert(names, filename) 128 | end 129 | end 130 | 131 | table.sort(names) 132 | 133 | local summary = {} 134 | local most_hits_length = ("%d"):format(most_hits):len() 135 | local empty_format = (" "):rep(most_hits_length+1) 136 | local zero_format = ("*"):rep(most_hits_length).."0" 137 | local count_format = ("%% %dd"):format(most_hits_length+1) 138 | 139 | local function excluded(exclusions,line) 140 | for _, e in ipairs(exclusions) do 141 | if e[1] then 142 | if line:match("^ *"..e[2].." *$") or line:match("^ *"..e[2].." *%-%-") then return true end 143 | else 144 | if line:match(e[2]) then return true end 145 | end 146 | end 147 | return false 148 | end 149 | 150 | for _, filename in ipairs(names) do 151 | local filedata = data[filename] 152 | local file = io.open(filename, "r") 153 | if file then 154 | report:write("\n") 155 | report:write("==============================================================================\n") 156 | report:write(filename, "\n") 157 | report:write("==============================================================================\n") 158 | local line_nr = 1 159 | local file_hits, file_miss = 0, 0 160 | local block_comment, equals = false, "" 161 | local in_long_string, ls_equals = false, "" 162 | while true do 163 | local line = file:read("*l") 164 | if not line then break end 165 | local true_line = line 166 | 167 | local new_block_comment = false 168 | if not block_comment then 169 | line = line:gsub("%s+", " ") 170 | local l, equals = line:match("^(.*)%-%-%[(=*)%[") 171 | if l then 172 | line = l 173 | new_block_comment = true 174 | end 175 | in_long_string, ls_equals = check_long_string(line, in_long_string, ls_equals, filedata[line_nr]) 176 | else 177 | local l = line:match("%]"..equals.."%](.*)$") 178 | if l then 179 | line = l 180 | block_comment = false 181 | end 182 | end 183 | 184 | local hits = filedata[line_nr] or 0 185 | if block_comment or in_long_string or excluded(exclusions,line) or (hits == 0 and excluded(hit0_exclusions,line)) then 186 | report:write(empty_format) 187 | else 188 | if hits == 0 then 189 | file_miss = file_miss + 1 190 | report:write(zero_format) 191 | else 192 | file_hits = file_hits + 1 193 | report:write(count_format:format(hits)) 194 | end 195 | end 196 | report:write("\t", true_line, "\n") 197 | if new_block_comment then block_comment = true end 198 | line_nr = line_nr + 1 199 | summary[filename] = { 200 | hits = file_hits, 201 | miss = file_miss 202 | } 203 | end 204 | file:close() 205 | end 206 | end 207 | 208 | report:write("\n") 209 | report:write("==============================================================================\n") 210 | report:write("Summary\n") 211 | report:write("==============================================================================\n") 212 | report:write("\n") 213 | 214 | local function write_total(hits, miss, filename) 215 | report:write(hits, "\t", miss, "\t", ("%.2f%%"):format(hits/(hits+miss)*100.0), "\t", filename, "\n") 216 | end 217 | 218 | local total_hits, total_miss = 0, 0 219 | for _, filename in ipairs(names) do 220 | local s = summary[filename] 221 | if s then 222 | write_total(s.hits, s.miss, filename) 223 | total_hits = total_hits + s.hits 224 | total_miss = total_miss + s.miss 225 | end 226 | end 227 | report:write("------------------------\n") 228 | write_total(total_hits, total_miss, "") 229 | 230 | report:close() 231 | 232 | if configuration.deletestats then 233 | os.remove(configuration.statsfile) 234 | end 235 | end 236 | 237 | return M 238 | -------------------------------------------------------------------------------- /test/scxml-suite/run_suite.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | #encoding: utf-8 3 | BASE = 'http://www.w3.org/Voice/2013/SCXML-irp/' 4 | CACHE = 'spec-cache' 5 | REPORT = 'scxml10-ir-results-lxsc.xml' 6 | 7 | require 'uri' 8 | require 'fileutils' 9 | require 'nokogiri' # gem install nokogiri 10 | 11 | def run! 12 | Dir.chdir(File.dirname(__FILE__)) do 13 | FileUtils.mkdir_p CACHE 14 | @manifest = Nokogiri.XML( get_file('manifest.xml'), &:noblanks ) 15 | @mod = Nokogiri.XML(IO.read('manifest-mod.xml')) 16 | @report = Nokogiri.XML(IO.read(REPORT),&:noblanks) 17 | run_tests 18 | File.open(REPORT,'w'){ |f| f<<@report } 19 | end 20 | end 21 | 22 | def run_tests 23 | Dir['*.scxml'].each{ |f| File.delete(f) } 24 | Dir['*.txml' ].each{ |f| File.delete(f) } 25 | FileUtils.rm_f('luacov.stats.out') 26 | @report.xpath('//assert').remove 27 | 28 | tests = @manifest.xpath('//test') 29 | required = tests.reject{ |t| t['conformance']=='optional' } 30 | required.sort_by{ |test| [test['manual']=='false' ? 0 : 1,test['id']] }.each.with_index do |test,i| 31 | id = test['id'] 32 | auto = test['manual']=='false' 33 | start = test.at('start') 34 | uri = start['uri'] 35 | 36 | print "Test ##{i+1}/#{required.length} #{uri} (#{auto ? :auto : :manual}): " 37 | 38 | scxml = prepare_scxml(uri) 39 | # Fetch dependent files, copy to working directory 40 | test.xpath('dep').each{ |d| File.open(File.basename(d['uri']),'w'){ |f| f<" 56 | # Destroy local copies of dependent files 57 | test.xpath('dep').each{ |d| FileUtils.rm_f(File.basename d['uri']) } 58 | FileUtils.rm_f(scxml) 59 | puts "pass" 60 | else 61 | @report.root << "" 62 | `subl #{scxml}` 63 | puts "fail" 64 | end 65 | else 66 | puts "trace" 67 | system("lua autotest.lua #{scxml} --trace") 68 | end 69 | end 70 | end 71 | 72 | def prepare_scxml(uri) 73 | doc = Nokogiri.XML( get_file(uri), &:noblanks ) 74 | convert_to_scxml!(doc) 75 | File.basename(uri).sub('txml','scxml').tap do |file| 76 | File.open(file,'w:utf-8'){ |f| f.puts doc } 77 | end 78 | end 79 | 80 | def convert_to_scxml!(doc) 81 | doc.at_xpath('//conf:pass').replace '' if doc.at_xpath('//conf:pass') 82 | doc.at_xpath('//conf:fail').replace '' if doc.at_xpath('//conf:fail') 83 | { 84 | arrayVar: ->(a){ ['array', "testvar#{a}" ]}, 85 | arrayTextVar: ->(a){ ['array', "testvar#{a}" ]}, 86 | eventdataVal: ->(a){ ['cond', "_event.data == #{a}" ]}, 87 | eventNameVal: ->(a){ ['cond', "_event.name == '#{a}'" ]}, 88 | originTypeEq: ->(a){ ['cond', "_event.origintype == '#{a}'" ]}, 89 | emptyEventData: ->(a){ ['cond', "_event.data == nil" ]}, 90 | eventFieldHasNoValue: ->(a){ ['cond', "_event.#{a} == ''" ]}, 91 | isBound: ->(a){ ['cond', "testvar#{a} ~= nil" ]}, 92 | inState: ->(a){ ['cond', "In('#{a}')" ]}, 93 | true: ->(a){ ['cond', 'true' ]}, 94 | false: ->(a){ ['cond', 'false' ]}, 95 | unboundVar: ->(a){ ['cond', "testvar#{a}==nil" ]}, 96 | noValue: ->(a){ ['cond', "testvar#{a}==nil or testvar#{a}==''" ]}, 97 | nameVarVal: ->(a){ ['cond', "_name == '#{a}'" ]}, 98 | nonBoolean: ->(a){ ['cond', "@@@@@@@@@@@@@@@@" ]}, 99 | systemVarIsBound: ->(a){ ['cond', "#{a} ~= nil" ]}, 100 | varPrefix: ->(a){ 101 | x,y = a.split /\s+/ 102 | ['cond',"string.sub(testvar#{y},1,string.len(testvar#{x}))==testvar#{x}"] 103 | }, 104 | VarEqVar: ->(a){ 105 | x,y = a.split /\s+/ 106 | ['cond',"testvar#{x}==testvar#{y}"] 107 | }, 108 | idQuoteVal: ->(a){ 109 | x,op,y = a.split(/([=<>]=?)/) 110 | ['cond',"testvar#{x} #{op=='=' ? '==' : op} '#{y}'"] 111 | }, 112 | idVal: ->(a){ 113 | x,op,y = a.split /([=<>]+)/ 114 | ['cond',"testvar#{x} #{op == '=' ? '==' : op} #{y}"] 115 | }, 116 | namelistIdVal: ->(a){ 117 | x,op,y = a.split /([=<>]+)/ 118 | ['cond',"testvar#{x} #{op == '=' ? '==' : op} #{y}"] 119 | }, 120 | idSystemVarVal: ->(a){ 121 | x,op,y = a.split /([=<>]+)/ 122 | ['cond',"testvar#{x} #{op == '=' ? '==' : op} #{y}"] 123 | }, 124 | compareIDVal: ->(a){ 125 | x,op,y = a.split /([=<>]+)/ 126 | ['cond',"testvar#{x} #{op == '=' ? '==' : op} testvar#{y}"] 127 | }, 128 | eventvarVal: ->(a){ 129 | x,op,y = a.split /([=<>]+)/ 130 | ['cond',"_event.data['testvar#{x}'] #{op == '=' ? '==' : op} #{y}"] 131 | }, 132 | VarEqVarStruct: ->(a){ 133 | x,y = a.split /\D+/ 134 | ['cond',"testvar#{x} == testvar#{y}"] 135 | }, 136 | eventFieldsAreBound: ->(a){ 137 | ['cond', "_event.name~=nil and _event.type~=nil and _event.sendid~=nil and _event.origin~=nil and _event.invokeid~=nil"] 138 | }, 139 | datamodel: ->(a){ ['datamodel', 'lua' ]}, 140 | delay: ->(a){ ['delay', "#{100*a.to_i}ms" ]}, 141 | delayExpr: ->(a){ ['delayexpr', "testvar#{a}" ]}, 142 | delayFromVar: ->(a){ ['delayexpr', "testvar#{a}" ]}, 143 | # delayFromVar: ->(a){ ['delayexpr', "100*tonumber(testvar#{a})..'ms'" ]}, 144 | eventExpr: ->(a){ ['eventexpr', "testvar#{a}" ]}, 145 | eventDataFieldValue: ->(a){ ['expr', "_event.data.#{a}" ]}, 146 | eventDataNamelistValue: ->(a){ ['expr', "_event.data.testvar#{a}" ]}, 147 | eventDataParamValue: ->(a){ ['expr', "_event.data.#{a}" ]}, 148 | eventField: ->(a){ ['expr', "_event.#{a}" ]}, 149 | eventName: ->(a){ ['expr', "_event.name" ]}, 150 | eventSendid: ->(a){ ['expr', "_event.sendid" ]}, 151 | eventType: ->(a){ ['expr', "_event.type" ]}, 152 | eventRaw: ->(a){ ['expr', "_event:inspect(true)"]}, 153 | expr: ->(a){ ['expr', a ]}, 154 | illegalArray: ->(a){ ['expr', "7" ]}, 155 | illegalExpr: ->(a){ ['expr', "!" ]}, 156 | invalidSendTypeExpr: ->(a){ ['expr', '27' ]}, 157 | invalidSessionID: ->(a){ ['expr', "-1" ]}, 158 | invalidName: ->(a){ ['name', "" ]}, 159 | varExpr: ->(a){ ['expr', "testvar#{a}" ]}, 160 | varChildExpr: ->(a){ ['expr', "testvar#{a}" ]}, 161 | quoteExpr: ->(a){ ['expr', "'#{a}'" ]}, 162 | systemVarExpr: ->(a){ ['expr', a ]}, 163 | scxmlEventIOLocation: ->(a){ ['expr', "FIXME" ]}, 164 | varNonexistentStruct: ->(a){ ['expr', "testvar#{a}.nonono" ]}, 165 | id: ->(a){ ['id', "testvar#{a}" ]}, 166 | idlocation: ->(a){ ['idlocation', "'testvar#{a}'" ]}, 167 | index: ->(a){ ['index', "testvar#{a}" ]}, 168 | item: ->(a){ ['item', "testvar#{a}" ]}, 169 | illegalItem: ->(a){ ['item', "_no" ]}, 170 | location: ->(a){ ['location', "testvar#{a}" ]}, 171 | invalidLocation: ->(a){ ['location', "" ]}, 172 | invalidParamLocation: ->(a){ ['location', "" ]}, 173 | systemVarLocation: ->(a){ ['location', a ]}, 174 | name: ->(a){ ['name', "testvar#{a}" ]}, 175 | namelist: ->(a){ ['namelist', "testvar#{a}" ]}, 176 | invalidNamelist: ->(a){ ['namelist', "" ]}, 177 | sendIDExpr: ->(a){ ['sendidexpr', "testvar#{a}" ]}, 178 | srcExpr: ->(a){ ['srcexpr', "testvar#{a}" ]}, 179 | scriptBadSrc: ->(a){ ['src', "-badfile-" ]}, 180 | targetpass: ->(a){ ['target', 'pass' ]}, 181 | targetfail: ->(a){ ['target', 'fail' ]}, 182 | illegalTarget: ->(a){ ['target', 'xxxxxxxxx' ]}, 183 | unreachableTarget: ->(a){ ['target', 'FIXME' ]}, 184 | targetVar: ->(a){ ['targetexpr', "testvar#{a}" ]}, 185 | targetExpr: ->(a){ ['targetexpr', "testvar#{a}" ]}, 186 | basicHTTPAccessURITarget: ->(a){ ['targetexpr', "FIXME" ]}, 187 | invalidSendType: ->(a){ ['type', '27' ]}, 188 | typeExpr: ->(a){ ['typeexpr', "testvar#{a}" ]}, 189 | }.each do |a1,proc| 190 | doc.xpath("//@conf:#{a1}").each{ |a| a2,v=proc[a.value]; a.parent[a2]=v; a.remove } 191 | end 192 | 193 | doc.xpath('//conf:incrementID').each{ |e| 194 | e.replace "" 195 | } 196 | doc.xpath('//conf:array123').each{ |e| e.replace "{1,2,3}" } 197 | doc.xpath('//conf:extendArray').each{ |e| e.replace "" } 198 | doc.xpath('//conf:sumVars').each{ |e| 199 | e.replace "" 200 | } 201 | doc.xpath('//conf:concatVars').each{ |e| 202 | e.replace "" 203 | } 204 | doc.xpath('//conf:contentFoo').each{ |e| e.replace %Q{} } 205 | doc.xpath('//conf:script').each{ |e| e.replace %Q{} } 206 | doc.xpath('//conf:sendToSender').each{ |e| 207 | e.replace %Q{} 208 | } 209 | 210 | if a = doc.at_xpath('//@*[namespace-uri()="http://www.w3.org/2005/scxml-conformance"]') 211 | puts a.parent 212 | exit 213 | end 214 | if a = doc.at_xpath('//conf:*') 215 | puts a 216 | exit 217 | end 218 | 219 | # HACK to remove the now-unused conf: namespace from the root. 220 | doc.remove_namespaces! 221 | doc.root.add_namespace(nil,'http://www.w3.org/2005/07/scxml') 222 | end 223 | 224 | def get_file(uri) 225 | Dir.chdir(CACHE) do 226 | unless File.exist?(uri) 227 | subdir = File.dirname(uri) 228 | FileUtils.mkdir_p subdir 229 | Dir.chdir(subdir){ `curl -s -L -O #{URI.join BASE, uri}` } 230 | end 231 | File.open( uri, 'r:UTF-8', &:read ) 232 | end 233 | end 234 | 235 | run! if __FILE__==$0 -------------------------------------------------------------------------------- /test/test.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | package.path = "../?.lua;" .. package.path 3 | require 'io' 4 | 5 | _ENV = require('lunity')('LXSC Tests') 6 | 7 | local LXSC = require 'lxsc' 8 | 9 | DIR = 'testcases' 10 | SHOULD_NOT_FINISH = {final2=true} 11 | 12 | XML = {} 13 | for filename in io.popen(string.format('ls "%s"',DIR)):lines() do 14 | local testName = filename:sub(1,-7) 15 | XML[testName] = io.open(DIR.."/"..filename):read("*all") 16 | end 17 | 18 | function test:parsing() 19 | local m = LXSC:parse(XML['internal_transition']) 20 | -- assertNil(m.id,"The scxml should not have an id") 21 | assertTrue(m.isCompound,'The root state should be compound') 22 | assertEqual(m.states[1].id,'outer') 23 | assertEqual(m.states[2].id,'fail') 24 | assertEqual(m.states[3].id,'pass') 25 | assertEqual(#m.states,3,"internal_transition.scxml should have 3 root states") 26 | local outer = m.states[1] 27 | assertEqual(#outer.states,2,"There should be 2 child states of the 'outer' state") 28 | assertEqual(#outer._onexits,1,"There should be 1 onexit command for the 'outer' state") 29 | assertEqual(#outer._onentrys,0,"There should be 0 onentry commands for the 'outer' state") 30 | 31 | m = LXSC:parse(XML['history']) 32 | assertSameKeys(m:allStateIds(),{["wrap"]=1,["universe"]=1,["history-actions"]=1,["action-1"]=1,["action-2"]=1,["action-3"]=1,["action-4"]=1,["modal-dialog"]=1,["pass"]=1,["fail"]=1}) 33 | assertSameKeys(m:atomicStateIds(),{["history-actions"]=1,["action-1"]=1,["action-2"]=1,["action-3"]=1,["action-4"]=1,["modal-dialog"]=1,["pass"]=1,["fail"]=1}) 34 | 35 | m = LXSC:parse(XML['parallel4']) 36 | assertSameKeys(m:allStateIds(),{["wrap"]=1,["p"]=1,["a"]=1,["a1"]=1,["a2"]=1,["b"]=1,["b1"]=1,["b2"]=1,["pass"]=1}) 37 | assertSameKeys(m:atomicStateIds(),{["a1"]=1,["a2"]=1,["b1"]=1,["b2"]=1,["pass"]=1}) 38 | end 39 | 40 | function test:dataAccess() 41 | local s = LXSC:parse[[ 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | ]] 50 | 51 | s:start() 52 | assert(s:isActive('errord'),"There should be an error when boot() can't be found") 53 | 54 | s:start{ data={ boot=function() end } } 55 | assert(s:isActive('s'),"There should be no error when boot() is supplied") 56 | 57 | -- s:start{ data={ boot=function() n=7 end } } 58 | -- assert(s:isActive('pass'),"Setting 'global' variables populates data model") 59 | 60 | s:start{ data={ boot=function() end, m=42 } } 61 | assertEqual(s:get("m"),42,"The data model should accept initial values") 62 | 63 | s:set("foo","bar") 64 | s:set("jim",false) 65 | s:set("n",6) 66 | assertEqual(s:get("foo"),"bar") 67 | assertEqual(s:get("jim"),false) 68 | assertEqual(s:get("n")*7,42) 69 | 70 | s:start() 71 | assertNil(s:get("boot"),"Starting the machine resets the datamodel") 72 | assertNil(s:get("foo"),"Starting the machine resets the datamodel") 73 | 74 | s:start{ data={ boot=function() end, n=6 } } 75 | assert(s:isActive('s')) 76 | s:set("n",7) 77 | assert(s:isActive('s')) 78 | s:step() 79 | assert(s:isActive('pass')) 80 | 81 | s:restart() 82 | assert(s:isActive('errord')) 83 | 84 | local s = LXSC:parse[[]] 85 | local values = {} 86 | s.onDataSet = function(name,value) values[name]=value end 87 | s:start() 88 | assertNil(values.foo) 89 | s:set("foo",42) 90 | assertEqual(values.foo,42) 91 | end 92 | 93 | function test:eventlist() 94 | local m = LXSC:parse[[ 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | ]] 105 | local possible = m:allEvents() 106 | local expected = {["a"]=1,["b.c"]=1,["d.e.f"]=1,["g"]=1,["h"]=1,["x"]=1,["y.z"]=1} 107 | assertSameKeys(possible,expected) 108 | 109 | assertNil(next(m:availableEvents()),"There should be no events before the machine has started.") 110 | m:start() 111 | 112 | local available = m:availableEvents() 113 | local expected = {["a"]=1,["b.c"]=1,["d.e.f"]=1,["g"]=1,["h"]=1} 114 | assertSameKeys(available,expected) 115 | end 116 | 117 | function test:customHandlers() 118 | local s = LXSC:parse[[ 119 | 121 | 122 | 123 | ]] 124 | local goSeen = {} 125 | function LXSC.Exec:go() goSeen[self._nsURI] = true; return true end 126 | assertNil(goSeen.foo) 127 | assertNil(goSeen.bar) 128 | s:start() 129 | assertTrue(goSeen.foo) 130 | assertTrue(goSeen.bar) 131 | end 132 | 133 | function test:customCallbacks() 134 | local s = LXSC:parse[[ 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | ]] 146 | local callbackCountById, eventsSeen = {}, {} 147 | local changesSeen = 0 148 | s.onAfterEnter = function(id,kind,atomic) 149 | assertType(id,'string') 150 | if not callbackCountById[id] then callbackCountById[id] = {} end 151 | callbackCountById[id].enter = (callbackCountById[id].enter or 0 ) + 1 152 | if id=='s1' or id=='s2' then 153 | assertEqual(kind,'state') 154 | assertTrue(atomic) 155 | elseif id=='s3' then 156 | assertEqual(kind,'final') 157 | assertTrue(atomic) 158 | else 159 | assertFalse(atomic) 160 | end 161 | end 162 | s.onBeforeExit = function(id,kind,atomic) 163 | assertType(id,'string') 164 | if not callbackCountById[id] then callbackCountById[id] = {} end 165 | callbackCountById[id].exit = (callbackCountById[id].exit or 0 ) + 1 166 | if id=='s1' or id=='s2' then 167 | assertEqual(kind,'state') 168 | assertTrue(atomic) 169 | elseif id=='s3' then 170 | assertEqual(kind,'final') 171 | assertTrue(atomic) 172 | else 173 | assertFalse(atomic) 174 | end 175 | end 176 | s.onEventFired = function(event) 177 | eventsSeen[event.name] = true 178 | end 179 | s.onEnteredAll = function() changesSeen = changesSeen+1 end 180 | s:start() 181 | for id,counts in pairs(callbackCountById) do 182 | if id=='s3' then 183 | assertEqual(counts.enter,1) 184 | assertNil(counts.exit, 0) 185 | else 186 | assertEqual(counts.enter,1) 187 | assertEqual(counts.exit, 1) 188 | end 189 | end 190 | s:fireEvent("foo.bar") 191 | assert(eventsSeen.e) 192 | assert(eventsSeen["foo.bar"]) 193 | assertEqual(changesSeen,1) 194 | end 195 | 196 | function test:delayedSend() 197 | local s = LXSC:parse[[ 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | ]] 222 | local elapsed = 0 223 | function s:elapsed() return elapsed end 224 | s:start() 225 | assert(s:isActive('s2')) 226 | s:step() 227 | assert(s:isActive('s2')) 228 | s:cancelDelayedSend('killme') 229 | elapsed = 0.5 230 | s:step() 231 | assert(s:isActive('pass')) 232 | end 233 | 234 | function test:eventMatching() 235 | local descriptors = { 236 | ["*"] = { 237 | shouldMatch={"a","a.b","b.c","b.c.d","c.d.e","c.d.e.f","d.e.f","d.e.f.g","f","f.g","alpha","b.charlie","d.e.frank","frank","b","z.a"}, 238 | shouldNotMatch={} }, 239 | ["a"] = { 240 | shouldMatch={"a","a.b"}, 241 | shouldNotMatch={"b.c","b.c.d","c.d.e","c.d.e.f","d.e.f","d.e.f.g","f","f.g","alpha","b.charlie","d.e.frank","frank","b","z.a"} }, 242 | ["b.c"] = { 243 | shouldMatch={"b.c","b.c.d"}, 244 | shouldNotMatch={"a","a.b","alpha","b.charlie","d.e.frank","frank","b","z.a","c.d.e","c.d.e.f","d.e.f","d.e.f.g","f","f.g"} }, 245 | ["c.d.e"] = { 246 | shouldMatch={"c.d.e","c.d.e.f"}, 247 | shouldNotMatch={"a","a.b","b.c","b.c.d","alpha","b.charlie","d.e.frank","frank","b","z.a","d.e.f","d.e.f.g","f","f.g"} }, 248 | ["d.e.f.*"] = { 249 | shouldMatch={"d.e.f","d.e.f.g"}, 250 | shouldNotMatch={"a","a.b","b.c","b.c.d","c.d.e","c.d.e.f","alpha","b.charlie","d.e.frank","frank","b","z.a","f","f.g"} }, 251 | ["f."] = { 252 | shouldMatch={"f","f.g"}, 253 | shouldNotMatch={"a","a.b","b.c","b.c.d","c.d.e","c.d.e.f","d.e.f","d.e.f.g","alpha","b.charlie","d.e.frank","frank","b","z.a"} }, 254 | } 255 | for descriptor,events in pairs(descriptors) do 256 | local t = LXSC:transition() 257 | t:attr('event',descriptor) 258 | 259 | for _,eventName in ipairs(events.shouldMatch) do 260 | local event = LXSC.Event(eventName,nil,{}) 261 | assertTrue(event:triggersDescriptor(descriptor)) 262 | assertTrue(event:triggersTransition(t)) 263 | end 264 | for _,eventName in ipairs(events.shouldNotMatch) do 265 | local event = LXSC.Event(eventName,nil,{}) 266 | assertTrue(not event:triggersDescriptor(descriptor)) 267 | assertTrue(not event:triggersTransition(t)) 268 | end 269 | end 270 | end 271 | 272 | function test:eval() 273 | local m = LXSC:parse[[ 274 | 275 | 276 | 277 | 278 | ]] 279 | m:start() 280 | assertEqual(m:get('a'),1) 281 | assertEqual(m:eval('a'),1) 282 | m:set('a',2) 283 | assertEqual(m:get('a'),2) 284 | assertEqual(m:eval('a'),2) 285 | m:run('a = 3') 286 | assertEqual(m:get('a'),3) 287 | assertEqual(m:eval('a'),3) 288 | 289 | m = LXSC:parse[[ 290 | 291 | ]] 292 | local d = {a=1} 293 | m:start{ data=d } 294 | assertEqual(m:get('a'),1) 295 | assertEqual(m:eval('a'),1) 296 | m:set('a',2) 297 | assertEqual(m:get('a'),2) 298 | assertEqual(m:eval('a'),2) 299 | assertEqual(d.a,2) 300 | m:run('a = 3') 301 | assertEqual(m:get('a'),3) 302 | assertEqual(m:eval('a'),3) 303 | assertEqual(d.a,3) 304 | end 305 | 306 | for testName,xml in pairs(XML) do 307 | test["testcase_"..testName] = function() 308 | local machine = LXSC:parse(xml) 309 | assertFalse(machine.running, testName.." should not be running before starting.") 310 | assertTableEmpty(machine:activeStateIds(), testName.." should be empty before running.") 311 | machine:start() 312 | local steps = 0 313 | while steps<1000 and (#machine._internalQueue>0 or #machine._externalQueue>0) do 314 | machine:step() 315 | steps = steps + 1 316 | end 317 | assert(machine:activeStateIds().pass, testName.." should finish in the 'pass' state.") 318 | assertEqual(#machine:activeAtomicIds(), 1, testName.." should only have a single atomic state active.") 319 | if SHOULD_NOT_FINISH[testName] then 320 | assertTrue(machine.running, testName.." should NOT run to completion.") 321 | else 322 | assertFalse(machine.running, testName.." should run to completion.") 323 | end 324 | end 325 | end 326 | 327 | 328 | 329 | test{ useANSI=false } 330 | -------------------------------------------------------------------------------- /test/lunity.lua: -------------------------------------------------------------------------------- 1 | --[=========================================================================[ 2 | Lunity v0.11 by Gavin Kistner 3 | See http://github.com/Phrogz/Lunity for usage documentation. 4 | Licensed under Creative Commons Attribution 3.0 United States License. 5 | See http://creativecommons.org/licenses/by/3.0/us/ for details. 6 | --]=========================================================================] 7 | 8 | -- FIXME: this will fail if two test suites are running interleaved 9 | local assertsPassed, assertsAttempted 10 | local function assertionSucceeded() 11 | assertsPassed = assertsPassed + 1 12 | io.write('.') 13 | return true 14 | end 15 | 16 | -- This is the table that will be used as the environment for the tests, 17 | -- making assertions available within the file. 18 | local lunity = setmetatable({}, {__index=_G}) 19 | 20 | function lunity.fail(msg) 21 | assertsAttempted = assertsAttempted + 1 22 | if not msg then msg = "(test failure)" end 23 | error(msg, 2) 24 | end 25 | 26 | function lunity.assert(testCondition, msg) 27 | assertsAttempted = assertsAttempted + 1 28 | if not testCondition then 29 | if not msg then msg = "assert() failed: value was "..tostring(testCondition) end 30 | error(msg, 2) 31 | end 32 | return assertionSucceeded() 33 | end 34 | 35 | function lunity.assertEqual(actual, expected, msg) 36 | assertsAttempted = assertsAttempted + 1 37 | if actual~=expected then 38 | if not msg then 39 | msg = string.format("assertEqual() failed: expected %s, was %s", 40 | tostring(expected), 41 | tostring(actual) 42 | ) 43 | end 44 | error(msg, 2) 45 | end 46 | return assertionSucceeded() 47 | end 48 | 49 | function lunity.assertType(actual, expectedType, msg) 50 | assertsAttempted = assertsAttempted + 1 51 | if type(actual) ~= expectedType then 52 | if not msg then 53 | msg = string.format("assertType() failed: value %s is a %s, expected to be a %s", 54 | tostring(actual), 55 | type(actual), 56 | expectedType 57 | ) 58 | end 59 | error(msg, 2) 60 | end 61 | return assertionSucceeded() 62 | end 63 | 64 | function lunity.assertTableEquals(actual, expected, msg, keyPath) 65 | assertsAttempted = assertsAttempted + 1 66 | -- Easy out 67 | if actual == expected then 68 | if not keyPath then 69 | return assertionSucceeded() 70 | else 71 | return true 72 | end 73 | end 74 | 75 | if not keyPath then keyPath = {} end 76 | 77 | if type(actual) ~= 'table' then 78 | if not msg then 79 | msg = "Value passed to assertTableEquals() was not a table." 80 | end 81 | error(msg, 2 + #keyPath) 82 | end 83 | 84 | -- Ensure all keys in t1 match in t2 85 | for key,expectedValue in pairs(expected) do 86 | keyPath[#keyPath+1] = tostring(key) 87 | local actualValue = actual[key] 88 | if type(expectedValue)=='table' then 89 | if type(actualValue)~='table' then 90 | if not msg then 91 | msg = "Tables not equal; expected "..table.concat(keyPath,'.').." to be a table, but was a "..type(actualValue) 92 | end 93 | error(msg, 1 + #keyPath) 94 | elseif expectedValue ~= actualValue then 95 | assertTableEquals(actualValue, expectedValue, msg, keyPath) 96 | end 97 | else 98 | if actualValue ~= expectedValue then 99 | if not msg then 100 | if actualValue == nil then 101 | msg = "Tables not equal; missing key '"..table.concat(keyPath,'.').."'." 102 | else 103 | msg = "Tables not equal; expected '"..table.concat(keyPath,'.').."' to be "..tostring(expectedValue)..", but was "..tostring(actualValue) 104 | end 105 | end 106 | error(msg, 1 + #keyPath) 107 | end 108 | end 109 | keyPath[#keyPath] = nil 110 | end 111 | 112 | -- Ensure actual doesn't have keys that aren't expected 113 | for k,_ in pairs(actual) do 114 | if expected[k] == nil then 115 | if not msg then 116 | msg = "Tables not equal; found unexpected key '"..table.concat(keyPath,'.').."."..tostring(k).."'" 117 | end 118 | error(msg, 2 + #keyPath) 119 | end 120 | end 121 | 122 | return assertionSucceeded() 123 | end 124 | 125 | function lunity.assertNotEqual(actual, expected, msg) 126 | assertsAttempted = assertsAttempted + 1 127 | if actual==expected then 128 | if not msg then 129 | msg = string.format("assertNotEqual() failed: value not allowed to be %s", 130 | tostring(actual) 131 | ) 132 | end 133 | error(msg, 2) 134 | end 135 | return assertionSucceeded() 136 | end 137 | 138 | function lunity.assertTrue(actual, msg) 139 | assertsAttempted = assertsAttempted + 1 140 | if actual ~= true then 141 | if not msg then 142 | msg = string.format("assertTrue() failed: value was %s, expected true", 143 | tostring(actual) 144 | ) 145 | end 146 | error(msg, 2) 147 | end 148 | return assertionSucceeded() 149 | end 150 | 151 | function lunity.assertFalse(actual, msg) 152 | assertsAttempted = assertsAttempted + 1 153 | if actual ~= false then 154 | if not msg then 155 | msg = string.format("assertFalse() failed: value was %s, expected false", 156 | tostring(actual) 157 | ) 158 | end 159 | error(msg, 2) 160 | end 161 | return assertionSucceeded() 162 | end 163 | 164 | function lunity.assertNil(actual, msg) 165 | assertsAttempted = assertsAttempted + 1 166 | if actual ~= nil then 167 | if not msg then 168 | msg = string.format("assertNil() failed: value was %s, expected nil", 169 | tostring(actual) 170 | ) 171 | end 172 | error(msg, 2) 173 | end 174 | return assertionSucceeded() 175 | end 176 | 177 | function lunity.assertNotNil(actual, msg) 178 | assertsAttempted = assertsAttempted + 1 179 | if actual == nil then 180 | if not msg then msg = "assertNotNil() failed: value was nil" end 181 | error(msg, 2) 182 | end 183 | return assertionSucceeded() 184 | end 185 | 186 | function lunity.assertTableEmpty(actual, msg) 187 | assertsAttempted = assertsAttempted + 1 188 | if type(actual) ~= "table" then 189 | msg = string.format("assertTableEmpty() failed: expected a table, but got a %s", 190 | type(table) 191 | ) 192 | error(msg, 2) 193 | else 194 | local key, value = next(actual) 195 | if key ~= nil then 196 | if not msg then 197 | msg = string.format("assertTableEmpty() failed: table has non-nil key %s=%s", 198 | tostring(key), 199 | tostring(value) 200 | ) 201 | end 202 | error(msg, 2) 203 | end 204 | return assertionSucceeded() 205 | end 206 | end 207 | 208 | function lunity.assertTableNotEmpty(actual, msg) 209 | assertsAttempted = assertsAttempted + 1 210 | if type(actual) ~= "table" then 211 | msg = string.format("assertTableNotEmpty() failed: expected a table, but got a %s", 212 | type(actual) 213 | ) 214 | error(msg, 2) 215 | else 216 | if next(actual) == nil then 217 | if not msg then 218 | msg = "assertTableNotEmpty() failed: table has no keys" 219 | end 220 | error(msg, 2) 221 | end 222 | return assertionSucceeded() 223 | end 224 | end 225 | 226 | function lunity.assertSameKeys(t1, t2, msg) 227 | assertsAttempted = assertsAttempted + 1 228 | local function bail(k,x,y) 229 | if not msg then msg = string.format("Table #%d has key '%s' not present in table #%d",x,tostring(k),y) end 230 | error(msg, 3) 231 | end 232 | for k,_ in pairs(t1) do if t2[k]==nil then bail(k,1,2) end end 233 | for k,_ in pairs(t2) do if t1[k]==nil then bail(k,2,1) end end 234 | return assertionSucceeded() 235 | end 236 | 237 | -- Ensures that the value is a function OR may be called as one 238 | function lunity.assertInvokable(value, msg) 239 | assertsAttempted = assertsAttempted + 1 240 | local meta = getmetatable(value) 241 | if (type(value) ~= 'function') and not (meta and meta.__call and (type(meta.__call)=='function')) then 242 | if not msg then 243 | msg = string.format("assertInvokable() failed: '%s' can not be called as a function", 244 | tostring(value) 245 | ) 246 | end 247 | error(msg, 2) 248 | end 249 | return assertionSucceeded() 250 | end 251 | 252 | function lunity.assertErrors(invokable, ...) 253 | assertInvokable(invokable) 254 | if pcall(invokable,...) then 255 | local msg = string.format("assertErrors() failed: %s did not raise an error", 256 | tostring(invokable) 257 | ) 258 | error(msg, 2) 259 | end 260 | return assertionSucceeded() 261 | end 262 | 263 | function lunity.assertDoesNotError(invokable, ...) 264 | assertInvokable(invokable) 265 | if not pcall(invokable,...) then 266 | local msg = string.format("assertDoesNotError() failed: %s raised an error", 267 | tostring(invokable) 268 | ) 269 | error(msg, 2) 270 | end 271 | return assertionSucceeded() 272 | end 273 | 274 | function lunity.is_nil(value) return type(value)=='nil' end 275 | function lunity.is_boolean(value) return type(value)=='boolean' end 276 | function lunity.is_number(value) return type(value)=='number' end 277 | function lunity.is_string(value) return type(value)=='string' end 278 | function lunity.is_table(value) return type(value)=='table' end 279 | function lunity.is_function(value) return type(value)=='function' end 280 | function lunity.is_thread(value) return type(value)=='thread' end 281 | function lunity.is_userdata(value) return type(value)=='userdata' end 282 | 283 | local function run(self, opts) 284 | if not opts then opts = {} end 285 | assertsPassed = 0 286 | assertsAttempted = 0 287 | 288 | local useANSI,useHTML = true, false 289 | if opts.useHTML ~= nil then useHTML=opts.useHTML end 290 | if not useHTML and opts.useANSI ~= nil then useANSI=opts.useANSI end 291 | 292 | local suiteName = getmetatable(self).name 293 | 294 | if useHTML then 295 | print("

"..suiteName.."

")
296 | 	else
297 | 		print(string.rep('=',78))
298 | 		print(suiteName)
299 | 		print(string.rep('=',78))
300 | 	end
301 | 	io.stdout:flush()
302 | 
303 | 
304 | 	local testnames = {}
305 | 	for name, test in pairs(self) do
306 | 		if type(test)=='function' and name~='before' and name~='after' then
307 | 			testnames[#testnames+1]=name
308 | 		end
309 | 	end
310 | 	table.sort(testnames)
311 | 
312 | 
313 | 	local startTime = os.clock()
314 | 	local passed = 0
315 | 	for _,name in ipairs(testnames) do
316 | 		local scratchpad = {}
317 | 		io.write(name..": ")
318 | 		if self.before then self.before(scratchpad) end
319 | 		local successFlag, errorMessage = pcall(self[name], scratchpad)
320 | 		if successFlag then
321 | 			print("pass")
322 | 			passed = passed + 1
323 | 		else
324 | 			if useANSI then
325 | 				print("\27[31m\27[1mFAIL!\27[0m")
326 | 				print("\27[31m"..errorMessage.."\27[0m")
327 | 			elseif useHTML then
328 | 				print("FAIL!")
329 | 				print(""..errorMessage.."")
330 | 			else
331 | 				print("FAIL!")
332 | 				print(errorMessage)
333 | 			end
334 | 		end
335 | 		io.stdout:flush()
336 | 		if self.after then self.after(scratchpad) end
337 | 	end
338 | 	local stopTime = os.clock()
339 | 	if useHTML then
340 | 		print("
") 341 | else 342 | print(string.rep('-', 78)) 343 | end 344 | 345 | print(string.format("%d/%d tests passed (%0.1f%%)", 346 | passed, 347 | #testnames, 348 | 100 * passed / #testnames 349 | )) 350 | 351 | if useHTML then print("
") end 352 | 353 | print(string.format("%d total successful assertion%s (%.1f assertions/second)", 354 | assertsPassed, 355 | assertsPassed == 1 and "" or "s", 356 | assertsAttempted / (stopTime - startTime) 357 | )) 358 | 359 | if not useHTML then print("") end 360 | io.stdout:flush() 361 | 362 | end 363 | 364 | return function(name) 365 | return setmetatable( 366 | {test=setmetatable({}, {__call=run, name=name or '(test suite)'})}, 367 | {__index=lunity} 368 | ) 369 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # About LXSC 2 | 3 | LXSC stands for "Lua XML StateCharts", and is pronounced _"Lexie"_. The LXSC library allows you to run [SCXML state machines][1] in [Lua][2]. The [Data Model][3] 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. 4 | 5 | # Table of Contents 6 | 7 | * [Usage](#usage) 8 | * [The Basics](#the-basics) 9 | * [Customizing the Data Model](#customizing-the-data-model) 10 | * [Callbacks as the Machine Changes](#callbacks-as-the-machine-changes) 11 | * [State Change Callbacks](#state-change-callbacks) 12 | * [Transition Callback](#transition-callback) 13 | * [Data Model Callback](#data-model-callback) 14 | * [Event Fire Callback](#event-fire-callback) 15 | * [Peeking and Poking at the Data Model](#peeking-and-poking-at-the-data-model) 16 | * [Examining the State of the Machine](#examining-the-state-of-the-machine) 17 | * [Customizing the Timer](#customizing-the-timer) 18 | * [Custom Executable Content](#custom-executable-content) 19 | * [SCXML Compliance](#scxml-compliance) 20 | * [TODO (aka Known Limitations)](#todo-aka-known-limitations) 21 | * [License & Contact](#license--contact) 22 | 23 | ## Usage 24 | 25 | ### The Basics 26 | 27 | ```lua 28 | local LXSC = require"lxsc-min-12" 29 | 30 | local scxml = io.open('my.scxml'):read('*all') 31 | local machine = LXSC:parse(scxml) 32 | machine:start() -- initiate the interpreter and run until stable 33 | 34 | machine:fireEvent("my.event") -- add events to the event queue to be processed 35 | machine:fireEvent("another.event.name") -- as many as you like; they won't have any effect until you 36 | machine:step() -- call step() to process all events and run until stable 37 | 38 | print("Is the machine still running?",machine.running) 39 | print("Is a state in the configuration?",machine:isActive('some-state-id')) 40 | 41 | -- Keep firing events and calling step() to process them 42 | ``` 43 | 44 | ### Customizing the Data Model 45 | 46 | The data model used by the interpreter is a Lua table. This table is used to store and retrieve the values created via [``](http://www.w3.org/TR/scxml/#data) or [``](http://www.w3.org/TR/scxml/#assign). This table is also used as the environment under which the [`` semantic callbacks 52 | * create a custom datatable that performs metamagic when new keys are accessed or modified by the state machine 53 | 54 | You supply a custom data model table by passing a named `data` parameter to the `start()` method: 55 | 56 | ```lua 57 | local mydata = { reloading=true, userName="Gavin" } -- populate initial data values 58 | local funcs = { print=print, doTheThing=utils.doIt } -- create 'global' functions 59 | setmetatable( mydata, {__index=funcs} ) 60 | machine:start{ data=mydata } 61 | ``` 62 | 63 | ### Callbacks as the Machine Changes 64 | 65 | There are six special keys that you may set to a function value on the machine to keep track of what the machine is doing: `onBeforeExit`, `onAfterEnter`, `onEnteredAll`, `onDataSet`, `onTransition`, and `onEventFired`. 66 | 67 | #### State Change Callbacks 68 | 69 | ```lua 70 | machine.onBeforeExit = function(stateId,stateKind,isAtomic) ... end 71 | machine.onAfterEnter = function(stateId,stateKind,isAtomic) ... end 72 | machine.onEnteredAll = function() ... end 73 | ``` 74 | 75 | The state-specific change callbacks are passed three parameters: 76 | 77 | * The string id of the state being exited or entered. 78 | * The string kind of the state: `"state"`, `"parallel"`, or `"final"`. 79 | * _The callbacks are not invoked for `history` or `initial` pseudo-states._ 80 | * A boolean indicating whether the state is atomic or not. 81 | 82 | As implied by the names the `onBeforeExit` callback is invoked right **before** leaving a state, while the `onAfterEnter` callback is invoked right **after** entering a state. 83 | 84 | The `onEnteredAll` callback will be invoked once after the last state is entered for a particular _microstep_. 85 | 86 | #### Data Model Callback 87 | 88 | ```lua 89 | machine.onDataSet = function(dataid,newvalue) ... end 90 | ``` 91 | 92 | If supplied, this callback will be invoked any time the data model is changed. 93 | 94 | **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](http://www.w3.org/TR/scxml/#SystemVariables)). 95 | 96 | #### Transition Callback 97 | 98 | ```lua 99 | machine.onTransition = function(transitionTable) ... end 100 | ``` 101 | 102 | The `onTransition` callback is invoked right before the executable content of a transition (if any) is run. 103 | 104 | **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: 105 | 106 | * `type` - the string `"internal"` or `"external"`. 107 | * `cond` - the string value of the `cond="…"` attribute, if any, or `nil`. 108 | * `_event` - the string value of the `event="…"` attribute, if any, or `nil`. 109 | * `_target` - the string of the `target="…"` attribute, if any, or `nil`. 110 | * `events` - an array of internal `LXSC.Event` tables, one for each event, or `nil`. 111 | * `targets` - an array of internal `LXSC.State` tables, one for each target, or `nil`. 112 | * Any custom attributes supplied on the transition appear as direct attributes (with no namespace information or protection). 113 | 114 | #### Event Fire Callback 115 | 116 | ```lua 117 | machine.onEventFired = function(eventTable) ... end 118 | ``` 119 | 120 | The `onEventFired` callback is invoked whenever `fireEvent()` is called on the machine (either by your own code or by internal machine code). The event has not been processed, and there is no guarantee that the event is going to cause any effect later on. This is mostly a debugging callback allowing you to ensure that events you thought that you were injecting were, in fact, making it in. 121 | 122 | The table supplied to this callback is a `LXSC.Event` object with the following keys: 123 | 124 | * `name` - the string name of the fired event, e.g. `"foo"` or `"foo.bar.jim.jam"`. 125 | * `data` - whatever data (if any) was supplied as the second parameter to `fireEvent()`. 126 | * `triggersDescriptor` - a function that can be used to determine if this event would trigger a particular transition's `event="…"` descriptor. 127 | 128 | ```lua 129 | machine.onEventFired = function(evt) 130 | print(evt.name, evt:triggersDescriptor('a'), evt:triggersDescriptor('a.b')) 131 | end 132 | machine:fireEvent("a") --> a true nil 133 | machine:fireEvent("a.b") --> a true true 134 | ``` 135 | * `triggersTransition` - similar to `triggersDescriptor()`, but it takes a transition table (as supplied to the `onTransition` callback) and uses the event descriptor(s) for that transition's `event="..."` attribute to evaluate if the event should cause the transition to be triggered. 136 | * _Note: this does not test any conditional code that may be present inthe transition's `cond="..."` attribute. This function may return true, and then the transition may subsequently not be triggered by this event if the conditions are not right._ 137 | * `_tokens` - an array of the event name split by periods (an implementation detail used for optimized transition descriptor matching). 138 | 139 | Note that the event object described above is also returned from `machine:fireEvent()`, in case you need that. 140 | 141 | ### Peeking and Poking at the Data Model 142 | 143 | While the machine is running (after you have called `start()`) you can peek at the data for a specific location via: 144 | 145 | ```lua 146 | local theValue = machine:get("dataId") 147 | ``` 148 | 149 | …and you can set the value for a particular location via: 150 | 151 | ```lua 152 | machine:set("dataId",someValue) 153 | ``` 154 | 155 | You can evaluate code in the data model (just like a `cond="…"` or `expr="…"` attribute does) by: 156 | 157 | ```lua 158 | local theResult = machine:eval("mycodestring") 159 | ``` 160 | 161 | …and you can run arbitrary code against the data model (just like a ` 230 |
231 | 232 | -------------------------------------------------------------------------------- /lib/runtime.lua: -------------------------------------------------------------------------------- 1 | local LXSC = require 'lib/lxsc' 2 | ;(function(S) 3 | S.MAX_ITERATIONS = 1000 4 | local OrderedSet,Queue,List = LXSC.OrderedSet, LXSC.Queue, LXSC.List 5 | 6 | -- **************************************************************************** 7 | 8 | local function entryOrder(a,b) return a._order < b._order end 9 | local function exitOrder(a,b) return b._order < a._order end 10 | local function isDescendant(a,b) return a:descendantOf(b) end 11 | local function isCancelEvent(e) return e.name=='quit.lxsc' end 12 | local function isFinalState(s) return s._kind=='final' end 13 | local function isScxmlState(s) return s._kind=='scxml' end 14 | local function isHistoryState(s) return s._kind=='history' end 15 | local function isParallelState(s) return s._kind=='parallel' end 16 | local function isCompoundState(s) return s.isCompound end 17 | local function isAtomicState(s) return s.isAtomic end 18 | local function getChildStates(s) return s.reals end 19 | local function findLCCA(first,rest) -- least common compound ancestor 20 | for _,anc in ipairs(first.ancestors) do 21 | if isCompoundState(anc) or isScxmlState(anc) then 22 | if rest:every(function(s) return isDescendant(s,anc) end) then 23 | return anc 24 | end 25 | end 26 | end 27 | end 28 | 29 | local emptyList = List() 30 | 31 | local depth=0 32 | local function logloglog(s) 33 | -- print(string.rep(' ',depth)..tostring(s)) 34 | end 35 | local function startfunc(s) logloglog(s) depth=depth+1 end 36 | local function closefunc(s) if s then logloglog(s) end depth=depth-1 end 37 | 38 | -- **************************************************************************** 39 | 40 | function S:interpret(options) 41 | self._delayedSend = { extraTime=0 } 42 | 43 | -- if not self:validate() then self:failWithError() end 44 | if not rawget(self,'_stateById') then self:expandScxmlSource() end 45 | self._configuration:clear() 46 | self._statesToInvoke = OrderedSet() 47 | self._internalQueue = Queue() 48 | self._externalQueue = Queue() 49 | self._historyValue = {} 50 | 51 | self._data = LXSC.Datamodel(self,options and options.data) 52 | self._data:_setSystem('_sessionid',LXSC.uuid4()) 53 | self._data:_setSystem('_name',self.name or LXSC.uuid4()) 54 | self._data:_setSystem('_ioprocessors',{}) 55 | if self.binding == "early" then self._data:initAll() end 56 | self.running = true 57 | self:executeGlobalScriptElement() 58 | self:enterStates(self.initial.transitions) 59 | self:mainEventLoop() 60 | end 61 | 62 | -- ****************************************************************************************************** 63 | -- ****************************************************************************************************** 64 | -- ****************************************************************************************************** 65 | 66 | function S:mainEventLoop() 67 | local anyTransition, enabledTransitions, macrostepDone, iterations 68 | while self.running do 69 | anyTransition = false -- (LXSC specific) 70 | iterations = 0 -- (LXSC specific) 71 | macrostepDone = false 72 | 73 | -- Here we handle eventless transitions and transitions 74 | -- triggered by internal events until macrostep is complete 75 | while self.running and not macrostepDone and iterations=S.MAX_ITERATIONS then print(string.format("Warning: stopped unstable system after %d internal iterations",S.MAX_ITERATIONS)) end 95 | 96 | -- Either we're in a final state, and we break out of the loop… 97 | if not self.running then break end 98 | -- …or we've completed a macrostep, so we start a new macrostep by waiting for an external event 99 | 100 | -- Here we invoke whatever needs to be invoked. The implementation of 'invoke' is platform-specific 101 | for _,state in ipairs(self._statesToInvoke) do for _,inv in ipairs(state._invokes) do self:invoke(inv) end end 102 | self._statesToInvoke:clear() 103 | 104 | -- Invoking may have raised internal error events; if so, we skip and iterate to handle them 105 | if self._internalQueue:isEmpty() then 106 | logloglog("-- External Queue: "..self._externalQueue:inspect()) 107 | local externalEvent = self._externalQueue:dequeue() 108 | if externalEvent then -- (LXSC specific) The queue might be empty. 109 | if isCancelEvent(externalEvent) then 110 | self.running = false 111 | else 112 | self._data:_setSystem('_event',externalEvent) 113 | for _,state in ipairs(self._configuration) do 114 | for _,inv in ipairs(state._invokes) do 115 | if inv.invokeid == externalEvent.invokeid then self:applyFinalize(inv, externalEvent) end 116 | if inv.autoforward then self:send(inv.id, externalEvent) end 117 | end 118 | end 119 | enabledTransitions = self:selectTransitions(externalEvent) 120 | if not enabledTransitions:isEmpty() then 121 | anyTransition = true 122 | self:microstep(enabledTransitions) 123 | end 124 | end 125 | end 126 | 127 | -- (LXSC specific) we stop iterating as soon as no transitions occur 128 | if not anyTransition then break end 129 | end 130 | end 131 | 132 | -- We re-check if we're running here because LXSC uses step-based processing; 133 | -- we may have exited the 'running' loop if there were no more events to process. 134 | if not self.running then self:exitInterpreter() end 135 | end 136 | 137 | -- ****************************************************************************************************** 138 | -- ****************************************************************************************************** 139 | -- ****************************************************************************************************** 140 | 141 | function S:executeGlobalScriptElement() 142 | if rawget(self,'_script') then self:executeSingle(self._script) end 143 | end 144 | 145 | function S:exitInterpreter() 146 | local statesToExit = self._configuration:toList():sort(exitOrder) 147 | for _,s in ipairs(statesToExit) do 148 | for _,content in ipairs(s._onexits) do self:executeContent(content) end 149 | for _,inv in ipairs(s._invokes) do self:cancelInvoke(inv) end 150 | 151 | -- (LXSC specific) We do not delete the configuration on exit so that it may be examined later. 152 | -- self._configuration:delete(s) 153 | 154 | if isFinalState(s) and isScxmlState(s.parent) then 155 | self:returnDoneEvent(self:donedata(s)) 156 | end 157 | end 158 | end 159 | 160 | function S:selectEventlessTransitions() 161 | startfunc('selectEventlessTransitions()') 162 | local enabledTransitions = OrderedSet() 163 | local atomicStates = self._configuration:toList():filter(isAtomicState):sort(entryOrder) 164 | for _,state in ipairs(atomicStates) do 165 | self:addEventlessTransition(state,enabledTransitions) 166 | end 167 | enabledTransitions = self:removeConflictingTransitions(enabledTransitions) 168 | closefunc('-- selectEventlessTransitions result: '..enabledTransitions:inspect()) 169 | return enabledTransitions 170 | end 171 | -- (LXSC specific) we use this function since Lua cannot break out of a nested loop 172 | function S:addEventlessTransition(state,enabledTransitions) 173 | for _,s in ipairs(state.selfAndAncestors) do 174 | for _,t in ipairs(s._eventlessTransitions) do 175 | if t:conditionMatched(self._data) then 176 | enabledTransitions:add(t) 177 | return 178 | end 179 | end 180 | end 181 | end 182 | 183 | function S:selectTransitions(event) 184 | startfunc('selectTransitions( '..event:inspect()..' )') 185 | local enabledTransitions = OrderedSet() 186 | local atomicStates = self._configuration:toList():filter(isAtomicState):sort(entryOrder) 187 | for _,state in ipairs(atomicStates) do 188 | self:addTransitionForEvent(state,event,enabledTransitions) 189 | end 190 | enabledTransitions = self:removeConflictingTransitions(enabledTransitions) 191 | closefunc('-- selectTransitions result: '..enabledTransitions:inspect()) 192 | return enabledTransitions 193 | end 194 | -- (LXSC specific) we use this function since Lua cannot break out of a nested loop 195 | function S:addTransitionForEvent(state,event,enabledTransitions) 196 | for _,s in ipairs(state.selfAndAncestors) do 197 | for _,t in ipairs(s._eventedTransitions) do 198 | if t:matchesEvent(event) and t:conditionMatched(self._data) then 199 | enabledTransitions:add(t) 200 | return 201 | end 202 | end 203 | end 204 | end 205 | 206 | function S:removeConflictingTransitions(enabledTransitions) 207 | startfunc('removeConflictingTransitions( enabledTransitions:'..enabledTransitions:inspect()..' )') 208 | local filteredTransitions = OrderedSet() 209 | for _,t1 in ipairs(enabledTransitions) do 210 | local t1Preempted = false 211 | local transitionsToRemove = OrderedSet() 212 | for _,t2 in ipairs(filteredTransitions) do 213 | if self:computeExitSet(List(t1)):hasIntersection(self:computeExitSet(List(t2))) then 214 | if isDescendant(t1.source,t2.source) then 215 | transitionsToRemove:add(t2) 216 | else 217 | t1Preempted = true 218 | break 219 | end 220 | end 221 | end 222 | 223 | if not t1Preempted then 224 | for _,t3 in ipairs(transitionsToRemove) do 225 | filteredTransitions:delete(t3) 226 | end 227 | filteredTransitions:add(t1) 228 | end 229 | end 230 | 231 | closefunc('-- removeConflictingTransitions result: '..filteredTransitions:inspect()) 232 | return filteredTransitions 233 | end 234 | 235 | function S:microstep(enabledTransitions) 236 | startfunc('microstep( enabledTransitions:'..enabledTransitions:inspect()..' )') 237 | 238 | self:exitStates(enabledTransitions) 239 | self:executeTransitionContent(enabledTransitions) 240 | self:enterStates(enabledTransitions) 241 | 242 | if rawget(self,'onEnteredAll') then self.onEnteredAll() end 243 | 244 | closefunc() 245 | end 246 | 247 | function S:exitStates(enabledTransitions) 248 | startfunc('exitStates( enabledTransitions:'..enabledTransitions:inspect()..' )') 249 | 250 | local statesToExit = self:computeExitSet(enabledTransitions) 251 | for _,s in ipairs(statesToExit) do self._statesToInvoke:delete(s) end 252 | statesToExit = statesToExit:toList():sort(exitOrder) 253 | 254 | -- Record history for states being exited 255 | for _,s in ipairs(statesToExit) do 256 | for _,h in ipairs(s.states) do 257 | if h._kind=='history' then 258 | self._historyValue[h.id] = self._configuration:toList():filter(function(s0) 259 | if h.type=='deep' then 260 | return isAtomicState(s0) and isDescendant(s0,s) 261 | else 262 | return s0.parent==s 263 | end 264 | end) 265 | end 266 | end 267 | end 268 | 269 | -- Exit the states 270 | for _,s in ipairs(statesToExit) do 271 | if rawget(self,'onBeforeExit') then self.onBeforeExit(s.id,s._kind,s.isAtomic) end 272 | for _,content in ipairs(s._onexits) do 273 | self:executeContent(content) 274 | end 275 | for _,inv in ipairs(s._invokes) do self:cancelInvoke(inv) end 276 | self._configuration:delete(s) 277 | logloglog(string.format("-- removed %s from the configuration; config is now {%s}",s:inspect(),table.concat(self:activeStateIds(),', '))) 278 | end 279 | 280 | closefunc() 281 | end 282 | 283 | function S:computeExitSet(transitions) 284 | startfunc('computeExitSet( transitions:'..transitions:inspect()..' )') 285 | local statesToExit = OrderedSet() 286 | for _,t in ipairs(transitions) do 287 | if t.targets then 288 | local domain = self:getTransitionDomain(t) 289 | for _,s in ipairs(self._configuration) do 290 | if isDescendant(s,domain) then 291 | statesToExit:add(s) 292 | end 293 | end 294 | end 295 | end 296 | closefunc('-- computeExitSet result '..statesToExit:inspect()) 297 | return statesToExit 298 | end 299 | 300 | function S:executeTransitionContent(enabledTransitions) 301 | startfunc('executeTransitionContent( enabledTransitions:'..enabledTransitions:inspect()..' )') 302 | for _,t in ipairs(enabledTransitions) do 303 | if rawget(self,'onTransition') then self.onTransition(t) end 304 | for _,executable in ipairs(t._exec) do 305 | if not self:executeSingle(executable) then break end 306 | end 307 | end 308 | closefunc() 309 | end 310 | 311 | function S:enterStates(enabledTransitions) 312 | startfunc('enterStates( enabledTransitions:'..enabledTransitions:inspect()..' )') 313 | 314 | local statesToEnter = OrderedSet() 315 | local statesForDefaultEntry = OrderedSet() 316 | local defaultHistoryContent = {} -- temporary table for default content in history states 317 | self:computeEntrySet(enabledTransitions,statesToEnter,statesForDefaultEntry,defaultHistoryContent) 318 | 319 | for _,s in ipairs(statesToEnter:toList():sort(entryOrder)) do 320 | self._configuration:add(s) 321 | logloglog(string.format("-- added %s '%s' to the configuration; config is now <%s>",s._kind,s.id,table.concat(self:activeStateIds(),', '))) 322 | if isScxmlState(s) then error("Added SCXML to configuration.") end 323 | self._statesToInvoke:add(s) 324 | 325 | if self.binding=="late" then 326 | -- The LXSC datamodel ensures this happens only once per state 327 | self._data:initState(s) 328 | end 329 | 330 | for _,content in ipairs(s._onentrys) do 331 | self:executeContent(content) 332 | end 333 | if rawget(self,'onAfterEnter') then self.onAfterEnter(s.id,s._kind,s.isAtomic) end 334 | 335 | if statesForDefaultEntry:isMember(s) then 336 | for _,t in ipairs(s.initial.transitions) do 337 | for _,executable in ipairs(t._exec) do 338 | if not self:executeSingle(executable) then break end 339 | end 340 | end 341 | end 342 | 343 | if defaultHistoryContent[s.id] then 344 | logloglog("-- executing defaultHistoryContent for "..s.id) 345 | for _,executable in ipairs(defaultHistoryContent[s.id]) do 346 | if not self:executeSingle(executable) then break end 347 | end 348 | end 349 | 350 | if isFinalState(s) then 351 | local parent = s.parent 352 | if isScxmlState(parent) then 353 | self.running = false 354 | else 355 | local grandparent = parent.parent 356 | self:fireEvent( "done.state."..parent.id, self:donedata(s), {type='internal'} ) 357 | if isParallelState(grandparent) then 358 | local allAreInFinal = true 359 | for _,child in ipairs(grandparent.reals) do 360 | if not self:isInFinalState(child) then 361 | allAreInFinal = false 362 | break 363 | end 364 | end 365 | if allAreInFinal then 366 | self:fireEvent( "done.state."..grandparent.id, nil, {type='internal'} ) 367 | end 368 | end 369 | end 370 | end 371 | 372 | end 373 | 374 | closefunc() 375 | end 376 | 377 | function S:computeEntrySet(transitions,statesToEnter,statesForDefaultEntry,defaultHistoryContent) 378 | startfunc('computeEntrySet( transitions:'..transitions:inspect()..', ... )') 379 | 380 | for _,t in ipairs(transitions) do 381 | if t.targets then 382 | for _,s in ipairs(t.targets) do 383 | self:addDescendantStatesToEnter(s,statesToEnter,statesForDefaultEntry,defaultHistoryContent) 384 | end 385 | end 386 | -- logloglog('-- after adding descendants statesToEnter is: '..statesToEnter:inspect()) 387 | 388 | local ancestor = self:getTransitionDomain(t) 389 | for _,s in ipairs(self:getEffectiveTargetStates(t)) do 390 | self:addAncestorStatesToEnter(s,ancestor,statesToEnter,statesForDefaultEntry,defaultHistoryContent) 391 | end 392 | end 393 | logloglog('-- computeEntrySet result statesToEnter: '..statesToEnter:inspect()) 394 | logloglog('-- computeEntrySet result statesForDefaultEntry: '..statesForDefaultEntry:inspect()) 395 | closefunc() 396 | end 397 | 398 | function S:addDescendantStatesToEnter(state,statesToEnter,statesForDefaultEntry,defaultHistoryContent) 399 | startfunc("addDescendantStatesToEnter( state:"..state:inspect()..", ... )") 400 | if isHistoryState(state) then 401 | 402 | if self._historyValue[state.id] then 403 | for _,s in ipairs(self._historyValue[state.id]) do 404 | self:addDescendantStatesToEnter(s,statesToEnter,statesForDefaultEntry,defaultHistoryContent) 405 | self:addAncestorStatesToEnter(s,state.parent,statesToEnter,statesForDefaultEntry,defaultHistoryContent) 406 | end 407 | else 408 | defaultHistoryContent[state.parent.id] = state.transitions[1]._exec 409 | logloglog("-- defaultHistoryContent['"..state.parent.id.."'] = "..(#state.transitions[1]._exec).." executables") 410 | for _,t in ipairs(state.transitions) do 411 | if t.targets then 412 | for _,s in ipairs(t.targets) do 413 | self:addDescendantStatesToEnter(s,statesToEnter,statesForDefaultEntry,defaultHistoryContent) 414 | self:addAncestorStatesToEnter(s,state.parent,statesToEnter,statesForDefaultEntry,defaultHistoryContent) 415 | end 416 | end 417 | end 418 | end 419 | 420 | else 421 | 422 | statesToEnter:add(state) 423 | logloglog("statesToEnter:add( "..state:inspect().." )") 424 | 425 | if isCompoundState(state) then 426 | statesForDefaultEntry:add(state) 427 | for _,t in ipairs(state.initial.transitions) do 428 | for _,s in ipairs(t.targets) do 429 | self:addDescendantStatesToEnter(s,statesToEnter,statesForDefaultEntry,defaultHistoryContent) 430 | self:addAncestorStatesToEnter(s,state,statesToEnter,statesForDefaultEntry,defaultHistoryContent) 431 | end 432 | end 433 | elseif isParallelState(state) then 434 | for _,child in ipairs(getChildStates(state)) do 435 | if not statesToEnter:some(function(s) return isDescendant(s,child) end) then 436 | self:addDescendantStatesToEnter(child,statesToEnter,statesForDefaultEntry,defaultHistoryContent) 437 | end 438 | end 439 | end 440 | 441 | end 442 | 443 | closefunc() 444 | end 445 | 446 | function S:addAncestorStatesToEnter(state,ancestor,statesToEnter,statesForDefaultEntry,defaultHistoryContent) 447 | startfunc("addAncestorStatesToEnter( state:"..state:inspect()..", ancestor:"..ancestor:inspect()..", ... )") 448 | 449 | for anc in state:ancestorsUntil(ancestor) do 450 | statesToEnter:add(anc) 451 | logloglog("statesToEnter:add( "..anc:inspect().." )") 452 | if isParallelState(anc) then 453 | for _,child in ipairs(getChildStates(anc)) do 454 | if not statesToEnter:some(function(s) return isDescendant(s,child) end) then 455 | self:addDescendantStatesToEnter(child,statesToEnter,statesForDefaultEntry,defaultHistoryContent) 456 | end 457 | end 458 | end 459 | end 460 | 461 | closefunc() 462 | end 463 | 464 | function S:isInFinalState(s) 465 | if isCompoundState(s) then 466 | return getChildStates(s):some(function(s) return isFinalState(s) and self._configuration:isMember(s) end) 467 | elseif isParallelState(s) then 468 | return getChildStates(s):every(function(s) self:isInFinalState(s) end) 469 | else 470 | return false 471 | end 472 | end 473 | 474 | function S:getTransitionDomain(t) 475 | startfunc('getTransitionDomain( t:'..t:inspect()..' )' ) 476 | local result 477 | local tstates = self:getEffectiveTargetStates(t) 478 | if not tstates[1] then 479 | result = nil 480 | elseif t.type=='internal' and isCompoundState(t.source) and tstates:every(function(s) return isDescendant(s,t.source) end) then 481 | result = t.source 482 | else 483 | result = findLCCA(t.source,t.targets or emptyList) 484 | end 485 | closefunc('-- getTransitionDomain result: '..tostring(result and result.id)) 486 | return result 487 | end 488 | 489 | function S:getEffectiveTargetStates(transition) 490 | startfunc('getEffectiveTargetStates( transition:'..transition:inspect()..' )') 491 | local targets = OrderedSet() 492 | if transition.targets then 493 | for _,s in ipairs(transition.targets) do 494 | if isHistoryState(s) then 495 | if self._historyValue[s.id] then 496 | targets:union(self._historyValue[s.id]) 497 | else 498 | -- History states can only have one transition, so we hard-code that here. 499 | targets:union(self:getEffectiveTargetStates(s.transitions[1])) 500 | end 501 | else 502 | targets:add(s) 503 | end 504 | end 505 | end 506 | closefunc('-- getEffectiveTargetStates result: '..targets:inspect()) 507 | return targets 508 | end 509 | 510 | function S:expandScxmlSource() 511 | self:convertInitials() 512 | self._stateById = {} 513 | for _,s in ipairs(self.states) do s:cacheReference(self._stateById) end 514 | self:resolveReferences(self._stateById) 515 | end 516 | 517 | function S:returnDoneEvent(donedata) 518 | -- TODO: implement 519 | end 520 | 521 | function S:invoke(invoke) 522 | -- TODO: implement 523 | error("Invoke not supported.") 524 | end 525 | 526 | function S:donedata(state) 527 | local c = state._donedatas[1] 528 | if c then 529 | if c._kind=='content' then 530 | local wrapper = {} 531 | self:executeSingle(c,wrapper) 532 | return wrapper.content 533 | else 534 | local map = {} 535 | for _,p in ipairs(state._donedatas) do 536 | local val = p.location and self._data:get(p.location) or p.expr and self._data:eval(p.expr) 537 | if val == LXSC.Datamodel.INVALIDLOCATION then 538 | self:fireEvent("error.execution.invalid-param-value","There was an error determining the value for a inside a ") 539 | elseif val ~= LXSC.Datamodel.EVALERROR then 540 | if p.name==nil or p.name=="" then 541 | self:fireEvent("error.execution.invalid-param-name","Unsupported name '"..tostring(p.name).."'") 542 | else 543 | map[p.name] = val 544 | end 545 | end 546 | end 547 | return next(map) and map 548 | end 549 | end 550 | end 551 | 552 | function S:fireEvent(name,data,eventValues) 553 | eventValues = eventValues or {} 554 | eventValues.type = eventValues.type or 'platform' 555 | local event = LXSC.Event(name,data,eventValues) 556 | logloglog(string.format("-- queued %s event '%s'",event.type,event.name)) 557 | if rawget(self,'onEventFired') then self.onEventFired(event) end 558 | self[eventValues.type=='external' and "_externalQueue" or "_internalQueue"]:enqueue(event) 559 | return event 560 | end 561 | 562 | -- Sensible aliases 563 | S.start = S.interpret 564 | S.restart = S.interpret 565 | 566 | function S:step() 567 | self:processDelayedSends() 568 | self:mainEventLoop() 569 | end 570 | 571 | end)(LXSC.SCXML) 572 | --------------------------------------------------------------------------------