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

LuaFunction cleanup: replace by LuaCall

This commit is contained in:
Étienne Fildadut 2024-01-07 20:31:28 +01:00
parent 85d17e9519
commit 51ad18c5a4
22 changed files with 192 additions and 162 deletions

View file

@ -1,30 +1,20 @@
-- note: functions only appear in non-evaluated nodes! once evaluated, they always become closures
local ast = require("anselme.ast")
local Overloadable = ast.abstract.Overloadable
local ReturnBoundary, Environment, Identifier, Symbol
local ReturnBoundary, Environment, Identifier
local operator_priority = require("anselme.common").operator_priority
local resume_manager, calling_environment_manager
local function list_cache_upvalues(v, state, list, scope)
if Identifier:is(v) then
list[v.name] = scope:precache(state, v)
elseif Symbol:is(v) then
list[v.string] = scope:precache(state, v:to_identifier())
end
v:traverse(list_cache_upvalues, state, list, scope)
end
local Function
Function = Overloadable {
Function = ast.abstract.Overloadable {
type = "function",
parameters = nil, -- ParameterTuple
expression = nil, -- function content
scope = nil, -- Environment; captured scope for closure (evaluated functions); not set when not evaluated
upvalues = nil, -- {[name]=variable metadata}; not set when not evaluated. Contain _at least_ all the upvalues explicitely defined in the function code.
upvalues = nil, -- {variable metadata, ...}; not set when not evaluated. Contain _at least_ all the upvalues explicitely defined in the function code.
init = function(self, parameters, expression, scope, upvalues)
self.parameters = parameters
@ -64,10 +54,15 @@ Function = Overloadable {
state.scope:pop()
-- list & cache upvalues so they aren't affected by future redefinition in a parent scope
local upvalues = {}
self.expression:traverse(list_cache_upvalues, state, upvalues, scope)
local used_identifiers = {}
self.parameters:list_used_identifiers(used_identifiers)
self.expression:list_used_identifiers(used_identifiers)
if scope:defined(state, Identifier:new("_")) then
scope:get(state, Identifier:new("_")):traverse(list_cache_upvalues, state, upvalues, scope)
scope:get(state, Identifier:new("_")):list_used_identifiers(used_identifiers)
end
local upvalues = {}
for _, identifier in ipairs(used_identifiers) do
table.insert(upvalues, scope:precache(state, identifier))
end
return Function:new(self.parameters:eval(state), self.expression, scope, upvalues)
@ -83,7 +78,7 @@ Function = Overloadable {
return self.parameters:hash()
end,
call_dispatched = function(self, state, args)
assert(self.scope, "can't call unevaluated function")
assert(self._evaluated, "can't call unevaluated function")
-- push captured closure scope
local calling_environment = state.scope:capture()
@ -102,7 +97,7 @@ Function = Overloadable {
end,
resume = function(self, state, target)
if self.parameters.min_arity > 0 then error("can't resume function with parameters") end
assert(self.scope, "can't resume unevaluated function")
assert(self._evaluated, "can't resume unevaluated function")
-- push captured closure scope
local calling_environment = state.scope:capture()
@ -126,28 +121,45 @@ Function = Overloadable {
-- Only the upvalues that explicitely appear in the function body and variables directly defined in the function scope will be saved, so we don't have to keep a full copy of the whole environment.
_serialize = function(self)
local state = require("anselme.serializer_state")
return { parameters = self.parameters, expression = self.expression, upvalues = self.upvalues, scope = self.scope.variables:to_struct(state) }
return self.parameters, self.expression, self.scope.variables:to_struct(state), self.upvalues
end,
_deserialize = function(self)
local state = require("anselme.serializer_state")
local scope
if self.upvalues then
-- rebuild scope: exported + normal layer so any upvalue that happen to be exported stay there
-- (and link again to current scope to allow internal vars that are not serialized to still work, like _translations)
scope = Environment:new(state, Environment:new(state, state.scope:capture(), nil, true))
for _, var in pairs(self.upvalues) do
scope:define(state, var:get_symbol(), var:get(state))
_deserialize = function(parameters, expression, variables, upvalues)
local r = Function:new(parameters, expression):set_source("saved")
r._deserialize_data = { variables = variables, upvalues = upvalues }
return r
end,
-- we need variables to be fully deserialized before rebuild the function scope
_post_deserialize = function(self, state, cache)
if self._deserialize_data then
local upvalues, variables = self._deserialize_data.upvalues, self._deserialize_data.variables
self._deserialize_data = nil
-- rebuild upvalues: exported + normal layer so any upvalue that happen to be exported stay there
-- (and link again to global scope to allow internal vars that are not serialized to still work, like _translations)
state.scope:push_global()
state.scope:push_export()
state.scope:push()
self.upvalues = {}
for _, var in ipairs(upvalues) do
state.scope:define(var:get_symbol(), var:get(state))
table.insert(self.upvalues, state.scope.current:precache(state, var:get_symbol():to_identifier()))
end
for _, var in self.scope:iter() do
scope:define(state, var:get_symbol(), var:get(state))
-- rebuild function variables
state.scope:push()
self.scope = state.scope:capture()
for _, var in variables:iter() do
self.scope:define(state, var:get_symbol(), var:get(state))
end
state.scope:pop()
state.scope:pop()
state.scope:pop()
state.scope:pop()
self._evaluated = true
end
return Function:new(self.parameters, self.expression, Environment:new(state, scope), self.upvalues):set_source("saved")
end
}
package.loaded[...] = Function
ReturnBoundary, Environment, Identifier, Symbol = ast.ReturnBoundary, ast.Environment, ast.Identifier, ast.Symbol
ReturnBoundary, Environment, Identifier = ast.ReturnBoundary, ast.Environment, ast.Identifier
resume_manager = require("anselme.state.resume_manager")
calling_environment_manager = require("anselme.state.calling_environment_manager")

View file

@ -23,6 +23,13 @@ Identifier = ast.abstract.Node {
return state.scope:get(self)
end,
list_used_identifiers = function(self, t)
if not t[self.name] then
t[self.name] = true
table.insert(t, self)
end
end,
to_string = function(self)
return String:new(self.name)
end,

60
anselme/ast/LuaCall.lua Normal file
View file

@ -0,0 +1,60 @@
local ast = require("anselme.ast")
local unpack = table.unpack or unpack
local calling_environment_manager
local LuaCall
LuaCall = ast.abstract.Runtime {
type = "lua call",
hide_in_stacktrace = true,
_evaluated = false,
parameters = nil, -- ParameterTuple, may be unevaluated
func = nil, -- lua function
init = function(self, parameters, func)
self.parameters = parameters
self.func = func
end,
make_function = function(self, state, parameters, func)
local fn = ast.Function:new(parameters, LuaCall:new(parameters, func))
return fn:eval(state)
end,
traverse = function(self, fn, ...)
fn(self.parameters, ...)
end,
_hash = function(self)
return ("%s<%s;%s>"):format(self.type, self.parameters:hash(), tostring(self.func))
end,
_format = function(self, ...)
return "<lua function>"
end,
_eval = function(self, state)
-- get arguments
local lua_args = { state }
for _, param in ipairs(self.parameters.list) do
table.insert(lua_args, state.scope:get(param.identifier))
end
-- run function, in calling environment
state.scope:push(calling_environment_manager:get_level(state, 1))
local r = self.func(unpack(lua_args))
assert(r, "lua function returned no value")
state.scope:pop()
return r
end,
to_lua = function(self, state)
return self.func
end,
}
package.loaded[...] = LuaCall
calling_environment_manager = require("anselme.state.calling_environment_manager")
return LuaCall

View file

@ -1,83 +0,0 @@
local ast = require("anselme.ast")
local Overloadable = ast.abstract.Overloadable
local operator_priority = require("anselme.common").operator_priority
local unpack = table.unpack or unpack
local calling_environment_manager
local LuaFunction
LuaFunction = ast.abstract.Runtime(Overloadable) {
type = "lua function",
parameters = nil, -- ParameterTuple
func = nil, -- lua function
init = function(self, parameters, func)
self.parameters = parameters
self.func = func
end,
traverse = function(self, fn, ...)
fn(self.parameters, ...)
end,
_hash = function(self)
return ("%s<%s;%s>"):format(self.type, self.parameters:hash(), tostring(self.func))
end,
_format = function(self, ...)
if self.parameters.assignment then
return "$"..self.parameters:format(...).."; <lua function>"
else
return "$"..self.parameters:format(...).." <lua function>"
end
end,
_format_priority = function(self)
return operator_priority["$_"]
end,
compatible_with_arguments = function(self, state, args)
return args:match_parameter_tuple(state, self.parameters)
end,
format_signature = function(self, state)
return "$"..self.parameters:format_short(state)
end,
hash_signature = function(self)
return self.parameters:hash()
end,
call_dispatched = function(self, state, args)
local calling_environment = state.scope:capture()
calling_environment_manager:push(state, calling_environment)
local lua_args = { state }
state.scope:push()
args:bind_parameter_tuple(state, self.parameters)
for _, param in ipairs(self.parameters.list) do
table.insert(lua_args, state.scope:get(param.identifier))
end
state.scope:pop()
local r = self.func(unpack(lua_args))
assert(r, "lua function returned no value")
calling_environment_manager:pop(state)
return r
end,
_eval = function(self, state)
return LuaFunction:new(self.parameters:eval(state), self.func)
end,
to_lua = function(self, state)
return self.func
end,
}
package.loaded[...] = LuaFunction
calling_environment_manager = require("anselme.state.calling_environment_manager")
return LuaFunction

View file

@ -1,5 +1,5 @@
local ast = require("anselme.ast")
local Identifier
local Identifier, Symbol
local String = ast.abstract.Node {
type = "string",
@ -25,10 +25,13 @@ local String = ast.abstract.Node {
to_identifier = function(self)
return Identifier:new(self.string)
end,
to_symbol = function(self)
return Symbol:new(self.string)
end
}
package.loaded[...] = String
Identifier = ast.Identifier
Identifier, Symbol = ast.Identifier, ast.Symbol
return String

View file

@ -44,7 +44,9 @@ local Translatable = ast.abstract.Node {
end,
list_translatable = function(self, t)
t = t or {}
table.insert(t, self)
return t
end
}

View file

@ -45,6 +45,9 @@ traverse = {
merge = function(self, state, cache)
self:merge(state, cache)
end,
post_deserialize = function(self, state, cache)
self:post_deserialize(state, cache)
end,
hash = function(self, t)
table.insert(t, self:hash())
end,
@ -55,6 +58,9 @@ traverse = {
for hash, target in pairs(self:list_resume_targets()) do
add_to_node._list_resume_targets_cache[hash] = target
end
end,
list_used_identifiers = function(self, t)
self:list_used_identifiers(t)
end
}
@ -134,7 +140,7 @@ Node = class {
end,
-- generate a list of translatable nodes that appear in this node
-- should only be called on non-evaluated nodes
-- should only be called on non-evaluated nodes (non-evaluated nodes don't contain cycles)
-- if a node is translatable, redefine this to add it to the table - note that it shouldn't call :traverse or :list_translatable on its children, as nested translations should not be needed
list_translatable = function(self, t)
t = t or {}
@ -142,6 +148,14 @@ Node = class {
return t
end,
-- generate a list of identifiers that are used in this node
-- should only be called on non-evaluated nodes
-- the list contains at least all used nodes; some unused nodes may be present
-- redefine on identifier nodes to add an identifier to the table
list_used_identifiers = function(self, t)
self:traverse(traverse.list_used_identifiers, t)
end,
-- generate anselme code that can be used as a base for a translation file
-- will include every translatable element found in this node and its children
generate_translation_template = function(self)
@ -388,8 +402,21 @@ Node = class {
package.loaded["anselme.serializer_state"] = state
local r = binser.deserializeN(str, 1, index)
package.loaded["anselme.serializer_state"] = nil
r:post_deserialize(state, {})
return r
end,
-- call :_post_deserialize on all children nodes
-- automatically called after a sucesfull deserialization, intended to perform finishing touches for deserialization
-- notably, there is no guarantee that children nodes are already deserialized when _deserialize is called on a node
-- anything that require such a guarantee should be done here
post_deserialize = function(self, state, cache)
if not cache[self] then
cache[self] = true
self:_post_deserialize(state, cache)
self:traverse(traverse.post_deserialize, state, cache)
end
end,
_post_deserialize = function(self, state, cache) end,
__tostring = function(self) return self:format() end,

View file

@ -6,7 +6,7 @@ local ast = require("anselme.ast")
local to_anselme = require("anselme.common.to_anselme")
local unpack = table.unpack or unpack
local LuaFunction, Environment, Node
local LuaCall, Environment, Node
local parameter_tuple = require("anselme.parser.expression.contextual.parameter_tuple")
local symbol = require("anselme.parser.expression.primary.symbol")
@ -64,7 +64,7 @@ local ScopeStack = class {
return to_anselme(original_func(unpack(lua_args)))
end
end
self:define_overloadable(sym, LuaFunction:new(parameters, func):eval(self.state))
self:define_overloadable(sym, LuaCall:make_function(self.state, parameters, func))
elseif Node:issub(value) then
self:define(sym, value)
else
@ -150,6 +150,6 @@ local ScopeStack = class {
}
package.loaded[...] = ScopeStack
LuaFunction, Environment, Node = ast.LuaFunction, ast.Environment, ast.abstract.Node
LuaCall, Environment, Node = ast.LuaCall, ast.Environment, ast.abstract.Node
return ScopeStack

View file

@ -10,7 +10,7 @@ return {
"attached block", "(level::is number=1, keep return=false)",
function(state, level, keep_return)
-- level 2: env of the function that called the function that called attached block
local env = calling_environment_manager:get_level(state, level:to_lua(state)+1)
local env = calling_environment_manager:get_level(state, level:to_lua(state))
local block = env:get(state, block_identifier).expression
local fn
if keep_return:truthy() then
@ -28,7 +28,7 @@ return {
"attached block", "(level::is number=1, keep return=false, default)",
function(state, level, keep_return, default)
-- level 2: env of the function that called the function that called attached block
local env = calling_environment_manager:get_level(state, level:to_lua(state)+1)
local env = calling_environment_manager:get_level(state, level:to_lua(state))
if env:defined(state, block_identifier) then
local block = env:get(state, block_identifier).expression
local fn

View file

@ -17,16 +17,10 @@ return {
return func:call(state, ArgumentTuple:new())
end
},
{
"resuming", "()",
function(state)
return Boolean:new(resume_manager:resuming(state))
end
},
{
"resuming", "(level::is number=0)",
function(state, level)
local env = calling_environment_manager:get_level(state, level:to_lua(state)+1)
local env = calling_environment_manager:get_level(state, level:to_lua(state))
state.scope:push(env)
local r = Boolean:new(resume_manager:resuming(state))
state.scope:pop()

View file

@ -9,9 +9,10 @@ return [[
fn.:check = $(anchor::is anchor)
fn.reached(anchor) = (fn.reached(anchor) | 0) + 1
fn.:checkpoint = $(anchor::is anchor, on resume=attached block(default=()))
:resuming = resuming(1) /* calling function is resuming */
if(on resume)
fn.current checkpoint = anchor
if(resume target == anchor | resuming(4))
if(resume target == anchor | resuming)
on resume!
else!
fn.reached(anchor) = (fn.reached(anchor) | 0) + 1

View file

@ -1,5 +1,5 @@
local ast = require("anselme.ast")
local Nil, List, Table, Number, LuaFunction, ParameterTuple, Boolean = ast.Nil, ast.List, ast.Table, ast.Number, ast.LuaFunction, ast.ParameterTuple, ast.Boolean
local Nil, List, Table, Number, LuaCall, ParameterTuple, Boolean = ast.Nil, ast.List, ast.Table, ast.Number, ast.LuaCall, ast.ParameterTuple, ast.Boolean
return {
-- tuple
@ -122,7 +122,7 @@ return {
"iter", "(s::is struct)",
function(state, struct)
local iter = struct:iter()
return LuaFunction:new(ParameterTuple:new(), function()
return LuaCall:make_function(state, ParameterTuple:new(), function()
local k = iter()
if k == nil then return Nil:new()
else return k end

View file

@ -5,7 +5,7 @@
--- text ---
| {}"" {}"42" {}"" |
--- error ---
identifier "z" is undefined in branch 46991f07-7340-4104-9f35-c1fb62e95088
identifier "z" is undefined in branch 1c25ebb8-5027-4ccf-105b9-370d83e78fd7
↳ from test/tests/exported variable nested.ans:12:3 in identifier: z
↳ from test/tests/exported variable nested.ans:12:1 in text interpolation: | {z} |
↳ from ? in block: :f = ($() _)…

View file

@ -1,6 +1,6 @@
--# run #--
--- error ---
can't call overload iter: no function match arguments (42), possible functions were:
can't call overload iter: no function match arguments (value), possible functions were:
• $(table::is table) (from stdlib/for.ans:46:1):
type check failure for parameter table
• $(tuple::is sequence) (from stdlib/for.ans:37:1):

View file

@ -4,7 +4,7 @@
--- text ---
| {}"" {}"idk" {}" is esperanto" |
--- error ---
can't call overload a: no function match arguments (type(5, "nope")), possible functions were:
can't call overload a: no function match arguments (value), possible functions were:
• $(name::($(x) type(x) == t)) (from test/tests/function custom type dispatch error.ans:7:1):
type check failure for parameter name
• $(name::($(x) type(x) == t)) (from test/tests/function custom type dispatch error.ans:4:1):

View file

@ -2,14 +2,14 @@
--- text ---
| {}"a" |
--- text ---
| {}"c" |
--- text ---
| {}"b" |
--- text ---
| {}"c" |
--- text ---
| {}"a" |
--- text ---
| {}"b" |
| {}"c" |
--- return ---
()
--# saved #--
{"a.checkpoint":false, "a.run":2, "b.checkpoint":false, "b.run":2, "c.checkpoint":false, "c.run":1}
{"a.checkpoint":false, "a.run":1, "b.checkpoint":false, "b.run":1, "c.checkpoint":false, "c.run":3}

View file

@ -1,6 +1,6 @@
--# run #--
--- error ---
identifier "b" is undefined in branch 46991f07-7340-4104-9f35-c1fb62e95088
identifier "b" is undefined in branch 1c25ebb8-5027-4ccf-105b9-370d83e78fd7
↳ from test/tests/function scope wrong.ans:4:7 in identifier: b
↳ from test/tests/function scope wrong.ans:4:1 in text interpolation: | a: {b} |
↳ from ? in block: :a = ($() _)…

View file

@ -1,17 +1,23 @@
--# run #--
--- error ---
can't call overload _._: no function match arguments (overload([($(b) _), ($(x) _), ($() _)]), "a"), possible functions were:
• $(s::is script, k::is symbol) = val (from stdlib/script.ans:44:1):
can't call overload _._: no function match arguments (value, "a"), possible functions were:
• $(s::is script, k::is symbol) = val (from stdlib/script.ans:45:1):
expected 3 arguments, received 2
• $(s::is script, k::is string) = val (from stdlib/script.ans:42:1):
• $(s::is script, k::is string) = val (from stdlib/script.ans:43:1):
expected 3 arguments, received 2
• $(s::is script, k::is string) (from stdlib/script.ans:40:1):
• $(s::is script, k::is string) (from stdlib/script.ans:41:1):
type check failure for parameter s
• $(c::is function, s::is symbol) = v (from test/tests/function separate variable from variants.ans:10:4):
expected 3 arguments, received 2
• $(c::is function, s::is string) = v (from test/tests/function separate variable from variants.ans:10:4):
expected 3 arguments, received 2
• $(c::is function, s::is string) (from test/tests/function separate variable from variants.ans:10:4):
type check failure for parameter c
• $(c::is environment, s::is symbol) = v (from test/tests/function separate variable from variants.ans:10:4):
expected 3 arguments, received 2
• $(c::is environment, s::is string) = v (from test/tests/function separate variable from variants.ans:10:4):
expected 3 arguments, received 2
• $(c::is environment, s::is string) (from test/tests/function separate variable from variants.ans:10:4):
type check failure for parameter c
↳ from test/tests/function separate variable from variants.ans:10:4 in call: f . "a"
↳ from test/tests/function separate variable from variants.ans:10:1 in text interpolation: | {f . "a"} = 2 |

View file

@ -4,14 +4,14 @@
↳ from test/tests/merge nested mutable error bis.ans:14:7 in call: error("abort")
↳ from test/tests/merge nested mutable error bis.ans:3:1 in block: insert(a, b)…
↳ from test/tests/merge nested mutable error bis.ans:3:18 in call: _
↳ from stdlib/script.ans:30:6 in call: fn!
↳ from stdlib/script.ans:28:3 in block: resume target = ()…
↳ from stdlib/script.ans:28:7 in call: else!
↳ from stdlib/script.ans:24:2 in block: if(fn . "current checkpoint")…
↳ from stdlib/script.ans:24:9 in call: _
↳ from stdlib/script.ans:38:9 in call: value(s)!
↳ from stdlib/script.ans:37:1 in block: value(s)!
↳ from stdlib/script.ans:37:20 in call: _
↳ from stdlib/script.ans:31:6 in call: fn!
↳ from stdlib/script.ans:29:3 in block: resume target = ()…
↳ from stdlib/script.ans:29:7 in call: else!
↳ from stdlib/script.ans:25:2 in block: if(fn . "current checkpoint")…
↳ from stdlib/script.ans:25:9 in call: _
↳ from stdlib/script.ans:39:9 in call: value(s)!
↳ from stdlib/script.ans:38:1 in block: value(s)!
↳ from stdlib/script.ans:38:20 in call: _
↳ from test/tests/merge nested mutable error bis.ans:19:2 in call: f!
↳ from ? in block: :a = *[1]…
--# post run check #--

View file

@ -4,14 +4,14 @@
↳ from test/tests/merge nested mutable error.ans:14:7 in call: error("abort")
↳ from test/tests/merge nested mutable error.ans:3:1 in block: insert(a, b)…
↳ from test/tests/merge nested mutable error.ans:3:18 in call: _
↳ from stdlib/script.ans:30:6 in call: fn!
↳ from stdlib/script.ans:28:3 in block: resume target = ()…
↳ from stdlib/script.ans:28:7 in call: else!
↳ from stdlib/script.ans:24:2 in block: if(fn . "current checkpoint")…
↳ from stdlib/script.ans:24:9 in call: _
↳ from stdlib/script.ans:38:9 in call: value(s)!
↳ from stdlib/script.ans:37:1 in block: value(s)!
↳ from stdlib/script.ans:37:20 in call: _
↳ from stdlib/script.ans:31:6 in call: fn!
↳ from stdlib/script.ans:29:3 in block: resume target = ()…
↳ from stdlib/script.ans:29:7 in call: else!
↳ from stdlib/script.ans:25:2 in block: if(fn . "current checkpoint")…
↳ from stdlib/script.ans:25:9 in call: _
↳ from stdlib/script.ans:39:9 in call: value(s)!
↳ from stdlib/script.ans:38:1 in block: value(s)!
↳ from stdlib/script.ans:38:20 in call: _
↳ from test/tests/merge nested mutable error.ans:19:2 in call: f!
↳ from ? in block: :a = *[1]…
--# post run check #--

View file

@ -1,3 +1,4 @@
:z = 1
:x = $
2+z
x.:z = 3

View file

@ -1,4 +1,4 @@
:choice = 1
:@choice = 1
:$ jump button
1 # | A