mirror of
https://github.com/Reuh/anselme.git
synced 2025-10-28 00:59:31 +00:00
Anselme v2.0.0-alpha rewrite
Woke up and felt like changing a couple things. It's actually been worked on for a while, little at a time... The goal was to make the language and implementation much simpler. Well I don't know if it really ended up being simpler but it sure is more robust. Main changes: * proper first class functions and closures supports! proper scoping rules! no more namespace shenanigans! * everything is an expression, no more statements! make the implementation both simpler and more complex, but it's much more consistent now! the syntax has massively changed as a result though. * much more organized and easy to modify codebase: one file for each AST node, no more random fields or behavior set by some random node exceptionally, everything should now follow the same API defined in ast.abstract.Node Every foundational feature should be implemented right now. The vast majority of things that were possible in v2 are possible now; some things aren't, but that's usually because v2 is a bit more sane. The main missing things before a proper release are tests and documentation. There's a few other things that might be implemented later, see the ideas.md file.
This commit is contained in:
parent
2ff494d108
commit
fe351b5ca4
484 changed files with 7099 additions and 18084 deletions
846
anselme.lua
846
anselme.lua
|
|
@ -1,794 +1,80 @@
|
|||
--- anselme main module
|
||||
--- The main module.
|
||||
|
||||
--- Anselme Lua API reference
|
||||
--
|
||||
-- We actively support LuaJIT and Lua 5.4. Lua 5.1, 5.2 and 5.3 *should* work but I don't always test against them.
|
||||
--
|
||||
-- This documentation is generated from the main module file `anselme.lua` using `ldoc --ext md anselme.lua`.
|
||||
--
|
||||
-- Example usage:
|
||||
-- Naming conventions:
|
||||
-- * Classes
|
||||
-- * everything_else
|
||||
-- * (note: "classes" that are not meat to be instancied and are just here to benefit from inheritance fall into everything_else, e.g. parsing classes)
|
||||
|
||||
--- Usage:
|
||||
-- ```lua
|
||||
-- local anselme = require("anselme") -- load main module
|
||||
-- local anselme = require("anselme")
|
||||
--
|
||||
-- local vm = anselme() -- create new VM
|
||||
-- vm:loadgame("game") -- load some scripts, etc.
|
||||
-- local interpreter = vm:rungame() -- create a new interpreter using what was loaded with :loadgame
|
||||
-- -- create a new state
|
||||
-- local state = anselme.new()
|
||||
-- state:load_stdlib()
|
||||
--
|
||||
-- -- simple function to convert text event data into a string
|
||||
-- -- in your game you may want to handle tags, here we ignore them for simplicity
|
||||
-- local function format_text(text)
|
||||
-- local r = ""
|
||||
-- for _, l in ipairs(t) do
|
||||
-- r = r .. l.text
|
||||
-- end
|
||||
-- return r
|
||||
-- -- read an anselme script file
|
||||
-- local f = assert(io.open("script.ans"))
|
||||
-- local script = anselme.parse(f:read("*a"), "script.ans")
|
||||
-- f:close()
|
||||
--
|
||||
-- -- load the script in a new branch
|
||||
-- local run_state = state:branch()
|
||||
-- run_state:run(script)
|
||||
--
|
||||
-- -- run the script
|
||||
-- while run_state:active() do
|
||||
-- local e, data = run_state:step()
|
||||
-- if e == "text" then
|
||||
-- for _, l in ipairs(data) do
|
||||
-- print(l:format(run_state))
|
||||
-- end
|
||||
-- elseif e == "choice" then
|
||||
-- for i, l in ipairs(data) do
|
||||
-- print(("%s> %s"):format(i, l:format(run_state)))
|
||||
-- end
|
||||
-- local choice = tonumber(io.read("*l"))
|
||||
-- data:choose(choice)
|
||||
-- elseif e == "return" then
|
||||
-- run_state:merge()
|
||||
-- elseif e == "error" then
|
||||
-- error(data)
|
||||
-- end
|
||||
-- end
|
||||
--
|
||||
-- -- event loop
|
||||
-- repeat
|
||||
-- local event, data = interpreter:step() -- progress script until next event
|
||||
-- if event == "text" then
|
||||
-- print(format_text(d))
|
||||
-- elseif event == "choice" then
|
||||
-- for j, choice in ipairs(d) do
|
||||
-- print(j.."> "..format_text(choice))
|
||||
-- end
|
||||
-- interpreter:choose(io.read())
|
||||
-- elseif event == "error" then
|
||||
-- error(data)
|
||||
-- end
|
||||
-- until t == "return" or t == "error"
|
||||
-- ```
|
||||
--
|
||||
-- Calling the Anselme main module will create a return a new [VM](#vms).
|
||||
--
|
||||
-- The main module also contain a few fields:
|
||||
--
|
||||
-- @type anselme
|
||||
local anselme = {
|
||||
--- Anselme version information table.
|
||||
--
|
||||
-- Contains version informations as number (higher means more recent) of Anselme divied in a few categories:
|
||||
--
|
||||
-- * `save`, which is incremented at each update which may break save compatibility
|
||||
-- * `language`, which is incremented at each update which may break script file compatibility
|
||||
-- * `api`, which is incremented at each update which may break Lua API compatibility
|
||||
|
||||
local parser = require("parser")
|
||||
local State = require("state.State")
|
||||
require("ast.abstract.Node"):_i_hate_cycles()
|
||||
|
||||
return {
|
||||
--- Global version string. Follow semver.
|
||||
version = "2.0.0-alpha",
|
||||
|
||||
--- Table containing per-category version numbers. Incremented by one for any change that may break compatibility.
|
||||
versions = {
|
||||
save = 2,
|
||||
language = 25,
|
||||
api = 6
|
||||
--- Version number for languages and standard library changes.
|
||||
language = 27,
|
||||
--- Version number for save/AST format changes.
|
||||
save = 4,
|
||||
--- Version number for Lua API changes.
|
||||
api = 8
|
||||
},
|
||||
--- General version number.
|
||||
--
|
||||
-- It is incremented at each update.
|
||||
version = 26,
|
||||
--- Currently running [interpreter](#interpreters).
|
||||
-- `nil` if no interpreter running.
|
||||
running = nil
|
||||
}
|
||||
package.loaded[...] = anselme
|
||||
|
||||
-- load libs
|
||||
local anselme_root = (...):gsub("anselme$", "")
|
||||
local preparse = require(anselme_root.."parser.preparser")
|
||||
local postparse = require(anselme_root.."parser.postparser")
|
||||
local expression = require(anselme_root.."parser.expression")
|
||||
local eval = require(anselme_root.."interpreter.expression")
|
||||
local injections = require(anselme_root.."parser.common").injections
|
||||
local run_line = require(anselme_root.."interpreter.interpreter").run_line
|
||||
local run = require(anselme_root.."interpreter.interpreter").run
|
||||
local to_lua = require(anselme_root.."interpreter.common").to_lua
|
||||
local merge_state = require(anselme_root.."interpreter.common").merge_state
|
||||
local stdfuncs = require(anselme_root.."stdlib.functions")
|
||||
local bootscript = require(anselme_root.."stdlib.bootscript")
|
||||
local copy = require(anselme_root.."common").copy
|
||||
local should_be_persisted = require(anselme_root.."interpreter.common").should_be_persisted
|
||||
local check_persistable = require(anselme_root.."interpreter.common").check_persistable
|
||||
|
||||
-- wrappers for love.filesystem / luafilesystem
|
||||
local function list_directory(path)
|
||||
local t = {}
|
||||
if love then
|
||||
t = love.filesystem.getDirectoryItems(path)
|
||||
else
|
||||
local lfs = require("lfs")
|
||||
for item in lfs.dir(path) do
|
||||
table.insert(t, item)
|
||||
end
|
||||
end
|
||||
return t
|
||||
end
|
||||
local function is_directory(path)
|
||||
if love then
|
||||
return not not love.filesystem.getInfo(path, "directory")
|
||||
else
|
||||
local lfs = require("lfs")
|
||||
return lfs.attributes(path, "mode") == "directory"
|
||||
end
|
||||
end
|
||||
local function is_file(path)
|
||||
if love then
|
||||
return not not love.filesystem.getInfo(path, "file")
|
||||
else
|
||||
local lfs = require("lfs")
|
||||
return lfs.attributes(path, "mode") == "file"
|
||||
end
|
||||
end
|
||||
|
||||
--- Interpreters
|
||||
--
|
||||
-- An interpreter is in charge of running Anselme code and is spawned from a [VM](#vms).
|
||||
-- Several interpreters from the same VM can run at the same time.
|
||||
--
|
||||
-- Typically, you would have a interpreter for each script that need at the same time, for example one for every NPC
|
||||
-- that is currently talking.
|
||||
--
|
||||
-- Each interpreter can only run one script at a time, and will run it sequentially.
|
||||
-- You can advance in the script by calling the `:step` method, which will run the script until an event is sent (for example some text needs to be displayed),
|
||||
-- which will pause the whole interpreter until `:step` is called again.
|
||||
--
|
||||
-- @type interpreter
|
||||
local interpreter_methods = {
|
||||
--- interpreter state
|
||||
-- for internal use, you shouldn't touch this
|
||||
-- @local
|
||||
state = nil,
|
||||
--- [VM](#vms) this interpreter belongs to.
|
||||
vm = nil,
|
||||
--- String, type of the event that stopped the interpreter (`nil` if interpreter is still running).
|
||||
end_event = nil,
|
||||
|
||||
--- Run the interpreter until the next event.
|
||||
-- Returns event type (string), data (any).
|
||||
--- Parse a `code` string and return the generated AST.
|
||||
--
|
||||
-- Will merge changed variables on successful script end.
|
||||
-- `source` is an optional string; it will be used as the code source name in error messages.
|
||||
--
|
||||
-- If event is `"return"` or `"error"`, the interpreter can not be stepped further and should be discarded.
|
||||
--
|
||||
-- Default event types and their associated data:
|
||||
-- * `text`: text to display, data is a list of text elements, each with a `text` field, containing the text contents, and a `tags` field, containing the tags associated with this text
|
||||
-- * `choice`: choices to choose from, data is a list of choices Each of these choice is a list of text elements like for the `text` event
|
||||
-- * `return`: when the script ends, data is the returned value (`nil` if nothing returned)
|
||||
-- * `error`: when there is an error, data is the error message.
|
||||
--
|
||||
-- See [LANGUAGE.md](LANGUAGE.md) for more details on events.
|
||||
step = function(self)
|
||||
-- check status
|
||||
if self.end_event then
|
||||
return "error", ("interpreter can't be restarted after receiving a %s event"):format(self.end_event)
|
||||
end
|
||||
if coroutine.status(self.state.interpreter.coroutine) ~= "suspended" then
|
||||
return "error", ("can't step interpreter because it has already finished or is already running (coroutine status: %s)"):format(coroutine.status(self.state.interpreter.coroutine))
|
||||
end
|
||||
-- handle interrupt
|
||||
if self.state.interpreter.interrupt then
|
||||
local expr = self.state.interpreter.interrupt
|
||||
if expr == true then
|
||||
return "return", "" -- nothing to do after interrupt
|
||||
else
|
||||
local line = self.state.interpreter.running_line
|
||||
local namespace = self:current_namespace()
|
||||
-- replace state with interrupted state
|
||||
local exp, err = expression(expr, self.state.interpreter.global_state, namespace or "", "interpreter:interrupt")
|
||||
if not exp then return "error", ("%s; during interrupt %q at %s"):format(err, expr, line and line.source or "unknown") end
|
||||
local r, e = self.vm:run(exp)
|
||||
if not r then return "error", e end
|
||||
self.state = r.state
|
||||
end
|
||||
end
|
||||
-- run
|
||||
local previous = anselme.running
|
||||
anselme.running = self
|
||||
local success, event, data = coroutine.resume(self.state.interpreter.coroutine)
|
||||
anselme.running = previous
|
||||
if not success then event, data = "error", event end
|
||||
if event == "return" then merge_state(self.state) end
|
||||
if event == "return" or event == "error" then self.end_event = event end
|
||||
return event, data
|
||||
-- Usage:
|
||||
-- ```lua
|
||||
-- local ast = anselme.parse("1 + 2", "test")
|
||||
-- ast:eval()
|
||||
-- ```
|
||||
parse = function(code, source)
|
||||
return parser(code, source)
|
||||
end,
|
||||
|
||||
--- Select a choice.
|
||||
-- `i` is the index (number) of the choice in the choice list (from the choice event's data).
|
||||
--
|
||||
-- The choice will be selected on the next interpreter step.
|
||||
--
|
||||
-- Returns this interpreter.
|
||||
choose = function(self, i)
|
||||
self.state.interpreter.choice_selected = tonumber(i)
|
||||
return self
|
||||
end,
|
||||
|
||||
--- Interrupt (abort the currently running script) the interpreter on the next step, executing an expression (string, if specified) in the current namespace instead.
|
||||
--
|
||||
-- Returns this interpreter.
|
||||
interrupt = function(self, expr)
|
||||
self.state.interpreter.interrupt = expr or true
|
||||
return self
|
||||
end,
|
||||
|
||||
--- Returns the namespace (string) the last ran line belongs to.
|
||||
current_namespace = function(self)
|
||||
local line = self.state.interpreter.running_line
|
||||
local namespace = ""
|
||||
if line then
|
||||
local cur_line = line
|
||||
namespace = cur_line.namespace
|
||||
while not namespace do
|
||||
local block = cur_line.parent_block
|
||||
if not block.parent_line then break end -- reached root
|
||||
cur_line = block.parent_line
|
||||
namespace = cur_line.namespace
|
||||
end
|
||||
end
|
||||
return namespace
|
||||
end,
|
||||
|
||||
--- Run an expression (string) or block, optionally in a specific namespace (string, will use root namespace if not specified).
|
||||
-- This may trigger events and must be called from within the interpreter coroutine (i.e. from a function called from a running script).
|
||||
--
|
||||
-- No automatic merge if this change the interpreter state, merge is done once we reach end of script in a call to `:step` as usual.
|
||||
--
|
||||
-- Returns the returned value (nil if nothing returned).
|
||||
run = function(self, expr, namespace)
|
||||
-- check status
|
||||
if coroutine.status(self.state.interpreter.coroutine) ~= "running" then
|
||||
error("run must be called from whithin the interpreter coroutine")
|
||||
end
|
||||
-- parse
|
||||
local err
|
||||
if type(expr) ~= "table" then expr, err = expression(tostring(expr), self.state.interpreter.global_state, namespace or "", "interpreter:run") end
|
||||
if not expr then coroutine.yield("error", err) end
|
||||
-- run
|
||||
local r, e
|
||||
if expr.type == "block" then
|
||||
r, e = run(self.state, expr)
|
||||
else
|
||||
r, e = eval(self.state, expr)
|
||||
end
|
||||
if not r then coroutine.yield("error", e) end
|
||||
if self.state.interpreter.current_event then -- flush final events
|
||||
local rf, re = run_line(self.state, { type = "flush events" })
|
||||
if re then coroutine.yield("error", re) end
|
||||
if rf then r = rf end
|
||||
end
|
||||
return to_lua(r, self.state)
|
||||
end,
|
||||
--- Evaluate an expression (string) or block, optionally in a specific namespace (string, will use root namespace if not specified).
|
||||
-- The expression can't yield events.
|
||||
-- Can be called from outside the interpreter coroutine. Will create a new coroutine that operate on this interpreter state.
|
||||
--
|
||||
-- No automatic merge if this change the interpreter state, merge is done once we reach end of script in a call to `:step` as usual.
|
||||
--
|
||||
-- Returns the returned value in case of success (nil if nothing returned).
|
||||
--
|
||||
-- Returns nil, error message in case of error.
|
||||
eval = function(self, expr, namespace)
|
||||
if self.end_event then
|
||||
return "error", ("interpreter can't be restarted after receiving a %s event"):format(self.end_event)
|
||||
end
|
||||
-- parse
|
||||
local err
|
||||
if type(expr) ~= "table" then expr, err = expression(tostring(expr), self.state.interpreter.global_state, namespace or "", "interpreter:eval") end
|
||||
if not expr then return nil, err end
|
||||
-- run
|
||||
local co = coroutine.create(function()
|
||||
local r, e
|
||||
if expr.type == "block" then
|
||||
r, e = run(self.state, expr)
|
||||
else
|
||||
r, e = eval(self.state, expr)
|
||||
end
|
||||
if not r then return "error", e end
|
||||
return "return", r
|
||||
end)
|
||||
local previous = anselme.running
|
||||
anselme.running = self
|
||||
local success, event, data = coroutine.resume(co)
|
||||
anselme.running = previous
|
||||
if not success then
|
||||
return nil, event
|
||||
elseif event == "error" then
|
||||
self.end_event = "error"
|
||||
return nil, data
|
||||
elseif event ~= "return" then
|
||||
return nil, ("evaluated expression generated an %q event; at %s"):format(event, self.state.interpreter.running_line.source)
|
||||
else
|
||||
return to_lua(data, self.state)
|
||||
end
|
||||
--- Return a new [State](#state).
|
||||
new = function()
|
||||
return State:new()
|
||||
end,
|
||||
}
|
||||
interpreter_methods.__index = interpreter_methods
|
||||
|
||||
--- VMs
|
||||
--
|
||||
-- A VM stores the state required to run Anselme scripts. Each VM is completely independant from each other.
|
||||
--
|
||||
-- @type vm
|
||||
local vm_mt = {
|
||||
--- anselme state
|
||||
-- for internal use, you shouldn't touch this
|
||||
-- @local
|
||||
state = nil,
|
||||
|
||||
--- loaded game state
|
||||
-- for internal use, you shouldn't touch this
|
||||
-- @local
|
||||
game = nil,
|
||||
|
||||
--- Wrapper for loading a whole set of scripts (a "game").
|
||||
-- Should be preferred to other loading functions if possible as this sets all the common options on its own.
|
||||
--
|
||||
-- Requires LÖVE or LuaFileSystem.
|
||||
--
|
||||
-- Will load from the directory given by `path` (string), in order:
|
||||
-- * `config.ans`, which will be executed in the "config" namespace and may contains various optional configuration options:
|
||||
-- * `anselme version`: number, version of the anselme language this game was made for
|
||||
-- * `game version`: any, version information of the game. Can be used to perform eventual migration of save with an old version in the main file.
|
||||
-- Always included in saved variables.
|
||||
-- * `language`: string, built-in language file to load
|
||||
-- * `inject directory`: string, directory that may contain "function start.ans", "checkpoint end.ans", etc. which content will be used to setup
|
||||
-- the custom code injection methods (see vm:setinjection)
|
||||
-- * `global directory`: string, path of global script directory. Every script file and subdirectory in the path will be loaded in the global namespace.
|
||||
-- * `start expression`: string, expression that will be ran when starting the game
|
||||
-- * files in the global directory, if defined in config.ans
|
||||
-- * every other file in the path and subdirectories, using their path as namespace (i.e., contents of path/world1/john.ans will be defined in a function world1.john)
|
||||
--
|
||||
-- Returns this VM in case of success.
|
||||
--
|
||||
-- Returns nil, error message in case of error.
|
||||
loadgame = function(self, path)
|
||||
if self.game then error("game already loaded") end
|
||||
-- load config
|
||||
if is_file(path.."/config.ans") then
|
||||
local s, e = self:loadfile(path.."/config.ans", "config")
|
||||
if not s then return s, e end
|
||||
s, e = self:eval("config")
|
||||
if e then return s, e end
|
||||
end
|
||||
-- get config
|
||||
self.game = {
|
||||
anselme_version = self:eval("config.anselme version"),
|
||||
game_version = self:eval("config.game version"),
|
||||
language = self:eval("config.language"),
|
||||
inject_directory = self:eval("config.inject directory"),
|
||||
global_directory = self:eval("config.global directory"),
|
||||
start_expression = self:eval("config.start expression")
|
||||
}
|
||||
-- check language version
|
||||
if self.game.anselme_version and self.game.anselme_version ~= anselme.versions.language then
|
||||
return nil, ("trying to load game made for Anselme language %s, but currently using version %s"):format(self.game.anselme_version, anselme.versions.language)
|
||||
end
|
||||
-- load language
|
||||
if self.game.language then
|
||||
local s, e = self:loadlanguage(self.game.language)
|
||||
if not s then return s, e end
|
||||
end
|
||||
-- load injections
|
||||
if self.game.inject_directory then
|
||||
for inject, ninject in pairs(injections) do
|
||||
local f = io.open(path.."/"..self.game.inject_directory.."/"..inject..".ans", "r")
|
||||
if f then
|
||||
self.state.inject[ninject] = f:read("*a")
|
||||
f:close()
|
||||
end
|
||||
end
|
||||
end
|
||||
-- load global scripts
|
||||
for _, item in ipairs(list_directory(path.."/"..self.game.global_directory)) do
|
||||
if item:match("[^%.]") then
|
||||
local p = path.."/"..self.game.global_directory.."/"..item
|
||||
local s, e
|
||||
if is_directory(p) then
|
||||
s, e = self:loaddirectory(p)
|
||||
elseif item:match("%.ans$") then
|
||||
s, e = self:loadfile(p)
|
||||
end
|
||||
if not s then return s, e end
|
||||
end
|
||||
end
|
||||
-- load other files
|
||||
for _, item in ipairs(list_directory(path)) do
|
||||
if item:match("[^%.]") and
|
||||
item ~= "config.ans" and
|
||||
item ~= self.game.global_directory and
|
||||
item ~= self.game.inject_directory
|
||||
then
|
||||
local p = path.."/"..item
|
||||
local s, e
|
||||
if is_directory(p) then
|
||||
s, e = self:loaddirectory(p, item)
|
||||
elseif item:match("%.ans$") then
|
||||
s, e = self:loadfile(p, item:gsub("%.ans$", ""))
|
||||
end
|
||||
if not s then return s, e end
|
||||
end
|
||||
end
|
||||
return self
|
||||
end,
|
||||
--- Return a interpreter which runs the game start expression (if given).
|
||||
--
|
||||
-- Returns interpreter in case of success.
|
||||
--
|
||||
-- Returns nil, error message in case of error.
|
||||
rungame = function(self)
|
||||
if not self.game then error("no game loaded") end
|
||||
if self.game.start_expression then
|
||||
return self:run(self.game.start_expression)
|
||||
else
|
||||
return self:run("()")
|
||||
end
|
||||
end,
|
||||
|
||||
--- Load code from a string.
|
||||
-- Similar to Lua's code loading functions.
|
||||
--
|
||||
-- Compared to their Lua equivalents, these also take an optional `name` argument (default="") that set the namespace to load the code in. Will define a new function is specified; otherwise, code will be parsed but not executable from an expression (as it is not named).
|
||||
--
|
||||
-- Returns parsed block in case of success.
|
||||
--
|
||||
-- Returns nil, error message in case of error.
|
||||
loadstring = function(self, str, name, source)
|
||||
local s, e = preparse(self.state, str, name or "", source)
|
||||
if not s then return s, e end
|
||||
return s
|
||||
end,
|
||||
--- Load code from a file.
|
||||
-- See `vm:loadstring`.
|
||||
loadfile = function(self, path, name)
|
||||
local content
|
||||
if love then
|
||||
local e
|
||||
content, e = love.filesystem.read(path)
|
||||
if not content then return content, e end
|
||||
else
|
||||
local f, e = io.open(path, "r")
|
||||
if not f then return f, e end
|
||||
content = f:read("*a")
|
||||
f:close()
|
||||
end
|
||||
local s, err = self:loadstring(content, name, path)
|
||||
if not s then return s, err end
|
||||
return s
|
||||
end,
|
||||
-- Load every file in a directory, using filename (without .ans extension) as its namespace.
|
||||
--
|
||||
-- Requires LÖVE or LuaFileSystem.
|
||||
--
|
||||
-- Returns this VM in case of success.
|
||||
--
|
||||
-- Returns nil, error message in case of error.
|
||||
loaddirectory = function(self, path, name)
|
||||
if not name then name = "" end
|
||||
name = name == "" and "" or name.."."
|
||||
for _, item in ipairs(list_directory(path)) do
|
||||
if item:match("[^%.]") then
|
||||
local p = path.."/"..item
|
||||
local s, e
|
||||
if is_directory(p) then
|
||||
s, e = self:loaddirectory(p, name..item)
|
||||
elseif item:match("%.ans$") then
|
||||
s, e = self:loadfile(p, name..item:gsub("%.ans$", ""))
|
||||
end
|
||||
if not s then return s, e end
|
||||
end
|
||||
end
|
||||
return self
|
||||
end,
|
||||
|
||||
--- Set aliases for built-in variables 👁️, 🔖 and 🏁 that will be defined on every new checkpoint and function.
|
||||
-- This does not affect variables that were defined before this function was called.
|
||||
-- Set to nil for no alias.
|
||||
--
|
||||
-- Returns this VM.
|
||||
setaliases = function(self, seen, checkpoint, reached)
|
||||
self.state.builtin_aliases["👁️"] = seen
|
||||
self.state.builtin_aliases["🔖"] = checkpoint
|
||||
self.state.builtin_aliases["🏁"] = reached
|
||||
return self
|
||||
end,
|
||||
--- Set some code that will be injected at specific places in all code loaded after this is called.
|
||||
-- Can typically be used to define variables for every function like 👁️, setting some value on every function resume, etc.
|
||||
--
|
||||
-- Possible inject types:
|
||||
-- * `"function start"`: injected at the start of every non-scoped function
|
||||
-- * `"function end"`: injected at the end of every non-scoped function
|
||||
-- * `"function return"`: injected at the end of each return's children that is contained in a non-scoped function
|
||||
-- * `"checkpoint start"`: injected at the start of every checkpoint
|
||||
-- * `"checkpoint end"`: injected at the end of every checkpoint
|
||||
-- * `"class start"`: injected at the start of every class
|
||||
-- * `"class end"`: injected at the end of every class
|
||||
-- * `"scoped function start"`: injected at the start of every scoped function
|
||||
-- * `"scoped function end"`: injected at the end of every scoped function
|
||||
-- * `"scoped function return"`: injected at the end of each return's children that is contained in a scoped function
|
||||
--
|
||||
-- Set `code` to nil to disable the inject.
|
||||
--
|
||||
-- Returns this VM.
|
||||
setinjection = function(self, inject, code)
|
||||
assert(injections[inject], ("unknown injection type %q"):format(inject))
|
||||
self.state.inject[injections[inject]] = code
|
||||
return self
|
||||
end,
|
||||
|
||||
--- Load and execute a built-in language file.
|
||||
--
|
||||
-- The language file may optionally contain the special variables:
|
||||
-- * alias 👁️: string, default alias for 👁️
|
||||
-- * alias 🏁: string, default alias for 🏁
|
||||
-- * alias 🔖: string, default alias for 🔖
|
||||
--
|
||||
-- Returns this VM in case of success.
|
||||
--
|
||||
-- Returns nil, error message in case of error.
|
||||
loadlanguage = function(self, lang)
|
||||
local namespace = "anselme.languages."..lang
|
||||
-- execute language file
|
||||
local code = require(anselme_root.."stdlib.languages."..lang)
|
||||
local s, e = self:loadstring(code, namespace, lang)
|
||||
if not s then return s, e end
|
||||
s, e = self:eval(namespace)
|
||||
if e then return s, e end
|
||||
-- set aliases for built-in variables
|
||||
local seen_alias = self:eval(namespace..".alias 👁️")
|
||||
local checkpoint_alias = self:eval(namespace..".alias 🔖")
|
||||
local reached_alias = self:eval(namespace..".alias 🏁")
|
||||
self:setaliases(seen_alias, checkpoint_alias, reached_alias)
|
||||
return self
|
||||
end,
|
||||
|
||||
--- Define functions from Lua.
|
||||
--
|
||||
-- * `signature`: string, full signature of the function
|
||||
-- * `fn`: function (Lua function or table, see examples in `stdlib/functions.lua`)
|
||||
--
|
||||
-- Alternatively, can also take a table as a sole argument to load several functions: { ["signature"] = fn, ... }
|
||||
--
|
||||
-- Returns this VM.
|
||||
loadfunction = function(self, signature, fn)
|
||||
if type(signature) == "table" then
|
||||
for k, v in pairs(signature) do
|
||||
local s, e = self:loadfunction(k, v)
|
||||
if not s then return nil, e end
|
||||
end
|
||||
else
|
||||
if type(fn) == "function" then fn = { value = fn } end
|
||||
self.state.link_next_function_definition_to_lua_function = fn
|
||||
local s, e = self:loadstring(":$"..signature, "", "lua")
|
||||
if not s then return nil, e end
|
||||
assert(self.state.link_next_function_definition_to_lua_function == nil, "unexpected error while defining lua function")
|
||||
return self
|
||||
end
|
||||
return self
|
||||
end,
|
||||
|
||||
--- Save/load script state
|
||||
--
|
||||
-- Only saves persistent variables' full names and values.
|
||||
-- Make sure to not change persistent variables names, class name, class attribute names, checkpoint names and functions names between a
|
||||
-- save and a load (alias can of course be changed), as Anselme will not be able to match them to the old names stored in the save file.
|
||||
--
|
||||
-- If a variable is stored in the save file but is not marked as persistent in the current scripts (e.g. if you updated the Anselme scripts to
|
||||
-- remove the persistence), it will not be loaded.
|
||||
--
|
||||
-- Loading should be done after loading all the game scripts (otherwise you will get "variable already defined" errors).
|
||||
--
|
||||
-- Returns this VM.
|
||||
load = function(self, data)
|
||||
assert(anselme.versions.save == data.anselme.versions.save, ("trying to load data from an incompatible version of Anselme; save was done using save version %s but current version is %s"):format(data.anselme.versions.save, anselme.versions.save))
|
||||
for k, v in pairs(data.variables) do
|
||||
if self.state.variable_metadata[k] then
|
||||
if self.state.variable_metadata[k].persistent then
|
||||
self.state.variables[k] = v
|
||||
end
|
||||
else
|
||||
self.state.variables[k] = v -- non-existent variable: keep it in case there was a mistake, it's not going to affect anything anyway
|
||||
end
|
||||
end
|
||||
return self
|
||||
end,
|
||||
--- Save script state.
|
||||
-- See `vm:load`.
|
||||
--
|
||||
-- Returns save data in case of success.
|
||||
--
|
||||
-- Returns nil, error message in case of error.
|
||||
save = function(self)
|
||||
local vars = {}
|
||||
for k, v in pairs(self.state.variables) do
|
||||
if should_be_persisted(self.state, k, v) then
|
||||
local s, e = check_persistable(v)
|
||||
if not s then return nil, ("%s; while saving variable %s"):format(e, k) end
|
||||
vars[k] = v
|
||||
end
|
||||
end
|
||||
return {
|
||||
anselme = {
|
||||
versions = anselme.versions,
|
||||
version = anselme.version
|
||||
},
|
||||
variables = vars
|
||||
}
|
||||
end,
|
||||
|
||||
--- Perform parsing that needs to be done after loading code.
|
||||
-- This is automatically ran before starting an interpreter, but you may want to execute it before if you want to check for parsing error manually.
|
||||
--
|
||||
-- Returns self in case of success.
|
||||
--
|
||||
-- Returns nil, error message in case of error.
|
||||
postload = function(self)
|
||||
if #self.state.queued_lines > 0 then
|
||||
local r, e = postparse(self.state)
|
||||
if not r then return nil, e end
|
||||
end
|
||||
return self
|
||||
end,
|
||||
|
||||
--- Enable feature flags.
|
||||
-- Available flags:
|
||||
-- * `"strip trailing spaces"`: remove trailing spaces from choice and text events (enabled by default)
|
||||
-- * `"strip duplicate spaces"`: remove duplicated spaces between text elements from choice and text events (enabled by default)
|
||||
--
|
||||
-- Returns this VM.
|
||||
enable = function(self, ...)
|
||||
for _, flag in ipairs{...} do
|
||||
self.state.feature_flags[flag] = true
|
||||
end
|
||||
return self
|
||||
end,
|
||||
--- Disable features flags.
|
||||
-- Returns this VM.
|
||||
disable = function(self, ...)
|
||||
for _, flag in ipairs{...} do
|
||||
self.state.feature_flags[flag] = nil
|
||||
end
|
||||
return self
|
||||
end,
|
||||
|
||||
--- Run code.
|
||||
-- Will merge state after successful execution
|
||||
--
|
||||
-- * `expr`: expression to evaluate (string or parsed expression), or a block to run
|
||||
-- * `namespace`(default=""): namespace to evaluate the expression in
|
||||
-- * `tags`(default={}): defaults tags when evaluating the expression (Lua value)
|
||||
--
|
||||
-- Return interpreter in case of success.
|
||||
--
|
||||
-- Returns nil, error message in case of error.
|
||||
run = function(self, expr, namespace, tags)
|
||||
local s, e = self:postload()
|
||||
if not s then return s, e end
|
||||
--
|
||||
local err
|
||||
if type(expr) ~= "table" then expr, err = expression(tostring(expr), self.state, namespace or "", "vm:run") end
|
||||
if not expr then return expr, err end
|
||||
-- interpreter state
|
||||
local interpreter
|
||||
interpreter = {
|
||||
state = {
|
||||
inject = self.state.inject,
|
||||
feature_flags = self.state.feature_flags,
|
||||
builtin_aliases = self.state.builtin_aliases,
|
||||
aliases = setmetatable({}, { __index = self.state.aliases }),
|
||||
functions = self.state.functions, -- no need for a cache as we can't define or modify any function from the interpreter for now
|
||||
variable_metadata = self.state.variable_metadata, -- no cache as metadata are expected to be constant
|
||||
variables = setmetatable({}, {
|
||||
__index = function(variables, k)
|
||||
local cache = getmetatable(variables).cache
|
||||
if cache[k] == nil then
|
||||
cache[k] = copy(self.state.variables[k], getmetatable(variables).copy_cache)
|
||||
end
|
||||
return cache[k]
|
||||
end,
|
||||
-- variables that keep current state and should be cleared at each checkpoint
|
||||
cache = {}, -- cache of previously read values (copies), to get repeatable reads & handle mutable types without changing global state
|
||||
modified_tables = {}, -- list of modified tables (copies) that should be merged with global state on next checkpoint
|
||||
copy_cache = {}, -- table of [original table] = copied table. Automatically filled by copy().
|
||||
-- keep track of scoped variables in scoped functions [fn line] = {{scoped variables}, next scope, ...}
|
||||
-- (scoped variables aren't merged on checkpoint, shouldn't be cleared at checkpoints)
|
||||
-- (only stores scoped variables that have been reassigned at some point (i.e. every accessed one since they start as undefined))
|
||||
scoped = {}
|
||||
}),
|
||||
interpreter = {
|
||||
-- constant
|
||||
global_state = self.state,
|
||||
coroutine = coroutine.create(function() return "return", interpreter:run(expr, namespace) end),
|
||||
-- status
|
||||
running_line = nil,
|
||||
-- choice event
|
||||
choice_selected = nil,
|
||||
-- skip next choices until next event change (to skip currently running choice block when resuming from a checkpoint)
|
||||
skip_choices_until_flush = nil,
|
||||
-- active event buffer stack
|
||||
event_buffer_stack = {},
|
||||
-- current event waiting to be sent
|
||||
current_event = nil,
|
||||
-- interrupt
|
||||
interrupt = nil,
|
||||
-- tag stack
|
||||
tags = {},
|
||||
-- default tags for everything in this interpreter (Lua values)
|
||||
base_lua_tags = tags,
|
||||
},
|
||||
},
|
||||
vm = self
|
||||
}
|
||||
return setmetatable(interpreter, interpreter_methods)
|
||||
end,
|
||||
--- Evaluate code.
|
||||
-- Behave like `:run`, except the expression can not emit events and will return the result of the expression directly.
|
||||
-- Merge state after sucessful execution automatically like `:run`.
|
||||
--
|
||||
-- * `expr`: expression to evaluate (string or parsed expression), or a block to evaluate
|
||||
-- * `namespace`(default=""): namespace to evaluate the expression in
|
||||
-- * `tags`(default={}): defaults tags when evaluating the expression (Lua value)
|
||||
--
|
||||
-- Return value in case of success (nil if nothing returned).
|
||||
--
|
||||
-- Returns nil, error message in case of error.
|
||||
eval = function(self, expr, namespace, tags)
|
||||
local interpreter, err = self:run("()", namespace, tags)
|
||||
if not interpreter then return interpreter, err end
|
||||
local r, e = interpreter:eval(expr, namespace)
|
||||
if e then return r, e end
|
||||
assert(interpreter:step() == "return", "evaluated expression can not emit events") -- trigger merge / end-of-script things
|
||||
return r
|
||||
end
|
||||
}
|
||||
vm_mt.__index = vm_mt
|
||||
|
||||
-- return anselme module
|
||||
return setmetatable(anselme, {
|
||||
__call = function()
|
||||
-- global state
|
||||
local state = {
|
||||
inject = {
|
||||
-- function_start = "code block...", ...
|
||||
},
|
||||
feature_flags = {
|
||||
["strip trailing spaces"] = true,
|
||||
["strip duplicate spaces"] = true
|
||||
},
|
||||
builtin_aliases = {
|
||||
-- ["👁️"] = "seen",
|
||||
-- ["🔖"] = "checkpoint",
|
||||
-- ["🏁"] = "reached"
|
||||
},
|
||||
aliases = {
|
||||
-- ["bonjour.salutation"] = "hello.greeting", ...
|
||||
},
|
||||
functions = {
|
||||
-- ["script.fn"] = {
|
||||
-- {
|
||||
-- function or checkpoint table
|
||||
-- }, ...
|
||||
-- }, ...
|
||||
},
|
||||
variable_metadata = {
|
||||
-- foo = { constant = true, persistent = true, constraint = constraint, ... }, ...
|
||||
},
|
||||
variables = {
|
||||
-- foo = {
|
||||
-- type = "number",
|
||||
-- value = 42
|
||||
-- }, ...
|
||||
},
|
||||
queued_lines = {
|
||||
-- { line = line, namespace = "foo" }, ...
|
||||
},
|
||||
link_next_function_definition_to_lua_function = nil -- temporarly set to tell the preparser to link a anselme function definition with a lua function
|
||||
}
|
||||
local vm = setmetatable({ state = state }, vm_mt)
|
||||
-- bootscript
|
||||
local boot = assert(vm:loadstring(bootscript, "", "boot script"))
|
||||
local _, e = vm:eval(boot)
|
||||
if e then error(e) end
|
||||
-- lua-defined functions
|
||||
assert(vm:loadfunction(stdfuncs.lua))
|
||||
-- anselme-defined functions
|
||||
local ansfunc = assert(vm:loadstring(stdfuncs.anselme, "", "built-in functions"))
|
||||
_, e = vm:eval(ansfunc)
|
||||
if e then return error(e) end
|
||||
return vm
|
||||
end
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue