--[[ Signs Bot ========= Copyright (C) 2019-2020 Joachim Stolberg GPL v3 See LICENSE.txt for more information Signs Bot: Command interpreter ]]-- -- Load support for intllib. local MP = minetest.get_modpath("signs_bot") local I,_ = dofile(MP.."/intllib.lua") local MAX_SIZE = 1000 -- max number of tokens local tCmdDef = {} local lCmdLookup = {} local tSymbolTbl = {} local CodeCache = {} local api = {} -- Possible command results api.BUSY = 1 -- execute the same command again api.DONE = 2 -- next command api.NEW = 3 -- switch to a new script, provided as second value api.ERROR = 4 -- stop execution with error, error message provided as second value api.EXIT = 5 -- stop execution ------------------------------------------------------------------------------- -- Compiler ------------------------------------------------------------------------------- local function trim(s) return (s:gsub("^%s*(.-)%s*$", "%1")) end local function get_line_tokens(script) local idx = 0 script = script or "" script = script:gsub("\r\n", "\n") script = script:gsub("\r", "\n") local lines = string.split(script, "\n", true) return function() while idx < #lines do idx = idx + 1 -- remove comments local line = string.split(lines[idx], "--", true, 1)[1] or "" -- remove blanks line = trim(line) if #line > 0 then -- split into tokens return idx, unpack(string.split(line, " ")) end end end end local function dbg_out(opcode, num_param, code, pc) if num_param == 0 then print(">>"..lCmdLookup[opcode][3]) elseif num_param == 1 then print(">>"..lCmdLookup[opcode][3].." "..code[pc+1]) elseif num_param == 2 then print(">>"..lCmdLookup[opcode][3].." "..code[pc+1].." "..code[pc+2]) else print(">>"..lCmdLookup[opcode][3].." "..code[pc+1].." "..code[pc+2].." "..code[pc+3]) end end local function tokenizer(script) local tokens = {} for _, cmnd, param1, param2, param3 in get_line_tokens(script) do if tCmdDef[cmnd] then local num_param = tCmdDef[cmnd].num_param tokens[#tokens + 1] = cmnd if num_param >= 1 then tokens[#tokens + 1] = param1 or "nil" end if num_param >= 2 then tokens[#tokens + 1] = param2 or "nil" end if num_param >= 3 then tokens[#tokens + 1] = param3 or "nil" end elseif cmnd:find("%w+:") then tokens[#tokens + 1] = cmnd end end tokens[#tokens + 1] = "exit" return tokens end local function pass1(tokens) local pc = 1 tSymbolTbl = {} for _, token in ipairs(tokens) do if token:find("%w+:") then tSymbolTbl[token] = pc else pc = pc + 1 end end end local function pass2(tokens) local code = {} local num_param = 0 for _, token in ipairs(tokens) do if num_param > 0 then code[#code + 1] = tonumber(token) or tSymbolTbl[token..":"] or token num_param = num_param - 1 elseif tCmdDef[token] then num_param = tCmdDef[token].num_param code[#code + 1] = tCmdDef[token].opcode end end return code end local function compile(script) local tokens = tokenizer(script) pass1(tokens) return pass2(tokens) end ------------------------------------------------------------------------------- -- Commands ------------------------------------------------------------------------------- local function register_command(cmnd_name, num_param, cmnd_func, check_func) assert(num_param, cmnd_name..": num_param = "..dump(num_param)) assert(cmnd_func, cmnd_name..": cmnd_func = "..dump(cmnd_func)) assert(check_func or num_param == 0, cmnd_name..": check_func = "..dump(check_func)) lCmdLookup[#lCmdLookup + 1] = {num_param, cmnd_func, cmnd_name} tCmdDef[cmnd_name] = { num_param = num_param, cmnd = cmnd_func, name = cmnd_name, check = check_func, opcode = #lCmdLookup, } end register_command("repeat", 1, function(base_pos, mem, cnt) mem.Stack[#mem.Stack + 1] = cnt mem.Stack[#mem.Stack + 1] = mem.pc + 1 return api.DONE end, function(cnt) return cnt and cnt > 0 and cnt < 1000 end ) register_command("end", 0, function(base_pos, mem) if #mem.Stack < 2 then return api.ERROR end mem.Stack[#mem.Stack - 1] = mem.Stack[#mem.Stack - 1] - 1 if mem.Stack[#mem.Stack - 1] > 0 then mem.pc = mem.Stack[#mem.Stack] else mem.Stack[#mem.Stack] = nil mem.Stack[#mem.Stack] = nil end return api.DONE end ) register_command("call", 1, function(base_pos, mem, addr) if #mem.Stack > 99 then return api.ERROR end mem.Stack[#mem.Stack + 1] = mem.pc + 2 mem.pc = addr - 2 return api.DONE end, function(addr) return tSymbolTbl[addr..":"] end ) register_command("return", 0, function(base_pos, mem) if #mem.Stack < 1 then return api.ERROR end mem.pc = (mem.Stack[#mem.Stack] or 1) - 1 mem.Stack[#mem.Stack] = nil return api.DONE end ) register_command("jump", 1, function(base_pos, mem, addr) mem.pc = addr - 2 return api.DONE end, function(addr) return tSymbolTbl[addr..":"] end ) register_command("exit", 0, function(base_pos, mem) return api.EXIT end ) ------------------------------------------------------------------------------- -- API functions ------------------------------------------------------------------------------- function api.register_command(cmnd_name, num_param, cmnd_func, check_func) register_command(cmnd_name, num_param, cmnd_func, check_func) end -- function returns: true/false, error_string, line-num function api.check_script(script) local tbl = {} local num_token = 0 -- to fill the symbol table local tokens = tokenizer(script) pass1(tokens) for idx, cmnd, param1, param2, param3 in get_line_tokens(script) do if tCmdDef[cmnd] then num_token = num_token + 1 + tCmdDef[cmnd].num_param if num_token > MAX_SIZE then return false, I("Maximum programm size exceeded"), idx end param1 = tonumber(param1) or param1 param2 = tonumber(param2) or param2 param3 = tonumber(param3) or param3 local num_param = (param1 and 1 or 0) + (param2 and 1 or 0) + (param3 and 1 or 0) if tCmdDef[cmnd].num_param < num_param then return false, I("Too many parameters"), idx end if tCmdDef[cmnd].num_param > 0 and not tCmdDef[cmnd].check(param1, param2, param3) then return false, I("Parameter error"), idx end elseif not cmnd:find("%w+:") then return false, I("Command error"), idx end tbl[cmnd] = (tbl[cmnd] or 0) + 1 end if (tbl["end"] or 0) > (tbl["repeat"] or 0) then return false, I("'repeat' missing"), 0 elseif (tbl["end"] or 0) < (tbl["repeat"] or 0) then return false, I("'end' missing"), 0 end return true, I("Checked and approved"), 0 end -- function returns: true/false, error-string -- default_cmnd is used for the 'cond_move' function api.run_script(base_pos, mem) local hash = minetest.hash_node_position(base_pos) CodeCache[hash] = CodeCache[hash] or compile(mem.script) local code = CodeCache[hash] mem.pc = mem.pc or 1 mem.Stack = mem.Stack or {} local opcode = code[mem.pc] if opcode then local num_param, func = unpack(lCmdLookup[opcode]) --dbg_out(opcode, num_param, code, mem.pc) local res, err = func(base_pos, mem, code[mem.pc+1], code[mem.pc+2], code[mem.pc+3]) if res == api.DONE then mem.pc = mem.pc + 1 + num_param elseif res == api.NEW then CodeCache[hash] = compile(mem.script) mem.pc = 1 mem.Stack = {} end return res, err end return api.EXIT end function api.reset_script(base_pos, mem) local hash = minetest.hash_node_position(base_pos) CodeCache[hash] = nil mem.pc = 1 mem.Stack = {} end return api