diff --git a/LANGUAGE.md b/LANGUAGE.md index 782f9b6..48bfe75 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -366,6 +366,14 @@ $ f :bar : alias = 12 ``` +You can also use two colons instead of one to define a constant: + +``` +::foo = 42 +``` + +After their declaration and first evaluation, constants cannot be reassigned and their value is marked as constant (i.e. can not be modified even it is of a mutable type). Constants are not stored in save files and should therefore always contain the result of the expression written in the script file, even if the script has been updated. + * empty line: flush the event buffer, i.e., if there are any pending lines of text or choices, send them to your game. See [Event buffer](#event-buffer). This line always keep the same identation as the last non-empty line, so you don't need to put invisible whitespace on an empty-looking line. Is also automatically added at the end of a file. * regular text: write some text into the [event buffer](#event-buffer). Support [text interpolation](#text-interpolation). Support [escape codes](#escape-codes). @@ -1040,6 +1048,8 @@ This only works on strings: `is a(v, type or annotation)`: check if v is of a certain type or annotation +`constant(v)`: create a constant copy of v and returns it. The resulting value is immutable, even if it contains mutable types (will raise an error if you try to change it). + #### Built-in variables Variables for default types (each is associated to a string of the internal variable type name): `nil`, `number`, `string`, `list`, `pair`, `function reference`, `variable reference`. diff --git a/anselme.lua b/anselme.lua index b096f65..b354251 100644 --- a/anselme.lua +++ b/anselme.lua @@ -668,6 +668,7 @@ local vm_mt = { aliases = setmetatable({}, { __index = self.state.aliases }), functions = self.state.functions, -- no need for a cache as we can't define or modify any function from the interpreter for now variable_constraints = self.state.variable_constraints, -- no cache as constraints are expected to be constant + variable_constants = self.state.variable_constants, variables = setmetatable({}, { __index = function(variables, k) local cache = getmetatable(variables).cache @@ -763,6 +764,9 @@ return setmetatable(anselme, { variable_constraints = { -- foo = { constraint }, ... }, + variable_constants = { + -- foo = true, ... + }, variables = { -- foo = { -- type = "number", diff --git a/interpreter/common.lua b/interpreter/common.lua index 42892c3..64806e0 100644 --- a/interpreter/common.lua +++ b/interpreter/common.lua @@ -3,6 +3,7 @@ local eval, run_block local replace_with_copied_values, fix_not_modified_references local common local identifier_pattern +local copy --- copy some text & process it to be suited to be sent to Lua in an event local function post_process_text(state, text) @@ -117,6 +118,31 @@ common = { end return math.huge end, + --- checks if the variable is mutable + -- returns true + -- returns nil, mutation illegal message + check_mutable = function(state, fqm) + if state.variable_constants[fqm] then + return nil, ("can't change the value of a constant %q"):format(fqm) + end + return true + end, + --- mark a value as constant, recursively affecting all the potentially mutable subvalues + mark_constant = function(v) + if v.type == "list" then + v.constant = true + for _, item in ipairs(v.value) do + common.mark_constant(item) + end + elseif v.type == "object" then + v.constant = true + elseif v.type == "pair" or v.type == "annotated" then + common.mark_constant(v.value[1]) + common.mark_constant(v.value[2]) + elseif v.type ~= "nil" and v.type ~= "number" and v.type ~= "string" and v.type ~= "function reference" and v.type ~= "variable reference" then + error("unknown type") + end + end, --- returns a variable's value, evaluating a pending expression if neccessary -- if you're sure the variable has already been evaluated, use state.variables[fqm] directly -- return var @@ -128,7 +154,7 @@ common = { if not v then return nil, ("%s; while evaluating default value for variable %q defined at %s"):format(e, fqm, var.value.source) end - local s, err = common.set_variable(state, fqm, v) + local s, err = common.set_variable(state, fqm, v, state.variable_constants[fqm]) if not s then return nil, err end return v else @@ -138,8 +164,19 @@ common = { --- set the value of a variable -- returns true -- returns nil, err - set_variable = function(state, name, val) + set_variable = function(state, name, val, defining_a_constant) if val.type ~= "pending definition" then + -- check constant + if defining_a_constant then + val = copy(val) + common.mark_constant(val) + else + local s, e = common.check_mutable(state, name) + if not s then + return nil, ("%s; while assigning value to variable %q"):format(e, name) + end + end + -- check constraint local s, e = common.check_constraint(state, name, val) if not s then return nil, ("%s; while assigning value to variable %q"):format(e, name) @@ -219,9 +256,9 @@ common = { table.insert(modified, v) end, --- returns true if a variable should be persisted on save - -- will exclude: undefined variables, variables in scoped functions, internal anselme variables + -- will exclude: undefined variables, variables in scoped functions, constants, internal anselme variables should_keep_variable = function(state, name, value) - return value.type ~= "undefined argument" and value.type ~= "pending definition" and name:match("^"..identifier_pattern.."$") and not name:match("^anselme%.") + return value.type ~= "undefined argument" and value.type ~= "pending definition" and name:match("^"..identifier_pattern.."$") and not name:match("^anselme%.") and not state.variable_constants[name] end, --- check truthyness of an anselme value truthy = function(val) @@ -560,5 +597,6 @@ run_block = require((...):gsub("common$", "interpreter")).run_block local acommon = require((...):gsub("interpreter%.common$", "common")) replace_with_copied_values, fix_not_modified_references = acommon.replace_with_copied_values, acommon.fix_not_modified_references identifier_pattern = require((...):gsub("interpreter%.common$", "parser.common")).identifier_pattern +copy = require((...):gsub("interpreter%.common$", "common")).copy return common diff --git a/parser/preparser.lua b/parser/preparser.lua index b841c97..8490192 100644 --- a/parser/preparser.lua +++ b/parser/preparser.lua @@ -291,8 +291,15 @@ local function parse_line(line, state, namespace, parent_function) elseif l:match("^:") then r.type = "definition" r.remove_from_block_ast = true + local rem = l:match("^:(.*)$") + -- check if constant + if rem:match("^:") then + rem = rem:match("^:(.*)$") + r.constant = true + end -- get identifier - local identifier, rem = l:match("^:("..identifier_pattern..")(.-)$") + local identifier + identifier, rem = rem:match("^("..identifier_pattern..")(.-)$") if not identifier then return nil, ("no valid identifier at start of definition line %q; at %s"):format(l, line.source) end -- format identifier local fqm = ("%s%s"):format(namespace, format_identifier(identifier)) @@ -319,6 +326,7 @@ local function parse_line(line, state, namespace, parent_function) r.fqm = fqm r.expression = exp state.variables[fqm] = { type = "pending definition", value = { expression = nil, source = r.source } } + if r.constant then state.variable_constants[fqm] = true end -- tag elseif l:match("^%#") then r.type = "tag" @@ -514,6 +522,7 @@ local function parse(state, s, name, source) inject = {}, aliases = setmetatable({}, { __index = state.aliases }), variable_constraints = setmetatable({}, { __index = state.variable_constraints }), + variable_constants = setmetatable({}, { __index = state.variable_constants }), variables = setmetatable({}, { __index = state.aliases }), functions = setmetatable({}, { __index = function(self, key) @@ -549,6 +558,9 @@ local function parse(state, s, name, source) for k,v in pairs(state_proxy.variable_constraints) do state.variable_constraints[k] = v end + for k,v in pairs(state_proxy.variable_constants) do + state.variable_constants[k] = v + end for k,v in pairs(state_proxy.variables) do state.variables[k] = v end diff --git a/stdlib/functions.lua b/stdlib/functions.lua index 8db855e..a551338 100644 --- a/stdlib/functions.lua +++ b/stdlib/functions.lua @@ -1,4 +1,4 @@ -local truthy, anselme, compare, is_of_type, identifier_pattern, format_identifier, find, get_variable, mark_as_modified, set_variable +local truthy, anselme, compare, is_of_type, identifier_pattern, format_identifier, find, get_variable, mark_as_modified, set_variable, check_mutable, copy, mark_constant local lua_functions lua_functions = { @@ -145,6 +145,13 @@ lua_functions = { local state = anselme.running.state local obj = r.value local name = n.value + -- check constant state + if r.constant then + return nil, "can't change the value of an attribute of a constant object" + end + if not check_mutable(state, obj.class.."."..name) then + return nil, "can't change the value of a constant attribute" + end -- attribute already present in object local var, vfqm = find(state.aliases, obj.attributes, "", obj.class.."."..name) if var then @@ -186,6 +193,7 @@ lua_functions = { value = function(l, i, v) local lv = l.type == "annotated" and l.value[1] or l local iv = i.type == "annotated" and i.value[1] or i + if lv.constant then return nil, "can't change the contents of a constant list" end lv.value[iv.value] = v mark_as_modified(anselme.running.state, lv.value) return v @@ -196,11 +204,12 @@ lua_functions = { value = function(l, k, v) local lv = l.type == "annotated" and l.value[1] or l local kv = k.type == "annotated" and k.value[1] or k + if lv.constant then return nil, "can't change the contents of a constant list" end -- 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(anselme.running.state, x.value) + mark_as_modified(anselme.running.state, x.value) -- FIXME i thought pairs were immutable... return v end end @@ -296,6 +305,7 @@ lua_functions = { mode = "raw", value = function(l, v) local lv = l.type == "annotated" and l.value[1] or l + if lv.constant then return nil, "can't insert values into a constant list" end table.insert(lv.value, v) mark_as_modified(anselme.running.state, lv.value) return l @@ -306,6 +316,7 @@ lua_functions = { value = function(l, i, v) local lv = l.type == "annotated" and l.value[1] or l local iv = i.type == "annotated" and i.value[1] or i + if lv.constant then return nil, "can't insert values into a constant list" end table.insert(lv.value, iv.value, v) mark_as_modified(anselme.running.state, lv.value) return l @@ -314,6 +325,7 @@ lua_functions = { ["remove(l::list)"] = { mode = "unannotated raw", value = function(l) + if l.constant then return nil, "can't remove values from a constant list" end mark_as_modified(anselme.running.state, l.value) return table.remove(l.value) end @@ -321,6 +333,7 @@ lua_functions = { ["remove(l::list, i::number)"] = { mode = "unannotated raw", value = function(l, i) + if l.constant then return nil, "can't remove values from a constant list" end mark_as_modified(anselme.running.state, l.value) return table.remove(l.value, i.value) end @@ -379,6 +392,14 @@ lua_functions = { value = is_of_type(v, t) or 0 } end + }, + ["constant(v)"] = { + mode = "raw", + value = function(v) + local c = copy(v) + mark_constant(c) + return c + end } } @@ -414,9 +435,10 @@ local functions = { package.loaded[...] = functions local icommon = require((...):gsub("stdlib%.functions$", "interpreter.common")) -truthy, compare, is_of_type, get_variable, mark_as_modified, set_variable = icommon.truthy, icommon.compare, icommon.is_of_type, icommon.get_variable, icommon.mark_as_modified, icommon.set_variable +truthy, compare, is_of_type, get_variable, mark_as_modified, set_variable, check_mutable, mark_constant = icommon.truthy, icommon.compare, icommon.is_of_type, icommon.get_variable, icommon.mark_as_modified, icommon.set_variable, icommon.check_mutable, icommon.mark_constant 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")) +copy = require((...):gsub("stdlib%.functions$", "common")).copy return functions diff --git a/test/tests/constant object attribute.ans b/test/tests/constant object attribute.ans new file mode 100644 index 0000000..80659b4 --- /dev/null +++ b/test/tests/constant object attribute.ans @@ -0,0 +1,8 @@ +% obj + ::a = 12 + +:x = obj() + +{x.a} + +{x.a := 52} diff --git a/test/tests/constant object attribute.lua b/test/tests/constant object attribute.lua new file mode 100644 index 0000000..ce7f8b8 --- /dev/null +++ b/test/tests/constant object attribute.lua @@ -0,0 +1,14 @@ +local _={} +_[5]={} +_[4]={tags=_[5],text="12"} +_[3]={_[4]} +_[2]={"error","can't change the value of a constant attribute; in Lua function \"_._\"; at test/tests/constant object attribute.ans:8"} +_[1]={"text",_[3]} +return {_[1],_[2]} +--[[ +{ "text", { { + tags = {}, + text = "12" + } } } +{ "error", "can't change the value of a constant attribute; in Lua function \"_._\"; at test/tests/constant object attribute.ans:8" } +]]-- \ No newline at end of file diff --git a/test/tests/constant object.ans b/test/tests/constant object.ans new file mode 100644 index 0000000..002fa35 --- /dev/null +++ b/test/tests/constant object.ans @@ -0,0 +1,8 @@ +% obj + :a = 12 + +::x = obj() + +{x.a} + +{x.a := 52} diff --git a/test/tests/constant object.lua b/test/tests/constant object.lua new file mode 100644 index 0000000..0c4c1b3 --- /dev/null +++ b/test/tests/constant object.lua @@ -0,0 +1,14 @@ +local _={} +_[5]={} +_[4]={tags=_[5],text="12"} +_[3]={_[4]} +_[2]={"error","can't change the value of an attribute of a constant object; in Lua function \"_._\"; at test/tests/constant object.ans:8"} +_[1]={"text",_[3]} +return {_[1],_[2]} +--[[ +{ "text", { { + tags = {}, + text = "12" + } } } +{ "error", "can't change the value of an attribute of a constant object; in Lua function \"_._\"; at test/tests/constant object.ans:8" } +]]-- \ No newline at end of file diff --git a/test/tests/constant values variable.ans b/test/tests/constant values variable.ans new file mode 100644 index 0000000..d8a5f84 --- /dev/null +++ b/test/tests/constant values variable.ans @@ -0,0 +1,15 @@ +:l = [1,2,3] + +::x = l + +{l} +{x} + +----- + +~ l!remove() + +{l} +{x} + +~ x!remove() diff --git a/test/tests/constant values variable.lua b/test/tests/constant values variable.lua new file mode 100644 index 0000000..8b388cf --- /dev/null +++ b/test/tests/constant values variable.lua @@ -0,0 +1,40 @@ +local _={} +_[17]={} +_[16]={} +_[15]={} +_[14]={} +_[13]={} +_[12]={text="[1, 2, 3]",tags=_[17]} +_[11]={text="[1, 2]",tags=_[16]} +_[10]={text="-----",tags=_[15]} +_[9]={text="[1, 2, 3]",tags=_[14]} +_[8]={text="[1, 2, 3]",tags=_[13]} +_[7]={_[11],_[12]} +_[6]={_[10]} +_[5]={_[8],_[9]} +_[4]={"error","can't remove values from a constant list; in Lua function \"remove\"; at test/tests/constant values variable.ans:15"} +_[3]={"text",_[7]} +_[2]={"text",_[6]} +_[1]={"text",_[5]} +return {_[1],_[2],_[3],_[4]} +--[[ +{ "text", { { + tags = {}, + text = "[1, 2, 3]" + }, { + tags = {}, + text = "[1, 2, 3]" + } } } +{ "text", { { + tags = {}, + text = "-----" + } } } +{ "text", { { + tags = {}, + text = "[1, 2]" + }, { + tags = {}, + text = "[1, 2, 3]" + } } } +{ "error", "can't remove values from a constant list; in Lua function \"remove\"; at test/tests/constant values variable.ans:15" } +]]-- \ No newline at end of file diff --git a/test/tests/constant values.ans b/test/tests/constant values.ans new file mode 100644 index 0000000..662999d --- /dev/null +++ b/test/tests/constant values.ans @@ -0,0 +1,15 @@ +:l = [1,2,3] + +:x = constant(l) + +{l} +{x} + +----- + +~ l!remove() + +{l} +{x} + +~ x!remove() diff --git a/test/tests/constant values.lua b/test/tests/constant values.lua new file mode 100644 index 0000000..b950bb2 --- /dev/null +++ b/test/tests/constant values.lua @@ -0,0 +1,40 @@ +local _={} +_[17]={} +_[16]={} +_[15]={} +_[14]={} +_[13]={} +_[12]={text="[1, 2, 3]",tags=_[17]} +_[11]={text="[1, 2]",tags=_[16]} +_[10]={text="-----",tags=_[15]} +_[9]={text="[1, 2, 3]",tags=_[14]} +_[8]={text="[1, 2, 3]",tags=_[13]} +_[7]={_[11],_[12]} +_[6]={_[10]} +_[5]={_[8],_[9]} +_[4]={"error","can't remove values from a constant list; in Lua function \"remove\"; at test/tests/constant values.ans:15"} +_[3]={"text",_[7]} +_[2]={"text",_[6]} +_[1]={"text",_[5]} +return {_[1],_[2],_[3],_[4]} +--[[ +{ "text", { { + tags = {}, + text = "[1, 2, 3]" + }, { + tags = {}, + text = "[1, 2, 3]" + } } } +{ "text", { { + tags = {}, + text = "-----" + } } } +{ "text", { { + tags = {}, + text = "[1, 2]" + }, { + tags = {}, + text = "[1, 2, 3]" + } } } +{ "error", "can't remove values from a constant list; in Lua function \"remove\"; at test/tests/constant values.ans:15" } +]]-- \ No newline at end of file diff --git a/test/tests/constant variable list.ans b/test/tests/constant variable list.ans new file mode 100644 index 0000000..1233ba1 --- /dev/null +++ b/test/tests/constant variable list.ans @@ -0,0 +1,7 @@ +::a = [3] + +{a} + +~ a!insert(52) + +{a} diff --git a/test/tests/constant variable list.lua b/test/tests/constant variable list.lua new file mode 100644 index 0000000..e86534d --- /dev/null +++ b/test/tests/constant variable list.lua @@ -0,0 +1,14 @@ +local _={} +_[5]={} +_[4]={tags=_[5],text="[3]"} +_[3]={_[4]} +_[2]={"error","can't insert values into a constant list; in Lua function \"insert\"; at test/tests/constant variable list.ans:5"} +_[1]={"text",_[3]} +return {_[1],_[2]} +--[[ +{ "text", { { + tags = {}, + text = "[3]" + } } } +{ "error", "can't insert values into a constant list; in Lua function \"insert\"; at test/tests/constant variable list.ans:5" } +]]-- \ No newline at end of file diff --git a/test/tests/constant variable.ans b/test/tests/constant variable.ans new file mode 100644 index 0000000..2b8c8d6 --- /dev/null +++ b/test/tests/constant variable.ans @@ -0,0 +1,7 @@ +::a = 3 + +{a} + +~ a := 52 + +{a} diff --git a/test/tests/constant variable.lua b/test/tests/constant variable.lua new file mode 100644 index 0000000..887f62c --- /dev/null +++ b/test/tests/constant variable.lua @@ -0,0 +1,14 @@ +local _={} +_[5]={} +_[4]={tags=_[5],text="3"} +_[3]={_[4]} +_[2]={"error","can't change the value of a constant \"constant variable.a\"; while assigning value to variable \"constant variable.a\"; at test/tests/constant variable.ans:5"} +_[1]={"text",_[3]} +return {_[1],_[2]} +--[[ +{ "text", { { + tags = {}, + text = "3" + } } } +{ "error", "can't change the value of a constant \"constant variable.a\"; while assigning value to variable \"constant variable.a\"; at test/tests/constant variable.ans:5" } +]]-- \ No newline at end of file