From 04c6683de8c343900a41a874a894b104486f08b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Reuh=20Fildadut?= Date: Mon, 6 Dec 2021 18:34:58 +0100 Subject: [PATCH] Proper checkpointing of mutable values --- REFERENCE.md | 2 + anselme.lua | 6 ++- common.lua | 43 ++++++++++++++++++- interpreter/common.lua | 39 +++++++++++------ stdlib/functions.lua | 18 +++++--- test/run.lua | 2 +- test/tests/merge nested mutable bis.ans | 17 ++++++++ test/tests/merge nested mutable bis.lua | 21 +++++++++ test/tests/merge nested mutable error bis.ans | 19 ++++++++ test/tests/merge nested mutable error bis.lua | 21 +++++++++ test/tests/merge nested mutable error.ans | 19 ++++++++ test/tests/merge nested mutable error.lua | 21 +++++++++ test/tests/merge nested mutable.ans | 17 ++++++++ test/tests/merge nested mutable.lua | 21 +++++++++ 14 files changed, 243 insertions(+), 23 deletions(-) create mode 100644 test/tests/merge nested mutable bis.ans create mode 100644 test/tests/merge nested mutable bis.lua create mode 100644 test/tests/merge nested mutable error bis.ans create mode 100644 test/tests/merge nested mutable error bis.lua create mode 100644 test/tests/merge nested mutable error.ans create mode 100644 test/tests/merge nested mutable error.lua create mode 100644 test/tests/merge nested mutable.ans create mode 100644 test/tests/merge nested mutable.lua diff --git a/REFERENCE.md b/REFERENCE.md index fc6cdad..821745a 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -49,6 +49,8 @@ $ parallel parallel: {main.var} ~ main + +(note: if two scripts try to modify the same value at the same time, one of them will win, but which one is undefined/a surprise) ``` The purpose of this system is both to allow several scripts to run at the same time with an easy way to avoid interferences, and to make sure the global state is always in a consistent (and not in the middle of a calculation): since scripts can be interrupted at any time, when it is interrupted, anything that was changed between the last checkpoint and the interruption will be discarded. If you're a RDBMS person, that's more-or-less equivalent to a transaction with a repeatable read isolation level (without any sort of locking or lost update protection though). diff --git a/anselme.lua b/anselme.lua index d8f8a2d..884dc8f 100644 --- a/anselme.lua +++ b/anselme.lua @@ -479,11 +479,13 @@ local vm_mt = { __index = function(variables, k) local cache = getmetatable(variables).cache if cache[k] == nil then - cache[k] = copy(self.state.variables[k]) + cache[k] = copy(self.state.variables[k], getmetatable(variables).copy_cache) end return cache[k] end, - cache = {} -- cache of previously read values, to get repeatable reads & handle mutable types without changing global state + copy_cache = {}, -- table of [original table] = copied table + modified_tables = {}, -- list of modified tables (copies) that should be merged with global state on next checkpoint + cache = {} -- cache of previously read values (copies), to get repeatable reads & handle mutable types without changing global state }), interpreter = { -- constant diff --git a/common.lua b/common.lua index a6f0e07..7e7d9d7 100644 --- a/common.lua +++ b/common.lua @@ -1,6 +1,20 @@ +--- replace values recursively in table t according to to_replace ([old table] = new table) +local function replace_in_table(t, to_replace, already_replaced) + already_replaced = already_replaced or {} + already_replaced[t] = true + for k, v in pairs(t) do + if to_replace[v] then + t[k] = to_replace[v] + elseif type(v) == "table" and not already_replaced[v] then + replace_in_table(v, to_replace, already_replaced) + end + end +end + local common common = { - --- recursively copy a table, handle cyclic references, no metatable + --- recursively copy a table, handle cyclic references, no metatable, don't copy keys + -- cache is table with copied tables [original table] = copied value, will use temporary table is omitted copy = function(t, cache) if type(t) == "table" then cache = cache or {} @@ -17,6 +31,33 @@ common = { else return t end + end, + --- given a table t from which some copy was issued, the copy cache, and a list of tables from the copied version, + -- put theses copied tables in t in place of their original values, preserving references to non-modified values + replace_with_copied_values = function(t, cache, copied_to_replace) + -- reverse copy cache + local ehcac = {} + for k, v in pairs(cache) do ehcac[v] = k end + -- build table of [original table] = replacement copied table + local to_replace = {} + for _, v in ipairs(copied_to_replace) do + local original = ehcac[v] + if original then -- table doesn't have an original value if it's a new table... + to_replace[original] = v + end + end + -- fix references to not-modified tables in modified values + local not_modified = {} + for original, modified in pairs(cache) do + if not to_replace[original] then + not_modified[modified] = original + end + end + for _, m in ipairs(copied_to_replace) do + replace_in_table(m, not_modified) + end + -- replace + replace_in_table(t, to_replace) end } return common diff --git a/interpreter/common.lua b/interpreter/common.lua index 7e56232..d6d0d7f 100644 --- a/interpreter/common.lua +++ b/interpreter/common.lua @@ -1,6 +1,6 @@ local atypes, ltypes local eval, run_block -local copy +local replace_with_copied_values local common --- copy some text & process it to be suited to be sent to Lua in an event @@ -49,17 +49,13 @@ common = { global.aliases[alias] = fqm state.aliases[alias] = nil end - -- variable state - -- move values modifed in-place from read cache to variables - local cache = getmetatable(state.variables).cache - for var, value in pairs(cache) do - if value.modified then - value.modified = nil - state.variables[var] = value - end - cache[var] = nil - end - -- merge modified variables + -- merge modified mutable varables + local mt = getmetatable(state.variables) + replace_with_copied_values(global.variables, mt.copy_cache, mt.modified_tables) + mt.copy_cache = {} + mt.modified = {} + mt.cache = {} + -- merge modified re-assigned variables for var, value in pairs(state.variables) do global.variables[var] = value state.variables[var] = nil @@ -109,6 +105,23 @@ common = { end end return true + elseif a.type == "function reference" then + if #a.value ~= #b.value then + return false + end + for _, aname in ipairs(a.value) do + local found = false + for _, bname in ipairs(b.value) do + if aname == bname then + found = true + break + end + end + if not found then + return false + end + end + return true else return a.value == b.value end @@ -394,6 +407,6 @@ local types = require((...):gsub("interpreter%.common$", "stdlib.types")) atypes, ltypes = types.anselme, types.lua eval = require((...):gsub("common$", "expression")) run_block = require((...):gsub("common$", "interpreter")).run_block -copy = require((...):gsub("interpreter%.common$", "common")).copy +replace_with_copied_values = require((...):gsub("interpreter%.common$", "common")).replace_with_copied_values return common diff --git a/stdlib/functions.lua b/stdlib/functions.lua index b0d3645..585c76a 100644 --- a/stdlib/functions.lua +++ b/stdlib/functions.lua @@ -1,5 +1,10 @@ local truthy, anselme, compare, is_of_type, identifier_pattern, format_identifier, find, get_variable +local function mark_as_modified(v) + local modified = getmetatable(anselme.running.state.variables).modified_tables + table.insert(modified, v) +end + local functions functions = { -- discard left @@ -107,23 +112,23 @@ functions = { ["()(l::list, i::number) := v"] = { mode = "raw", value = function(l, i, v) - l.modified = true local lv = l.type == "type" and l.value[1] or l local iv = i.type == "type" and i.value[1] or i lv.value[iv.value] = v + mark_as_modified(lv.value) return v end }, ["()(l::list, k::string) := v"] = { mode = "raw", value = function(l, k, v) - l.modified = true local lv = l.type == "type" and l.value[1] or l local kv = k.type == "type" and k.value[1] or k -- update index for _, x in ipairs(lv.value) do if x.type == "pair" and compare(x.value[1], kv) then x.value[2] = v + mark_as_modified(x.value) return v end end @@ -132,6 +137,7 @@ functions = { type = "pair", value = { kv, v } }) + mark_as_modified(lv.value) return v end }, @@ -192,33 +198,33 @@ functions = { ["insert(l::list, v)"] = { mode = "raw", value = function(l, v) - l.modified = true local lv = l.type == "type" and l.value[1] or l table.insert(lv.value, v) + mark_as_modified(lv.value) return l end }, ["insert(l::list, i::number, v)"] = { mode = "raw", value = function(l, i, v) - l.modified = true local lv = l.type == "type" and l.value[1] or l local iv = i.type == "type" and i.value[1] or i table.insert(lv.value, iv.value, v) + mark_as_modified(lv.value) return l end }, ["remove(l::list)"] = { mode = "untyped raw", value = function(l) - l.modified = true + mark_as_modified(l.value) return table.remove(l.value) end }, ["remove(l::list, i::number)"] = { mode = "untyped raw", value = function(l, i) - l.modified = true + mark_as_modified(l.value) return table.remove(l.value, i.value) end }, diff --git a/test/run.lua b/test/run.lua index ae6e1af..7462adb 100644 --- a/test/run.lua +++ b/test/run.lua @@ -44,7 +44,7 @@ local function write_result(filebase, result) o:write(ser(result)) o:write("\n--[[\n") for _, v in ipairs(result) do - o:write(inspect(v).."\n") + o:write(inspect(v):gsub("]]", "] ]").."\n") -- professional-level bandaid when ]] appear in the output end o:write("]]--") o:close() diff --git a/test/tests/merge nested mutable bis.ans b/test/tests/merge nested mutable bis.ans new file mode 100644 index 0000000..952b4a3 --- /dev/null +++ b/test/tests/merge nested mutable bis.ans @@ -0,0 +1,17 @@ +:post run = "check" + +:a = [1] +:b = [2] + +~ a!insert(b) + +§ c + +~ b!insert(3) + +§ d + +~ b!insert(4) + +$ check + \[1,\[2,3,4]]: {a} diff --git a/test/tests/merge nested mutable bis.lua b/test/tests/merge nested mutable bis.lua new file mode 100644 index 0000000..e5ac112 --- /dev/null +++ b/test/tests/merge nested mutable bis.lua @@ -0,0 +1,21 @@ +local _={} +_[8]={} +_[7]={} +_[6]={text="[1, [2, 3, 4]]",tags=_[8]} +_[5]={text="[1,[2,3,4]]: ",tags=_[7]} +_[4]={_[5],_[6]} +_[3]={"return"} +_[2]={"text",_[4]} +_[1]={"return"} +return {_[1],_[2],_[3]} +--[[ +{ "return" } +{ "text", { { + tags = {}, + text = "[1,[2,3,4] ]: " + }, { + tags = {}, + text = "[1, [2, 3, 4] ]" + } } } +{ "return" } +]]-- \ No newline at end of file diff --git a/test/tests/merge nested mutable error bis.ans b/test/tests/merge nested mutable error bis.ans new file mode 100644 index 0000000..e36a041 --- /dev/null +++ b/test/tests/merge nested mutable error bis.ans @@ -0,0 +1,19 @@ +:post run = "check" + +:a = [1] +:b = [2] + +~ a!insert(b) + +§ c + +~ b!insert(3) + +§ d + +~ b!insert(4) + +~ error("abort") + +$ check + \[1,\[2,3]]: {a} diff --git a/test/tests/merge nested mutable error bis.lua b/test/tests/merge nested mutable error bis.lua new file mode 100644 index 0000000..eb95ba6 --- /dev/null +++ b/test/tests/merge nested mutable error bis.lua @@ -0,0 +1,21 @@ +local _={} +_[8]={} +_[7]={} +_[6]={text="[1, [2, 3]]",tags=_[8]} +_[5]={text="[1,[2,3]]: ",tags=_[7]} +_[4]={_[5],_[6]} +_[3]={"return"} +_[2]={"text",_[4]} +_[1]={"error","abort; in Lua function \"error\"; at test/tests/merge nested mutable error bis.ans:16"} +return {_[1],_[2],_[3]} +--[[ +{ "error", 'abort; in Lua function "error"; at test/tests/merge nested mutable error bis.ans:16' } +{ "text", { { + tags = {}, + text = "[1,[2,3] ]: " + }, { + tags = {}, + text = "[1, [2, 3] ]" + } } } +{ "return" } +]]-- \ No newline at end of file diff --git a/test/tests/merge nested mutable error.ans b/test/tests/merge nested mutable error.ans new file mode 100644 index 0000000..767c70f --- /dev/null +++ b/test/tests/merge nested mutable error.ans @@ -0,0 +1,19 @@ +:post run = "check" + +:a = [1] +:b = [2] + +~ a!insert(b) + +§ c + +~ b!insert(3) + +§ d + +~ a!insert(4) + +~ error("abort") + +$ check + \[1,\[2,3]]: {a} diff --git a/test/tests/merge nested mutable error.lua b/test/tests/merge nested mutable error.lua new file mode 100644 index 0000000..21a94d3 --- /dev/null +++ b/test/tests/merge nested mutable error.lua @@ -0,0 +1,21 @@ +local _={} +_[8]={} +_[7]={} +_[6]={text="[1, [2, 3]]",tags=_[8]} +_[5]={text="[1,[2,3]]: ",tags=_[7]} +_[4]={_[5],_[6]} +_[3]={"return"} +_[2]={"text",_[4]} +_[1]={"error","abort; in Lua function \"error\"; at test/tests/merge nested mutable error.ans:16"} +return {_[1],_[2],_[3]} +--[[ +{ "error", 'abort; in Lua function "error"; at test/tests/merge nested mutable error.ans:16' } +{ "text", { { + tags = {}, + text = "[1,[2,3] ]: " + }, { + tags = {}, + text = "[1, [2, 3] ]" + } } } +{ "return" } +]]-- \ No newline at end of file diff --git a/test/tests/merge nested mutable.ans b/test/tests/merge nested mutable.ans new file mode 100644 index 0000000..af459c6 --- /dev/null +++ b/test/tests/merge nested mutable.ans @@ -0,0 +1,17 @@ +:post run = "check" + +:a = [1] +:b = [2] + +~ a!insert(b) + +§ c + +~ b!insert(3) + +§ d + +~ a!insert(4) + +$ check + \[1,\[2,3],4]: {a} diff --git a/test/tests/merge nested mutable.lua b/test/tests/merge nested mutable.lua new file mode 100644 index 0000000..641a55a --- /dev/null +++ b/test/tests/merge nested mutable.lua @@ -0,0 +1,21 @@ +local _={} +_[8]={} +_[7]={} +_[6]={text="[1, [2, 3], 4]",tags=_[8]} +_[5]={text="[1,[2,3],4]: ",tags=_[7]} +_[4]={_[5],_[6]} +_[3]={"return"} +_[2]={"text",_[4]} +_[1]={"return"} +return {_[1],_[2],_[3]} +--[[ +{ "return" } +{ "text", { { + tags = {}, + text = "[1,[2,3],4]: " + }, { + tags = {}, + text = "[1, [2, 3], 4]" + } } } +{ "return" } +]]-- \ No newline at end of file