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:
		
							parent
							
								
									5c3e9d2c5d
								
							
						
					
					
						commit
						f9edaff7e7
					
				
					 8 changed files with 353 additions and 35 deletions
				
			
		
							
								
								
									
										10
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										10
									
								
								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: | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
| 			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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
							
								
								
									
										26
									
								
								test/tests/resume from nested paragraph.ans
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								test/tests/resume from nested paragraph.ans
									
										
									
									
									
										Normal 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() | ||||
							
								
								
									
										135
									
								
								test/tests/resume from nested paragraph.lua
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								test/tests/resume from nested paragraph.lua
									
										
									
									
									
										Normal 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" } | ||||
| ]]-- | ||||
							
								
								
									
										16
									
								
								test/tests/resume from paragraph restore tags.ans
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								test/tests/resume from paragraph restore tags.ans
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | |||
| $ f | ||||
| 	# "a":"a" | ||||
| 		a | ||||
| 		~ 1 # "b":"b" | ||||
| 			§ p | ||||
| 				b # "c":"c" | ||||
| 
 | ||||
| 			c | ||||
| 
 | ||||
| 		d | ||||
| 
 | ||||
| 	e | ||||
| 
 | ||||
| ~ f | ||||
| 
 | ||||
| ~ f | ||||
							
								
								
									
										85
									
								
								test/tests/resume from paragraph restore tags.lua
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								test/tests/resume from paragraph restore tags.lua
									
										
									
									
									
										Normal 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" } | ||||
| ]]-- | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue