aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorywang <yw05@forksworld.de>2021-02-02 17:13:04 +0100
committerywang <yw05@forksworld.de>2021-06-09 15:31:21 +0200
commitc12107d306aefa77a7f4c6aa5caf4ed431fe4f92 (patch)
tree2de81bb3519c998603a652502cd994c0b86ad1b6
parente5b053c0d824543116d322634d2cf1019d6754bc (diff)
downloadadvtrains-c12107d306aefa77a7f4c6aa5caf4ed431fe4f92.tar.gz
advtrains-c12107d306aefa77a7f4c6aa5caf4ed431fe4f92.tar.bz2
advtrains-c12107d306aefa77a7f4c6aa5caf4ed431fe4f92.zip
JIT-compile ATC commands
-rw-r--r--advtrains/atc.lua198
-rw-r--r--advtrains/atcjit.lua206
-rw-r--r--advtrains/init.lua2
-rw-r--r--advtrains/tests/atcjit_spec.lua156
4 files changed, 373 insertions, 189 deletions
diff --git a/advtrains/atc.lua b/advtrains/atc.lua
index 64cdcec..9f909c5 100644
--- a/advtrains/atc.lua
+++ b/advtrains/atc.lua
@@ -53,15 +53,7 @@ function atc.send_command(pos, par_tid)
atwarn("ATC rail at", pos, ": Rail not on train's path! Can't determine arrow direction. Assuming +!")
end
- local command = atc.controllers[pts].command
- command = eval_conditional(command, iconnid==1, train.velocity)
- if not command then command="" end
- command=string.match(command, "^%s*(.*)$")
-
- if command == "" then
- atprint("Sending ATC Command to", train_id, ": Not modifying, conditional evaluated empty.")
- return true
- end
+ local command = atc.controllers[pts].command
atc.train_set_command(train, command, iconnid==1)
atprint("Sending ATC Command to", train_id, ":", command, "iconnid=",iconnid)
@@ -188,189 +180,17 @@ function atc.get_atc_controller_formspec(pos, meta)
return formspec.."button_exit[0.5,4.5;7,1;save;"..attrans("Save").."]"
end
---from trainlogic.lua train step
-local matchptn={
- ["SM"]=function(id, train)
- train.tarvelocity=train.max_speed
- return 2
- end,
- ["S([0-9]+)"]=function(id, train, match)
- train.tarvelocity=tonumber(match)
- return #match+1
- end,
- ["B([0-9]+)"]=function(id, train, match)
- if train.velocity>tonumber(match) then
- train.atc_brake_target=tonumber(match)
- if not train.tarvelocity or train.tarvelocity>train.atc_brake_target then
- train.tarvelocity=train.atc_brake_target
- end
- end
- return #match+1
- end,
- ["BB"]=function(id, train)
- train.atc_brake_target = -1
- train.tarvelocity = 0
- return 2
- end,
- ["W"]=function(id, train)
- train.atc_wait_finish=true
- return 1
- end,
- ["D([0-9]+)"]=function(id, train, match)
- train.atc_delay=tonumber(match)
- return #match+1
- end,
- ["R"]=function(id, train)
- if train.velocity<=0 then
- advtrains.invert_train(id)
- advtrains.train_ensure_init(id, train)
- -- no one minds if this failed... this shouldn't even be called without train being initialized...
- else
- atwarn(sid(id), attrans("ATC Reverse command warning: didn't reverse train, train moving!"))
- end
- return 1
- end,
- ["O([LRC])"]=function(id, train, match)
- local tt={L=-1, R=1, C=0}
- local arr=train.atc_arrow and 1 or -1
- train.door_open = tt[match]*arr
- return 2
- end,
- ["K"] = function(id, train)
- if train.door_open == 0 then
- atwarn(sid(id), attrans("ATC Kick command warning: Doors closed"))
- return 1
- end
- if train.velocity > 0 then
- atwarn(sid(id), attrans("ATC Kick command warning: Train moving"))
- return 1
- end
- local tp = train.trainparts
- for i=1,#tp do
- local data = advtrains.wagons[tp[i]]
- local obj = advtrains.wagon_objects[tp[i]]
- if data and obj then
- local ent = obj:get_luaentity()
- if ent then
- for seatno,seat in pairs(ent.seats) do
- if data.seatp[seatno] and not ent:is_driver_stand(seat) then
- ent:get_off(seatno)
- end
- end
- end
- end
- end
- return 1
- end,
- ["A([01])"]=function(id, train, match)
- if not advtrains.interlocking then return 2 end
- advtrains.interlocking.ars_set_disable(train, match=="0")
- return 2
- end,
-}
-
-eval_conditional = function(command, arrow, speed)
- --conditional statement?
- local is_cond, cond_applies, compare
- local cond, rest=string.match(command, "^I([%+%-])(.+)$")
- if cond then
- is_cond=true
- if cond=="+" then
- cond_applies=arrow
- end
- if cond=="-" then
- cond_applies=not arrow
- end
- else
- cond, compare, rest=string.match(command, "^I([<>]=?)([0-9]+)(.+)$")
- if cond and compare then
- is_cond=true
- if cond=="<" then
- cond_applies=speed<tonumber(compare)
- end
- if cond==">" then
- cond_applies=speed>tonumber(compare)
- end
- if cond=="<=" then
- cond_applies=speed<=tonumber(compare)
- end
- if cond==">=" then
- cond_applies=speed>=tonumber(compare)
- end
- end
- end
- if is_cond then
- atprint("Evaluating if statement: "..command)
- atprint("Cond: "..(cond or "nil"))
- atprint("Applies: "..(cond_applies and "true" or "false"))
- atprint("Rest: "..rest)
- --find end of conditional statement
- local nest, pos, elsepos=0, 1
- while nest>=0 do
- if pos>#rest then
- atwarn(sid(id), attrans("ATC command syntax error: I statement not closed: @1",command))
- return ""
- end
- local char=string.sub(rest, pos, pos)
- if char=="I" then
- nest=nest+1
- end
- if char==";" then
- nest=nest-1
- end
- if nest==0 and char=="E" then
- elsepos=pos+0
- end
- pos=pos+1
- end
- if not elsepos then elsepos=pos-1 end
- if cond_applies then
- command=string.sub(rest, 1, elsepos-1)..string.sub(rest, pos)
- else
- command=string.sub(rest, elsepos+1, pos-2)..string.sub(rest, pos)
- end
- atprint("Result: "..command)
- end
- return command
-end
-
function atc.execute_atc_command(id, train)
- --strip whitespaces
- local command=string.match(train.atc_command, "^%s*(.*)$")
-
-
- if string.match(command, "^%s*$") then
- train.atc_command=nil
- return
- end
-
- train.atc_command = eval_conditional(command, train.atc_arrow, train.velocity)
-
- if not train.atc_command then return end
- command=string.match(train.atc_command, "^%s*(.*)$")
-
- if string.match(command, "^%s*$") then
- train.atc_command=nil
- return
- end
-
- for pattern, func in pairs(matchptn) do
- local match=string.match(command, "^"..pattern)
- if match then
- local patlen=func(id, train, match)
-
- atprint("Executing: "..string.sub(command, 1, patlen))
-
- train.atc_command=string.sub(command, patlen+1)
- if train.atc_delay<=0 and not train.atc_wait_finish then
- --continue (recursive, cmds shouldn't get too long, and it's a end-recursion.)
- atc.execute_atc_command(id, train)
- end
- return
+ local w, e = advtrains.atcjit.execute(id, train)
+ if w then
+ for i = 1, #w, 1 do
+ atwarn(sid(id),w[i])
end
end
- atwarn(sid(id), attrans("ATC command parse error: Unknown command: @1", command))
- atc.train_reset_command(train, true)
+ if e then
+ atwarn(sid(id),e)
+ atc.train_reset_command(train, true)
+ end
end
diff --git a/advtrains/atcjit.lua b/advtrains/atcjit.lua
new file mode 100644
index 0000000..0d400ea
--- /dev/null
+++ b/advtrains/atcjit.lua
@@ -0,0 +1,206 @@
+local aj_cache = {}
+
+local aj_tostring
+
+local matchptn = {
+ ["A[01FT]"] = function(match)
+ return string.format(
+ "advtrains.interlocking.set_ars_disable(train,%s)",
+ (match=="0" or match=="F") and "true" or "false"), false
+ end,
+ ["BB"] = function()
+ return [[do
+ train.atc_brake_target = -1
+ train.tarvelocity = 0
+ end]], 2
+ end,
+ ["B([0-9]+)"] = function(match)
+ return string.format([[do
+ train.atc_brake_target = %s
+ if not train.tarvelocity or train.tarvelocity > train.atc_brake_target then
+ train.tarvelocity = train.atc_brake_target
+ end
+ end]], match),#match+1
+ end,
+ ["D([0-9]+)"] = function(match)
+ return string.format("train.atc_delay = %s", match), #match+1, true
+ end,
+ ["(%bI;)"] = function(match)
+ local i = 2
+ local l = #match
+ local epos
+ while (i<l) do
+ local b, e, c = string.find(match,"([IE])",i)
+ if c == "I" then
+ b, e = string.find(match,"%bI;", b)
+ -- This is unlikely to happen since the %b pattern is balanced
+ if not (b and e) then return nil, "Malformed nested I statement" end
+ i = e+1
+ else
+ epos = b
+ break
+ end
+ end
+ local endp = string.find(match,"[WDI]") and true
+ local cond = string.match(match,"^I([+-])")
+ if cond then
+ local vtrue = string.sub(match, 3, epos and epos-1 or -2)
+ local vfalse = epos and string.sub(match, epos+1, -2)
+ local cstr = (cond == "-") and "not" or ""
+ local tstr, err = aj_tostring(vtrue, 1, true)
+ if not tstr then return nil, err end
+ if vfalse then
+ local fstr, err = aj_tostring(vfalse, 1, true)
+ if not fstr then return nil, err end
+ return string.format("if %s train.atc_arrow then %s else %s end",
+ cstr, tstr, fstr), l, endp
+ else
+ return string.format("if %s train.atc_arrow then %s end", cstr, tstr), l, endp
+ end
+ else
+ local op, ref = string.match(match,"^I([<>]=?)([0-9]+)")
+ if not op then
+ return _, "Invalid I statement"
+ end
+ local spos = 2+#op+#ref
+ local vtrue = string.sub(match, spos, epos and epos-1 or -2)
+ local vfalse = epos and string.sub(match, epos+1, -2)
+ local cstr = string.format("train.velocity %s %s", op, ref)
+ local tstr = aj_tostring(vtrue, 1, true)
+ if vfalse then
+ local fstr, err = aj_tostring(vfalse, 1, true)
+ if not fstr then return nil, err end
+ return string.format("if %s then %s else %s end", cstr, tstr, fstr), l, endp
+ else
+ return string.format("if %s then %s end", cstr, tstr), l, endp
+ end
+ end
+ end,
+ ["K"] = function()
+ return [=[do
+ if train.door_open == 0 then
+ _w[#_w+1] = attrans("ATC Kick command warning: Doors are closed")
+ elseif train.velocity>0 then
+ _w[#_w+1] = attrans("ATC Kick command warning: Train moving")
+ else
+ local tp = train.trainparts
+ for i = 1, #tp do
+ local data = advtrains.wagons[tp[i]]
+ local obj = advtrains.wagon_objects[tp[i]]
+ if data and obj then
+ local ent = obj:get_luaentity()
+ if ent then
+ for seatno, seat in pairs(ent.seats) do
+ if data.seatp[seatno] and not ent:is_driver_stand(seat) then
+ ent:get_off(seatno)
+ end
+ end
+ end
+ end
+ end
+ end
+ end]=], 1
+ end,
+ ["O([LR])"] = function(match)
+ local tt = {L = -1, R = 1}
+ return string.format("train.door_open = %d*(train.atc_arrow and 1 or -1)",tt[match]), 2
+ end,
+ ["OC"] = function(match)
+ return "train.door_open = 0", 2
+ end,
+ ["R"] = function()
+ return [[
+ if train.velocity<=0 then
+ advtrains.invert_train(id)
+ advtrains.train_ensure_init(id, train)
+ else
+ _w[#_w+1] = attrans("ATC Reverse command warning: didn't reverse train, train moving!")
+ end]], 1
+ end,
+ ["SM"] = function()
+ return "train.tarvelocity=train.max_speed", 2
+ end,
+ ["S([0-9]+)"] = function(match)
+ return string.format("train.tarvelocity=%s",match), #match+1
+ end,
+ ["W"] = function()
+ return "train.atc_wait_finish=true", 1, true
+ end,
+}
+
+local function aj_tostring_single(cmd, pos)
+ if not pos then pos = 1 end
+ for pattern, func in pairs(matchptn) do
+ local match = {string.match(cmd, "^"..pattern, pos)}
+ if match[1] then
+ return func(unpack(match))
+ end
+ end
+ return nil
+end
+
+aj_tostring = function(cmd, pos, noreset)
+ if not pos then pos = 1 end
+ local t = {}
+ local endp = false
+ while pos <= #cmd do
+ if string.match(cmd,"^%s+$", pos) then break end
+ local _, e = string.find(cmd, "^%s+", pos)
+ if e then pos = e+1 end
+ local str, len
+ str, len, endp = aj_tostring_single(cmd, pos)
+ if not str then
+ return nil, (len or "Invalid command or malformed I statement: "..string.sub(cmd,pos))
+ end
+ t[#t+1] = str
+ pos = pos+len
+ if endp then
+ if endp then
+ t[#t+1] = string.format("_c[#_c+1]=%q",string.sub(cmd, pos))
+ end
+ break
+ end
+ end
+ return table.concat(t,"\n"), pos
+end
+
+local function aj_compile(cmd)
+ if aj_cache[cmd] then
+ local x = aj_cache[cmd]
+ if type(x) == "function" then
+ return x
+ else
+ return nil, x
+ end
+ end
+ local str, err = aj_tostring(cmd)
+ if not str then
+ aj_cache[cmd] = err
+ return nil, err
+ end
+ local str = string.format([[return function(id,train)
+ local _c = {}
+ local _w = {}
+ %s
+ if _c[1] then train.atc_command=table.concat(_c)
+ else train.atc_command = nil end
+ return _w, nil
+ end]], str)
+ local f, e = loadstring(str)
+ if not f then return nil, e end
+ f = f()
+ aj_cache[cmd] = f
+ return f
+end
+
+local function aj_execute(id,train)
+ if not train.atc_command then return end
+ local func, err = aj_compile(train.atc_command)
+ if func then return func(id,train) end
+ return nil, err
+end
+
+return {
+ compile = aj_compile,
+ execute = aj_execute
+}
diff --git a/advtrains/init.lua b/advtrains/init.lua
index 96352df..b7ba08f 100644
--- a/advtrains/init.lua
+++ b/advtrains/init.lua
@@ -198,6 +198,8 @@ advtrains.meseconrules =
advtrains.fpath=minetest.get_worldpath().."/advtrains"
+advtrains.atcjit=dofile(advtrains.modpath.."/atcjit.lua")
+
dofile(advtrains.modpath.."/path.lua")
dofile(advtrains.modpath.."/trainlogic.lua")
dofile(advtrains.modpath.."/trainhud.lua")
diff --git a/advtrains/tests/atcjit_spec.lua b/advtrains/tests/atcjit_spec.lua
new file mode 100644
index 0000000..8e2a8b7
--- /dev/null
+++ b/advtrains/tests/atcjit_spec.lua
@@ -0,0 +1,156 @@
+package.path = "../?.lua;" .. package.path
+advtrains = {}
+_G.advtrains = advtrains
+function _G.attrans(...) return ... end
+function advtrains.invert_train() end
+function advtrains.train_ensure_init() end
+
+local atcjit = require("atcjit")
+
+local function assert_atc(train, warn, err, res)
+ local w, e = atcjit.execute(train.id,train)
+ assert.same(err, e)
+ if w then assert.same(warn, w) end
+ assert.same(res, train)
+end
+
+local function thisatc(desc, train, warn, err, res)
+ it(desc, function() assert_atc(train, warn, err, res) end)
+end
+
+describe("simple ATC track", function()
+ local t = {
+ atc_arrow = true,
+ atc_command = " B12WB8WBBWOLD15ORD15OCD1RS10WSM",
+ door_open = 0,
+ max_speed = 20,
+ tarvelocity = 10,
+ velocity = 0,
+ }
+ thisatc("should make the train slow down to 12", t, {}, nil,{
+ atc_arrow = true,
+ atc_brake_target = 12,
+ atc_command = "B8WBBWOLD15ORD15OCD1RS10WSM",
+ atc_wait_finish = true,
+ door_open = 0,
+ max_speed = 20,
+ tarvelocity = 10,
+ velocity = 0,
+ })
+ thisatc("should make the train brake to 8", t, {}, nil, {
+ atc_arrow = true,
+ atc_brake_target = 8,
+ atc_command = "BBWOLD15ORD15OCD1RS10WSM",
+ atc_wait_finish = true,
+ door_open = 0,
+ max_speed = 20,
+ tarvelocity = 8,
+ velocity = 0,
+ })
+ thisatc("should make the train stop", t, {}, nil, {
+ atc_arrow = true,
+ atc_brake_target = -1,
+ atc_command = "OLD15ORD15OCD1RS10WSM",
+ atc_wait_finish = true,
+ door_open = 0,
+ max_speed = 20,
+ tarvelocity = 0,
+ velocity = 0,
+ })
+ thisatc("should make the train open its left doors", t, {}, nil, {
+ atc_arrow = true,
+ atc_brake_target = -1,
+ atc_command = "ORD15OCD1RS10WSM",
+ atc_delay = 15,
+ atc_wait_finish = true,
+ door_open = -1,
+ max_speed = 20,
+ tarvelocity = 0,
+ velocity = 0,
+ })
+ thisatc("should make the train open its right doors", t, {}, nil,{
+ atc_arrow = true,
+ atc_brake_target = -1,
+ atc_command = "OCD1RS10WSM",
+ atc_delay = 15,
+ atc_wait_finish = true,
+ door_open = 1,
+ max_speed = 20,
+ tarvelocity = 0,
+ velocity = 0,
+ })
+ thisatc("should make the train close its doors", t, {}, nil, {
+ atc_arrow = true,
+ atc_brake_target = -1,
+ atc_command = "RS10WSM",
+ atc_delay = 1,
+ atc_wait_finish = true,
+ door_open = 0,
+ max_speed = 20,
+ tarvelocity = 0,
+ velocity = 0,
+ })
+ thisatc("should make the train depart and accelerate to 10", t, {}, nil, {
+ atc_arrow = true,
+ atc_brake_target = -1,
+ atc_command = "SM",
+ atc_delay = 1,
+ atc_wait_finish = true,
+ door_open = 0,
+ max_speed = 20,
+ tarvelocity = 10,
+ velocity = 0,
+ })
+ thisatc("should make the train accelerate to 20", t, {}, nil, {
+ atc_arrow = true,
+ atc_brake_target = -1,
+ atc_delay = 1,
+ atc_wait_finish = true,
+ door_open = 0,
+ max_speed = 20,
+ tarvelocity = 20,
+ velocity = 0,
+ })
+end)
+
+describe("ATC track with whitespaces", function()
+ local t = {
+ atc_command = " \t\n OC \n S20 \r "
+ }
+ thisatc("should not cause errors", t, {}, nil, {
+ door_open = 0,
+ tarvelocity = 20,
+ })
+end)
+
+describe("empty ATC track", function()
+ local t = {atc_command = ""}
+ thisatc("should not do anything", t, {}, nil, {})
+end)
+
+describe("ATC track with nested I statements", function()
+ local t = {
+ atc_arrow = false,
+ atc_command = "I+OREI>5I<=10S16WORES12;D15;;OC",
+ velocity = 10,
+ door_open = 0,
+ }
+ thisatc("should make the train accelerate to 16", t, {}, nil,{
+ atc_arrow = false,
+ atc_command = "ORD15OC",
+ atc_wait_finish = true,
+ velocity = 10,
+ door_open = 0,
+ tarvelocity = 16,
+ })
+end)
+
+describe("ATC track with invalid statement", function()
+ local t = { atc_command = "Ifoo" }
+ thisatc("should report an error", t, {}, "Invalid command or malformed I statement: Ifoo", t)
+end)
+
+describe("ATC track with invalid I condition", function()
+ local t = { atc_command = "I?;" }
+ thisatc("should report an error", t, {}, "Invalid I statement", t)
+end)