1
0
Fork 0
mirror of https://github.com/Reuh/anselme.git synced 2025-10-28 09:09:31 +00:00

Add constants

This commit is contained in:
Étienne Fildadut 2022-09-08 22:09:11 +09:00
parent 3e658e4780
commit 95683a0ffd
17 changed files with 290 additions and 8 deletions

View file

@ -366,6 +366,14 @@ $ f
:bar : alias = 12 :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. * 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). * 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 `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 #### 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`. 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`.

View file

@ -668,6 +668,7 @@ local vm_mt = {
aliases = setmetatable({}, { __index = self.state.aliases }), 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 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_constraints = self.state.variable_constraints, -- no cache as constraints are expected to be constant
variable_constants = self.state.variable_constants,
variables = setmetatable({}, { variables = setmetatable({}, {
__index = function(variables, k) __index = function(variables, k)
local cache = getmetatable(variables).cache local cache = getmetatable(variables).cache
@ -763,6 +764,9 @@ return setmetatable(anselme, {
variable_constraints = { variable_constraints = {
-- foo = { constraint }, ... -- foo = { constraint }, ...
}, },
variable_constants = {
-- foo = true, ...
},
variables = { variables = {
-- foo = { -- foo = {
-- type = "number", -- type = "number",

View file

@ -3,6 +3,7 @@ local eval, run_block
local replace_with_copied_values, fix_not_modified_references local replace_with_copied_values, fix_not_modified_references
local common local common
local identifier_pattern local identifier_pattern
local copy
--- copy some text & process it to be suited to be sent to Lua in an event --- copy some text & process it to be suited to be sent to Lua in an event
local function post_process_text(state, text) local function post_process_text(state, text)
@ -117,6 +118,31 @@ common = {
end end
return math.huge return math.huge
end, 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 --- 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 -- if you're sure the variable has already been evaluated, use state.variables[fqm] directly
-- return var -- return var
@ -128,7 +154,7 @@ common = {
if not v then if not v then
return nil, ("%s; while evaluating default value for variable %q defined at %s"):format(e, fqm, var.value.source) return nil, ("%s; while evaluating default value for variable %q defined at %s"):format(e, fqm, var.value.source)
end 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 if not s then return nil, err end
return v return v
else else
@ -138,8 +164,19 @@ common = {
--- set the value of a variable --- set the value of a variable
-- returns true -- returns true
-- returns nil, err -- returns nil, err
set_variable = function(state, name, val) set_variable = function(state, name, val, defining_a_constant)
if val.type ~= "pending definition" then 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) local s, e = common.check_constraint(state, name, val)
if not s then if not s then
return nil, ("%s; while assigning value to variable %q"):format(e, name) return nil, ("%s; while assigning value to variable %q"):format(e, name)
@ -219,9 +256,9 @@ common = {
table.insert(modified, v) table.insert(modified, v)
end, end,
--- returns true if a variable should be persisted on save --- 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) 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, end,
--- check truthyness of an anselme value --- check truthyness of an anselme value
truthy = function(val) truthy = function(val)
@ -560,5 +597,6 @@ run_block = require((...):gsub("common$", "interpreter")).run_block
local acommon = require((...):gsub("interpreter%.common$", "common")) 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 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 identifier_pattern = require((...):gsub("interpreter%.common$", "parser.common")).identifier_pattern
copy = require((...):gsub("interpreter%.common$", "common")).copy
return common return common

View file

@ -291,8 +291,15 @@ local function parse_line(line, state, namespace, parent_function)
elseif l:match("^:") then elseif l:match("^:") then
r.type = "definition" r.type = "definition"
r.remove_from_block_ast = true 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 -- 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 if not identifier then return nil, ("no valid identifier at start of definition line %q; at %s"):format(l, line.source) end
-- format identifier -- format identifier
local fqm = ("%s%s"):format(namespace, format_identifier(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.fqm = fqm
r.expression = exp r.expression = exp
state.variables[fqm] = { type = "pending definition", value = { expression = nil, source = r.source } } state.variables[fqm] = { type = "pending definition", value = { expression = nil, source = r.source } }
if r.constant then state.variable_constants[fqm] = true end
-- tag -- tag
elseif l:match("^%#") then elseif l:match("^%#") then
r.type = "tag" r.type = "tag"
@ -514,6 +522,7 @@ local function parse(state, s, name, source)
inject = {}, inject = {},
aliases = setmetatable({}, { __index = state.aliases }), aliases = setmetatable({}, { __index = state.aliases }),
variable_constraints = setmetatable({}, { __index = state.variable_constraints }), variable_constraints = setmetatable({}, { __index = state.variable_constraints }),
variable_constants = setmetatable({}, { __index = state.variable_constants }),
variables = setmetatable({}, { __index = state.aliases }), variables = setmetatable({}, { __index = state.aliases }),
functions = setmetatable({}, { functions = setmetatable({}, {
__index = function(self, key) __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 for k,v in pairs(state_proxy.variable_constraints) do
state.variable_constraints[k] = v state.variable_constraints[k] = v
end 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 for k,v in pairs(state_proxy.variables) do
state.variables[k] = v state.variables[k] = v
end end

View file

@ -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 local lua_functions
lua_functions = { lua_functions = {
@ -145,6 +145,13 @@ lua_functions = {
local state = anselme.running.state local state = anselme.running.state
local obj = r.value local obj = r.value
local name = n.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 -- attribute already present in object
local var, vfqm = find(state.aliases, obj.attributes, "", obj.class.."."..name) local var, vfqm = find(state.aliases, obj.attributes, "", obj.class.."."..name)
if var then if var then
@ -186,6 +193,7 @@ lua_functions = {
value = function(l, i, v) value = function(l, i, v)
local lv = l.type == "annotated" and l.value[1] or l local lv = l.type == "annotated" and l.value[1] or l
local iv = i.type == "annotated" and i.value[1] or i 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 lv.value[iv.value] = v
mark_as_modified(anselme.running.state, lv.value) mark_as_modified(anselme.running.state, lv.value)
return v return v
@ -196,11 +204,12 @@ lua_functions = {
value = function(l, k, v) value = function(l, k, v)
local lv = l.type == "annotated" and l.value[1] or l local lv = l.type == "annotated" and l.value[1] or l
local kv = k.type == "annotated" and k.value[1] or k 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 -- update index
for _, x in ipairs(lv.value) do for _, x in ipairs(lv.value) do
if x.type == "pair" and compare(x.value[1], kv) then if x.type == "pair" and compare(x.value[1], kv) then
x.value[2] = v 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 return v
end end
end end
@ -296,6 +305,7 @@ lua_functions = {
mode = "raw", mode = "raw",
value = function(l, v) value = function(l, v)
local lv = l.type == "annotated" and l.value[1] or l 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) table.insert(lv.value, v)
mark_as_modified(anselme.running.state, lv.value) mark_as_modified(anselme.running.state, lv.value)
return l return l
@ -306,6 +316,7 @@ lua_functions = {
value = function(l, i, v) value = function(l, i, v)
local lv = l.type == "annotated" and l.value[1] or l local lv = l.type == "annotated" and l.value[1] or l
local iv = i.type == "annotated" and i.value[1] or i 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) table.insert(lv.value, iv.value, v)
mark_as_modified(anselme.running.state, lv.value) mark_as_modified(anselme.running.state, lv.value)
return l return l
@ -314,6 +325,7 @@ lua_functions = {
["remove(l::list)"] = { ["remove(l::list)"] = {
mode = "unannotated raw", mode = "unannotated raw",
value = function(l) 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) mark_as_modified(anselme.running.state, l.value)
return table.remove(l.value) return table.remove(l.value)
end end
@ -321,6 +333,7 @@ lua_functions = {
["remove(l::list, i::number)"] = { ["remove(l::list, i::number)"] = {
mode = "unannotated raw", mode = "unannotated raw",
value = function(l, i) 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) mark_as_modified(anselme.running.state, l.value)
return table.remove(l.value, i.value) return table.remove(l.value, i.value)
end end
@ -379,6 +392,14 @@ lua_functions = {
value = is_of_type(v, t) or 0 value = is_of_type(v, t) or 0
} }
end 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 package.loaded[...] = functions
local icommon = require((...):gsub("stdlib%.functions$", "interpreter.common")) 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")) local pcommon = require((...):gsub("stdlib%.functions$", "parser.common"))
identifier_pattern, format_identifier, find = pcommon.identifier_pattern, pcommon.format_identifier, pcommon.find identifier_pattern, format_identifier, find = pcommon.identifier_pattern, pcommon.format_identifier, pcommon.find
anselme = require((...):gsub("stdlib%.functions$", "anselme")) anselme = require((...):gsub("stdlib%.functions$", "anselme"))
copy = require((...):gsub("stdlib%.functions$", "common")).copy
return functions return functions

View file

@ -0,0 +1,8 @@
% obj
::a = 12
:x = obj()
{x.a}
{x.a := 52}

View file

@ -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" }
]]--

View file

@ -0,0 +1,8 @@
% obj
:a = 12
::x = obj()
{x.a}
{x.a := 52}

View file

@ -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" }
]]--

View file

@ -0,0 +1,15 @@
:l = [1,2,3]
::x = l
{l}
{x}
-----
~ l!remove()
{l}
{x}
~ x!remove()

View file

@ -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" }
]]--

View file

@ -0,0 +1,15 @@
:l = [1,2,3]
:x = constant(l)
{l}
{x}
-----
~ l!remove()
{l}
{x}
~ x!remove()

View file

@ -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" }
]]--

View file

@ -0,0 +1,7 @@
::a = [3]
{a}
~ a!insert(52)
{a}

View file

@ -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" }
]]--

View file

@ -0,0 +1,7 @@
::a = 3
{a}
~ a := 52
{a}

View file

@ -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" }
]]--