Created
August 16, 2023 17:19
-
-
Save nulta/b9c3270e26453d1f1502ed3685460c12 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| MAX_TICK_TIME = 1 | |
| MAX_STACK_DEPTH = 100 | |
| DO_RUNTIME_TYPE_CHECK = true | |
| Sodacore = {} | |
| Sodacore.Commands = {} | |
| ---@alias DataType | |
| ---| "string" | |
| ---| "number" | |
| ---| "boolean" | |
| ---@alias VariableData {type: DataType, value: any, name: string} | |
| ---@alias LiteralData number|string|boolean|nil | |
| ---@alias SubActionData { [1]: string, [2]: any, [3]: any, [4]: any } | LiteralData | |
| ---@alias ActionData { indent: number, [1]: string, [2]: SubActionData?, [3]: SubActionData?, [4]: SubActionData? } | |
| ---@alias TypeName "number"|"string"|"boolean"|"any" | |
| ---@alias CommandParams { [integer]: TypeName } | |
| ---@alias CommandData { id: string, params: CommandParams, returnType: TypeName|nil, run: fun(self: Interpreter, ...: unknown): any } | |
| ---@class Script | |
| ---@field id string | |
| ---@field name string | |
| ---@field runOn string | |
| ---@field variables table<integer, VariableData> | |
| ---@field action ActionData[] | |
| local Script = {} | |
| ---@param data Script? | |
| function Script.new(data) | |
| local script = setmetatable({}, { __index = Script }) | |
| data = data or {} | |
| script.id = data.id or ("script_" .. math.random(10000, 99999)) | |
| script.name = data.name or "Untitled Script" | |
| script.runOn = data.runOn or "" | |
| script.variables = data.variables or {} | |
| script.action = data.action or {} | |
| return script | |
| end | |
| ---@class Interpreter | |
| ---@field id string | |
| ---@field script Script | |
| ---@field variables table<integer, any> | |
| ---@field currentLine integer | |
| ---@field currentIndent integer | |
| ---@field tickTimeStart number | |
| ---@field private co thread | |
| ---@field stopped boolean | |
| local Interpreter = {} | |
| Sodacore.Interpreter = Interpreter | |
| --- Initialize the new Interpreter. | |
| ---@param script Script | |
| ---@return Interpreter | |
| function Interpreter.new(script) | |
| local interpreter = setmetatable({}, { __index = Interpreter, __tostring = Interpreter.__tostring }) | |
| assert(script, "Interpreter requires script") | |
| interpreter.id = script.id .. " #" .. math.random(100000, 999999) | |
| interpreter.script = script | |
| interpreter.variables = {} | |
| interpreter:reset() | |
| return interpreter | |
| end | |
| --- Reset the current Interpreter to initial state. | |
| ---@private | |
| function Interpreter:reset() | |
| -- Reset variables | |
| for k, v in pairs(self.script.variables) do | |
| self.variables[k] = v.value | |
| end | |
| -- Reset status | |
| self.currentIndent = 0 | |
| self.currentLine = 1 | |
| self.tickTimeStart = math.huge | |
| self.stopped = false | |
| -- Reinitialize coroutine | |
| self.co = coroutine.create(function() | |
| while not self.stopped do | |
| self:evalLine() | |
| end | |
| end) | |
| end | |
| function Interpreter:__tostring() | |
| return "[INT:" .. self.id .. "]" | |
| end | |
| --- Halt the interpreter if too much ticktime had been spent. | |
| ---@private | |
| function Interpreter:checkTickTimeLimit() | |
| local currentTime = os.clock() | |
| local spentTime = currentTime - self.tickTimeStart | |
| if spentTime >= MAX_TICK_TIME then | |
| self:error("Spent too much time on a tick!") | |
| end | |
| end | |
| --- Halt the interpreter if the stack depth is too deep. | |
| ---@private | |
| function Interpreter:checkStackOverflow(depth) | |
| if depth > MAX_STACK_DEPTH then | |
| self:error("Stack overflow!") | |
| end | |
| end | |
| ---@private | |
| ---@param action SubActionData | |
| ---@param stackDepth integer | |
| ---@return LiteralData | |
| function Interpreter:evalAction(action, stackDepth) | |
| self:checkTickTimeLimit() | |
| -- typeof action is Literal? | |
| if type(action) ~= "table" then | |
| return action | |
| end | |
| -- typeof action is Action | |
| local command = action[1] | |
| local commandData = Sodacore.Commands[command] | |
| local evaluatedParams = {} | |
| if not commandData then | |
| self:error("Command " .. command .. " not found") | |
| return | |
| end | |
| for i=2, #action do | |
| evaluatedParams[i-1] = self:evalAction(action[i], stackDepth + 1) | |
| end | |
| return commandData.run(self, unpack(evaluatedParams)) | |
| end | |
| --- Evaulate the current line and step by 1 line. | |
| ---@private | |
| function Interpreter:evalLine() | |
| self:evalAction(self:peek(), 0) | |
| self:step() | |
| end | |
| --- Return the current line's action. | |
| ---@return ActionData | |
| function Interpreter:peek() | |
| return self.script.action[self.currentLine] | |
| end | |
| --- Return the next line's action. | |
| ---@return ActionData? | |
| function Interpreter:peekNext() | |
| return self.script.action[self.currentLine + 1] | |
| end | |
| --- Return the next line's action id. | |
| ---@return string? | |
| function Interpreter:peekNextId() | |
| return (self:peekNext() or {})[1] | |
| end | |
| --- Step by 1 line. | |
| function Interpreter:step() | |
| self.currentLine = self.currentLine + 1 | |
| if self.currentLine > #self.script.action then | |
| self:halt() | |
| end | |
| end | |
| --- Step until the next line's action id exists in params. | |
| ---@param indent number | |
| ---@param ... string | |
| function Interpreter:stepUntil(indent, ...) | |
| local targets = {} | |
| for _,v in ipairs({...}) do | |
| targets[v] = true | |
| end | |
| while self:peekNext() do | |
| local nextAction = self:peekNext() ---@cast nextAction ActionData | |
| local found = targets[nextAction[1]] | |
| local indentMatch = (nextAction.indent == indent) | |
| if found and indentMatch then | |
| break | |
| end | |
| self:step() | |
| end | |
| end | |
| function Interpreter:increaseIndent() | |
| self.currentIndent = self.currentIndent + 1 | |
| end | |
| function Interpreter:decreaseIndent() | |
| self.currentIndent = self.currentIndent - 1 | |
| end | |
| ---@param t TypeName | |
| local function typecheck(t, x) | |
| if t == "any" then | |
| return true | |
| end | |
| return type(x) == t | |
| end | |
| --- Set the interpreter variable | |
| ---@param varId integer | |
| ---@param data any | |
| function Interpreter:setVariable(varId, data) | |
| if DO_RUNTIME_TYPE_CHECK then | |
| local var = self.script.variables[varId] | |
| if not var then | |
| return self:error("Variable ID is out of range: " .. varId) | |
| elseif not typecheck(var.type, data) then | |
| return self:error("Variable type mismatch (#" .. varId .. " <- " .. tostring(data) .. ")") | |
| end | |
| end | |
| self.variables[varId] = data | |
| end | |
| --- Get the interpreter variable | |
| ---@param varId integer | |
| ---@return any | |
| function Interpreter:getVariable(varId) | |
| if DO_RUNTIME_TYPE_CHECK then | |
| local var = self.script.variables[varId] | |
| if not var then | |
| return self:error("Variable ID is out of range: " .. varId) | |
| end | |
| end | |
| return self.variables[varId] | |
| end | |
| --- Set the event callback parameters. Event parameters are negative indexed variables. | |
| --- This should be called before executeTick(). | |
| ---@param ... any | |
| function Interpreter:pushEventParams(...) | |
| for i, value in ipairs({...}) do | |
| self:setVariable(-i, value) | |
| end | |
| end | |
| --- Execute by 1 tick. Powered by coroutine. Should be regularly run. | |
| ---@return boolean | |
| function Interpreter:executeTick() | |
| if self.stopped then | |
| return false | |
| end | |
| self.tickTimeStart = os.clock() | |
| local ok, msg = coroutine.resume(self.co) | |
| self.tickTimeStart = math.huge | |
| if not ok then | |
| self:error( | |
| "Coroutine dead - " .. tostring(msg) .. "\n" .. debug.traceback(self.co) | |
| ) | |
| end | |
| return ok | |
| end | |
| --- Print the error and halt. | |
| ---@param message string | |
| ---@async | |
| function Interpreter:error(message) | |
| print(tostring(self), " [ERROR] ", message) | |
| print(" at ln " .. self.currentLine) | |
| self:halt() | |
| end | |
| --- Halt the interpreter. | |
| ---@async | |
| function Interpreter:halt() | |
| self.stopped = true | |
| if coroutine.running() then | |
| coroutine.yield("stop") | |
| end | |
| end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment