From d42c35facd86884031b44e86207a9fd8263a67f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Reuh=20Fildadut?= Date: Sat, 5 Jun 2021 17:19:42 +0200 Subject: [PATCH] Change versioning to separate language/API/save; allow executing blocks directly; add version to game config --- anselme.lua | 143 +++++++++++++++++++++++++++++++----------- parser/preparser.lua | 4 +- stdlib/bootscript.lua | 1 + test/run.lua | 22 +++++-- 4 files changed, 124 insertions(+), 46 deletions(-) diff --git a/anselme.lua b/anselme.lua index a08c5a3..0cf14c4 100644 --- a/anselme.lua +++ b/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 diff --git a/parser/preparser.lua b/parser/preparser.lua index 7bbde7e..a859f6a 100644 --- a/parser/preparser.lua +++ b/parser/preparser.lua @@ -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 diff --git a/stdlib/bootscript.lua b/stdlib/bootscript.lua index 89ca8c8..55028c6 100644 --- a/stdlib/bootscript.lua +++ b/stdlib/bootscript.lua @@ -1,3 +1,4 @@ +-- Script run when creating a VM return [[ (Built-in type definition) :nil="nil" diff --git a/test/run.lua b/test/run.lua index 6b24493..f4f95b4 100644 --- a/test/run.lua +++ b/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