diff --git a/LANGUAGE.md b/LANGUAGE.md index 5f924ec..2985245 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -281,15 +281,6 @@ Checkpoints always have the following variable defined in its namespace by defau Tagged with a red color and blink. ``` -#### Lines that can't have children: - -* `:`: variable declaration. Followed by an [identifier](#identifiers) (with eventually an [alias](#aliases)), a `=` and an [expression](#expressions). Defines a variable with a default value and this identifier in the current [namespace]("identifiers"). The expression is not evaluated instantly, but the first time the variable is used. - -``` -:foo = 42 -:bar : alias = 12 -``` - * `@`: return line. Can be followed by an [expression](#expressions); otherwise nil expression is assumed. Exit the current function and returns the expression's value. ``` @@ -299,6 +290,23 @@ $ hey {hey} = 5 ``` +If this line has children, they will be ran _after_ evaluating the returned expression but _before_ exiting the current function. If the children return a value, it is used instead. + +``` +(Returns 0 and print 5) +$ fn + :i=0 + + @i + ~ i:=5 + {i} + +(Returns 3) +$ g + @0 + @3 +``` + Please note that Anselme will discard returns values sent from within a choice block. Returns inside choice block still have the expected behaviour of stopping the execution of the choice block. This is the case because choice blocks are not ran right as they are read, but only at the next event flush (i.e. empty line). This means that if there is no flush in the function itself, the choice will be ran *after* the function has already been executed and returning a value at this point makes no sense: @@ -315,7 +323,15 @@ $ f Yes. (Choice block is actually ran right before the "Yes" line, when the choice event is flushed.) +``` +#### Lines that can't have children: + +* `:`: variable declaration. Followed by an [identifier](#identifiers) (with eventually an [alias](#aliases)), a `=` and an [expression](#expressions). Defines a variable with a default value and this identifier in the current [namespace]("identifiers"). The expression is not evaluated instantly, but the first time the variable is used. + +``` +:foo = 42 +:bar : alias = 12 ``` * empty line: flush the event buffer, i.e., if there are any pending lines of text or choices, send them to your game. See [Event buffer](#event-buffer). This line always keep the same identation as the last non-empty line, so you don't need to put invisible whitespace on an empty-looking line. Is also automatically added at the end of a file. diff --git a/anselme.lua b/anselme.lua index fdb5a1b..6128838 100644 --- a/anselme.lua +++ b/anselme.lua @@ -22,6 +22,7 @@ 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 @@ -230,7 +231,7 @@ local vm_mt = { -- 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 like vm:injectfunctionstart + -- 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 -- * main file, if defined in config.ans @@ -266,10 +267,10 @@ local vm_mt = { end -- load injections if self.game.inject_directory then - for _, inject in ipairs{"function start", "function end", "scoped function start", "scoped function end", "checkpoint start", "checkpoint end"} do + 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[inject:gsub(" ", "_")] = f:read("*a") + self.state.inject[ninject] = f:read("*a") f:close() end end @@ -376,47 +377,22 @@ local vm_mt = { self.state.builtin_aliases["🏁"] = reached return self end, - --- set some code that will be added at the start of every non-scoped function defined after this is called - -- nil to disable - -- can typically be used to define variables for every function like 👁️ + --- set some code that will be injected at specific places in all code loaded after this is called + -- 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 + -- * "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 to nil to disable + -- can typically be used to define variables for every function like 👁️, setting some value on every function resume, etc. -- return self - injectfunctionstart = function(self, code) - self.state.inject.function_start = code - return self - end, - --- same as injectfunctionstart, but inject code at the start of every scoped function - -- nil to disable - -- return self - injectscopedfunctionstart = function(self, code) - self.state.inject.scoped_function_start = code - return self - end, - --- same as injectfunctionstart, but inject code at the start of every checkpoint - -- nil to disable - -- return self - injectcheckpointstart = function(self, code) - self.state.inject.checkpoint_start = code - return self - end, - --- same as injectfunctionstart, but inject code at the end of every non-scoped function - -- nil to disable - -- return self - injectfunctionend = function(self, code) - self.state.inject.function_end = code - return self - end, - --- same as injectfunctionstart, but inject code at the end of every scoped function - -- nil to disable - -- return self - injectscopedfunctionend = function(self, code) - self.state.inject.scoped_function_end = code - return self - end, - --- same as injectfunctionend, but inject code at the end of every checkpoint - -- nil to disable - -- return self - injectcheckpointend = function(self, code) - self.state.inject.checkpoint_end = code + setinjection = function(self, inject, code) + assert(injections[inject], ("unknown injection type %q"):format(inject)) + self.state.inject[injections[inject]] = code return self end, @@ -614,9 +590,7 @@ return setmetatable(anselme, { -- global state local state = { inject = { - function_start = nil, function_end = nil, - scoped_function_start = nil, scoped_function_end = nil, - checkpoint_start = nil, checkpoint_end = nil + -- function_start = "code block...", ... }, feature_flags = { ["strip trailing spaces"] = true, diff --git a/interpreter/interpreter.lua b/interpreter/interpreter.lua index 3cb911f..a7b44b8 100644 --- a/interpreter/interpreter.lua +++ b/interpreter/interpreter.lua @@ -92,6 +92,9 @@ run_line = function(state, line) elseif line.type == "return" then local v, e = eval(state, line.expression) if not v then return v, ("%s; at %s"):format(e, line.source) end + local cv, ce = run_block(state, line.child) + if ce then return cv, ce end + if cv then return cv end return v elseif line.type == "text" then local v, e = events:make_space_for(state, "text") -- do this before any evaluation start diff --git a/parser/common.lua b/parser/common.lua index 0ec595b..dd3c48a 100644 --- a/parser/common.lua +++ b/parser/common.lua @@ -70,6 +70,12 @@ common = { -- decorators ["\\$"] = "$" }, + -- list of possible injections and their associated name in vm.state.inject + injections = { + ["function start"] = "function_start", ["function end"] = "function_end", ["function return"] = "function_return", + ["scoped function start"] = "scoped_function_start", ["scoped function end"] = "scoped_function_end", ["scoped function return"] = "scoped_function_return", + ["checkpoint start"] = "checkpoint_start", ["checkpoint end"] = "checkpoint_end" + }, --- escape a string to be used as an exact match pattern escape = function(str) if not escapeCache[str] then diff --git a/parser/preparser.lua b/parser/preparser.lua index 1784415..d63a529 100644 --- a/parser/preparser.lua +++ b/parser/preparser.lua @@ -1,4 +1,4 @@ -local format_identifier, identifier_pattern, escape, special_functions_names, pretty_signature, signature, copy +local format_identifier, identifier_pattern, escape, special_functions_names, pretty_signature, signature, copy, injections local parse_indented @@ -27,7 +27,7 @@ end --- parse a single line into AST -- * ast: if success -- * nil, error: in case of error -local function parse_line(line, state, namespace) +local function parse_line(line, state, namespace, parent_function) local l = line.content local r = { source = line.source @@ -59,7 +59,7 @@ local function parse_line(line, state, namespace) r.child = true -- store parent function and run checkpoint when line is read if r.type == "checkpoint" then - r.parent_function = true + r.parent_function = parent_function end -- don't keep function node in block AST if r.type == "function" then @@ -311,13 +311,29 @@ local function parse_line(line, state, namespace) -- return elseif l:match("^%@") then r.type = "return" - r.parent_function = true + r.child = true + r.parent_function = parent_function local expr = l:match("^%@(.*)$") if expr:match("[^%s]") then r.expression = expr else r.expression = "()" end + -- custom code injection + if not line.children then line.children = {} end + if parent_function.scoped then + if state.inject.scoped_function_return then + for _, ll in ipairs(state.inject.scoped_function_return) do + table.insert(line.children, copy(ll)) + end + end + else + if state.inject.function_return then + for _, ll in ipairs(state.inject.function_return) do + table.insert(line.children, copy(ll)) + end + end + end -- text elseif l:match("[^%s]") then r.type = "text" @@ -337,10 +353,8 @@ local function parse_block(indented, state, namespace, parent_function) local block = { type = "block" } for _, l in ipairs(indented) do -- parsable line - local ast, err = parse_line(l, state, namespace) + local ast, err = parse_line(l, state, namespace, parent_function) if err then return nil, err end - -- store parent function - if ast.parent_function then ast.parent_function = parent_function end -- add to block AST if not ast.remove_from_block_ast then ast.parent_block = block @@ -476,11 +490,7 @@ local function parse(state, s, name, source) if not indented then return nil, e end -- build state proxy local state_proxy = { - inject = { - function_start = nil, function_end = nil, - scoped_function_start = nil, scoped_function_end = nil, - checkpoint_start = nil, checkpoint_end = nil - }, + inject = {}, aliases = setmetatable({}, { __index = state.aliases }), variables = setmetatable({}, { __index = state.aliases }), functions = setmetatable({}, { @@ -500,11 +510,11 @@ local function parse(state, s, name, source) global_state = state } -- parse injects - for _, inject in ipairs{"function_start", "function_end", "scoped_function_start", "scoped_function_end", "checkpoint_start", "checkpoint_end"} do - if state.inject[inject] then - local inject_indented, err = parse_indented(state.inject[inject], nil, "injected "..inject:gsub("_", " ")) + for inject, ninject in pairs(injections) do + if state.inject[ninject] then + local inject_indented, err = parse_indented(state.inject[ninject], nil, "injected "..inject) if not inject_indented then return nil, err end - state_proxy.inject[inject] = inject_indented + state_proxy.inject[ninject] = inject_indented end end -- parse @@ -535,7 +545,7 @@ end package.loaded[...] = parse local common = require((...):gsub("preparser$", "common")) -format_identifier, identifier_pattern, escape, special_functions_names, pretty_signature, signature = common.format_identifier, common.identifier_pattern, common.escape, common.special_functions_names, common.pretty_signature, common.signature +format_identifier, identifier_pattern, escape, special_functions_names, pretty_signature, signature, injections = common.format_identifier, common.identifier_pattern, common.escape, common.special_functions_names, common.pretty_signature, common.signature, common.injections copy = require((...):gsub("parser%.preparser$", "common")).copy return parse diff --git a/test/tests/return children.ans b/test/tests/return children.ans new file mode 100644 index 0000000..452a8a4 --- /dev/null +++ b/test/tests/return children.ans @@ -0,0 +1,13 @@ +$ fn + :i=0 + @i + ~ i:=5 + {i} + +{fn} = 50 + +$ g + @0 + @3 + +{g} = 3 diff --git a/test/tests/return children.lua b/test/tests/return children.lua new file mode 100644 index 0000000..b3ab770 --- /dev/null +++ b/test/tests/return children.lua @@ -0,0 +1,37 @@ +local _={} +_[15]={} +_[14]={} +_[13]={} +_[12]={} +_[11]={} +_[10]={text=" = 3",tags=_[15]} +_[9]={text="3",tags=_[14]} +_[8]={text=" = 50",tags=_[13]} +_[7]={text="0",tags=_[12]} +_[6]={text="5",tags=_[11]} +_[5]={_[9],_[10]} +_[4]={_[6],_[7],_[8]} +_[3]={"return"} +_[2]={"text",_[5]} +_[1]={"text",_[4]} +return {_[1],_[2],_[3]} +--[[ +{ "text", { { + tags = {}, + text = "5" + }, { + tags = {}, + text = "0" + }, { + tags = {}, + text = " = 50" + } } } +{ "text", { { + tags = {}, + text = "3" + }, { + tags = {}, + text = " = 3" + } } } +{ "return" } +]]-- \ No newline at end of file