ywang: 5 JIT-compile ATC commands Optimizations Fix incomplete comment Allow ATC patterns to catch multiple entries Add command to view generated lua code; add unittests for repeating code 11 files changed, 490 insertions(+), 265 deletions(-)
I got an observations: How does the _c table gain more than one entry? I would make the compiler slightly smarter to only create _c and _w tables if absolutely needed by the commands, which would simplify the code a lot. I don't know how table.concat actually works, but it seems to me that it could add a fair bit of overhead for something that could be fixed at compile-time even if it concats a single entry, just by virtue of pairs() being quite inefficient.
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~gpcf/advtrains/patches/19969/mbox | git am -3Learn more about email & git
--- advtrains/aj_unittest.lua | 155 +++++++++++++++++++++++++++++ advtrains/atc.lua | 192 ++---------------------------------- advtrains/atcjit.lua | 201 ++++++++++++++++++++++++++++++++++++++ advtrains/init.lua | 2 + 4 files changed, 367 insertions(+), 183 deletions(-) create mode 100644 advtrains/aj_unittest.lua create mode 100644 advtrains/atcjit.lua diff --git a/advtrains/aj_unittest.lua b/advtrains/aj_unittest.lua new file mode 100644 index 0000000..9dacbc3 --- /dev/null +++ b/advtrains/aj_unittest.lua @@ -0,0 +1,155 @@ +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) diff --git a/advtrains/atc.lua b/advtrains/atc.lua index 2fa3929..eff6420 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) @@ -182,184 +174,18 @@ 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, -} - -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..8e01239 --- /dev/null +++ b/advtrains/atcjit.lua @@ -0,0 +1,201 @@ +local aj_cache = {} + +local aj_tostring + +local matchptn = { + ["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 then + return func(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 6e622e5..2c3c7fc 100644 --- a/advtrains/init.lua +++ b/advtrains/init.lua @@ -195,6 +195,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") -- 2.30.0
--- advtrains/atc.lua | 7 +-- advtrains/atcjit.lua | 102 ++++++++++++++++++++++++------------------- 2 files changed, 58 insertions(+), 51 deletions(-) diff --git a/advtrains/atc.lua b/advtrains/atc.lua index 186c506..7b1ec2f 100644 --- a/advtrains/atc.lua +++ b/advtrains/atc.lua @@ -175,12 +175,7 @@ function atc.get_atc_controller_formspec(pos, meta) end function atc.execute_atc_command(id, train) - local w, e = advtrains.atcjit.execute(id, train) - if w then - for i = 1, #w, 1 do - atwarn(sid(id),w[i]) - end - end + local _, e = advtrains.atcjit.execute(id, train) if e then atwarn(sid(id),e) atc.train_reset_command(train, true) diff --git a/advtrains/atcjit.lua b/advtrains/atcjit.lua index 52a03c2..e3df1d1 100644 --- a/advtrains/atcjit.lua +++ b/advtrains/atcjit.lua @@ -3,25 +3,39 @@ local aj_strout = {} local aj_tostring +--[[ Notes on the pattern matching functions: +- Patterns can have multiple captures (e.g. coordinates). These captures +are passed starting from the second argument. +- The commands after the match are passed as the first argument. If the +command involves waiting, it should: + - Set train.atc_command to the first argument passed to the function, + - End with "do return end", and + - Return true as the second argument +- The function should work on +]] local matchptn = { ["BB"] = function() return [[do train.atc_brake_target = -1 train.tarvelocity = 0 - end]], 2 + end]] end, - ["B([0-9]+)"] = function(match) + ["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]], match) end, - ["D([0-9]+)"] = function(match) - return string.format("train.atc_delay = %s", match), #match+1, true + ["D([0-9]+)"] = function(cont, match) + return string.format([[do + train.atc_delay = %s + train.atc_command = %q + return + end]], match, cont), true end, - ["(%bI;)"] = function(match) + ["(%bI;)"] = function(cont, match) local i = 2 local l = #match local epos @@ -43,15 +57,15 @@ local matchptn = { 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) + local tstr, err = aj_tostring(vtrue, 1, cont) if not tstr then return nil, err end if vfalse then - local fstr, err = aj_tostring(vfalse, 1, true) + local fstr, err = aj_tostring(vfalse, 1, cont) 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 + cstr, tstr, fstr) else - return string.format("if %s train.atc_arrow then %s end", cstr, tstr), l, endp + return string.format("if %s train.atc_arrow then %s end", cstr, tstr) end else local op, ref = string.match(match,"^I([<>]=?)([0-9]+)") @@ -62,22 +76,22 @@ local matchptn = { 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) + local tstr = aj_tostring(vtrue, 1, cont) if vfalse then - local fstr, err = aj_tostring(vfalse, 1, true) + local fstr, err = aj_tostring(vfalse, 1, cont) if not fstr then return nil, err end - return string.format("if %s then %s else %s end", cstr, tstr, fstr), l, endp + return string.format("if %s then %s else %s end", cstr, tstr, fstr) else - return string.format("if %s then %s end", cstr, tstr), l, endp + return string.format("if %s then %s end", cstr, tstr) end end end, ["K"] = function() return [=[do if train.door_open == 0 then - _w[#_w+1] = attrans("ATC Kick command warning: Doors are closed") + atwarn(sid(id),attrans("ATC Kick command warning: Doors are closed")) elseif train.velocity>0 then - _w[#_w+1] = attrans("ATC Kick command warning: Train moving") + atwarn(sid(id),attrans("ATC Kick command warning: Train moving")) else local tp = train.trainparts for i = 1, #tp do @@ -95,14 +109,14 @@ local matchptn = { end end end - end]=], 1 + end]=] end, - ["O([LR])"] = function(match) + ["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 + return string.format("train.door_open = %d*(train.atc_arrow and 1 or -1)",tt[match]) end, - ["OC"] = function(match) - return "train.door_open = 0", 2 + ["OC"] = function() + return "train.door_open = 0" end, ["R"] = function() return [[ @@ -110,32 +124,38 @@ local matchptn = { 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 + atwarn(sid(id),attrans("ATC Reverse command warning: didn't reverse train, train moving!")) + end]] end, ["SM"] = function() - return "train.tarvelocity=train.max_speed", 2 + return "train.tarvelocity=train.max_speed" end, - ["S([0-9]+)"] = function(match) - return string.format("train.tarvelocity=%s",match), #match+1 + ["S([0-9]+)"] = function(_, match) + return string.format("train.tarvelocity=%s",match) end, - ["W"] = function() - return "train.atc_wait_finish=true", 1, true + ["W"] = function(cont) + return string.format([[do + train.atc_wait_finish=true + train.atc_command=%q + return + end]], cont), true end, } -local function aj_tostring_single(cmd, pos) +local function aj_tostring_single(cmd, pos, cont) if not pos then pos = 1 end for pattern, func in pairs(matchptn) do - local match = {string.match(cmd, "^"..pattern, pos)} + local match = {string.find(cmd, "^"..pattern, pos)} if match[1] then - return func(unpack(match)) + local e = match[2] + match[2] = string.sub(cmd, e+1)..(cont or "") + return e+1, func(unpack(match, 2)) end end return nil end -aj_tostring = function(cmd, pos, noreset) +aj_tostring = function(cmd, pos, cont) if not pos then pos = 1 end local t = {} local endp = false @@ -143,18 +163,14 @@ aj_tostring = function(cmd, pos, noreset) 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) + local nxt, str + nxt, str, endp = aj_tostring_single(cmd, pos, cont or "") if not str then - return nil, (len or "Invalid command or malformed I statement: "..string.sub(cmd,pos)) + return nil, (endp or "Invalid command or malformed I statement: "..string.sub(cmd,pos)) end t[#t+1] = str - pos = pos+len + pos = nxt if endp then - local cont = string.sub(cmd, pos) - if not string.match(cont, "^%s*$") then - t[#t+1] = string.format("_c[#_c+1]=%q",cont) - end break end end @@ -176,12 +192,8 @@ local function aj_compile(cmd) return nil, err end 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 + train.atc_command=nil end]], str) local f, e = loadstring(str) if not f then -- 2.30.0
--- advtrains/atcjit.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/advtrains/atcjit.lua b/advtrains/atcjit.lua index e3df1d1..973e27a 100644 --- a/advtrains/atcjit.lua +++ b/advtrains/atcjit.lua @@ -11,7 +11,8 @@ command involves waiting, it should: - Set train.atc_command to the first argument passed to the function, - End with "do return end", and - Return true as the second argument -- The function should work on +- The function should return a string to be compiled as the first result +and should not fail. ]] local matchptn = { ["BB"] = function() -- 2.30.0
--- advtrains/atcjit.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/advtrains/atcjit.lua b/advtrains/atcjit.lua index 8e01239..7d4af04 100644 --- a/advtrains/atcjit.lua +++ b/advtrains/atcjit.lua @@ -126,9 +126,9 @@ local matchptn = { 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 then - return func(match) + local match = {string.match(cmd, "^"..pattern, pos)} + if match[1] then + return func(unpack(match)) end end return nil -- 2.30.0
--- advtrains/aj_unittest.lua | 15 +++++++++++ advtrains/atc.lua | 15 +++++++++-- advtrains/atcjit.lua | 57 ++++++++++++++++++++++----------------- 3 files changed, 60 insertions(+), 27 deletions(-) diff --git a/advtrains/aj_unittest.lua b/advtrains/aj_unittest.lua index 9dacbc3..a660131 100644 --- a/advtrains/aj_unittest.lua +++ b/advtrains/aj_unittest.lua @@ -153,3 +153,18 @@ describe("ATC track with invalid I condition", function() local t = { atc_command = "I?;" } thisatc("should report an error", t, {}, "Invalid I statement", t) end) + +describe("ATC track reusing existing code", function() + local t = { atc_command = " B12WB8WBBWOLD15ORD15OCD1RS10WSM", tarvelocity = 15 } + thisatc("should do the same thing as in the first test", t, {}, nil, { + atc_brake_target = 12, + atc_command = "B8WBBWOLD15ORD15OCD1RS10WSM", + atc_wait_finish = true, + tarvelocity = 12 + }) +end) + +describe("ATC track reusing malformed code", function() + local t = {atc_command = "I?;"} + thisatc("Should report the invalid I statement", t, {}, "Invalid I statement", t) +end) diff --git a/advtrains/atc.lua b/advtrains/atc.lua index eff6420..186c506 100644 --- a/advtrains/atc.lua +++ b/advtrains/atc.lua @@ -174,7 +174,6 @@ function atc.get_atc_controller_formspec(pos, meta) return formspec.."button_exit[0.5,4.5;7,1;save;"..attrans("Save").."]" end - function atc.execute_atc_command(id, train) local w, e = advtrains.atcjit.execute(id, train) if w then @@ -188,7 +187,19 @@ function atc.execute_atc_command(id, train) end end - +minetest.register_chatcommand("at_get_lua",{ + params = "<command>", + description = "Compile the given ATC command into Lua code and show the given code", + privs = {train_admin = true}, + func = function(name, params) + local f, s = advtrains.atcjit.compile(params) + if not f then + return false, s or ("Unknown compilation error") + else + return true, s + end + end, +}) --move table to desired place advtrains.atc=atc diff --git a/advtrains/atcjit.lua b/advtrains/atcjit.lua index 7d4af04..52a03c2 100644 --- a/advtrains/atcjit.lua +++ b/advtrains/atcjit.lua @@ -1,4 +1,5 @@ local aj_cache = {} +local aj_strout = {} local aj_tostring @@ -150,8 +151,9 @@ aj_tostring = function(cmd, pos, noreset) 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)) + local cont = string.sub(cmd, pos) + if not string.match(cont, "^%s*$") then + t[#t+1] = string.format("_c[#_c+1]=%q",cont) end break end @@ -160,32 +162,37 @@ aj_tostring = function(cmd, pos, noreset) end local function aj_compile(cmd) - if aj_cache[cmd] then - local x = aj_cache[cmd] - if type(x) == "function" then - return x + local c = aj_cache[cmd] + if c then + if type(c) == "function" then + return c, aj_strout[cmd] else - return nil, x + return nil, c end + else + local str, err = aj_tostring(cmd) + if not str then + aj_cache[cmd] = err + return nil, err + end + 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 + aj_cache[cmd] = e + return nil, e + end + f = f() + aj_cache[cmd] = f + aj_strout[cmd] = str + return f, str 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) -- 2.30.0
I got an observations: How does the _c table gain more than one entry? I would make the compiler slightly smarter to only create _c and _w tables if absolutely needed by the commands, which would simplify the code a lot. I don't know how table.concat actually works, but it seems to me that it could add a fair bit of overhead for something that could be fixed at compile-time even if it concats a single entry, just by virtue of pairs() being quite inefficient.