1
0
Fork 0
mirror of https://github.com/Reuh/anselme.git synced 2025-10-27 08:39:30 +00:00

Replace checkpoint system

The previous system needed to store of the scope and full AST to build a Resumable object, which means that if persisted, updating the resumable script will have no effect.
The new system instead uses an anchor token and does not require any information besides the anchor name.
This commit is contained in:
Étienne Fildadut 2023-12-27 17:06:35 +01:00
parent c4636343b4
commit 56ed6c912b
21 changed files with 217 additions and 234 deletions

35
ast/Anchor.lua Normal file
View file

@ -0,0 +1,35 @@
local ast = require("ast")
local resume_manager
local Anchor
Anchor = ast.abstract.Node {
type = "anchor",
name = nil,
init = function(self, name)
self.name = name
self._list_anchors_cache = { [name] = true }
end,
_hash = function(self)
return ("anchor<%q>"):format(self.name)
end,
_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("state.resume_manager")
return Anchor

View file

@ -1,6 +1,8 @@
local ast = require("ast")
local Nil, Return, AutoCall, ArgumentTuple, Flush
local resume_manager = require("state.resume_manager")
local Block = ast.abstract.Node {
type = "block",
@ -34,11 +36,11 @@ local Block = ast.abstract.Node {
_eval = function(self, state)
local r
state.scope:push()
if self:resuming(state) then
local resuming = self:get_resume_data(state)
if self:contains_resume_target(state) then
local anchor = resume_manager:get(state)
local resumed = false
for _, e in ipairs(self.expressions) do
if e == resuming then resumed = true end
if e:contains_anchor(anchor) then resumed = true end
if resumed then
r = e:eval(state)
if AutoCall:issub(r) then
@ -51,7 +53,6 @@ local Block = ast.abstract.Node {
end
else
for _, e in ipairs(self.expressions) do
self:set_resume_data(state, e)
r = e:eval(state)
if AutoCall:issub(r) then
r = r:call(state, ArgumentTuple:new())

View file

@ -45,7 +45,7 @@ Function = Overloadable {
state.scope:push()
args:bind_parameter_tuple(state, self.parameters)
local exp = self.expression:eval_resumable(state)
local exp = self.expression:eval(state)
state.scope:pop()

View file

@ -1,60 +0,0 @@
local ast = require("ast")
local Table
local resumable_manager
local Resumable
Resumable = ast.abstract.Runtime {
type = "resumable",
resuming = false,
expression = nil,
scope = nil,
data = nil,
init = function(self, state, expression, scope, data)
self.expression = expression
self.scope = scope
self.data = data or Table:new(state)
end,
_format = function(self)
return "<resumable>"
end,
traverse = function(self, fn, ...)
fn(self.expression, ...)
fn(self.data, ...)
fn(self.scope, ...)
end,
-- returns a copy with the data copied
capture = function(self, state)
return Resumable:new(state, self.expression, self.scope, self.data:copy(state))
end,
-- resume from this resumable
call = function(self, state, args)
assert(args.arity == 0, "Resumable! does not accept arguments")
state.scope:push(self.scope)
local resuming = self:capture(state)
resuming.resuming = true
resumable_manager:push(state, resuming)
local r = self.expression:eval(state)
resumable_manager:pop(state)
state.scope:pop()
return r
end,
}
package.loaded[...] = Resumable
Table = ast.Table
resumable_manager = require("state.resumable_manager")
return Resumable

View file

@ -1,44 +0,0 @@
-- intended to be wrapped in a Function, so that when resuming from the function, will keep resuming to where the function was called from
-- used in Choices to resume back from where the event was flushed
-- note: when resuming, the return value will be discarded, instead returning what the parent function will return
local ast = require("ast")
local ArgumentTuple
local resumable_manager
local ResumeParentFunction = ast.abstract.Node {
type = "resume parent function",
expression = nil,
init = function(self, expression)
self.expression = expression
self.format_priority = expression.format_priority
end,
_format = function(self, ...)
return self.expression:format(...)
end,
traverse = function(self, fn, ...)
fn(self.expression, ...)
end,
_eval = function(self, state)
if self:resuming(state) then
self.expression:eval(state)
return self:get_data(state):call(state, ArgumentTuple:new())
else
self:set_data(state, resumable_manager:capture(state, 1))
return self.expression:eval(state)
end
end
}
package.loaded[...] = ResumeParentFunction
ArgumentTuple = ast.ArgumentTuple
resumable_manager = require("state.resumable_manager")
return ResumeParentFunction

View file

@ -12,8 +12,8 @@ local binser = require("lib.binser")
local uuid = require("common").uuid
local State, Runtime
local resumable_manager
local State, Runtime, Call, Identifier, ArgumentTuple
local resume_manager
local custom_call_identifier
@ -99,8 +99,8 @@ Node = class {
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
-- i.e. when :prepare is called on a node, it should be in a similar scope stack context as will be when it will be evaluated
-- 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
@ -122,6 +122,30 @@ Node = class {
self:traverse(traverse.prepare, state)
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]
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))
end,
-- generate a list of translatable nodes that appear in this node
-- should only be called on non-runtime nodes
-- 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
@ -131,26 +155,6 @@ Node = class {
return t
end,
-- same as eval, but make the evaluated expression as a resume boundary
-- i.e. if a checkpoint is defined somewhere in this eval, it will start back from this node eval when resuming
eval_resumable = function(self, state)
return resumable_manager:eval(state, self)
end,
-- set the current resume data for this node
-- (relevant inside :eval)
set_resume_data = function(self, state, data)
resumable_manager:set_data(state, self, data)
end,
-- get the current resume data for this node
get_resume_data = function(self, state)
return resumable_manager:get_data(state, self)
end,
-- returns true if the current node is in a resuming state
-- (relevant inside :eval)
resuming = function(self, state)
return resumable_manager:resuming(state, self)
end,
-- return result AST
-- arg is a ArgumentTuple node (already evaluated)
-- redefine if relevant
@ -276,11 +280,11 @@ 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("ast")
custom_call_identifier = ast.Identifier:new("_!")
Runtime = ast.abstract.Runtime
Runtime, Call, Identifier, ArgumentTuple = ast.abstract.Runtime, ast.Call, ast.Identifier, ast.ArgumentTuple
custom_call_identifier = Identifier:new("_!")
State = require("state.State")
resumable_manager = require("state.resumable_manager")
resume_manager = require("state.resume_manager")
end,
_debug_traverse = function(self, level)

View file

@ -35,8 +35,8 @@ local common = {
{ "!", 12 }
},
infixes = {
{ ";", 1 }, { "->", 1 },
{ "#", 2 },
{ ";", 1 },
{ "#", 2 }, { "->", 2 }, { "~>", 2 },
{ "~", 4 }, { "~?", 4 },
{ "|>", 5 }, { "&", 5 }, { "|", 5 },
{ "==", 7 }, { "!=", 7 }, { ">=", 7 }, { "<=", 7 }, { "<", 7 }, { ">", 7 },

View file

@ -0,0 +1,25 @@
local primary = require("parser.expression.primary.primary")
local identifier = require("parser.expression.primary.identifier")
local ast = require("ast")
local Anchor = ast.Anchor
return primary {
match = function(self, str)
if str:match("^#") then
return identifier:match(str:match("^#(.-)$"))
end
return false
end,
parse = function(self, source, str)
local start_source = source:clone()
local rem = source:consume(str:match("^(#)(.-)$"))
local ident
ident, rem = identifier:parse(source, rem)
return Anchor:new(ident.name):set_source(start_source), rem
end
}

View file

@ -12,6 +12,7 @@ local primaries = {
r("function_definition"),
r("symbol"),
r("identifier"),
r("anchor"),
r("block_identifier"),
r("tuple"),
r("struct"),

View file

@ -3,7 +3,7 @@ local infix = require("parser.expression.secondary.infix.infix")
local operator_priority = require("common").operator_priority
local ast = require("ast")
local Call, Identifier, ArgumentTuple, ResumeParentFunction, ParameterTuple, Function = ast.Call, ast.Identifier, ast.ArgumentTuple, ast.ResumeParentFunction, ast.ParameterTuple, ast.Function
local Call, Identifier, ArgumentTuple, ParameterTuple, Function = ast.Call, ast.Identifier, ast.ArgumentTuple, ast.ParameterTuple, ast.Function
return infix {
operator = "|>",
@ -11,7 +11,7 @@ return infix {
priority = operator_priority["_|>_"],
build_ast = function(self, left, right)
right = Function:new(ParameterTuple:new(), ResumeParentFunction:new(right))
right = Function:new(ParameterTuple:new(), right)
return Call:new(Identifier:new(self.identifier), ArgumentTuple:new(left, right))
end
}

View file

@ -0,0 +1,9 @@
local infix_quote_right = require("parser.expression.secondary.infix.infix_quote_right")
local operator_priority = require("common").operator_priority
return infix_quote_right {
operator = "~>",
identifier = "_~>_",
priority = operator_priority["_~>_"]
}

View file

@ -8,10 +8,11 @@ local secondaries = {
-- binary infix operators
-- 1
r("infix.semicolon"),
r("infix.translate"),
-- 2
r("infix.tuple"),
r("infix.tag"),
r("infix.translate"),
r("infix.resume"),
-- 4
r("infix.while"),
r("infix.if"),

View file

@ -76,7 +76,7 @@ local ScopeStack = class {
define_overloadable = function(self, symbol, exp) return self.current:define_overloadable(self.state, symbol, exp) end,
defined = function(self, identifier) return self.current:defined(self.state, identifier) end,
defined_in_current = function(self, symbol) return self.current:defined_in_current(self.state, symbol) end,
set = function(self, identifier, exp) self.current:set(self.state, identifier, exp) end,
set = function(self, identifier, exp) self.current:set(self.state, identifier, exp) end,
get = function(self, identifier) return self.current:get(self.state, identifier) end,
depth = function(self) return self.current:depth() end,

View file

@ -5,7 +5,6 @@ local class = require("class")
local ScopeStack = require("state.ScopeStack")
local tag_manager = require("state.tag_manager")
local event_manager = require("state.event_manager")
local resumable_manager = require("state.resumable_manager")
local translation_manager = require("state.translation_manager")
local uuid = require("common").uuid
local parser = require("parser")
@ -25,14 +24,12 @@ State = class {
self.scope = ScopeStack:new(self, branch_from)
event_manager:reset(self) -- events are isolated per branch
resumable_manager:reset(self) -- resumable stack is isolated per branch
-- create new empty state
else
self.scope = ScopeStack:new(self)
event_manager:setup(self)
tag_manager:setup(self)
resumable_manager:setup(self)
translation_manager:setup(self)
end
end,

View file

@ -1,7 +1,7 @@
local class = require("class")
local ast = require("ast")
local Nil, String, List, Identifier = ast.Nil, ast.String, ast.List, ast.Identifier
local Nil, String, List, Identifier, Boolean = ast.Nil, ast.String, ast.List, ast.Identifier, ast.Boolean
-- list of event data
local event_buffer_identifier = Identifier:new("_event_buffer")
@ -11,18 +11,26 @@ local event_buffer_symbol = event_buffer_identifier:to_symbol{ confined_to_branc
local last_event_type_identifier = Identifier:new("_last_event_type")
local last_event_type_symbol = last_event_type_identifier:to_symbol{ confined_to_branch = true }
-- indicate if the next flush should be ignored for the current buffered event
local discard_next_flush_identifier = Identifier:new("_discard_next_flush")
local discard_next_flush_symbol = discard_next_flush_identifier:to_symbol{ confined_to_branch = true }
return class {
init = false,
setup = function(self, state)
state.scope:define(event_buffer_symbol, List:new(state))
state.scope:define(last_event_type_symbol, Nil:new())
state.scope:define(discard_next_flush_symbol, Nil:new())
end,
reset = function(self, state)
state.scope:set(event_buffer_identifier, List:new(state))
state.scope:set(last_event_type_identifier, Nil:new())
state.scope:set(discard_next_flush_identifier, Nil:new())
end,
-- write an event into the event buffer
-- will flush if an event of a different type is present in the buffer
write = function(self, state, event)
local current_type = state.scope:get(last_event_type_identifier):to_lua(state)
if current_type ~= nil and current_type ~= event.type then
@ -31,22 +39,33 @@ return class {
state.scope:set(last_event_type_identifier, String:new(event.type))
state.scope:get(event_buffer_identifier):insert(state, event)
end,
-- same as :write, but the buffer will be discarded instead of yielded on the next flush
write_and_discard_on_flush = function(self, state, event)
self:write(state, event)
state.scope:set(discard_next_flush_identifier, Boolean:new(true))
end,
-- flush the event buffer: build the event data and yield it
flush = function(self, state)
local last_type = state.scope:get(last_event_type_identifier):to_lua(state)
if last_type then
local last_buffer = state.scope:get(event_buffer_identifier)
local event_president = last_buffer:get(state, 1) -- elected representative of all concerned events
-- yield event data
local data = event_president:build_event_data(state, last_buffer)
coroutine.yield(last_type, data)
-- clear room for the future
state.scope:set(last_event_type_identifier, Nil:new())
state.scope:set(event_buffer_identifier, List:new(state))
-- post callback
if event_president.post_flush_callback then event_president:post_flush_callback(state, last_buffer, data) end
local discard_next_flush = state.scope:get(discard_next_flush_identifier):truthy()
if discard_next_flush then
self:reset(state)
else
local last_buffer = state.scope:get(event_buffer_identifier)
local event_president = last_buffer:get(state, 1) -- elected representative of all concerned events
-- yield event data
local data = event_president:build_event_data(state, last_buffer)
coroutine.yield(last_type, data)
-- clear room for the future
self:reset(state)
-- post callback
if event_president.post_flush_callback then event_president:post_flush_callback(state, last_buffer, data) end
end
end
end,
-- keep flushing until nothing is left (a flush may re-fill the buffer during its execution)
final_flush = function(self, state)
while state.scope:get(last_event_type_identifier):to_lua(state) do self:flush(state) end
end

View file

@ -1,70 +0,0 @@
local class = require("class")
local ast = require("ast")
local Resumable, Nil, List, Identifier
-- stack of resumable contexts
local resumable_stack_identifier, resumable_stack_symbol
local resumable_manager = class {
init = false,
setup = function(self, state)
state.scope:define(resumable_stack_symbol, List:new(state))
self:push(state, Resumable:new(state, Nil:new(), state.scope:capture()))
end,
reset = function(self, state)
state.scope:set(resumable_stack_identifier, List:new(state))
self:push(state, Resumable:new(state, Nil:new(), state.scope:capture()))
end,
push = function(self, state, resumable)
local stack = state.scope:get(resumable_stack_identifier)
stack:insert(state, resumable)
end,
pop = function(self, state)
local stack = state.scope:get(resumable_stack_identifier)
stack:remove(state)
end,
_get = function(self, state)
return state.scope:get(resumable_stack_identifier):get(state, -1)
end,
-- returns the Resumable object that resumes from this point
-- level indicate which function to resume: level=0 means resume the current function, level=1 the parent function (resume from the call to the current function in the parent function), etc.
capture = function(self, state, level)
level = level or 0
return state.scope:get(resumable_stack_identifier):get(state, -1-level):capture(state)
end,
eval = function(self, state, exp)
self:push(state, Resumable:new(state, exp, state.scope:capture()))
local r = exp:eval(state)
self:pop(state)
return r
end,
set_data = function(self, state, node, data)
self:_get(state).data:set(state, node, data)
end,
get_data = function(self, state, node)
return self:_get(state).data:get(state, node)
end,
resuming = function(self, state, node)
local resumable = self:_get(state)
if node then
return resumable.resuming and resumable.data:has(state, node)
else
return resumable.resuming
end
end
}
package.loaded[...] = resumable_manager
Resumable, Nil, List, Identifier = ast.Resumable, ast.Nil, ast.List, ast.Identifier
resumable_stack_identifier = Identifier:new("_resumable_stack")
resumable_stack_symbol = resumable_stack_identifier:to_symbol{ confined_to_branch = true } -- per-branch, global variables
return resumable_manager

44
state/resume_manager.lua Normal file
View file

@ -0,0 +1,44 @@
local class = require("class")
local ast = require("ast")
local Nil, Identifier, Anchor
-- stack of resumable contexts
local resume_anchor_identifier, resume_anchor_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)
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
resuming = function(self, state)
return state.scope:defined(resume_anchor_identifier) and not Nil:is(state.scope:get(resume_anchor_identifier))
end,
-- returns the anchor we are trying to resume to
get = function(self, state)
return state.scope:get(resume_anchor_identifier)
end,
-- mark the anchor as reached and stop the resume
set_reached = function(self, state)
state.scope:set(resume_anchor_identifier, Nil:new())
end,
}
package.loaded[...] = resume_manager
Nil, Identifier, Anchor = ast.Nil, ast.Identifier, ast.Anchor
resume_anchor_identifier = Identifier:new("_resume_anchor")
resume_anchor_symbol = resume_anchor_identifier:to_symbol()
return resume_manager

View file

@ -1,10 +1,22 @@
local resumable_manager = require("state.resumable_manager")
local ast = require("ast")
local ArgumentTuple = ast.ArgumentTuple
local resume_manager = require("state.resume_manager")
return {
{
"new checkpoint", "(level::number=0)",
function(state, level)
return resumable_manager:capture(state, level.number)
"_~>_", "(anchor::anchor, quote)",
function(state, anchor, quote)
resume_manager:push(state, anchor)
local r = quote:call(state, ArgumentTuple:new())
resume_manager:pop(state)
return r
end
},
{
"_~>_", "(anchor::nil, quote)",
function(state, anchor, quote)
return quote:call(state, ArgumentTuple:new())
end
}
}

View file

@ -21,8 +21,9 @@ return {
"_~_", "(condition, expression)", function(state, condition, expression)
ensure_if_variable(state)
if condition:truthy() then
local r = expression:call(state, ArgumentTuple:new())
set_if_variable(state, true)
return expression:call(state, ArgumentTuple:new())
return r
else
set_if_variable(state, false)
return Nil:new()
@ -36,8 +37,9 @@ return {
if last_if_success(state) then
return Nil:new()
else
local r = expression:call(state, ArgumentTuple:new())
set_if_variable(state, true)
return expression:call(state, ArgumentTuple:new())
return r
end
end
},

View file

@ -1,5 +1,5 @@
local ast = require("ast")
local Nil, Choice, AttachBlock = ast.Nil, ast.Choice, ast.AttachBlock
local Nil, Choice, AttachBlock, ArgumentTuple = ast.Nil, ast.Choice, ast.AttachBlock, ast.ArgumentTuple
local event_manager = require("state.event_manager")
local translation_manager = require("state.translation_manager")
@ -19,7 +19,12 @@ return {
{
"_|>_", "(txt::text, fn)",
function(state, text, func)
event_manager:write(state, Choice:new(text, func))
if func:contains_resume_target(state) then
func:call(state, ArgumentTuple:new())
event_manager:write_and_discard_on_flush(state, Choice:new(text, func))
else
event_manager:write(state, Choice:new(text, func))
end
return Nil:new()
end
},

View file

@ -2,10 +2,12 @@ local ast = require("ast")
local Boolean = ast.Boolean
return {
{ "nil", "(x)", function(state, x) return Boolean:new(x.type == "nil") end },
{ "number", "(x)", function(state, x) return Boolean:new(x.type == "number") end },
{ "string", "(x)", function(state, x) return Boolean:new(x.type == "string") end },
{ "boolean", "(x)", function(state, x) return Boolean:new(x.type == "boolean") end },
{ "symbol", "(x)", function(state, x) return Boolean:new(x.type == "symbol") end },
{ "anchor", "(x)", function(state, x) return Boolean:new(x.type == "anchor") end },
{ "text", "(x)", function(state, x) return Boolean:new(x.type == "text") end },