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

Exported variables predefinition: replace prepare system with generic resume system

This commit is contained in:
Étienne Fildadut 2023-12-30 15:31:00 +01:00
parent e2ec105a4b
commit 3edf65dc2a
19 changed files with 155 additions and 136 deletions

View file

@ -1,16 +1,13 @@
local ast = require("anselme.ast")
local resume_manager
local Anchor
Anchor = ast.abstract.Node {
Anchor = ast.abstract.ResumeTarget {
type = "anchor",
name = nil,
init = function(self, name)
self.name = name
self._list_anchors_cache = { [name] = true }
end,
_hash = function(self)
@ -20,16 +17,8 @@ Anchor = ast.abstract.Node {
_format = function(self, ...)
return "#"..self.name
end,
_eval = function(self, state)
if self:contains_resume_target(state) then
resume_manager:set_reached(state)
end
return Anchor:new(self.name)
end
}
package.loaded[...] = Anchor
resume_manager = require("anselme.state.resume_manager")
return Anchor

View file

@ -36,17 +36,18 @@ local Block = ast.abstract.Node {
_eval = function(self, state)
local r
state.scope:push()
if self:contains_resume_target(state) then
local anchor = resume_manager:get(state)
if self:contains_current_resume_target(state) then
local target = resume_manager:get(state)
local no_continue = resume_manager:no_continue(state)
local resumed = false
for _, e in ipairs(self.expressions) do
if e:contains_anchor(anchor) then resumed = true end
if e:contains_resume_target(target) then resumed = true end
if resumed then
r = e:eval(state)
if AutoCall:issub(r) then
r = r:call(state, ArgumentTuple:new())
end
if Return:is(r) then
if Return:is(r) or no_continue then
break -- pass on to parent block until we reach a function boundary
end
end
@ -65,14 +66,6 @@ local Block = ast.abstract.Node {
state.scope:pop()
return r or Nil:new()
end,
_prepare = function(self, state)
state.scope:push()
for _, e in ipairs(self.expressions) do
e:prepare(state)
end
state.scope:pop()
end
}
package.loaded[...] = Block

View file

@ -4,6 +4,8 @@ local ast = require("anselme.ast")
local Overloadable, Runtime = ast.abstract.Overloadable, ast.abstract.Runtime
local Definition
local resume_manager
local Closure
Closure = Runtime(Overloadable) {
type = "closure",
@ -21,8 +23,14 @@ Closure = Runtime(Overloadable) {
self.exported_scope = state.scope:capture()
-- pre-define exports
for sym, exp in pairs(self.func.exports) do
Definition:new(sym, exp):eval(state)
for _, target in pairs(self:list_resume_targets()) do
if Definition:is(target) and target.symbol.exported then
resume_manager:push_no_continue(state, target)
state.scope:push() -- create temp func scope, in case non-export definitions are done in the resume
self.func.expression:eval(state)
state.scope:pop()
resume_manager:pop(state)
end
end
state.scope:pop()
@ -54,5 +62,6 @@ Closure = Runtime(Overloadable) {
package.loaded[...] = Closure
Definition = ast.Definition
resume_manager = require("anselme.state.resume_manager")
return Closure

View file

@ -3,7 +3,7 @@ local Nil, Overloadable
local operator_priority = require("anselme.common").operator_priority
local Definition = ast.abstract.Node {
local Definition = ast.abstract.ResumeTarget {
type = "definition",
symbol = nil,
@ -46,17 +46,6 @@ local Definition = ast.abstract.Node {
return Nil:new()
end,
_prepare = function(self, state)
local symbol, val = self.symbol, self.expression
symbol:prepare(state)
val:prepare(state)
-- predefine exported variables
if symbol.exported then
self:eval(state)
end
end
}
package.loaded[...] = Definition

View file

@ -13,8 +13,6 @@ Function = Overloadable {
parameters = nil, -- ParameterTuple
expression = nil,
exports = nil, -- { [sym] = exp, ... }, exctracted from expression during :prepare
init = function(self, parameters, expression, exports)
self.parameters = parameters
self.expression = ReturnBoundary:new(expression)
@ -55,7 +53,8 @@ Function = Overloadable {
state.scope:pop()
-- reminder: don't do any additionnal processing here as that won't be executed when resuming self.expression
-- reminder: don't do any additionnal processing here as that won't be executed when resuming self.expression directly
-- which is done in a few places, notably to predefine exports in Closure
-- instead wrap it in some additional node, like our friend ReturnBoundary
return exp
@ -64,18 +63,6 @@ Function = Overloadable {
_eval = function(self, state)
return Closure:new(Function:new(self.parameters:eval(state), self.expression, self.exports), state)
end,
_prepare = function(self, state)
state.scope:push_export() -- recreate scope context that will be created by closure
state.scope:push()
self.parameters:prepare(state)
self.expression:prepare(state)
state.scope:pop()
self.exports = state.scope:capture():list_exported(state)
state.scope:pop()
end,
}
package.loaded[...] = Function

View file

@ -29,12 +29,6 @@ Identifier = ast.abstract.Node {
to_symbol = function(self, modifiers)
return Symbol:new(self.name, modifiers)
end,
_prepare = function(self, state)
if state.scope:defined(self) then
state.scope:get(self):prepare(state)
end
end
}
package.loaded[...] = Identifier

View file

@ -56,13 +56,6 @@ PartialScope = ast.abstract.Node {
return exp
end,
_prepare = function(self, state)
state.scope:push_partial(unpack(self._identifiers))
for sym, val in pairs(self.definitions) do state.scope:define(sym, val) end
self.expression:prepare(state)
state.scope:pop()
end,
-- class method: if the identifier is currently defined, wrap node in an PartialScope so the identifier is still defined in this node
-- used to e.g. preserve the defined _ block without the need to build a full closure
-- used e.g. for -> translation, as we want to preserve _ while still executing the translation in the Translatable scope and not restore a different scope from a closure

View file

@ -13,7 +13,7 @@ local utf8 = utf8 or require("lua-utf8")
local uuid = require("anselme.common").uuid
local State, Runtime, Call, Identifier, ArgumentTuple
local Call, Identifier, ArgumentTuple
local resume_manager
local custom_call_identifier
@ -37,9 +37,6 @@ traverse = {
set_source = function(self, source)
self:set_source(source)
end,
prepare = function(self, state)
self:prepare(state)
end,
merge = function(self, state, cache)
self:merge(state, cache)
end,
@ -48,6 +45,11 @@ traverse = {
end,
list_translatable = function(self, t)
self:list_translatable(t)
end,
list_resume_targets = function(self, add_to_node)
for hash, target in pairs(self:list_resume_targets()) do
add_to_node._list_resume_targets_cache[hash] = target
end
end
}
@ -100,52 +102,29 @@ Node = class {
return self
end,
-- prepare the AST after parsing and before evaluation
-- this behave like a cached :traverse through the AST, except this keeps track of the scope stack and preserve evaluation order
-- i.e. when :prepare is called on a node, it should be in a similar scope stack context and order as will be when it will be evaluated
-- used to predefine exported variables and other compile-time variable handling
-- note: the state here is a temporary state only used during the prepare step
-- the actual preparation is done in _prepare
-- (this can mutate the node as needed and is automatically called after each parse)
prepare = function(self, state)
assert(not Runtime:issub(self), ("can't prepare a %s node that should only exist at runtime"):format(self.type))
state = state or State:new()
if self._prepared then return end
local s, r = pcall(self._prepare, self, state)
if s then
self._prepared = true
else
error(format_error(state, self, r), 0)
-- returns a reversed list { [target hash] = true, ... } of the resume targets contained in this node and its children
-- this is cached, redefine _list_resume_targets if needed, not this function
list_resume_targets = function(self)
if not self._list_resume_targets_cache then
self._list_resume_targets_cache = {}
self:_list_resume_targets()
end
return self._list_resume_targets_cache
end,
_prepared = false, -- indicate that the node was prepared and :prepare should nop
-- prepare this node. can mutate the node (considered to be part of construction).
_prepare = function(self, state)
self:traverse(traverse.prepare, state)
_list_resume_targets_cache = nil, -- list resume target cache { [target hash] = target, ... }
-- add resume targets to _list_resume_targets_cache
_list_resume_targets = function(self)
self:traverse(traverse.list_resume_targets, self)
end,
-- returns a reversed list { [anchor] = true, ... } of the anchors contained in this node and its children
_list_anchors = function(self)
if not self._list_anchors_cache then
self._list_anchors_cache = {}
self:traverse(function(v)
for name in pairs(v:_list_anchors()) do
self._list_anchors_cache[name] = true
end
end)
end
return self._list_anchors_cache
end,
_list_anchors_cache = nil,
-- returns true if the node or its children contains the anchor
contains_anchor = function(self, anchor)
return not not self:_list_anchors()[anchor.name]
-- returns true if the node or its children contains the resume target
contains_resume_target = function(self, target)
return not not self:list_resume_targets()[target:hash()]
end,
-- returns true if we are currently trying to resume to an anchor target contained in the current node
contains_resume_target = function(self, state)
return resume_manager:resuming(state) and self:contains_anchor(resume_manager:get(state))
-- returns true if we are currently trying to resume to a resume target contained in the current node
contains_current_resume_target = function(self, state)
return resume_manager:resuming(state) and self:contains_resume_target(resume_manager:get(state))
end,
-- generate a list of translatable nodes that appear in this node
@ -332,10 +311,9 @@ Node = class {
-- Thus, any require here that may require other Nodes shall be done here. This method is called in anselme.lua after everything else is required.
_i_hate_cycles = function(self)
local ast = require("anselme.ast")
Runtime, Call, Identifier, ArgumentTuple = ast.abstract.Runtime, ast.Call, ast.Identifier, ast.ArgumentTuple
Call, Identifier, ArgumentTuple = ast.Call, ast.Identifier, ast.ArgumentTuple
custom_call_identifier = Identifier:new("_!")
State = require("anselme.state.State")
resume_manager = require("anselme.state.resume_manager")
end,

View file

@ -0,0 +1,27 @@
-- nodes that can be resumed to
local ast = require("anselme.ast")
local Node = ast.abstract.Node
local resume_manager
local ResumeTarget = Node {
type = "resume target",
init = false,
_list_resume_targets = function(self)
self._list_resume_targets_cache[self:hash()] = self
end,
eval = function(self, state)
if self:contains_current_resume_target(state) then
resume_manager:set_reached(state)
end
return Node.eval(self, state)
end
}
package.loaded[...] = ResumeTarget
resume_manager = require("anselme.state.resume_manager")
return ResumeTarget

View file

@ -1,5 +1,5 @@
-- indicate a Runtime node: it should not exist in the AST generated by the parser but only as a result of an evaluation or call
-- is assumed to be already evaluated and prepared (will actually error on prepare)
-- is assumed to be already evaluated; we reserve the right to error if a Runtime node occurs in something that was never evaluated
local ast = require("anselme.ast")
@ -8,5 +8,4 @@ return ast.abstract.Node {
init = false,
_evaluated = true,
_prepared = true
}

View file

@ -158,7 +158,7 @@ end
return primary {
match = function(self, str)
return str:match("^%::?[&@]?%$")
return str:match("^%::?&?@?%$")
end,
parse = function(self, source, str, limit_pattern)

View file

@ -8,8 +8,8 @@ local Nil = ast.Nil
return primary {
match = function(self, str)
if str:match("^%::?[&@]?") then
return identifier:match(str:match("^%::?[&@]?(.-)$"))
if str:match("^%::?&?@?") then
return identifier:match(str:match("^%::?&?@?(.-)$"))
end
return false
end,

View file

@ -7,7 +7,5 @@ return function(code, source)
local tree = code_to_tree(code, source)
local block = tree_to_ast(tree)
block:prepare()
return block
end

View file

@ -1,44 +1,64 @@
local class = require("anselme.lib.class")
local ast = require("anselme.ast")
local Nil, Identifier, Anchor
local Nil, Identifier, ResumeTarget, Boolean
-- stack of resumable contexts
local resume_anchor_identifier, resume_anchor_symbol
local resume_target_identifier, resume_target_symbol, resume_no_continue_identifier, resume_no_continue_symbol
local resume_manager = class {
init = false,
-- push a new resume context: all run code between this and the next push will try to resume to anchor
push = function(self, state, anchor)
assert(Anchor:is(anchor), "can only resume to an anchor target") -- well technically it wouldn't be hard to allow to resume to any node, but I feel like there's already enough stuff in Anselme that was done just because it could be done
state.scope:push_partial(resume_anchor_identifier)
state.scope:define(resume_anchor_symbol, anchor)
-- push a new resume context: all run code between this and the next push will try to resume to target
push = function(self, state, target)
assert(ResumeTarget:issub(target), "can only resume to a resume target")
state.scope:push_partial(resume_target_identifier, resume_no_continue_identifier)
state.scope:define(resume_target_symbol, target)
state.scope:define(resume_no_continue_symbol, Boolean:new(false))
end,
-- same as :push, but the resume will stop immediately after reaching the target or a node containing the target
-- (we will stop even if the node is not directly reached - this is used to run a specific line containing a node,
-- notably for Definition of exported variables)
push_no_continue = function(self, state, target)
assert(ResumeTarget:issub(target), "can only resume to a resume target")
state.scope:push_partial(resume_target_identifier, resume_no_continue_identifier)
state.scope:define(resume_target_symbol, target)
state.scope:define(resume_no_continue_symbol, Boolean:new(true))
end,
-- pop the current resume context
pop = function(self, state)
state.scope:pop()
end,
-- returns true if we are currently trying to resume to an anchor
-- returns true if we are currently trying to resume to a target
resuming = function(self, state)
return state.scope:defined(resume_anchor_identifier) and not Nil:is(state.scope:get(resume_anchor_identifier))
return state.scope:defined(resume_target_identifier) and not Nil:is(state.scope:get(resume_target_identifier))
end,
-- returns the anchor we are trying to resume to
-- returns the target we are trying to resume to
-- (assumes that we are currently :resuming)
get = function(self, state)
return state.scope:get(resume_anchor_identifier)
return state.scope:get(resume_target_identifier)
end,
-- mark the anchor as reached and stop the resume
-- mark the target as reached and stop the resume
-- (assumes that we are currently :resuming)
set_reached = function(self, state)
state.scope:set(resume_anchor_identifier, Nil:new())
state.scope:set(resume_target_identifier, Nil:new())
end,
-- indicate if the evaluation should stop after reaching a node containing the target
-- (assumes that we are currently :resuming)
no_continue = function(self, state)
return state.scope:get(resume_no_continue_identifier):to_lua(state)
end
}
package.loaded[...] = resume_manager
Nil, Identifier, Anchor = ast.Nil, ast.Identifier, ast.Anchor
Nil, Identifier, ResumeTarget, Boolean = ast.Nil, ast.Identifier, ast.abstract.ResumeTarget, ast.Boolean
resume_anchor_identifier = Identifier:new("_resume_anchor")
resume_anchor_symbol = resume_anchor_identifier:to_symbol()
resume_target_identifier = Identifier:new("_resume_target")
resume_target_symbol = resume_target_identifier:to_symbol()
resume_no_continue_identifier = Identifier:new("_resume_no_continue")
resume_no_continue_symbol = resume_no_continue_identifier:to_symbol()
return resume_manager

View file

@ -19,7 +19,7 @@ return {
{
"_|>_", "(txt::text, fn)",
function(state, text, func)
if func:contains_resume_target(state) then
if func:contains_current_resume_target(state) then
func:call(state, ArgumentTuple:new())
event_manager:write_and_discard_on_flush(state, Choice:new(text, func))
else

View file

@ -0,0 +1,11 @@
--# run #--
--- text ---
| {}"" {}"42" {}"" |
--- error ---
./anselme/stdlib/closure.lua:15: no exported variable "y" defined in closure
↳ from test/tests/exported variable nested.ans:10:4 in call: f . "y"
↳ from test/tests/exported variable nested.ans:10:1 in text interpolation: | {f . "y"} |
↳ from test/tests/exported variable nested.ans:10:1 in translatable: | {f . "y"} |
↳ from ? in block: :f = ($() _)…
--# saved #--
{}

View file

@ -0,0 +1,11 @@
--# run #--
--- text ---
| {}"c=" {}"2" {}" (2)" |
| {}"l=" {}"*[1, 2, 3]" {}" (*[1,2,3])" |
--- text ---
| {}"c=" {}"5" {}" (5)" |
| {}"l=" {}"*[1, 5, 3]" {}" (*[1,5,3])" |
--- return ---
()
--# saved #--
{}

View file

@ -0,0 +1,10 @@
:f = $
| kk
:@x = 42
:y = $
:@z = 12
| ko
|{f.x}
|{f.y}

View file

@ -0,0 +1,11 @@
:@l = *[1,2,3]
:&@c = l(2)
| c={c} (2)
| l={l} (*[1,2,3])
c = 5
| c={c} (5)
| l={l} (*[1,5,3])