From 3edf65dc2ae6ba4b1b30f82dee37708666c10c4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Reuh=20Fildadut?= Date: Sat, 30 Dec 2023 15:31:00 +0100 Subject: [PATCH] Exported variables predefinition: replace prepare system with generic resume system --- anselme/ast/Anchor.lua | 13 +--- anselme/ast/Block.lua | 17 ++--- anselme/ast/Closure.lua | 13 +++- anselme/ast/Definition.lua | 13 +--- anselme/ast/Function.lua | 17 +---- anselme/ast/Identifier.lua | 6 -- anselme/ast/PartialScope.lua | 7 -- anselme/ast/abstract/Node.lua | 70 +++++++------------ anselme/ast/abstract/ResumeTarget.lua | 27 +++++++ anselme/ast/abstract/Runtime.lua | 3 +- .../primary/function_definition.lua | 2 +- anselme/parser/expression/primary/symbol.lua | 4 +- anselme/parser/init.lua | 2 - anselme/state/resume_manager.lua | 52 +++++++++----- anselme/stdlib/text.lua | 2 +- test/results/exported variable nested.ans | 11 +++ test/results/symbol alias exported.ans | 11 +++ test/tests/exported variable nested.ans | 10 +++ test/tests/symbol alias exported.ans | 11 +++ 19 files changed, 155 insertions(+), 136 deletions(-) create mode 100644 anselme/ast/abstract/ResumeTarget.lua create mode 100644 test/results/exported variable nested.ans create mode 100644 test/results/symbol alias exported.ans create mode 100644 test/tests/exported variable nested.ans create mode 100644 test/tests/symbol alias exported.ans diff --git a/anselme/ast/Anchor.lua b/anselme/ast/Anchor.lua index 08b422f..67c7a03 100644 --- a/anselme/ast/Anchor.lua +++ b/anselme/ast/Anchor.lua @@ -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 diff --git a/anselme/ast/Block.lua b/anselme/ast/Block.lua index 6d451e2..e996120 100644 --- a/anselme/ast/Block.lua +++ b/anselme/ast/Block.lua @@ -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 diff --git a/anselme/ast/Closure.lua b/anselme/ast/Closure.lua index a0c7e90..c1539eb 100644 --- a/anselme/ast/Closure.lua +++ b/anselme/ast/Closure.lua @@ -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 diff --git a/anselme/ast/Definition.lua b/anselme/ast/Definition.lua index 3c26d2b..3837564 100644 --- a/anselme/ast/Definition.lua +++ b/anselme/ast/Definition.lua @@ -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 diff --git a/anselme/ast/Function.lua b/anselme/ast/Function.lua index cfed1e2..d347f27 100644 --- a/anselme/ast/Function.lua +++ b/anselme/ast/Function.lua @@ -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 diff --git a/anselme/ast/Identifier.lua b/anselme/ast/Identifier.lua index 5161a0b..4308ae2 100644 --- a/anselme/ast/Identifier.lua +++ b/anselme/ast/Identifier.lua @@ -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 diff --git a/anselme/ast/PartialScope.lua b/anselme/ast/PartialScope.lua index eb1976b..73f9ab1 100644 --- a/anselme/ast/PartialScope.lua +++ b/anselme/ast/PartialScope.lua @@ -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 diff --git a/anselme/ast/abstract/Node.lua b/anselme/ast/abstract/Node.lua index 220f68e..39abd75 100644 --- a/anselme/ast/abstract/Node.lua +++ b/anselme/ast/abstract/Node.lua @@ -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, diff --git a/anselme/ast/abstract/ResumeTarget.lua b/anselme/ast/abstract/ResumeTarget.lua new file mode 100644 index 0000000..a9300f9 --- /dev/null +++ b/anselme/ast/abstract/ResumeTarget.lua @@ -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 diff --git a/anselme/ast/abstract/Runtime.lua b/anselme/ast/abstract/Runtime.lua index 7e7ae37..2a3c9bb 100644 --- a/anselme/ast/abstract/Runtime.lua +++ b/anselme/ast/abstract/Runtime.lua @@ -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 } diff --git a/anselme/parser/expression/primary/function_definition.lua b/anselme/parser/expression/primary/function_definition.lua index 08d0c24..1a545a1 100644 --- a/anselme/parser/expression/primary/function_definition.lua +++ b/anselme/parser/expression/primary/function_definition.lua @@ -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) diff --git a/anselme/parser/expression/primary/symbol.lua b/anselme/parser/expression/primary/symbol.lua index 5036dd4..e74e25a 100644 --- a/anselme/parser/expression/primary/symbol.lua +++ b/anselme/parser/expression/primary/symbol.lua @@ -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, diff --git a/anselme/parser/init.lua b/anselme/parser/init.lua index 4787d26..7fbccb1 100644 --- a/anselme/parser/init.lua +++ b/anselme/parser/init.lua @@ -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 diff --git a/anselme/state/resume_manager.lua b/anselme/state/resume_manager.lua index a03e5af..fe9dc30 100644 --- a/anselme/state/resume_manager.lua +++ b/anselme/state/resume_manager.lua @@ -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 diff --git a/anselme/stdlib/text.lua b/anselme/stdlib/text.lua index fd2ea75..5627232 100644 --- a/anselme/stdlib/text.lua +++ b/anselme/stdlib/text.lua @@ -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 diff --git a/test/results/exported variable nested.ans b/test/results/exported variable nested.ans new file mode 100644 index 0000000..67821c7 --- /dev/null +++ b/test/results/exported variable nested.ans @@ -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 #-- +{} \ No newline at end of file diff --git a/test/results/symbol alias exported.ans b/test/results/symbol alias exported.ans new file mode 100644 index 0000000..6be1c1d --- /dev/null +++ b/test/results/symbol alias exported.ans @@ -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 #-- +{} \ No newline at end of file diff --git a/test/tests/exported variable nested.ans b/test/tests/exported variable nested.ans new file mode 100644 index 0000000..165e2bb --- /dev/null +++ b/test/tests/exported variable nested.ans @@ -0,0 +1,10 @@ +:f = $ + | kk + :@x = 42 + :y = $ + :@z = 12 + | ko + +|{f.x} + +|{f.y} diff --git a/test/tests/symbol alias exported.ans b/test/tests/symbol alias exported.ans new file mode 100644 index 0000000..51425f6 --- /dev/null +++ b/test/tests/symbol alias exported.ans @@ -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])