--- Contains all state relative to an Anselme interpreter. Each State is fully independant from each other. -- Each State can run a single script at a time, and variable changes are isolated between each State (see [branching](#branching-and-merging)). local class = require("class") local ScopeStack = require("state.ScopeStack") local tag_manager = require("state.tag_manager") local event_manager = require("state.event_manager") local resumable_manager = require("state.resumable_manager") local uuid = require("common").uuid local parser = require("parser") local binser = require("lib.binser") local anselme local State State = class { type = "anselme state", init = function(self, branch_from) -- create a new branch from an existing state -- note: the existing state must not currently have an active script if branch_from then self.branch_id = uuid() self.source_branch_id = branch_from.branch_id self.scope = ScopeStack:new(self, branch_from) event_manager:reset(self) -- events are isolated per branch resumable_manager:reset(self) -- resumable stack is isolated per branch -- create new empty state else self.scope = ScopeStack:new(self) event_manager:setup(self) tag_manager:setup(self) resumable_manager:setup(self) end end, --- Load standard library. -- You will probably want to call this on every State right after creation. load_stdlib = function(self) require("stdlib")(self) end, ---## Branching and merging --- Name of the branch associated to this State. branch_id = "main", --- Name of the branch this State was branched from. source_branch_id = "main", --- Return a new branch of this State. -- -- Branches act as indepent copies of this State where any change will not be reflected in the source State until it is merged back into the source branch. -- Note: probably makes the most sense to create branches from the main State only. branch = function(self) assert(not self:active(), "can't branch while a script is active") return State:new(self) end, --- Merge everything that was changed in this branch back into the main State branch. -- -- Recommendation: only merge if you know that the state of the variables is consistent, for example at the end of the script, checkpoints, ... -- If your script errored or was interrupted at an unknown point in the script, you might be in the middle of a calculation and variables won't contain -- values you want to merge. merge = function(self) self.scope:merge() end, ---## Variable definition scope = nil, -- ScopeStack associated with the State. Contains *all* scopes related to this State. --- Define a value in the global scope, converting it from Lua to Anselme if needed. -- -- * for lua functions: `define("name", "(x, y, z=5)", function(x, y, z) ... end)`, where arguments and return values of the function are automatically converted between anselme and lua values -- * for other lua values: `define("name", value)` -- * for anselme AST: `define("name", value)` -- -- `name` can be prefixed with symbol modifiers, for example ":name" for a constant variable. -- -- If `raw_mode` is true, no anselme-to/from-lua conversion will be performed in the function. -- The function will receive the state followed by AST nodes as arguments, and is expected to return an AST node. define = function(self, name, value, func, raw_mode) self.scope:push_global() self:define_local(name, value, func, raw_mode) self.scope:pop() end, --- Same as `:define`, but define the expression in the current scope. define_local = function(self, name, value, func, raw_mode) self.scope:define_lua(name, value, func, raw_mode) end, --- For anything more advanced, you can directly access the current scope stack stored in `state.scope`. -- See [state/ScopeStack.lua](../state/ScopeStack.lua) for details; the documentation is not as polished as this file but you should still be able to find your way around. ---## Saving and loading persistent variables --- Return a serialized (string) representation of all global persistent variables in this State. -- -- This can be loaded back later using `:load`. save = function(self) local list = self.scope:list_persistent_global() return binser.serialize(anselme.versions.save, list) end, --- Load a string generated by `:save`. -- -- Variables that do not exist currently in the global scope will be defined, those that do will be overwritten with the loaded data. load = function(self, save) local version, list = binser.deserializeN(save, 2) if version ~= anselme.versions.save then print("Loading a save file generated by a different Anselme version, things may break!") end self.scope:push_global() for sym, val in pairs(list) do if self.scope:defined_in_current(sym) then self.scope:set(sym:to_identifier(), val) else self.scope:define(sym, val) end end self.scope:pop() end, ---## Current script state -- Currently active script _coroutine = nil, --- Indicate if a script is currently loaded in this branch. active = function(self) return not not self._coroutine end, --- Returns `"running`" if a script is currently loaded and running (i.e. this was called from the script). -- -- Returns `"active"` if a script is loaded but not currently running (i.e. the script has not started or is waiting on an event). -- -- Returns `"inactive"` if no script is loaded. state = function(self) if self:active() then return coroutine.status(self._coroutine) == "running" and "running" or "active" else return "inactive" end end, --- Load a script in this branch. It will become the active script. -- -- `code` is the code string or AST to run, `source` is the source name string to show in errors (optional). -- -- Note that this will only load the script; execution will only start by using the `:step` method. Will error if a script is already active in this State. run = function(self, code, source) assert(not self:active(), "a script is already active") self._coroutine = coroutine.create(function() local r = assert(self:eval_local(code, source)) event_manager:final_flush(self) return "return", r end) end, --- When a script is active, will resume running it until the next event. -- -- Will error if no script is active. -- -- Returns `event type string, event data`. step = function(self) assert(self:active(), "trying to step but no script is currently active") local success, type, data = coroutine.resume(self._coroutine) if not success then self.scope:reset() type, data = "error", type end if coroutine.status(self._coroutine) == "dead" then self._coroutine = nil end return type, data end, --- Stops the currently active script. -- -- Will error if no script is active. -- -- If `code` is given, the script will not be disabled but instead will be immediately replaced with this new script. -- The new script will then be started on the next `:step` and will preserve the current scope. This can be used to trigger an exit function or similar in the active script. interrupt = function(self, code, source) assert(self:active(), "trying to interrupt but no script is currently active") if code then self._coroutine = coroutine.create(function() local r = assert(self:eval_local(code, source)) event_manager:final_flush(self) self.scope:reset() -- scope stack is probably messed up after the switch return "return", r end) else self.scope:reset() self._coroutine = nil end end, --- Evaluate an expression in the global scope. -- -- This can be called from outside a running script, but an error will be triggered the expression raise any event other than return. -- -- * returns AST in case of success. Run `:to_lua(state)` on it to convert to a Lua value. -- * returns `nil, error message` in case of error. eval = function(self, code, source) self.scope:push_global() local r, e = self:eval_local(code, source) self.scope:pop() return r, e end, --- Same as `:eval`, but evaluate the expression in the current scope. eval_local = function(self, code, source) if type(code) == "string" then code = parser(code, source) end local stack_size = self.scope:size() local s, e = pcall(code.eval, code, self) if not s then self.scope:reset(stack_size) return nil, e else return e end end, --- If you want to perform more advanced manipulation of the resulting AST nodes, look at the `ast` modules. -- In particular, every Node inherits the methods from [ast.abstract.Node](../ast/abstract/Node.lua). -- Otherwise, each Node has its own module file defined in the [ast/](../ast) directory. __tostring = function(self) return ("anselme state, branch %s, %s"):format(self.branch_id, self:state()) end } package.loaded[...] = State anselme = require("anselme") return State