diff --git a/anselme.lua b/anselme.lua index 3d4740e..0b8747b 100644 --- a/anselme.lua +++ b/anselme.lua @@ -25,11 +25,11 @@ local eval = require(anselme_root.."interpreter.expression") local run_line = require(anselme_root.."interpreter.interpreter").run_line local run = require(anselme_root.."interpreter.interpreter").run local to_lua = require(anselme_root.."interpreter.common").to_lua -local identifier_pattern = require(anselme_root.."parser.common").identifier_pattern local merge_state = require(anselme_root.."interpreter.common").merge_state local stdfuncs = require(anselme_root.."stdlib.functions") local bootscript = require(anselme_root.."stdlib.bootscript") local copy = require(anselme_root.."common").copy +local should_keep_variable = require(anselme_root.."interpreter.common").should_keep_variable -- wrappers for love.filesystem / luafilesystem local function list_directory(path) @@ -410,7 +410,7 @@ local vm_mt = { save = function(self) local vars = {} for k, v in pairs(self.state.variables) do - if v.type ~= "undefined argument" and v.type ~= "pending definition" and k:match("^"..identifier_pattern.."$") then + if should_keep_variable(self.state, k) then vars[k] = v end end @@ -479,15 +479,17 @@ local vm_mt = { functions = self.state.functions, -- no need for a cache as we can't define or modify any function from the interpreter for now variables = setmetatable({}, { __index = function(variables, k) - local cache = getmetatable(variables).cache + local mt = getmetatable(variables) + local cache = mt.cache if cache[k] == nil then cache[k] = copy(self.state.variables[k], getmetatable(variables).copy_cache) end return cache[k] end, + -- variables that keep current state and should be cleared at each checkpoint 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 + 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 7e7d9d7..381f5d0 100644 --- a/common.lua +++ b/common.lua @@ -1,4 +1,7 @@ +local identifier_pattern + --- replace values recursively in table t according to to_replace ([old table] = new table) +-- already_replaced is a temporary table to avoid infinite loop & duplicate processing, no need to give it local function replace_in_table(t, to_replace, already_replaced) already_replaced = already_replaced or {} already_replaced[t] = true @@ -56,8 +59,12 @@ common = { for _, m in ipairs(copied_to_replace) do replace_in_table(m, not_modified) end - -- replace + -- replace in t replace_in_table(t, to_replace) end } + +package.loaded[...] = common +identifier_pattern = require((...):gsub("common$", "parser.common")).identifier_pattern + return common diff --git a/interpreter/common.lua b/interpreter/common.lua index d6d0d7f..40a746e 100644 --- a/interpreter/common.lua +++ b/interpreter/common.lua @@ -2,6 +2,7 @@ local atypes, ltypes local eval, run_block local replace_with_copied_values local common +local identifier_pattern --- copy some text & process it to be suited to be sent to Lua in an event local function post_process_text(state, text) @@ -57,7 +58,9 @@ common = { mt.cache = {} -- merge modified re-assigned variables for var, value in pairs(state.variables) do - global.variables[var] = value + if common.should_keep_variable(state, var) then + global.variables[var] = value + end state.variables[var] = nil end end, @@ -78,6 +81,20 @@ common = { return var end end, + set_variable = function(state, name, val) + state.variables[name] = val + end, + --- mark a table as modified, so it will be merged on the next checkpoint if it appears somewhere in a value + mark_as_modified = function(state, v) + local modified = getmetatable(state.variables).modified_tables + table.insert(modified, v) + end, + --- returns true if a variable should be persisted on save/merge + -- will exclude: undefined variables, variables in functions defined with parentheses, internal anselme variables + should_keep_variable = function(state, name) + local v = state.variables[name] + return v.type ~= "undefined argument" and v.type ~= "pending definition" and name:match("^"..identifier_pattern.."$") and not name:match("^anselme%.") + end, --- check truthyness of an anselme value truthy = function(val) if val.type == "number" then @@ -408,5 +425,6 @@ atypes, ltypes = types.anselme, types.lua eval = require((...):gsub("common$", "expression")) run_block = require((...):gsub("common$", "interpreter")).run_block replace_with_copied_values = require((...):gsub("interpreter%.common$", "common")).replace_with_copied_values +identifier_pattern = require((...):gsub("interpreter%.common$", "parser.common")).identifier_pattern return common diff --git a/interpreter/expression.lua b/interpreter/expression.lua index 7f4d4c5..5fe49b0 100644 --- a/interpreter/expression.lua +++ b/interpreter/expression.lua @@ -1,5 +1,5 @@ local expression -local to_lua, from_lua, eval_text, is_of_type, truthy, format, pretty_type, get_variable, tags, eval_text_callback, events, flatten_list +local to_lua, from_lua, eval_text, is_of_type, truthy, format, pretty_type, get_variable, tags, eval_text_callback, events, flatten_list, set_variable local run @@ -85,7 +85,7 @@ local function eval(state, exp) local name = exp.left.name local val, vale = eval(state, exp.right) if not val then return val, vale end - state.variables[name] = val + set_variable(state, name, val) return val else return nil, ("don't know how to perform assignment on %s expression"):format(exp.left.type) @@ -257,11 +257,11 @@ local function eval(state, exp) depths[j] = math.huge end -- set - state.variables[param.full_name] = val + set_variable(state, param.full_name, val) -- default: evaluate once function is selected -- there's no need to type check because the type annotation is already the default value's type, because of syntax elseif param.default then - state.variables[param.full_name] = { type = "pending definition", value = { expression = param.default, source = fn.source } } + set_variable(state, param.full_name, { type = "pending definition", value = { expression = param.default, source = fn.source } }) else ok = false table.insert(tried_function_error_messages, ("%s: missing mandatory argument %q in function %q call"):format(fn.pretty_signature, param.name, fn.name)) @@ -300,7 +300,7 @@ local function eval(state, exp) depths.assignment = math.huge end -- set - state.variables[param.full_name] = assignment + set_variable(state, param.full_name, assignment) end if ok then if not selected_variant.variant then @@ -340,7 +340,7 @@ local function eval(state, exp) return nil, ("unknown function type %q"):format(fn.type) end end - -- function successfully selected + -- function successfully selected: run if selected_variant.variant then local fn = selected_variant.variant if fn.type == "checkpoint" then @@ -398,7 +398,9 @@ local function eval(state, exp) elseif lua_fn.mode == nil then local l_lua = {} for _, v in ipairs(final_args) do - table.insert(l_lua, to_lua(v)) + local lv, e = to_lua(v) + if e then return nil, e end + table.insert(l_lua, lv) end local r, e if _VERSION == "Lua 5.1" and not jit then -- PUC Lua 5.1 doesn't allow yield from a pcall @@ -429,10 +431,10 @@ local function eval(state, exp) if not ret then return ret, e end end -- update function vars - state.variables[fn.namespace.."๐Ÿ‘๏ธ"] = { + set_variable(state, fn.namespace.."๐Ÿ‘๏ธ", { type = "number", value = seen.value + 1 - } + }) -- return value if not ret then return nil, ("function %q didn't return a value"):format(exp.called_name) end return ret @@ -464,6 +466,6 @@ run = require((...):gsub("expression$", "interpreter")).run expression = require((...):gsub("interpreter%.expression$", "parser.expression")) flatten_list = require((...):gsub("interpreter%.expression$", "parser.common")).flatten_list local common = require((...):gsub("expression$", "common")) -to_lua, from_lua, eval_text, is_of_type, truthy, format, pretty_type, get_variable, tags, eval_text_callback, events = common.to_lua, common.from_lua, common.eval_text, common.is_of_type, common.truthy, common.format, common.pretty_type, common.get_variable, common.tags, common.eval_text_callback, common.events +to_lua, from_lua, eval_text, is_of_type, truthy, format, pretty_type, get_variable, tags, eval_text_callback, events, set_variable = common.to_lua, common.from_lua, common.eval_text, common.is_of_type, common.truthy, common.format, common.pretty_type, common.get_variable, common.tags, common.eval_text_callback, common.events, common.set_variable return eval diff --git a/interpreter/interpreter.lua b/interpreter/interpreter.lua index 152065c..20c1d45 100644 --- a/interpreter/interpreter.lua +++ b/interpreter/interpreter.lua @@ -1,5 +1,5 @@ local eval -local truthy, merge_state, escape, get_variable, tags, events +local truthy, merge_state, escape, get_variable, tags, events, set_variable local run_line, run_block -- returns var in case of success and there is a return @@ -92,14 +92,14 @@ run_line = function(state, line) elseif line.type == "checkpoint" then local reached, reachede = get_variable(state, line.namespace.."๐Ÿ") if not reached then return nil, reachede end - state.variables[line.namespace.."๐Ÿ"] = { + set_variable(state, line.namespace.."๐Ÿ", { type = "number", value = reached.value + 1 - } - state.variables[line.parent_function.namespace.."๐Ÿ”–"] = { + }) + set_variable(state, line.parent_function.namespace.."๐Ÿ”–", { type = "string", value = line.name - } + }) merge_state(state) else return nil, ("unknown line type %q; at %s"):format(line.type, line.source) @@ -138,22 +138,22 @@ run_block = function(state, block, resume_from_there, i, j) if not seen then return nil, seene end local checkpoint, checkpointe = get_variable(state, parent_line.parent_function.namespace.."๐Ÿ”–") if not checkpoint then return nil, checkpointe end - state.variables[parent_line.namespace.."๐Ÿ‘๏ธ"] = { + set_variable(state, parent_line.namespace.."๐Ÿ‘๏ธ", { type = "number", value = seen.value + 1 - } - state.variables[parent_line.namespace.."๐Ÿ"] = { + }) + set_variable(state, parent_line.namespace.."๐Ÿ", { type = "number", value = reached.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 = checkpoint.value if not current_checkpoint:match("^"..escape(parent_line.name)) then - state.variables[parent_line.parent_function.namespace.."๐Ÿ”–"] = { + set_variable(state, parent_line.parent_function.namespace.."๐Ÿ”–", { type = "string", value = parent_line.name - } + }) end merge_state(state) end @@ -231,7 +231,7 @@ local interpreter = { package.loaded[...] = interpreter eval = require((...):gsub("interpreter$", "expression")) local common = require((...):gsub("interpreter$", "common")) -truthy, merge_state, tags, get_variable, events = common.truthy, common.merge_state, common.tags, common.get_variable, common.events +truthy, merge_state, tags, get_variable, events, set_variable = common.truthy, common.merge_state, common.tags, common.get_variable, common.events, common.set_variable escape = require((...):gsub("interpreter%.interpreter$", "parser.common")).escape return interpreter diff --git a/notes.txt b/notes.txt index 751d88b..55a733a 100644 --- a/notes.txt +++ b/notes.txt @@ -53,7 +53,9 @@ TODO: the function decorator feels a bit glued-on to the current syntax TODO: simplify language, it is much too complicated. Less line types? (var def, func, checkpoint, tag). Rewrite some ad hoc syntax using the expression system? -TODO: functions/checkpoint: separate from scoping and/or actual functions? with proper scoping & stuff +TODO: functions: might be nice to have actual scoping for functions that are called with arguments; to allow proper recursion & stuff (as right now if you call the same function from itself both instances will share variables...). Would require a fair amount of changes to the code though. + +TODO: a way to make loops TODO: fn/checkpoint/tag: maybe consider them a regular func call that takes children as arg; can keep compatibility using $/ยง as shortcut for the actual call. would allow more flexibility esp. for tags... diff --git a/stdlib/functions.lua b/stdlib/functions.lua index 083e82b..8b13de8 100644 --- a/stdlib/functions.lua +++ b/stdlib/functions.lua @@ -1,10 +1,4 @@ -local truthy, anselme, compare, is_of_type, identifier_pattern, format_identifier, find, get_variable - ---- mark a table as modified, so it will be merged on the next checkpoint if it appears somewhere in a value -local function mark_as_modified(v) - local modified = getmetatable(anselme.running.state.variables).modified_tables - table.insert(modified, v) -end +local truthy, anselme, compare, is_of_type, identifier_pattern, format_identifier, find, get_variable, mark_as_modified local lua_functions lua_functions = { @@ -116,7 +110,7 @@ lua_functions = { 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) + mark_as_modified(anselme.running.state, lv.value) return v end }, @@ -129,7 +123,7 @@ lua_functions = { 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) + mark_as_modified(anselme.running.state, x.value) return v end end @@ -138,7 +132,7 @@ lua_functions = { type = "pair", value = { kv, v } }) - mark_as_modified(lv.value) + mark_as_modified(anselme.running.state, lv.value) return v end }, @@ -151,7 +145,7 @@ lua_functions = { ["_!(fn::variable reference)"] = { mode = "untyped raw", value = function(v) - return anselme.running.state.variables[v.value] + return get_variable(anselme.running.state, v.value) end }, -- format @@ -226,7 +220,7 @@ lua_functions = { value = function(l, v) local lv = l.type == "type" and l.value[1] or l table.insert(lv.value, v) - mark_as_modified(lv.value) + mark_as_modified(anselme.running.state, lv.value) return l end }, @@ -236,21 +230,21 @@ lua_functions = { 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) + mark_as_modified(anselme.running.state, lv.value) return l end }, ["remove(l::list)"] = { mode = "untyped raw", value = function(l) - mark_as_modified(l.value) + mark_as_modified(anselme.running.state, l.value) return table.remove(l.value) end }, ["remove(l::list, i::number)"] = { mode = "untyped raw", value = function(l, i) - mark_as_modified(l.value) + mark_as_modified(anselme.running.state, l.value) return table.remove(l.value, i.value) end }, @@ -337,7 +331,7 @@ local functions = { package.loaded[...] = functions local icommon = require((...):gsub("stdlib%.functions$", "interpreter.common")) -truthy, compare, is_of_type, get_variable = icommon.truthy, icommon.compare, icommon.is_of_type, icommon.get_variable +truthy, compare, is_of_type, get_variable, mark_as_modified = icommon.truthy, icommon.compare, icommon.is_of_type, icommon.get_variable, icommon.mark_as_modified local pcommon = require((...):gsub("stdlib%.functions$", "parser.common")) identifier_pattern, format_identifier, find = pcommon.identifier_pattern, pcommon.format_identifier, pcommon.find anselme = require((...):gsub("stdlib%.functions$", "anselme"))