mirror of
https://github.com/Reuh/anselme.git
synced 2025-10-27 16:49:31 +00:00
Change versioning to separate language/API/save; allow executing blocks directly; add version to game config
This commit is contained in:
parent
9388a22a0f
commit
d42c35facd
4 changed files with 124 additions and 46 deletions
143
anselme.lua
143
anselme.lua
|
|
@ -1,10 +1,16 @@
|
|||
-- anselme module
|
||||
local anselme = {
|
||||
-- version
|
||||
-- major.minor.fix
|
||||
-- saves files are incompatible between major versions
|
||||
-- scripts files may break between minor versions
|
||||
version = "0.15.0",
|
||||
-- save is incremented a each update which may break save compatibility
|
||||
-- language is incremented a each update which may break script file compatibility
|
||||
-- api is incremented a each update which may break Lua API compatibility
|
||||
versions = {
|
||||
save = 1,
|
||||
language = 15,
|
||||
api = 1
|
||||
},
|
||||
-- version is incremented at each update
|
||||
version = 16,
|
||||
--- currently running interpreter
|
||||
running = nil
|
||||
}
|
||||
|
|
@ -17,6 +23,7 @@ local postparse = require(anselme_root.."parser.postparser")
|
|||
local expression = require(anselme_root.."parser.expression")
|
||||
local eval = require(anselme_root.."interpreter.expression")
|
||||
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 identifier_pattern = require(anselme_root.."parser.common").identifier_pattern
|
||||
local merge_state = require(anselme_root.."interpreter.common").merge_state
|
||||
|
|
@ -31,7 +38,7 @@ local function list_directory(path)
|
|||
else
|
||||
local lfs = require("lfs")
|
||||
for item in lfs.dir(path) do
|
||||
table.insert(t, path.."/"..item)
|
||||
table.insert(t, item)
|
||||
end
|
||||
end
|
||||
return t
|
||||
|
|
@ -125,15 +132,20 @@ local interpreter_methods = {
|
|||
return namespace
|
||||
end,
|
||||
|
||||
--- run an expression: may trigger events and must be called from within the interpreter coroutine
|
||||
-- return lua value
|
||||
--- run an expression or block: may trigger events and must be called from within the interpreter coroutine
|
||||
-- return lua value (nil if nothing returned)
|
||||
run = function(self, expr, namespace)
|
||||
-- parse
|
||||
local err
|
||||
if type(expr) ~= "table" then expr, err = expression(tostring(expr), self.state.interpreter.global_state, namespace or "") end
|
||||
if not expr then coroutine.yield("error", err) end
|
||||
-- run
|
||||
local r, e = eval(self.state, expr)
|
||||
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.event_buffer then -- flush final events
|
||||
local rf, re = run_line(self.state, { type = "flush_events" })
|
||||
|
|
@ -142,8 +154,9 @@ local interpreter_methods = {
|
|||
end
|
||||
return to_lua(r)
|
||||
end,
|
||||
--- evaluate an expression
|
||||
-- return value in case of success
|
||||
--- evaluate an expression or block
|
||||
-- can be called from outside the coroutine
|
||||
-- return value in case of success (nil if nothing returned)
|
||||
-- return nil, err in case of error
|
||||
eval = function(self, expr, namespace)
|
||||
-- parse
|
||||
|
|
@ -152,7 +165,12 @@ local interpreter_methods = {
|
|||
if not expr then return nil, err end
|
||||
-- run
|
||||
local co = coroutine.create(function()
|
||||
local r, e = eval(self.state, expr)
|
||||
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)
|
||||
|
|
@ -163,7 +181,7 @@ local interpreter_methods = {
|
|||
if not success then
|
||||
return nil, event
|
||||
elseif event ~= "return" then
|
||||
return nil, ("evaluated expression generated an %q event"):format(event)
|
||||
return nil, ("evaluated expression generated an %q event; at %s"):format(event, self.state.interpreter.running_line.source)
|
||||
else
|
||||
return to_lua(data)
|
||||
end
|
||||
|
|
@ -176,38 +194,56 @@ local vm_mt = {
|
|||
-- anselme state
|
||||
state = nil,
|
||||
|
||||
-- loaded game state
|
||||
game = nil,
|
||||
|
||||
--- wrapper for loading a whole set of scripts
|
||||
-- should be preferred to other loading functions if possible
|
||||
-- requires LÖVE or LuaFileSystem
|
||||
-- will load in path, in order:
|
||||
-- * config.ans, which contains various optional configuration options:
|
||||
-- * config.ans, which will be executed in the "config" namespace and may contains various optional configuration options:
|
||||
-- * language: string, built-in language file to load
|
||||
-- * main file: string, name (without .ans extension) of a file that will be loaded into the root namespace
|
||||
-- * 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.
|
||||
-- * main file: string, name (without .ans extension) of a file that will be loaded into the root namespace and ran when starting the game
|
||||
-- * main file, 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 self in case of success
|
||||
-- returns nil, err in case of error
|
||||
loadgame = function(self, path)
|
||||
-- get config
|
||||
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
|
||||
local main_file = self:eval("config.main file")
|
||||
local language = self:eval("config.language")
|
||||
-- get config
|
||||
self.game = {
|
||||
language = self:eval("config.language"),
|
||||
version = self:eval("config.version"),
|
||||
main_file = self:eval("config.main file"),
|
||||
main_block = nil
|
||||
}
|
||||
-- force merging version into state
|
||||
local interpreter, err = self:run("config.version")
|
||||
if not interpreter then return interpreter, err end
|
||||
interpreter:step()
|
||||
-- load language
|
||||
if language then
|
||||
local s, e = self:loadlanguage(language)
|
||||
if self.game.language then
|
||||
local s, e = self:loadlanguage(self.game.language)
|
||||
if not s then return s, e end
|
||||
end
|
||||
-- load main file
|
||||
if main_file then
|
||||
local s, e = self:loadfile(path.."/"..main_file..".ans")
|
||||
if self.game.main_file then
|
||||
local s, e = self:loadfile(path.."/"..self.game.main_file..".ans")
|
||||
if not s then return s, e end
|
||||
self.game.main_block = s
|
||||
end
|
||||
-- load other files
|
||||
for _, item in ipairs(list_directory(path)) do
|
||||
if item:match("[^%.]") and item ~= "config.ans" and item ~= main_file then
|
||||
if item:match("[^%.]") and item ~= "config.ans" and item ~= self.game.main_file..".ans" then
|
||||
local p = path.."/"..item
|
||||
local s, e
|
||||
if is_directory(p) then
|
||||
|
|
@ -220,16 +256,27 @@ local vm_mt = {
|
|||
end
|
||||
return self
|
||||
end,
|
||||
--- return a interpreter which runs the game main file
|
||||
-- return interpreter in case of success
|
||||
-- returns nil, err in case of error
|
||||
rungame = function(self)
|
||||
if not self.game then error("no game loaded") end
|
||||
if self.game.main_block then
|
||||
return self:run(self.game.main_block)
|
||||
else
|
||||
return self:run("()")
|
||||
end
|
||||
end,
|
||||
|
||||
--- load code
|
||||
-- similar to Lua's code loading functions.
|
||||
-- name(default=""): namespace to load the code in. Will define a new function is specified; otherwise, code will be parsed but not executable from an expression.
|
||||
-- return self in case of success
|
||||
-- return parsed block in case of success
|
||||
-- returns nil, err 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 self
|
||||
return s
|
||||
end,
|
||||
loadfile = function(self, path, name)
|
||||
local content
|
||||
|
|
@ -245,9 +292,13 @@ local vm_mt = {
|
|||
end
|
||||
local s, err = self:loadstring(content, name, path)
|
||||
if not s then return s, err end
|
||||
return self
|
||||
return s
|
||||
end,
|
||||
loaddirectory = function(self, path, name) -- requires LÖVE or LuaFileSystem
|
||||
-- load every file in a directory, using filename (without .ans extension) as its namespace
|
||||
-- requires LÖVE or LuaFileSystem
|
||||
-- return self in case of success
|
||||
-- returns nil, err 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
|
||||
|
|
@ -283,7 +334,7 @@ local vm_mt = {
|
|||
-- return self in case of success
|
||||
-- returns nil, err in case of error
|
||||
loadlanguage = function(self, lang)
|
||||
local namespace = "anselme."..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)
|
||||
|
|
@ -324,8 +375,7 @@ local vm_mt = {
|
|||
-- only save variables with usable identifiers, so will skip functions with arguments, operators, etc.
|
||||
-- loading should be after loading scripts (otherwise you will "variable already defined" errors)
|
||||
load = function(self, data)
|
||||
local saveMajor, currentMajor = data.anselme_version:match("^[^%.]*"), anselme.version:match("^[^%.]*")
|
||||
assert(saveMajor == currentMajor, ("trying to load data from an incompatible version of Anselme; save was done using %s but current version is %s"):format(data.anselme_version, anselme.version))
|
||||
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
|
||||
self.state.variables[k] = v
|
||||
end
|
||||
|
|
@ -339,22 +389,36 @@ local vm_mt = {
|
|||
end
|
||||
end
|
||||
return {
|
||||
anselme_version = anselme.version,
|
||||
anselme = {
|
||||
versions = anselme.versions,
|
||||
version = anselme.version
|
||||
},
|
||||
variables = vars
|
||||
}
|
||||
end,
|
||||
|
||||
--- perform parsing that needs to be done after loading code
|
||||
-- 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, err 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 r, e end
|
||||
end
|
||||
return self
|
||||
end,
|
||||
|
||||
--- run code
|
||||
-- expr: expression to evaluate
|
||||
-- expr: expression to evaluate (string or parsed expression), or a block to run
|
||||
-- will merge state after successful execution
|
||||
-- namespace(default=""): namespace to evaluate the expression in
|
||||
-- tags(default={}): defaults tag when evaluating the expression
|
||||
-- return interpreter in case of success
|
||||
-- returns nil, err in case of error
|
||||
run = function(self, expr, namespace, tags)
|
||||
if #self.state.queued_lines > 0 then
|
||||
local r, e = postparse(self.state)
|
||||
if not r then return r, e end
|
||||
end
|
||||
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 "") end
|
||||
|
|
@ -393,13 +457,14 @@ local vm_mt = {
|
|||
end,
|
||||
--- eval code
|
||||
-- unlike :run, this does not support events and will return the result of the expression directly.
|
||||
-- expr: expression to evaluate
|
||||
-- does not merge state after execution automatically
|
||||
-- expr: expression to evaluate (string or parsed expression), or a block to evaluate
|
||||
-- namespace(default=""): namespace to evaluate the expression in
|
||||
-- tags(default={}): defaults tag when evaluating the expression
|
||||
-- return value in case of success (nil if nothing returned)
|
||||
-- returns nil, err in case of error
|
||||
eval = function(self, expr, namespace, tags)
|
||||
local interpreter, err = self:run("0", namespace, tags)
|
||||
local interpreter, err = self:run("()", namespace, tags)
|
||||
if not interpreter then return interpreter, err end
|
||||
return interpreter:eval(expr, namespace)
|
||||
end
|
||||
|
|
@ -438,7 +503,9 @@ return setmetatable(anselme, {
|
|||
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)
|
||||
assert(vm:loadstring(bootscript, "", "boot script"))
|
||||
local boot = assert(vm:loadstring(bootscript, "", "boot script"))
|
||||
local _, e = vm:eval(boot)
|
||||
if e then error(e) end
|
||||
assert(vm:loadfunction(stdfuncs))
|
||||
return vm
|
||||
end
|
||||
|
|
|
|||
|
|
@ -404,7 +404,7 @@ end
|
|||
|
||||
--- preparse shit: create AST structure, define variables and functions, but don't parse expression or perform any type checking
|
||||
-- (wait for other files to be parsed before doing this with postparse)
|
||||
-- * state: in case of success
|
||||
-- * block: in case of success
|
||||
-- * nil, err: in case of error
|
||||
local function parse(state, s, name, source)
|
||||
-- parse lines
|
||||
|
|
@ -425,7 +425,7 @@ local function parse(state, s, name, source)
|
|||
-- parse
|
||||
local root, err = parse_block(indented, state, "")
|
||||
if not root then return nil, err end
|
||||
return state
|
||||
return root
|
||||
end
|
||||
|
||||
package.loaded[...] = parse
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
-- Script run when creating a VM
|
||||
return [[
|
||||
(Built-in type definition)
|
||||
:nil="nil"
|
||||
|
|
|
|||
22
test/run.lua
22
test/run.lua
|
|
@ -73,14 +73,24 @@ end
|
|||
table.sort(files)
|
||||
|
||||
-- test script
|
||||
if args.script then
|
||||
if args.script or args.game then
|
||||
local vm = anselme()
|
||||
if args.lang then
|
||||
assert(vm:loadlanguage(args.lang))
|
||||
end
|
||||
local state, err = vm:loadfile(args.script, "script")
|
||||
local state, err
|
||||
if args.script then
|
||||
state, err = vm:loadfile(args.script, "script")
|
||||
else
|
||||
state, err = vm:loadgame(args.game)
|
||||
end
|
||||
if state then
|
||||
local istate, e = vm:run("script")
|
||||
local istate, e
|
||||
if args.script then
|
||||
istate, e = vm:run("script")
|
||||
elseif args.game then
|
||||
istate, e = vm:rungame()
|
||||
end
|
||||
if not istate then
|
||||
print("error", e)
|
||||
else
|
||||
|
|
@ -98,12 +108,12 @@ if args.script then
|
|||
end
|
||||
until t == "return" or t == "error"
|
||||
end
|
||||
if args.save then
|
||||
print(inspect(vm:save()))
|
||||
end
|
||||
else
|
||||
print("error", err)
|
||||
end
|
||||
if args.save then
|
||||
print(inspect(vm:save()))
|
||||
end
|
||||
-- run tests
|
||||
else
|
||||
local total, success = #files, 0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue