Skip to content

Instantly share code, notes, and snippets.

@nulta
Created August 16, 2023 17:19
Show Gist options
  • Select an option

  • Save nulta/b9c3270e26453d1f1502ed3685460c12 to your computer and use it in GitHub Desktop.

Select an option

Save nulta/b9c3270e26453d1f1502ed3685460c12 to your computer and use it in GitHub Desktop.
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