1
0
Fork 0
mirror of https://github.com/Reuh/anselme.git synced 2025-10-27 16:49:31 +00:00

Add "function return" and "scoped function return" injections, allow children for return lines

This commit is contained in:
Étienne Fildadut 2022-01-16 15:32:59 +01:00
parent b60f53df01
commit 933e8fb0ee
7 changed files with 131 additions and 72 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,13 @@
$ fn
:i=0
@i
~ i:=5
{i}
{fn} = 50
$ g
@0
@3
{g} = 3

View file

@ -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" }
]]--