diff --git a/README.md b/README.md index 8502ca2..6110267 100644 --- a/README.md +++ b/README.md @@ -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 -* `#`: 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" @@ -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. -* `~`: 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. @@ -559,6 +559,12 @@ Method style calling is also possible, like with functions. 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 Built-in operators: diff --git a/interpreter/expression.lua b/interpreter/expression.lua index fb2d4c7..8938429 100644 --- a/interpreter/expression.lua +++ b/interpreter/expression.lua @@ -103,25 +103,7 @@ local function eval(state, exp) if fn.value.type == "paragraph" or fn.value.paragraph then local r, e if fn.value.type == "paragraph" then - r, e = run_block(state, fn.value.child) - 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 + r, e = run(state, fn.value.child, not exp.explicit_call) -- paragraph decorators: run single line or resume from it. -- checkpoint & seen variables will be updated from the interpreter usual paragraph-reaching code. elseif exp.explicit_call then diff --git a/interpreter/interpreter.lua b/interpreter/interpreter.lua index 5b10103..3c55eb6 100644 --- a/interpreter/interpreter.lua +++ b/interpreter/interpreter.lua @@ -1,11 +1,12 @@ local eval -local truthy, flush_state, to_lua, eval_text +local truthy, flush_state, to_lua, eval_text, escape local tags = { + --- push new tags on top of the stack, from Anselme values push = function(self, state, val) local new = {} -- 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 -- merge with new values if val.type ~= "list" then val = { type = "list", value = { val } } end @@ -13,14 +14,27 @@ local tags = { -- add table.insert(state.interpreter.tags, new) 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) table.remove(state.interpreter.tags) end, + --- return current lua tags table current = function(self, state) return state.interpreter.tags[#state.interpreter.tags] or {} end, - push_ignore_past = function(self, state, tags) - table.insert(state.interpreter.tags, tags) + --- returns length of tags stack + 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 } @@ -94,13 +108,11 @@ local function run_line(state, line) }) write_event(state, "choice", t) elseif line.type == "tag" then - if line.expression then - local v, e = eval(state, line.expression) - if not v then return v, ("%s; at %s"):format(e, line.source) end - tags:push(state, v) - end - local v, e = run_block(state, line.child) - if line.expression then tags:pop(state) end + local v, e = eval(state, line.expression) + if not v then return v, ("%s; at %s"):format(e, line.source) end + tags:push(state, v) + v, e = run_block(state, line.child) + tags:pop(state) if e then return v, e end if v then return v end elseif line.type == "return" then @@ -127,7 +139,7 @@ local function run_line(state, line) else local choice = state.interpreter.choice_available[sel] 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) tags:pop(state) if e then return v, e end @@ -142,7 +154,7 @@ local function run_line(state, line) if line.tag then tags:pop(state) end - -- paragraph decorator + -- paragraph decorator and line if line.paragraph then state.variables[line.namespace.."๐Ÿ‘๏ธ"] = { type = "number", @@ -182,10 +194,31 @@ run_block = function(state, block, resume_from_there, i, j) end i = i + 1 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 -- 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 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 local parent_line = block.parent_line 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 parent_line.parent_block.last_condition_success = true 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) if e then return v, e end if v then return v, e end @@ -203,8 +239,39 @@ end -- returns var in case of success -- return nil, err in case of error 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 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 v then return v @@ -227,5 +294,6 @@ package.loaded[...] = interpreter eval = require((...):gsub("interpreter$", "expression")) local common = require((...):gsub("interpreter$", "common")) 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 diff --git a/parser/preparser.lua b/parser/preparser.lua index 887677d..a6751ca 100644 --- a/parser/preparser.lua +++ b/parser/preparser.lua @@ -307,7 +307,7 @@ local function parse_line(line, state, namespace) if expr:match("[^%s]") then r.expression = expr else - r.expression = nil + r.expression = "()" end -- return elseif l:match("^%@") then diff --git a/test/tests/resume from nested paragraph.ans b/test/tests/resume from nested paragraph.ans new file mode 100644 index 0000000..6246246 --- /dev/null +++ b/test/tests/resume from nested paragraph.ans @@ -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() diff --git a/test/tests/resume from nested paragraph.lua b/test/tests/resume from nested paragraph.lua new file mode 100644 index 0000000..a53ccbe --- /dev/null +++ b/test/tests/resume from nested paragraph.lua @@ -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" } +]]-- \ No newline at end of file diff --git a/test/tests/resume from paragraph restore tags.ans b/test/tests/resume from paragraph restore tags.ans new file mode 100644 index 0000000..1224233 --- /dev/null +++ b/test/tests/resume from paragraph restore tags.ans @@ -0,0 +1,16 @@ +$ f + # "a":"a" + a + ~ 1 # "b":"b" + ยง p + b # "c":"c" + + c + + d + + e + +~ f + +~ f diff --git a/test/tests/resume from paragraph restore tags.lua b/test/tests/resume from paragraph restore tags.lua new file mode 100644 index 0000000..f3540c6 --- /dev/null +++ b/test/tests/resume from paragraph restore tags.lua @@ -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" } +]]-- \ No newline at end of file