1
0
Fork 0
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:
Étienne Fildadut 2021-06-05 17:19:42 +02:00
parent 9388a22a0f
commit d42c35facd
4 changed files with 124 additions and 46 deletions

View file

@ -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

View file

@ -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

View file

@ -1,3 +1,4 @@
-- Script run when creating a VM
return [[
(Built-in type definition)
:nil="nil"

View file

@ -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