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:
parent
e2ec105a4b
commit
3edf65dc2a
19 changed files with 155 additions and 136 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
||||
|
|
|
|||
27
anselme/ast/abstract/ResumeTarget.lua
Normal file
27
anselme/ast/abstract/ResumeTarget.lua
Normal 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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
11
test/results/exported variable nested.ans
Normal file
11
test/results/exported variable nested.ans
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
--# run #--
|
||||
--- text ---
|
||||
| {}"" {}"42" {}"" |
|
||||
--- error ---
|
||||
[0m[31m[0m[31m[0m[31m[0m[31m./anselme/stdlib/closure.lua:15: no exported variable "y" defined in closure[0m
|
||||
↳ from [4mtest/tests/exported variable nested.ans:10:4[0m in call: [2mf . "y"[0m[0m
|
||||
↳ from [4mtest/tests/exported variable nested.ans:10:1[0m in text interpolation: [2m| {f . "y"} |[0m[0m
|
||||
↳ from [4mtest/tests/exported variable nested.ans:10:1[0m in translatable: [2m| {f . "y"} |[0m[0m
|
||||
↳ from [4m?[0m in block: [2m:f = ($() _)…[0m
|
||||
--# saved #--
|
||||
{}
|
||||
11
test/results/symbol alias exported.ans
Normal file
11
test/results/symbol alias exported.ans
Normal 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 #--
|
||||
{}
|
||||
10
test/tests/exported variable nested.ans
Normal file
10
test/tests/exported variable nested.ans
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
:f = $
|
||||
| kk
|
||||
:@x = 42
|
||||
:y = $
|
||||
:@z = 12
|
||||
| ko
|
||||
|
||||
|{f.x}
|
||||
|
||||
|{f.y}
|
||||
11
test/tests/symbol alias exported.ans
Normal file
11
test/tests/symbol alias exported.ans
Normal 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])
|
||||
Loading…
Add table
Add a link
Reference in a new issue