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
|
-- anselme module
|
||||||
local anselme = {
|
local anselme = {
|
||||||
-- version
|
-- version
|
||||||
-- major.minor.fix
|
-- save is incremented a each update which may break save compatibility
|
||||||
-- saves files are incompatible between major versions
|
-- language is incremented a each update which may break script file compatibility
|
||||||
-- scripts files may break between minor versions
|
-- api is incremented a each update which may break Lua API compatibility
|
||||||
version = "0.15.0",
|
versions = {
|
||||||
|
save = 1,
|
||||||
|
language = 15,
|
||||||
|
api = 1
|
||||||
|
},
|
||||||
|
-- version is incremented at each update
|
||||||
|
version = 16,
|
||||||
--- currently running interpreter
|
--- currently running interpreter
|
||||||
running = nil
|
running = nil
|
||||||
}
|
}
|
||||||
|
|
@ -17,6 +23,7 @@ local postparse = require(anselme_root.."parser.postparser")
|
||||||
local expression = require(anselme_root.."parser.expression")
|
local expression = require(anselme_root.."parser.expression")
|
||||||
local eval = require(anselme_root.."interpreter.expression")
|
local eval = require(anselme_root.."interpreter.expression")
|
||||||
local run_line = require(anselme_root.."interpreter.interpreter").run_line
|
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 to_lua = require(anselme_root.."interpreter.common").to_lua
|
||||||
local identifier_pattern = require(anselme_root.."parser.common").identifier_pattern
|
local identifier_pattern = require(anselme_root.."parser.common").identifier_pattern
|
||||||
local merge_state = require(anselme_root.."interpreter.common").merge_state
|
local merge_state = require(anselme_root.."interpreter.common").merge_state
|
||||||
|
|
@ -31,7 +38,7 @@ local function list_directory(path)
|
||||||
else
|
else
|
||||||
local lfs = require("lfs")
|
local lfs = require("lfs")
|
||||||
for item in lfs.dir(path) do
|
for item in lfs.dir(path) do
|
||||||
table.insert(t, path.."/"..item)
|
table.insert(t, item)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return t
|
return t
|
||||||
|
|
@ -125,15 +132,20 @@ local interpreter_methods = {
|
||||||
return namespace
|
return namespace
|
||||||
end,
|
end,
|
||||||
|
|
||||||
--- run an expression: may trigger events and must be called from within the interpreter coroutine
|
--- run an expression or block: may trigger events and must be called from within the interpreter coroutine
|
||||||
-- return lua value
|
-- return lua value (nil if nothing returned)
|
||||||
run = function(self, expr, namespace)
|
run = function(self, expr, namespace)
|
||||||
-- parse
|
-- parse
|
||||||
local err
|
local err
|
||||||
if type(expr) ~= "table" then expr, err = expression(tostring(expr), self.state.interpreter.global_state, namespace or "") end
|
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
|
if not expr then coroutine.yield("error", err) end
|
||||||
-- run
|
-- 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 not r then coroutine.yield("error", e) end
|
||||||
if self.state.interpreter.event_buffer then -- flush final events
|
if self.state.interpreter.event_buffer then -- flush final events
|
||||||
local rf, re = run_line(self.state, { type = "flush_events" })
|
local rf, re = run_line(self.state, { type = "flush_events" })
|
||||||
|
|
@ -142,8 +154,9 @@ local interpreter_methods = {
|
||||||
end
|
end
|
||||||
return to_lua(r)
|
return to_lua(r)
|
||||||
end,
|
end,
|
||||||
--- evaluate an expression
|
--- evaluate an expression or block
|
||||||
-- return value in case of success
|
-- can be called from outside the coroutine
|
||||||
|
-- return value in case of success (nil if nothing returned)
|
||||||
-- return nil, err in case of error
|
-- return nil, err in case of error
|
||||||
eval = function(self, expr, namespace)
|
eval = function(self, expr, namespace)
|
||||||
-- parse
|
-- parse
|
||||||
|
|
@ -152,7 +165,12 @@ local interpreter_methods = {
|
||||||
if not expr then return nil, err end
|
if not expr then return nil, err end
|
||||||
-- run
|
-- run
|
||||||
local co = coroutine.create(function()
|
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
|
if not r then return "error", e end
|
||||||
return "return", r
|
return "return", r
|
||||||
end)
|
end)
|
||||||
|
|
@ -163,7 +181,7 @@ local interpreter_methods = {
|
||||||
if not success then
|
if not success then
|
||||||
return nil, event
|
return nil, event
|
||||||
elseif event ~= "return" then
|
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
|
else
|
||||||
return to_lua(data)
|
return to_lua(data)
|
||||||
end
|
end
|
||||||
|
|
@ -176,38 +194,56 @@ local vm_mt = {
|
||||||
-- anselme state
|
-- anselme state
|
||||||
state = nil,
|
state = nil,
|
||||||
|
|
||||||
|
-- loaded game state
|
||||||
|
game = nil,
|
||||||
|
|
||||||
--- wrapper for loading a whole set of scripts
|
--- wrapper for loading a whole set of scripts
|
||||||
-- should be preferred to other loading functions if possible
|
-- should be preferred to other loading functions if possible
|
||||||
-- requires LÖVE or LuaFileSystem
|
-- requires LÖVE or LuaFileSystem
|
||||||
-- will load in path, in order:
|
-- 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
|
-- * 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
|
-- * 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)
|
-- * 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 self in case of success
|
||||||
-- returns nil, err in case of error
|
-- returns nil, err in case of error
|
||||||
loadgame = function(self, path)
|
loadgame = function(self, path)
|
||||||
-- get config
|
if self.game then error("game already loaded") end
|
||||||
|
-- load config
|
||||||
if is_file(path.."/config.ans") then
|
if is_file(path.."/config.ans") then
|
||||||
local s, e = self:loadfile(path.."/config.ans", "config")
|
local s, e = self:loadfile(path.."/config.ans", "config")
|
||||||
if not s then return s, e end
|
if not s then return s, e end
|
||||||
|
s, e = self:eval("config")
|
||||||
|
if e then return s, e end
|
||||||
end
|
end
|
||||||
local main_file = self:eval("config.main file")
|
-- get config
|
||||||
local language = self:eval("config.language")
|
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
|
-- load language
|
||||||
if language then
|
if self.game.language then
|
||||||
local s, e = self:loadlanguage(language)
|
local s, e = self:loadlanguage(self.game.language)
|
||||||
if not s then return s, e end
|
if not s then return s, e end
|
||||||
end
|
end
|
||||||
-- load main file
|
-- load main file
|
||||||
if main_file then
|
if self.game.main_file then
|
||||||
local s, e = self:loadfile(path.."/"..main_file..".ans")
|
local s, e = self:loadfile(path.."/"..self.game.main_file..".ans")
|
||||||
if not s then return s, e end
|
if not s then return s, e end
|
||||||
|
self.game.main_block = s
|
||||||
end
|
end
|
||||||
-- load other files
|
-- load other files
|
||||||
for _, item in ipairs(list_directory(path)) do
|
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 p = path.."/"..item
|
||||||
local s, e
|
local s, e
|
||||||
if is_directory(p) then
|
if is_directory(p) then
|
||||||
|
|
@ -220,16 +256,27 @@ local vm_mt = {
|
||||||
end
|
end
|
||||||
return self
|
return self
|
||||||
end,
|
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
|
--- load code
|
||||||
-- similar to Lua's code loading functions.
|
-- 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.
|
-- 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
|
-- returns nil, err in case of error
|
||||||
loadstring = function(self, str, name, source)
|
loadstring = function(self, str, name, source)
|
||||||
local s, e = preparse(self.state, str, name or "", source)
|
local s, e = preparse(self.state, str, name or "", source)
|
||||||
if not s then return s, e end
|
if not s then return s, e end
|
||||||
return self
|
return s
|
||||||
end,
|
end,
|
||||||
loadfile = function(self, path, name)
|
loadfile = function(self, path, name)
|
||||||
local content
|
local content
|
||||||
|
|
@ -245,9 +292,13 @@ local vm_mt = {
|
||||||
end
|
end
|
||||||
local s, err = self:loadstring(content, name, path)
|
local s, err = self:loadstring(content, name, path)
|
||||||
if not s then return s, err end
|
if not s then return s, err end
|
||||||
return self
|
return s
|
||||||
end,
|
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
|
if not name then name = "" end
|
||||||
name = name == "" and "" or name.."."
|
name = name == "" and "" or name.."."
|
||||||
for _, item in ipairs(list_directory(path)) do
|
for _, item in ipairs(list_directory(path)) do
|
||||||
|
|
@ -283,7 +334,7 @@ local vm_mt = {
|
||||||
-- return self in case of success
|
-- return self in case of success
|
||||||
-- returns nil, err in case of error
|
-- returns nil, err in case of error
|
||||||
loadlanguage = function(self, lang)
|
loadlanguage = function(self, lang)
|
||||||
local namespace = "anselme."..lang
|
local namespace = "anselme.languages."..lang
|
||||||
-- execute language file
|
-- execute language file
|
||||||
local code = require(anselme_root.."stdlib.languages."..lang)
|
local code = require(anselme_root.."stdlib.languages."..lang)
|
||||||
local s, e = self:loadstring(code, namespace, 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.
|
-- 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)
|
-- loading should be after loading scripts (otherwise you will "variable already defined" errors)
|
||||||
load = function(self, data)
|
load = function(self, data)
|
||||||
local saveMajor, currentMajor = data.anselme_version:match("^[^%.]*"), anselme.version:match("^[^%.]*")
|
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))
|
||||||
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))
|
|
||||||
for k, v in pairs(data.variables) do
|
for k, v in pairs(data.variables) do
|
||||||
self.state.variables[k] = v
|
self.state.variables[k] = v
|
||||||
end
|
end
|
||||||
|
|
@ -339,22 +389,36 @@ local vm_mt = {
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return {
|
return {
|
||||||
anselme_version = anselme.version,
|
anselme = {
|
||||||
|
versions = anselme.versions,
|
||||||
|
version = anselme.version
|
||||||
|
},
|
||||||
variables = vars
|
variables = vars
|
||||||
}
|
}
|
||||||
end,
|
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
|
--- 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
|
-- namespace(default=""): namespace to evaluate the expression in
|
||||||
-- tags(default={}): defaults tag when evaluating the expression
|
-- tags(default={}): defaults tag when evaluating the expression
|
||||||
-- return interpreter in case of success
|
-- return interpreter in case of success
|
||||||
-- returns nil, err in case of error
|
-- returns nil, err in case of error
|
||||||
run = function(self, expr, namespace, tags)
|
run = function(self, expr, namespace, tags)
|
||||||
if #self.state.queued_lines > 0 then
|
local s, e = self:postload()
|
||||||
local r, e = postparse(self.state)
|
if not s then return s, e end
|
||||||
if not r then return r, e end
|
|
||||||
end
|
|
||||||
--
|
--
|
||||||
local err
|
local err
|
||||||
if type(expr) ~= "table" then expr, err = expression(tostring(expr), self.state, namespace or "") end
|
if type(expr) ~= "table" then expr, err = expression(tostring(expr), self.state, namespace or "") end
|
||||||
|
|
@ -393,13 +457,14 @@ local vm_mt = {
|
||||||
end,
|
end,
|
||||||
--- eval code
|
--- eval code
|
||||||
-- unlike :run, this does not support events and will return the result of the expression directly.
|
-- 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
|
-- namespace(default=""): namespace to evaluate the expression in
|
||||||
-- tags(default={}): defaults tag when evaluating the expression
|
-- tags(default={}): defaults tag when evaluating the expression
|
||||||
-- return value in case of success (nil if nothing returned)
|
-- return value in case of success (nil if nothing returned)
|
||||||
-- returns nil, err in case of error
|
-- returns nil, err in case of error
|
||||||
eval = function(self, expr, namespace, tags)
|
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
|
if not interpreter then return interpreter, err end
|
||||||
return interpreter:eval(expr, namespace)
|
return interpreter:eval(expr, namespace)
|
||||||
end
|
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
|
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)
|
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))
|
assert(vm:loadfunction(stdfuncs))
|
||||||
return vm
|
return vm
|
||||||
end
|
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
|
--- 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)
|
-- (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
|
-- * nil, err: in case of error
|
||||||
local function parse(state, s, name, source)
|
local function parse(state, s, name, source)
|
||||||
-- parse lines
|
-- parse lines
|
||||||
|
|
@ -425,7 +425,7 @@ local function parse(state, s, name, source)
|
||||||
-- parse
|
-- parse
|
||||||
local root, err = parse_block(indented, state, "")
|
local root, err = parse_block(indented, state, "")
|
||||||
if not root then return nil, err end
|
if not root then return nil, err end
|
||||||
return state
|
return root
|
||||||
end
|
end
|
||||||
|
|
||||||
package.loaded[...] = parse
|
package.loaded[...] = parse
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
-- Script run when creating a VM
|
||||||
return [[
|
return [[
|
||||||
(Built-in type definition)
|
(Built-in type definition)
|
||||||
:nil="nil"
|
:nil="nil"
|
||||||
|
|
|
||||||
22
test/run.lua
22
test/run.lua
|
|
@ -73,14 +73,24 @@ end
|
||||||
table.sort(files)
|
table.sort(files)
|
||||||
|
|
||||||
-- test script
|
-- test script
|
||||||
if args.script then
|
if args.script or args.game then
|
||||||
local vm = anselme()
|
local vm = anselme()
|
||||||
if args.lang then
|
if args.lang then
|
||||||
assert(vm:loadlanguage(args.lang))
|
assert(vm:loadlanguage(args.lang))
|
||||||
end
|
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
|
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
|
if not istate then
|
||||||
print("error", e)
|
print("error", e)
|
||||||
else
|
else
|
||||||
|
|
@ -98,12 +108,12 @@ if args.script then
|
||||||
end
|
end
|
||||||
until t == "return" or t == "error"
|
until t == "return" or t == "error"
|
||||||
end
|
end
|
||||||
if args.save then
|
|
||||||
print(inspect(vm:save()))
|
|
||||||
end
|
|
||||||
else
|
else
|
||||||
print("error", err)
|
print("error", err)
|
||||||
end
|
end
|
||||||
|
if args.save then
|
||||||
|
print(inspect(vm:save()))
|
||||||
|
end
|
||||||
-- run tests
|
-- run tests
|
||||||
else
|
else
|
||||||
local total, success = #files, 0
|
local total, success = #files, 0
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue