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

Variable must now be explicitly marked as persistent

This commit is contained in:
Étienne Fildadut 2022-09-27 18:41:40 +09:00
parent e9606cdee0
commit 2c6d66c222
11 changed files with 384 additions and 106 deletions

View file

@ -305,7 +305,7 @@ When a parameter list is given (or just empty parentheses `()`), the function is
~ g ~ g
``` ```
This is basically the behaviour you'd expect from functions in most other programming languages, and what you would use in Anselme any time you don't care about storing the function variables or want the exact same initial function variables each time you call the function (e.g. recursion). Scoped variables are not kept in save files, and are not affected by checkpointing. This is basically the behaviour you'd expect from functions in most other programming languages, and what you would use in Anselme any time you don't care about storing the function variables or want the exact same initial function variables each time you call the function (e.g. recursion). Scoped variables can not be persistent, and are not affected by checkpointing.
Functions with the same name can be defined, as long as they have a different arguments. Functions will be selected based on the number of arguments given, their name and their type constraint: Functions with the same name can be defined, as long as they have a different arguments. Functions will be selected based on the number of arguments given, their name and their type constraint:
@ -362,6 +362,8 @@ Functions always have the following variables defined in its namespace by defaul
`👁️`: number, number of times the function was executed before `👁️`: number, number of times the function was executed before
`🔖`: function reference, last reached checkpoint. `nil` if no checkpoint reached. `🔖`: function reference, last reached checkpoint. `nil` if no checkpoint reached.
These variables are persistent, unless the function is scoped.
* `:!`: checkpoint definition. Followed by an [identifier](#identifiers), then eventually an [alias](#aliases). Define a checkpoint. Also define a new namespace for its children. * `:!`: checkpoint definition. Followed by an [identifier](#identifiers), then eventually an [alias](#aliases). Define a checkpoint. Also define a new namespace for its children.
Checkpoints share most of their behavior with functions, with several exceptions. Like functions, the body is not executed when the line is reached; it must either be explicitely called in an expression or executed when resuming the parent function (see checkpoint behaviour below). Can be called in an expression. See [expressions](#checkpoint-calls) to see the different ways of calling a checkpoint manually. Checkpoints share most of their behavior with functions, with several exceptions. Like functions, the body is not executed when the line is reached; it must either be explicitely called in an expression or executed when resuming the parent function (see checkpoint behaviour below). Can be called in an expression. See [expressions](#checkpoint-calls) to see the different ways of calling a checkpoint manually.
@ -383,6 +385,8 @@ Checkpoints always have the following variable defined in its namespace by defau
`👁️`: number, number of times the checkpoint was executed before `👁️`: number, number of times the checkpoint was executed before
`🏁`: number, number of times the checkpoint was reached before (includes times where it was resumed from and executed) `🏁`: number, number of times the checkpoint was reached before (includes times where it was resumed from and executed)
These variables are persistent.
* `:%`: class definition. Followed by an [identifier](#identifiers), then eventually an [alias](#aliases). Define a class. Also define a new namespace for its children. * `:%`: class definition. Followed by an [identifier](#identifiers), then eventually an [alias](#aliases). Define a class. Also define a new namespace for its children.
Classes share most of their behavior with functions, with a few exceptions. Classes can not take arguments or be scoped; and when called, if the function does not return a value or returns `()` (nil), it will returns a new object instead based on this class. The object can be used to access variables ("attributes") defined in the class, but if one of these attributes is modified on the object it will not change the value in the base class but only in the object. Classes share most of their behavior with functions, with a few exceptions. Classes can not take arguments or be scoped; and when called, if the function does not return a value or returns `()` (nil), it will returns a new object instead based on this class. The object can be used to access variables ("attributes") defined in the class, but if one of these attributes is modified on the object it will not change the value in the base class but only in the object.
@ -415,6 +419,8 @@ Note that the new object returned by the class is also automatically given an an
~ object!show ~ object!show
``` ```
Classes have the same default variable defined as functions.
* `:`: variable declaration. Followed by an [identifier](#identifiers) (with eventually an [alias](#aliases)), a `=` and an [expression](#expressions). Defines a variable with a default value and this identifier in the current [namespace]("identifiers"). The expression is not evaluated instantly, but the first time the variable is used. Don't accept children lines. * `:`: variable declaration. Followed by an [identifier](#identifiers) (with eventually an [alias](#aliases)), a `=` and an [expression](#expressions). Defines a variable with a default value and this identifier in the current [namespace]("identifiers"). The expression is not evaluated instantly, but the first time the variable is used. Don't accept children lines.
``` ```
@ -422,12 +428,19 @@ Note that the new object returned by the class is also automatically given an an
:bar : alias = 12 :bar : alias = 12
``` ```
* `::`: constant declaration. Work the same way as a variable declaration, but the variable can't be reassigned after their declaration and first evaluation, 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. * `::`: constant declaration. Work the same way as a variable declaration, but the variable can't be reassigned after their declaration and first evaluation, and their value is marked as constant (i.e. can not be modified even it is of a mutable type).
``` ```
::foo = 42 ::foo = 42
``` ```
* `:@`: persistent variable declaration. Work the same way as a variable declaration, but the variable will be stored in the save file, and if we loaded a save file its value will be retrieved from the save file instead of from the expression's result.
```
:@foo = 42
:@bar : alias = 12
```
### Text interpolation ### Text interpolation
Text and choice lines allow for arbitrary text. Expression can be evaluated and inserted into the text as the line is executed by enclosing the [expression](#expressions) into brackets. The expressions are evaluated in the same order as the reading direction. Text and choice lines allow for arbitrary text. Expression can be evaluated and inserted into the text as the line is executed by enclosing the [expression](#expressions) into brackets. The expressions are evaluated in the same order as the reading direction.
@ -575,7 +588,7 @@ Var1 in the fn1 namespace = 2: {fn1.var1}
#### Aliases #### Aliases
When defining identifiers (in variables, functions or checkpoint definitions), they can be followed by a colon and another identifier. This identifier can be used as a new way to access the identifier (i.e., an alias). When defining identifiers (in variables, functions, checkpoint or class definitions), they can be followed by a colon and another identifier. This identifier can be used as a new way to access the identifier (i.e., an alias).
``` ```
:name: alias = 42 :name: alias = 42
@ -585,20 +598,20 @@ When defining identifiers (in variables, functions or checkpoint definitions), t
Note that alias have priority over normal identifiers; if both an identifier and an alias have the same name, the alias will be used. Note that alias have priority over normal identifiers; if both an identifier and an alias have the same name, the alias will be used.
The main purpose of aliases is translation. When saving the state of your game's script, Anselme will store the name of the variables and their contents, and require the name to be the same when loading the save later, in order to correctly restore their values. The main purpose of aliases is translation. When saving the state of your game's script, Anselme will store the name of the persistent variables and their contents, and require the name to be the same when loading the save later, in order to correctly restore their values.
This behaviour is fine if you only have one language; but if you want to translate your game, this means the translations will need to keep using the original, untranslated variables and functions names if it wants to be compatible with saves in differents languages. Which is not very practical or nice to read. This behaviour is fine if you only have one language; but if you want to translate your game, this means the translations will need to keep using the original, untranslated persistent variables and functions names if it wants to be compatible with saves in differents languages. Which is not very practical or nice to read.
Anselme's solution is to keep the original name in the translated script file, but alias them with a translated name. This way, the translated script can be written withou constantly switching languages: Anselme's solution is to keep the original name in the translated script file, but alias them with a translated name. This way, the translated script can be written withou constantly switching languages:
``` ```
(in the original, english script) (in the original, english script)
:player name = "John Pizzapone" :@player name = "John Pizzapone"
Hi {player name}! Hi {player name}!
(in a translated, french script) (in a translated, french script)
:player name : nom du joueur = "John Pizzapone" :@player name : nom du joueur = "John Pizzapone"
Salut {nom du joueur} ! Salut {nom du joueur} !
``` ```

View file

@ -560,15 +560,26 @@ local vm_mt = {
--- Save/load script state --- Save/load script state
-- --
-- Only saves variables full names and values, so make sure to not change important variables, checkpoints and functions names between a save and a load. -- Only saves persistent variables' full names and values.
-- Also only save variables with usable identifiers, so will skip functions with arguments, operators, etc. (i.e. every scoped functions). -- Make sure to not change persistent variables names, class name, class attribute names, checkpoint names and functions names between a
-- Loading should be done after loading all the game scripts (otherwise you will "variable already defined" errors). -- save and a load (alias can of course be changed), as Anselme will not be able to match them to the old names stored in the save file.
--
-- If a variable is stored in the save file but is not marked as persistent in the current scripts (e.g. if you updated the Anselme scripts to
-- remove the persistence), it will not be loaded.
--
-- Loading should be done after loading all the game scripts (otherwise you will get "variable already defined" errors).
-- --
-- Returns this VM. -- Returns this VM.
load = function(self, data) load = function(self, data)
assert(anselme.versions.save == data.anselme.versions.save, ("trying to load data from an incompatible version of Anselme; save was done using save version %s but current version is %s"):format(data.anselme.versions.save, anselme.versions.save)) assert(anselme.versions.save == data.anselme.versions.save, ("trying to load data from an incompatible version of Anselme; save was done using save version %s but current version is %s"):format(data.anselme.versions.save, anselme.versions.save))
for k, v in pairs(data.variables) do for k, v in pairs(data.variables) do
self.state.variables[k] = v if self.state.variable_metadata[k] then
if self.state.variable_metadata[k].persistent then
self.state.variables[k] = v
end
else
self.state.variables[k] = v -- non-existent variable: keep it in case there was a mistake, it's not going to affect anything anyway
end
end end
return self return self
end, end,
@ -580,23 +591,7 @@ local vm_mt = {
local vars = {} local vars = {}
for k, v in pairs(self.state.variables) do for k, v in pairs(self.state.variables) do
if should_keep_variable(self.state, k, v) then if should_keep_variable(self.state, k, v) then
if v.type == "object" then -- filter object attributes vars[k] = v
local attributes = {}
for kk, vv in pairs(v.value.attributes) do
if should_keep_variable(self.state, kk, vv) then
attributes[kk] = vv
end
end
vars[k] = {
type = "object",
value = {
class = v.value.class,
attributes = attributes
}
}
else
vars[k] = v
end
end end
end end
return { return {
@ -669,8 +664,7 @@ local vm_mt = {
builtin_aliases = self.state.builtin_aliases, builtin_aliases = self.state.builtin_aliases,
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_metadata = self.state.variable_metadata, -- no cache as metadata 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,11 +757,8 @@ return setmetatable(anselme, {
-- }, ... -- }, ...
-- }, ... -- }, ...
}, },
variable_constraints = { variable_metadata = {
-- foo = { constraint }, ... -- foo = { constant = true, persistent = true, constraint = constraint, ... }, ...
},
variable_constants = {
-- foo = true, ...
}, },
variables = { variables = {
-- foo = { -- foo = {

View file

@ -214,6 +214,8 @@ Set some code that will be injected at specific places in all code loaded after
* `"function return"`: injected at the end of each return's children that is contained in a non-scoped function * `"function return"`: injected at the end of each return's children that is contained in a non-scoped function
* `"checkpoint start"`: injected at the start of every checkpoint * `"checkpoint start"`: injected at the start of every checkpoint
* `"checkpoint end"`: injected at the end of every checkpoint * `"checkpoint end"`: injected at the end of every checkpoint
* `"class start"`: injected at the start of every class
* `"class end"`: injected at the end of every class
* `"scoped function start"`: injected at the start of every scoped function * `"scoped function start"`: injected at the start of every scoped function
* `"scoped function end"`: injected at the end of every scoped function * `"scoped function end"`: injected at the end of every scoped function
* `"scoped function return"`: injected at the end of each return's children that is contained in a scoped function * `"scoped function return"`: injected at the end of each return's children that is contained in a scoped function
@ -248,9 +250,14 @@ Define functions from Lua.
Save/load script state Save/load script state
Only saves variables full names and values, so make sure to not change important variables, checkpoints and functions names between a save and a load. Only saves persistent variables' full names and values.
Also only save variables with usable identifiers, so will skip functions with arguments, operators, etc. (i.e. every scoped functions). Make sure to not change persistent variables names, class name, class attribute names, checkpoint names and functions names between a
Loading should be done after loading all the game scripts (otherwise you will "variable already defined" errors). save and a load (alias can of course be changed), as Anselme will not be able to match them to the old names stored in the save file.
If a variable is stored in the save file but is not marked as persistent in the current scripts (e.g. if you updated the Anselme scripts to
remove the persistence), it will not be loaded.
Loading should be done after loading all the game scripts (otherwise you will get "variable already defined" errors).
Returns this VM. Returns this VM.

View file

@ -66,7 +66,7 @@ common = {
-- returns depth, or math.huge if no constraint -- returns depth, or math.huge if no constraint
-- returns nil, err -- returns nil, err
check_constraint = function(state, fqm, val) check_constraint = function(state, fqm, val)
local constraint = state.variable_constraints[fqm] local constraint = state.variable_metadata[fqm].constraint
if constraint then if constraint then
if not constraint.value then if not constraint.value then
local v, e = eval(state, constraint.pending) local v, e = eval(state, constraint.pending)
@ -87,7 +87,7 @@ common = {
-- returns true -- returns true
-- returns nil, mutation illegal message -- returns nil, mutation illegal message
check_mutable = function(state, fqm) check_mutable = function(state, fqm)
if state.variable_constants[fqm] then if state.variable_metadata[fqm].constant then
return nil, ("can't change the value of a constant %q"):format(fqm) return nil, ("can't change the value of a constant %q"):format(fqm)
end end
return true return true
@ -114,12 +114,12 @@ common = {
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
-- make constant if variable is constant -- make constant if variable is constant
if state.variable_constants[fqm] then if state.variable_metadata[fqm].constant then
v = copy(v) v = copy(v)
common.mark_constant(v) common.mark_constant(v)
end end
-- set variable -- set variable
local s, err = common.set_variable(state, fqm, v, state.variable_constants[fqm]) local s, err = common.set_variable(state, fqm, v, state.variable_metadata[fqm].constant)
if not s then return nil, err end if not s then return nil, err end
return v return v
else else
@ -218,9 +218,10 @@ 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, constants, internal anselme variables -- will exclude: variable that have not been evaluated yet and non-persistent variable
-- this will by consequence excludes variable in scoped variables (can be neither persistent not evaluated into global state), constants (can not be persistent), internal anselme variables (not marked persistent), etc.
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%.") and not state.variable_constants[name] return value.type ~= "pending definition" and state.variable_metadata[name].persistent
end, end,
--- check truthyness of an anselme value --- check truthyness of an anselme value
truthy = function(val) truthy = function(val)

View file

@ -309,7 +309,7 @@ local function eval(state, exp)
local depth, err = check_constraint(state, param.full_name, val) local depth, err = check_constraint(state, param.full_name, val)
if not depth then if not depth then
ok = false ok = false
local v = state.variable_constraints[param.full_name].value local v = state.variable_metadata[param.full_name].constraint.value
table.insert(tried_function_error_messages, ("%s: argument %s is not of expected type %s"):format(fn.pretty_signature, param.name, format(v) or v)) table.insert(tried_function_error_messages, ("%s: argument %s is not of expected type %s"):format(fn.pretty_signature, param.name, format(v) or v))
break break
end end
@ -347,7 +347,7 @@ local function eval(state, exp)
local depth, err = check_constraint(state, param.full_name, assignment) local depth, err = check_constraint(state, param.full_name, assignment)
if not depth then if not depth then
ok = false ok = false
local v = state.variable_constraints[param.full_name].value local v = state.variable_metadata[param.full_name].constraint.value
table.insert(tried_function_error_messages, ("%s: argument %s is not of expected type %s"):format(fn.pretty_signature, param.name, format(v) or v)) table.insert(tried_function_error_messages, ("%s: argument %s is not of expected type %s"):format(fn.pretty_signature, param.name, format(v) or v))
end end
depths.assignment = depth depths.assignment = depth
@ -418,8 +418,11 @@ local function eval(state, exp)
if not s then return nil, e end if not s then return nil, e end
end end
-- get function vars -- get function vars
local checkpoint, checkpointe = get_variable(state, fn.namespace.."🔖") local checkpoint, checkpointe
if not checkpoint then return nil, checkpointe end if fn.resumable then
checkpoint, checkpointe = get_variable(state, fn.namespace.."🔖")
if not checkpoint then return nil, checkpointe end
end
local seen, seene = get_variable(state, fn.namespace.."👁️") local seen, seene = get_variable(state, fn.namespace.."👁️")
if not seen then return nil, seene end if not seen then return nil, seene end
-- execute lua functions -- execute lua functions
@ -488,7 +491,7 @@ local function eval(state, exp)
else else
local e local e
-- eval function from start -- eval function from start
if paren_call or checkpoint.type == "nil" then if paren_call or not fn.resumable or checkpoint.type == "nil" then
ret, e = run(state, fn.child) ret, e = run(state, fn.child)
-- resume at last checkpoint -- resume at last checkpoint
else else

View file

@ -119,7 +119,7 @@ run_line = function(state, line)
value = reached.value + 1 value = reached.value + 1
}) })
if not s then return nil, e end if not s then return nil, e end
s, e = set_variable(state, line.parent_function.namespace.."🔖", { s, e = set_variable(state, line.parent_resumable.namespace.."🔖", {
type = "function reference", type = "function reference",
value = { line.name } value = { line.name }
}) })
@ -160,7 +160,7 @@ run_block = function(state, block, resume_from_there, i, j)
if not reached then return nil, reachede end if not reached then return nil, reachede end
local seen, seene = get_variable(state, parent_line.namespace.."👁️") local seen, seene = get_variable(state, parent_line.namespace.."👁️")
if not seen then return nil, seene end if not seen then return nil, seene end
local checkpoint, checkpointe = get_variable(state, parent_line.parent_function.namespace.."🔖") local checkpoint, checkpointe = get_variable(state, parent_line.parent_resumable.namespace.."🔖")
if not checkpoint then return nil, checkpointe end if not checkpoint then return nil, checkpointe end
local s, e = set_variable(state, parent_line.namespace.."👁️", { local s, e = set_variable(state, parent_line.namespace.."👁️", {
type = "number", type = "number",
@ -175,7 +175,7 @@ run_block = function(state, block, resume_from_there, i, j)
-- don't update checkpoint if an already more precise checkpoint is set -- 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) -- (since we will go up the whole checkpoint hierarchy when resuming from a nested checkpoint)
if checkpoint.type == "nil" or not checkpoint.value[1]:match("^"..escape(parent_line.name)) then if checkpoint.type == "nil" or not checkpoint.value[1]:match("^"..escape(parent_line.name)) then
s, e = set_variable(state, parent_line.parent_function.namespace.."🔖", { s, e = set_variable(state, parent_line.parent_resumable.namespace.."🔖", {
type = "function reference", type = "function reference",
value = { parent_line.name } value = { parent_line.name }
}) })
@ -184,11 +184,11 @@ run_block = function(state, block, resume_from_there, i, j)
merge_state(state) merge_state(state)
end end
-- go up hierarchy if asked to resume -- go up hierarchy if asked to resume
-- will stop at function boundary -- will stop at resumable 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 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 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 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 not block.parent_line.resume_boundary then if resume_from_there and block.parent_line and not block.parent_line.resumable then
local parent_line = block.parent_line local parent_line = block.parent_line
if parent_line.type == "choice" then if parent_line.type == "choice" then
state.interpreter.skip_choices_until_flush = true state.interpreter.skip_choices_until_flush = true
@ -212,9 +212,9 @@ local function run(state, block, resume_from_there, i, j)
local tags_len = tags:len(state) local tags_len = tags:len(state)
if resume_from_there then if resume_from_there then
local tags_to_add = {} local tags_to_add = {}
-- go up in hierarchy in ascending order until function boundary -- go up in hierarchy in ascending order until resumable function boundary
local parent_line = block.parent_line local parent_line = block.parent_line
while parent_line and not parent_line.resume_boundary do while parent_line and not parent_line.resumable do
if parent_line.type == "tag" then if parent_line.type == "tag" then
local v, e = eval(state, parent_line.expression) local v, e = eval(state, parent_line.expression)
if not v then return v, ("%s; at %s"):format(e, parent_line.source) end if not v then return v, ("%s; at %s"):format(e, parent_line.source) end

View file

@ -18,7 +18,7 @@ local function parse(state)
if rem:match("[^%s]") then if rem:match("[^%s]") then
return nil, ("unexpected characters after parameter %q: %q; at %s"):format(param.full_name, rem, line.source) return nil, ("unexpected characters after parameter %q: %q; at %s"):format(param.full_name, rem, line.source)
end end
state.variable_constraints[param.full_name] = { pending = type_exp } state.variable_metadata[param.full_name].constraint = { pending = type_exp }
end end
-- get default value -- get default value
if param.default then if param.default then
@ -30,7 +30,7 @@ local function parse(state)
param.default = default_exp param.default = default_exp
-- extract type constraint from default value -- extract type constraint from default value
if default_exp.type == "function call" and default_exp.called_name == "_::_" then if default_exp.type == "function call" and default_exp.called_name == "_::_" then
state.variable_constraints[param.full_name] = { pending = default_exp.argument.expression.right } state.variable_metadata[param.full_name].constraint = { pending = default_exp.argument.expression.right }
end end
end end
end end
@ -41,7 +41,7 @@ local function parse(state)
if rem:match("[^%s]") then if rem:match("[^%s]") then
return nil, ("unexpected characters after parameter %q: %q; at %s"):format(line.assignment.full_name, rem, line.source) return nil, ("unexpected characters after parameter %q: %q; at %s"):format(line.assignment.full_name, rem, line.source)
end end
state.variable_constraints[line.assignment.full_name] = { pending = type_exp } state.variable_metadata[line.assignment.full_name].constraint = { pending = type_exp }
end end
-- get list of scoped variables -- get list of scoped variables
-- (note includes every variables in the namespace of subnamespace, so subfunctions are scoped alongside this function) -- (note includes every variables in the namespace of subnamespace, so subfunctions are scoped alongside this function)
@ -49,6 +49,7 @@ local function parse(state)
line.scoped = {} line.scoped = {}
for name in pairs(state.variables) do for name in pairs(state.variables) do
if name:sub(1, #namespace) == namespace then if name:sub(1, #namespace) == namespace then
if state.variable_metadata[name].persistent then return nil, ("variable %q can not be persistent as it is in a scoped function"):format(name) end
table.insert(line.scoped, name) table.insert(line.scoped, name)
end end
end end
@ -80,7 +81,7 @@ local function parse(state)
if rem2:match("[^%s]") then if rem2:match("[^%s]") then
return nil, ("unexpected characters after variable %q: %q; at %s"):format(line.name, rem2, line.source) return nil, ("unexpected characters after variable %q: %q; at %s"):format(line.name, rem2, line.source)
end end
state.variable_constraints[line.name] = { pending = type_exp } state.variable_metadata[line.name].constraint = { pending = type_exp }
end end
end end
end end

View file

@ -48,7 +48,7 @@ end
--- parse a single line into AST --- parse a single line into AST
-- * ast: if success -- * ast: if success
-- * nil, error: in case of error -- * nil, error: in case of error
local function parse_line(line, state, namespace, parent_function) local function parse_line(line, state, namespace, parent_resumable, in_scoped)
local l = line.content local l = line.content
local r = { local r = {
source = line.source source = line.source
@ -93,10 +93,10 @@ local function parse_line(line, state, namespace, parent_function)
local keep_in_ast = false local keep_in_ast = false
if lr:match("^%$") then if lr:match("^%$") then
r.subtype = "function" r.subtype = "function"
r.resume_boundary = true r.resumable = true
elseif lr:match("^%%") then elseif lr:match("^%%") then
r.subtype = "class" r.subtype = "class"
r.resume_boundary = true r.resumable = true
r.properties = true r.properties = true
allow_params = false allow_params = false
allow_assign = false allow_assign = false
@ -105,7 +105,7 @@ local function parse_line(line, state, namespace, parent_function)
allow_params = false allow_params = false
allow_assign = false allow_assign = false
keep_in_ast = true keep_in_ast = true
r.parent_function = parent_function -- store parent function and run checkpoint when line is read r.parent_resumable = parent_resumable -- store parent resumable function and run checkpoint when line is read
else else
error("unknown function line type") error("unknown function line type")
end end
@ -231,36 +231,23 @@ local function parse_line(line, state, namespace, parent_function)
end end
-- define variables -- define variables
if not line.children then line.children = {} end if not line.children then line.children = {} end
local scoped = in_scoped or r.scoped
-- define 👁️ variable -- define 👁️ variable
local seen_alias = state.global_state.builtin_aliases["👁️"] local seen_alias = state.global_state.builtin_aliases["👁️"]
if seen_alias then table.insert(line.children, 1, { content = (":%s👁%s=0"):format(scoped and "" or "@", seen_alias and ":"..seen_alias or ""), source = line.source })
table.insert(line.children, 1, { content = (":👁️:%s=0"):format(seen_alias), source = line.source }) -- define 🔖 variable
else if r.resumable then
table.insert(line.children, 1, { content = ":👁️=0", source = line.source })
end
if r.subtype ~= "checkpoint" then
-- define 🔖 variable
local checkpoint_alias = state.global_state.builtin_aliases["🔖"] local checkpoint_alias = state.global_state.builtin_aliases["🔖"]
if checkpoint_alias then table.insert(line.children, 1, { content = (":%s🔖%s=()"):format(scoped and "" or "@", checkpoint_alias and ":"..checkpoint_alias or ""), source = line.source })
table.insert(line.children, 1, { content = (":🔖:%s=()"):format(checkpoint_alias), source = line.source })
else
table.insert(line.children, 1, { content = ":🔖=()", source = line.source })
end
-- custom code injection
inject(state, r, "start", line.children, 2)
inject(state, r, "end", line.children)
elseif r.subtype == "checkpoint" then
-- define 🏁 variable
local reached_alias = state.global_state.builtin_aliases["🏁"]
if reached_alias then
table.insert(line.children, 1, { content = (":🏁:%s=0"):format(reached_alias), source = line.source })
else
table.insert(line.children, 1, { content = ":🏁=0", source = line.source })
end
-- custom code injection
inject(state, r, "start", line.children, 2)
inject(state, r, "end", line.children)
end end
-- define 🏁 variable
if r.subtype == "checkpoint" then
local reached_alias = state.global_state.builtin_aliases["🏁"]
table.insert(line.children, 1, { content = (":%s🏁%s=0"):format(scoped and "" or "@", reached_alias and ":"..reached_alias or ""), source = line.source })
end
-- custom code injection
inject(state, r, "start", line.children, 2)
inject(state, r, "end", line.children)
-- define args -- define args
for _, param in ipairs(r.params) do for _, param in ipairs(r.params) do
if not state.variables[param.full_name] then if not state.variables[param.full_name] then
@ -268,6 +255,7 @@ local function parse_line(line, state, namespace, parent_function)
type = "undefined argument", type = "undefined argument",
value = nil value = nil
} }
state.variable_metadata[param.full_name] = {}
else else
return nil, ("trying to define parameter %q, but a variable with the same name exists; at %s"):format(param.full_name, line.source) return nil, ("trying to define parameter %q, but a variable with the same name exists; at %s"):format(param.full_name, line.source)
end end
@ -278,6 +266,7 @@ local function parse_line(line, state, namespace, parent_function)
type = "undefined argument", type = "undefined argument",
value = nil value = nil
} }
state.variable_metadata[r.assignment.full_name] = {}
else else
return nil, ("trying to define parameter %q, but a variable with the same name exists; at %s"):format(r.assignment.full_name, line.source) return nil, ("trying to define parameter %q, but a variable with the same name exists; at %s"):format(r.assignment.full_name, line.source)
end end
@ -298,6 +287,9 @@ local function parse_line(line, state, namespace, parent_function)
if rem:match("^:") then if rem:match("^:") then
rem = rem:match("^:(.*)$") rem = rem:match("^:(.*)$")
r.constant = true r.constant = true
elseif rem:match("^@") then
rem = rem:match("^@(.*)$")
r.persistent = true
end end
-- get identifier -- get identifier
local identifier local identifier
@ -328,7 +320,9 @@ local function parse_line(line, state, namespace, parent_function)
r.name = fqm r.name = 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 state.variable_metadata[fqm] = {}
if r.constant then state.variable_metadata[fqm].constant = true end
if r.persistent then state.variable_metadata[fqm].persistent = true end
end end
-- add expression line after to perform the immediate execution -- add expression line after to perform the immediate execution
if run_immediately then if run_immediately then
@ -344,7 +338,6 @@ local function parse_line(line, state, namespace, parent_function)
elseif l:match("^%@") then elseif l:match("^%@") then
r.type = "return" r.type = "return"
r.child = true r.child = true
r.parent_function = parent_function
local expr = l:match("^%@(.*)$") local expr = l:match("^%@(.*)$")
if expr:match("[^%s]") then if expr:match("[^%s]") then
r.expression = expr r.expression = expr
@ -353,7 +346,7 @@ local function parse_line(line, state, namespace, parent_function)
end end
-- custom code injection -- custom code injection
if not line.children then line.children = {} end if not line.children then line.children = {} end
inject(state, parent_function, "return", line.children) inject(state, parent_resumable, "return", line.children)
-- text -- text
elseif l:match("[^%s]") then elseif l:match("[^%s]") then
r.type = "text" r.type = "text"
@ -369,11 +362,11 @@ end
--- parse an indented into final AST --- parse an indented into final AST
-- * block: in case of success -- * block: in case of success
-- * nil, err: in case of error -- * nil, err: in case of error
local function parse_block(indented, state, namespace, parent_function) local function parse_block(indented, state, namespace, parent_resumable, in_scoped)
local block = { type = "block" } local block = { type = "block" }
for i, l in ipairs(indented) do for i, l in ipairs(indented) do
-- parsable line -- parsable line
local ast, err = parse_line(l, state, namespace, parent_function) local ast, err = parse_line(l, state, namespace, parent_resumable, in_scoped)
if err then return nil, err end if err then return nil, err end
-- add to block AST -- add to block AST
if not ast.remove_from_block_ast then if not ast.remove_from_block_ast then
@ -392,7 +385,7 @@ local function parse_block(indented, state, namespace, parent_function)
if not ast.child then if not ast.child then
return nil, ("line %s (%s) can't have children"):format(ast.source, ast.type) return nil, ("line %s (%s) can't have children"):format(ast.source, ast.type)
else else
local r, e = parse_block(l.children, state, ast.namespace or namespace, (ast.type == "function" and ast.subtype ~= "checkpoint") and ast or parent_function) local r, e = parse_block(l.children, state, ast.namespace or namespace, (ast.type == "function" and ast.resumable) and ast or parent_resumable, (ast.type == "function" and ast.scoped) or in_scoped)
if not r then return r, e end if not r then return r, e end
r.parent_line = ast r.parent_line = ast
ast.child = r ast.child = r
@ -507,8 +500,7 @@ local function parse(state, s, name, source)
local state_proxy = { local state_proxy = {
inject = {}, inject = {},
aliases = setmetatable({}, { __index = state.aliases }), aliases = setmetatable({}, { __index = state.aliases }),
variable_constraints = setmetatable({}, { __index = state.variable_constraints }), variable_metadata = setmetatable({}, { __index = state.variable_metadata }),
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)
@ -541,11 +533,8 @@ local function parse(state, s, name, source)
for k,v in pairs(state_proxy.aliases) do for k,v in pairs(state_proxy.aliases) do
state.aliases[k] = v state.aliases[k] = v
end end
for k,v in pairs(state_proxy.variable_constraints) do for k,v in pairs(state_proxy.variable_metadata) do
state.variable_constraints[k] = v state.variable_metadata[k] = v
end
for k,v in pairs(state_proxy.variable_constants) do
state.variable_constants[k] = v
end 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

View file

@ -158,12 +158,10 @@ lua_functions = {
if r.constant then if r.constant then
return nil, "can't change the value of an attribute of a constant object" return nil, "can't change the value of an attribute of a constant object"
end 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
if not check_mutable(state, vfqm) then return nil, "can't change the value of a constant attribute" end
obj.attributes[vfqm] = v obj.attributes[vfqm] = v
mark_as_modified(anselme.running.state, obj.attributes) mark_as_modified(anselme.running.state, obj.attributes)
return v return v
@ -171,6 +169,7 @@ lua_functions = {
-- search for attribute in base class -- search for attribute in base class
local cvar, cvfqm = find(state.aliases, state.interpreter.global_state.variables, "", obj.class.."."..name) local cvar, cvfqm = find(state.aliases, state.interpreter.global_state.variables, "", obj.class.."."..name)
if cvar then if cvar then
if not check_mutable(state, cvfqm) then return nil, "can't change the value of a constant attribute" end
obj.attributes[cvfqm] = v obj.attributes[cvfqm] = v
mark_as_modified(anselme.running.state, obj.attributes) mark_as_modified(anselme.running.state, obj.attributes)
return v return v

View file

@ -0,0 +1,44 @@
:$ f()
:a = 1
{a}
~ a := a + 1
:$ g
:a = 1
{a}
~ a := a + 1
:$ h()
:a = 1
{a}
~ a := a + 1
\> depth 2, unscoped:
~ g
~ g
~ g
\> depth 2, scoped:
~ h
~ h
~ h
depth 1:
~ f
~ f
~ f

View file

@ -0,0 +1,230 @@
local _={}
_[113]={}
_[112]={}
_[111]={}
_[110]={}
_[109]={}
_[108]={}
_[107]={}
_[106]={}
_[105]={}
_[104]={}
_[103]={}
_[102]={}
_[101]={}
_[100]={}
_[99]={}
_[98]={}
_[97]={}
_[96]={}
_[95]={}
_[94]={}
_[93]={}
_[92]={}
_[91]={}
_[90]={}
_[89]={}
_[88]={}
_[87]={}
_[86]={}
_[85]={tags=_[113],text="1"}
_[84]={tags=_[112],text="1"}
_[83]={tags=_[111],text="1"}
_[82]={tags=_[110],text="> depth 2, scoped:"}
_[81]={tags=_[109],text="3"}
_[80]={tags=_[108],text="2"}
_[79]={tags=_[107],text="1"}
_[78]={tags=_[106],text="> depth 2, unscoped:"}
_[77]={tags=_[105],text="1"}
_[76]={tags=_[104],text="1"}
_[75]={tags=_[103],text="1"}
_[74]={tags=_[102],text="1"}
_[73]={tags=_[101],text="> depth 2, scoped:"}
_[72]={tags=_[100],text="3"}
_[71]={tags=_[99],text="2"}
_[70]={tags=_[98],text="1"}
_[69]={tags=_[97],text="> depth 2, unscoped:"}
_[68]={tags=_[96],text="1"}
_[67]={tags=_[95],text="1"}
_[66]={tags=_[94],text="1"}
_[65]={tags=_[93],text="1"}
_[64]={tags=_[92],text="> depth 2, scoped:"}
_[63]={tags=_[91],text="3"}
_[62]={tags=_[90],text="2"}
_[61]={tags=_[89],text="1"}
_[60]={tags=_[88],text="> depth 2, unscoped:"}
_[59]={tags=_[87],text="1"}
_[58]={tags=_[86],text="depth 1:"}
_[57]={_[85]}
_[56]={_[84]}
_[55]={_[83]}
_[54]={_[82]}
_[53]={_[81]}
_[52]={_[80]}
_[51]={_[79]}
_[50]={_[78]}
_[49]={_[77]}
_[48]={_[76]}
_[47]={_[75]}
_[46]={_[74]}
_[45]={_[73]}
_[44]={_[72]}
_[43]={_[71]}
_[42]={_[70]}
_[41]={_[69]}
_[40]={_[68]}
_[39]={_[67]}
_[38]={_[66]}
_[37]={_[65]}
_[36]={_[64]}
_[35]={_[63]}
_[34]={_[62]}
_[33]={_[61]}
_[32]={_[60]}
_[31]={_[59]}
_[30]={_[58]}
_[29]={"return"}
_[28]={"text",_[57]}
_[27]={"text",_[56]}
_[26]={"text",_[55]}
_[25]={"text",_[54]}
_[24]={"text",_[53]}
_[23]={"text",_[52]}
_[22]={"text",_[51]}
_[21]={"text",_[50]}
_[20]={"text",_[49]}
_[19]={"text",_[48]}
_[18]={"text",_[47]}
_[17]={"text",_[46]}
_[16]={"text",_[45]}
_[15]={"text",_[44]}
_[14]={"text",_[43]}
_[13]={"text",_[42]}
_[12]={"text",_[41]}
_[11]={"text",_[40]}
_[10]={"text",_[39]}
_[9]={"text",_[38]}
_[8]={"text",_[37]}
_[7]={"text",_[36]}
_[6]={"text",_[35]}
_[5]={"text",_[34]}
_[4]={"text",_[33]}
_[3]={"text",_[32]}
_[2]={"text",_[31]}
_[1]={"text",_[30]}
return {_[1],_[2],_[3],_[4],_[5],_[6],_[7],_[8],_[9],_[10],_[11],_[12],_[13],_[14],_[15],_[16],_[17],_[18],_[19],_[20],_[21],_[22],_[23],_[24],_[25],_[26],_[27],_[28],_[29]}
--[[
{ "text", { {
tags = {},
text = "depth 1:"
} } }
{ "text", { {
tags = {},
text = "1"
} } }
{ "text", { {
tags = {},
text = "> depth 2, unscoped:"
} } }
{ "text", { {
tags = {},
text = "1"
} } }
{ "text", { {
tags = {},
text = "2"
} } }
{ "text", { {
tags = {},
text = "3"
} } }
{ "text", { {
tags = {},
text = "> depth 2, scoped:"
} } }
{ "text", { {
tags = {},
text = "1"
} } }
{ "text", { {
tags = {},
text = "1"
} } }
{ "text", { {
tags = {},
text = "1"
} } }
{ "text", { {
tags = {},
text = "1"
} } }
{ "text", { {
tags = {},
text = "> depth 2, unscoped:"
} } }
{ "text", { {
tags = {},
text = "1"
} } }
{ "text", { {
tags = {},
text = "2"
} } }
{ "text", { {
tags = {},
text = "3"
} } }
{ "text", { {
tags = {},
text = "> depth 2, scoped:"
} } }
{ "text", { {
tags = {},
text = "1"
} } }
{ "text", { {
tags = {},
text = "1"
} } }
{ "text", { {
tags = {},
text = "1"
} } }
{ "text", { {
tags = {},
text = "1"
} } }
{ "text", { {
tags = {},
text = "> depth 2, unscoped:"
} } }
{ "text", { {
tags = {},
text = "1"
} } }
{ "text", { {
tags = {},
text = "2"
} } }
{ "text", { {
tags = {},
text = "3"
} } }
{ "text", { {
tags = {},
text = "> depth 2, scoped:"
} } }
{ "text", { {
tags = {},
text = "1"
} } }
{ "text", { {
tags = {},
text = "1"
} } }
{ "text", { {
tags = {},
text = "1"
} } }
{ "return" }
]]--