From c12107d306aefa77a7f4c6aa5caf4ed431fe4f92 Mon Sep 17 00:00:00 2001 From: ywang Date: Tue, 2 Feb 2021 17:13:04 +0100 Subject: JIT-compile ATC commands --- advtrains/atc.lua | 198 ++------------------------------------ advtrains/atcjit.lua | 206 ++++++++++++++++++++++++++++++++++++++++ advtrains/init.lua | 2 + advtrains/tests/atcjit_spec.lua | 156 ++++++++++++++++++++++++++++++ 4 files changed, 373 insertions(+), 189 deletions(-) create mode 100644 advtrains/atcjit.lua create mode 100644 advtrains/tests/atcjit_spec.lua 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" 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]=?)([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) -- cgit v1.2.3