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

Restore tags when resuming from a checkpoint; simply resuming code

This commit is contained in:
Étienne Fildadut 2021-04-12 17:08:35 +02:00
parent 5c3e9d2c5d
commit f9edaff7e7
8 changed files with 353 additions and 35 deletions

View file

@ -237,7 +237,7 @@ Paragraphs always have the following variable defined in its namespace by defaul
`👁️`: number, number of times the paragraph was reached or executed before `👁️`: number, number of times the paragraph was reached or executed before
* `#`: tag line. Can be followed by an [expression](#expressions); otherwise empty expression is assumed. The results of the [expression](#expressions) will be added to the tags send along with any event sent from its children. Can be nested. * `#`: tag line. Can be followed by an [expression](#expressions); otherwise nil expression is assumed. The results of the [expression](#expressions) will be added to the tags send along with any event sent from its children. Can be nested.
``` ```
# "color": "red" # "color": "red"
@ -279,7 +279,7 @@ And this is more text, in a different event.
Every line can also be followed with decorators, which are appended at the end of the line and affect its behaviour. Every line can also be followed with decorators, which are appended at the end of the line and affect its behaviour.
* `~`: expression decorator. Same as an expression line, behaving as if this line was it sole child. Typically used to conditionally execute line. * `~`: expression decorator. Same as an expression line, behaving as if this line was it sole child. Typically used to conditionally execute line. Does not affect following else-conditions.
* `§`: paragraph decorator. Same as a paragraph line, behaving as if this line was it sole child. * `§`: paragraph decorator. Same as a paragraph line, behaving as if this line was it sole child.
@ -559,6 +559,12 @@ Method style calling is also possible, like with functions.
Paragraphs commit variables after a call. Paragraphs commit variables after a call.
Please also be aware that when resuming from a paragraph, Anselme will try to restore the interpreter state as if the function was correctly executed from the start up to this paragraph. This includes:
* if the paragraph is in a expression block, it will assume the expression was true (but will not re-evaluate it)
* if the paragraph is in a choice block, it will assume this choice was selected (but will not re-evaluate any of the choices from the same choice group)
* will try to re-add every tag from parent lines; this require Anselme to re-evaluate every tag line and decorator that's a parent of the paragraph in the function. Be careful if your tag expressions have side-effects.
#### Operators #### Operators
Built-in operators: Built-in operators:

View file

@ -103,25 +103,7 @@ local function eval(state, exp)
if fn.value.type == "paragraph" or fn.value.paragraph then if fn.value.type == "paragraph" or fn.value.paragraph then
local r, e local r, e
if fn.value.type == "paragraph" then if fn.value.type == "paragraph" then
r, e = run_block(state, fn.value.child) r, e = run(state, fn.value.child, not exp.explicit_call)
if e then return r, e end
state.variables[fn.value.namespace.."👁️"] = {
type = "number",
value = state.variables[fn.value.namespace.."👁️"].value + 1
}
state.variables[fn.value.parent_function.namespace.."🏁"] = {
type = "string",
value = fn.value.name
}
flush_state(state)
if r then
return r, e
-- resume function from paragraph
elseif not exp.explicit_call then
r, e = run(state, fn.value.parent_block, true, fn.value.parent_position+1)
else
r = { type = "nil", value = nil }
end
-- paragraph decorators: run single line or resume from it. -- paragraph decorators: run single line or resume from it.
-- checkpoint & seen variables will be updated from the interpreter usual paragraph-reaching code. -- checkpoint & seen variables will be updated from the interpreter usual paragraph-reaching code.
elseif exp.explicit_call then elseif exp.explicit_call then

View file

@ -1,11 +1,12 @@
local eval local eval
local truthy, flush_state, to_lua, eval_text local truthy, flush_state, to_lua, eval_text, escape
local tags = { local tags = {
--- push new tags on top of the stack, from Anselme values
push = function(self, state, val) push = function(self, state, val)
local new = {} local new = {}
-- copy -- copy
local last = state.interpreter.tags[#state.interpreter.tags] or {} local last = self:current(state)
for k,v in pairs(last) do new[k] = v end for k,v in pairs(last) do new[k] = v end
-- merge with new values -- merge with new values
if val.type ~= "list" then val = { type = "list", value = { val } } end if val.type ~= "list" then val = { type = "list", value = { val } } end
@ -13,14 +14,27 @@ local tags = {
-- add -- add
table.insert(state.interpreter.tags, new) table.insert(state.interpreter.tags, new)
end, end,
--- same but do not merge with last stack item
push_lua_no_merge = function(self, state, val)
table.insert(state.interpreter.tags, val)
end,
-- pop tag table on top of the stack
pop = function(self, state) pop = function(self, state)
table.remove(state.interpreter.tags) table.remove(state.interpreter.tags)
end, end,
--- return current lua tags table
current = function(self, state) current = function(self, state)
return state.interpreter.tags[#state.interpreter.tags] or {} return state.interpreter.tags[#state.interpreter.tags] or {}
end, end,
push_ignore_past = function(self, state, tags) --- returns length of tags stack
table.insert(state.interpreter.tags, tags) len = function(self, state)
return #state.interpreter.tags
end,
--- pop item until we reached desired stack length
trim = function(self, state, len)
while #state.interpreter.tags > len do
self:pop(state)
end
end end
} }
@ -94,13 +108,11 @@ local function run_line(state, line)
}) })
write_event(state, "choice", t) write_event(state, "choice", t)
elseif line.type == "tag" then elseif line.type == "tag" then
if line.expression then local v, e = eval(state, line.expression)
local v, e = eval(state, line.expression) if not v then return v, ("%s; at %s"):format(e, line.source) end
if not v then return v, ("%s; at %s"):format(e, line.source) end tags:push(state, v)
tags:push(state, v) v, e = run_block(state, line.child)
end tags:pop(state)
local v, e = run_block(state, line.child)
if line.expression then tags:pop(state) end
if e then return v, e end if e then return v, e end
if v then return v end if v then return v end
elseif line.type == "return" then elseif line.type == "return" then
@ -127,7 +139,7 @@ local function run_line(state, line)
else else
local choice = state.interpreter.choice_available[sel] local choice = state.interpreter.choice_available[sel]
state.interpreter.choice_available = {} state.interpreter.choice_available = {}
tags:push_ignore_past(state, choice.tags) tags:push_lua_no_merge(state, choice.tags)
local v, e = run_block(state, choice.block) local v, e = run_block(state, choice.block)
tags:pop(state) tags:pop(state)
if e then return v, e end if e then return v, e end
@ -142,7 +154,7 @@ local function run_line(state, line)
if line.tag then if line.tag then
tags:pop(state) tags:pop(state)
end end
-- paragraph decorator -- paragraph decorator and line
if line.paragraph then if line.paragraph then
state.variables[line.namespace.."👁️"] = { state.variables[line.namespace.."👁️"] = {
type = "number", type = "number",
@ -182,10 +194,31 @@ run_block = function(state, block, resume_from_there, i, j)
end end
i = i + 1 i = i + 1
end end
-- if we are exiting a paragraph block, mark it as ran and update checkpoint
-- (when resuming from a checkpoint, execution is resumed from inside the paragraph, the line.paragraph check in run_line is never called)
-- (and we want this to be done after executing the checkpoint block anyway)
if block.parent_line and block.parent_line.paragraph then
local parent_line = block.parent_line
state.variables[parent_line.namespace.."👁️"] = {
type = "number",
value = state.variables[parent_line.namespace.."👁️"].value + 1
}
-- don't update checkpoint if an already more precise checkpoint is set
-- (since we will go up the whole checkpoint hierarchy when resuming from a nested checkpoint)
local current_checkpoint = state.variables[parent_line.parent_function.namespace.."🏁"].value
if not current_checkpoint:match("^"..escape(parent_line.name)) then
state.variables[parent_line.parent_function.namespace.."🏁"] = {
type = "string",
value = parent_line.name
}
end
flush_state(state)
end
-- go up hierarchy if asked to resume -- go up hierarchy if asked to resume
-- will stop at function boundary -- will stop at function boundary
-- if parent is a choice, will ignore choices that belong to the same block (like the whole block was executed naturally from a higher parent) -- if parent is a choice, will ignore choices that belong to the same block (like the whole block was executed naturally from a higher parent)
-- if parent if a condition, will mark it as a success (skipping following else-conditions) (for the same reasons as for choices) -- if parent if a condition, will mark it as a success (skipping following else-conditions) (for the same reasons as for choices)
-- if parent pushed a tag, will pop it (tags from parents are added to the stack in run())
if resume_from_there and block.parent_line and block.parent_line.type ~= "function" then if resume_from_there and block.parent_line and block.parent_line.type ~= "function" then
local parent_line = block.parent_line local parent_line = block.parent_line
if parent_line.type == "choice" then if parent_line.type == "choice" then
@ -193,6 +226,9 @@ run_block = function(state, block, resume_from_there, i, j)
elseif parent_line.type == "condition" or parent_line.type == "else-condition" then elseif parent_line.type == "condition" or parent_line.type == "else-condition" then
parent_line.parent_block.last_condition_success = true parent_line.parent_block.last_condition_success = true
end end
if parent_line.type == "tag" or parent_line.tag then
tags:pop(state)
end
local v, e = run_block(state, parent_line.parent_block, resume_from_there, parent_line.parent_position+1) local v, e = run_block(state, parent_line.parent_block, resume_from_there, parent_line.parent_position+1)
if e then return v, e end if e then return v, e end
if v then return v, e end if v then return v, e end
@ -203,8 +239,39 @@ end
-- returns var in case of success -- returns var in case of success
-- return nil, err in case of error -- return nil, err in case of error
local function run(state, block, resume_from_there, i, j) local function run(state, block, resume_from_there, i, j)
-- restore tags from parents when resuming
local tags_len = tags:len(state)
if resume_from_there then
local tags_to_add = {}
-- go up in hierarchy in ascending order until function boundary
local parent_line = block.parent_line
while parent_line and parent_line.type ~= "function" do
if parent_line.type == "tag" then
local v, e = eval(state, parent_line.expression)
if not v then return v, ("%s; at %s"):format(e, parent_line.source) end
table.insert(tags_to_add, v)
end
if parent_line.tag then
local v, e = eval(state, parent_line.tag)
if not v then return v, ("%s; in tag decorator at %s"):format(e, parent_line.source) end
table.insert(tags_to_add, v)
end
parent_line = parent_line.parent_block.parent_line
end
-- re-add tag in desceding order
for k=#tags_to_add, 1, -1 do
tags:push(state, tags_to_add[k])
end
end
-- run -- run
local v, e = run_block(state, block, resume_from_there, i, j) local v, e = run_block(state, block, resume_from_there, i, j)
-- return to previous tag state
-- tag stack pop when resuming is done when exiting the tag block
-- stray elements may be left on the stack if there is a return before we exit all the tag block, so we trim them
if resume_from_there then
tags:trim(state, tags_len)
end
-- return
if e then return v, e end if e then return v, e end
if v then if v then
return v return v
@ -227,5 +294,6 @@ package.loaded[...] = interpreter
eval = require((...):gsub("interpreter$", "expression")) eval = require((...):gsub("interpreter$", "expression"))
local common = require((...):gsub("interpreter$", "common")) local common = require((...):gsub("interpreter$", "common"))
truthy, flush_state, to_lua, eval_text = common.truthy, common.flush_state, common.to_lua, common.eval_text truthy, flush_state, to_lua, eval_text = common.truthy, common.flush_state, common.to_lua, common.eval_text
escape = require((...):gsub("interpreter%.interpreter$", "parser.common")).escape
return interpreter return interpreter

View file

@ -307,7 +307,7 @@ local function parse_line(line, state, namespace)
if expr:match("[^%s]") then if expr:match("[^%s]") then
r.expression = expr r.expression = expr
else else
r.expression = nil r.expression = "()"
end end
-- return -- return
elseif l:match("^%@") then elseif l:match("^%@") then

View file

@ -0,0 +1,26 @@
$ f
x
§ p
a
§ q
b
c
d
From start:
~ f
From p checkpoint:
~ f
From q checkpoint:
~ f
From q checkpoint again:
~ f
Force p checkpoint:
~ f.p()

View file

@ -0,0 +1,135 @@
local _={}
_[63]={}
_[62]={}
_[61]={}
_[60]={}
_[59]={}
_[58]={}
_[57]={}
_[56]={}
_[55]={}
_[54]={}
_[53]={}
_[52]={}
_[51]={}
_[50]={}
_[49]={}
_[48]={}
_[47]={}
_[46]={}
_[45]={tags=_[63],data="c"}
_[44]={tags=_[62],data="a"}
_[43]={tags=_[61],data="Force p checkpoint:"}
_[42]={tags=_[60],data="d"}
_[41]={tags=_[59],data="c"}
_[40]={tags=_[58],data="b"}
_[39]={tags=_[57],data="From q checkpoint again:"}
_[38]={tags=_[56],data="d"}
_[37]={tags=_[55],data="c"}
_[36]={tags=_[54],data="b"}
_[35]={tags=_[53],data="From q checkpoint:"}
_[34]={tags=_[52],data="d"}
_[33]={tags=_[51],data="c"}
_[32]={tags=_[50],data="a"}
_[31]={tags=_[49],data="From p checkpoint:"}
_[30]={tags=_[48],data="d"}
_[29]={tags=_[47],data="x"}
_[28]={tags=_[46],data="From start:"}
_[27]={_[45]}
_[26]={_[43],_[44]}
_[25]={_[42]}
_[24]={_[41]}
_[23]={_[39],_[40]}
_[22]={_[38]}
_[21]={_[37]}
_[20]={_[35],_[36]}
_[19]={_[34]}
_[18]={_[33]}
_[17]={_[31],_[32]}
_[16]={_[30]}
_[15]={_[28],_[29]}
_[14]={"return"}
_[13]={"text",_[27]}
_[12]={"text",_[26]}
_[11]={"text",_[25]}
_[10]={"text",_[24]}
_[9]={"text",_[23]}
_[8]={"text",_[22]}
_[7]={"text",_[21]}
_[6]={"text",_[20]}
_[5]={"text",_[19]}
_[4]={"text",_[18]}
_[3]={"text",_[17]}
_[2]={"text",_[16]}
_[1]={"text",_[15]}
return {_[1],_[2],_[3],_[4],_[5],_[6],_[7],_[8],_[9],_[10],_[11],_[12],_[13],_[14]}
--[[
{ "text", { {
data = "From start:",
tags = {}
}, {
data = "x",
tags = {}
} } }
{ "text", { {
data = "d",
tags = {}
} } }
{ "text", { {
data = "From p checkpoint:",
tags = {}
}, {
data = "a",
tags = {}
} } }
{ "text", { {
data = "c",
tags = {}
} } }
{ "text", { {
data = "d",
tags = {}
} } }
{ "text", { {
data = "From q checkpoint:",
tags = {}
}, {
data = "b",
tags = {}
} } }
{ "text", { {
data = "c",
tags = {}
} } }
{ "text", { {
data = "d",
tags = {}
} } }
{ "text", { {
data = "From q checkpoint again:",
tags = {}
}, {
data = "b",
tags = {}
} } }
{ "text", { {
data = "c",
tags = {}
} } }
{ "text", { {
data = "d",
tags = {}
} } }
{ "text", { {
data = "Force p checkpoint:",
tags = {}
}, {
data = "a",
tags = {}
} } }
{ "text", { {
data = "c",
tags = {}
} } }
{ "return" }
]]--

View file

@ -0,0 +1,16 @@
$ f
# "a":"a"
a
~ 1 # "b":"b"
§ p
b # "c":"c"
c
d
e
~ f
~ f

View file

@ -0,0 +1,85 @@
local _={}
_[32]={}
_[31]={a="a"}
_[30]={a="a",b="b"}
_[29]={a="a",c="c",b="b"}
_[28]={}
_[27]={a="a",b="b"}
_[26]={a="a"}
_[25]={tags=_[32],data="e"}
_[24]={tags=_[31],data="d"}
_[23]={tags=_[30],data="c"}
_[22]={tags=_[29],data="b"}
_[21]={tags=_[28],data="e"}
_[20]={tags=_[26],data="d"}
_[19]={tags=_[27],data="c"}
_[18]={tags=_[26],data="a"}
_[17]={_[25]}
_[16]={_[24]}
_[15]={_[23]}
_[14]={_[22]}
_[13]={_[21]}
_[12]={_[20]}
_[11]={_[19]}
_[10]={_[18]}
_[9]={"return"}
_[8]={"text",_[17]}
_[7]={"text",_[16]}
_[6]={"text",_[15]}
_[5]={"text",_[14]}
_[4]={"text",_[13]}
_[3]={"text",_[12]}
_[2]={"text",_[11]}
_[1]={"text",_[10]}
return {_[1],_[2],_[3],_[4],_[5],_[6],_[7],_[8],_[9]}
--[[
{ "text", { {
data = "a",
tags = {
a = "a"
}
} } }
{ "text", { {
data = "c",
tags = {
a = "a",
b = "b"
}
} } }
{ "text", { {
data = "d",
tags = {
a = "a"
}
} } }
{ "text", { {
data = "e",
tags = {}
} } }
{ "text", { {
data = "b",
tags = {
a = "a",
b = "b",
c = "c"
}
} } }
{ "text", { {
data = "c",
tags = {
a = "a",
b = "b"
}
} } }
{ "text", { {
data = "d",
tags = {
a = "a"
}
} } }
{ "text", { {
data = "e",
tags = {}
} } }
{ "return" }
]]--