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

Improve alias system

This commit is contained in:
Étienne Fildadut 2021-04-08 16:34:33 +02:00
parent b0d7a0bfb5
commit 51e8c82181
6 changed files with 165 additions and 40 deletions

View file

@ -164,7 +164,7 @@ There's different types of lines, depending on their first character(s) (after i
> Last choice
```
* `$`: function line. Followed by an [identifier](#identifiers), and eventually a parameter list. Define a function using its children as function body.
* `$`: function line. Followed by an [identifier](#identifiers), then eventually an [alias](#aliases), and eventually a parameter list. Define a function using its children as function body.
The function body is not executed when the line is reached; it must be explicitely called in an expression. See [expressions](#function-calls) to see the different ways of calling a function.
@ -217,7 +217,7 @@ Functions always have the following variables defined in its namespace by defaul
`👁️`: number, number of times the function was executed before
`🏁`: string, name of last reached checkpoint/paragraph
* `§`: paragraph. Followed by an [identifier](#identifiers). Define a paragraph. A paragraph act as a checkpoint.
* `§`: paragraph. Followed by an [identifier](#identifiers), then eventually an [alias](#aliases). Define a paragraph. A paragraph act as a checkpoint.
The function 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](#paragraph-calls) to see the different ways of calling a paragraph.
@ -249,7 +249,7 @@ Paragraphs always have the following variable defined in its namespace by defaul
#### Lines that can't have children:
* `:`: variable declaration. Followed by an [expression](#expressions) and an [identifier](#identifiers). Defines a variable with a default value and this identifier in the current [namespace]("identifiers"). Once defined, the type of a variable can not change.
* `:`: variable declaration. Followed by an [expression](#expressions) and an [identifier](#identifiers), then eventually an [alias](#aliases). Defines a variable with a default value and this identifier in the current [namespace]("identifiers"). Once defined, the type of a variable can not change.
```
:42 foo
@ -375,6 +375,38 @@ Var1 in the fn1 namespace = 2: {fn1.var1}
~ fn1.var1 = 3
```
#### Aliases
When defining identifiers (in variables, functions or paragraph 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).
```
:42 name: alias
{name} is the same as {alias}
```
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.
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.
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)
:"John Pizzapone" player name
Hi {player name}!
(in a translated, french script)
:"John Pizzapone" player name : nom du joueur
Salut {nom du joueur} !
```
Variables that are defined automatically by Anselme (`👁️` and `🏁` in paragraphs and functions) can be automatically aliased using `vm:setaliases("👁alias", "🏁alias")`. See [API](#api-reference).
### Expressions
Besides lines, plenty of things in Anselme take expressions, which allow various operations on values and variables.

View file

@ -194,19 +194,6 @@ local vm_mt = {
return self
end,
--- set aliases
-- return self
loadalias = function(self, name, dest)
if type(name) == "table" then
for k, v in pairs(name) do
self:loadalias(k, v)
end
else
self.state.aliases[name] = dest
end
return self
end,
--- define functions
-- return self
loadfunction = function(self, name, fn)
@ -237,6 +224,14 @@ local vm_mt = {
return self
end,
--- set aliases for built-in variables 👁️ and 🏁 that will be defined on every new paragraph and function
-- return self
setaliases = function(self, seen, checkpoint)
self.state.builtin_aliases["👁️"] = seen
self.state.builtin_aliases["🏁"] = checkpoint
return self
end,
--- save/load
load = function(self, data)
assert(data.anselme_version == anselme.version, ("trying to load a save from Anselme %s but current Anselme version is %s"):format(data.anselme_version, anselme.version))
@ -268,6 +263,7 @@ local vm_mt = {
local interpreter
interpreter = {
state = {
builtin_aliases = self.builtin_aliases,
aliases = self.state.aliases,
functions = self.state.functions,
variables = setmetatable({}, { __index = self.state.variables }),
@ -310,9 +306,12 @@ return setmetatable(anselme, {
__call = function()
-- global state
local state = {
builtin_aliases = {
-- ["👁️"] = "seen",
-- ["🏁"] = "checkpoint"
},
aliases = {
-- seen = "👁️",
-- checkpoint = "🏁"
-- ["bonjour.salutation"] = "hello.greeting",
},
functions = {
-- [":="] = {

View file

@ -3,6 +3,21 @@ local expression
local escapeCache = {}
local common
--- rewrite name to use defined aliases (under namespace only)
-- namespace should not contain aliases
local replace_aliases = function(aliases, namespace, name)
namespace = namespace == "" and "" or namespace.."."
local name_list = common.split(name)
for i=1, #name_list, 1 do
local n = ("%s%s"):format(namespace, table.concat(name_list, ".", 1, i))
if aliases[n] then
name_list[i] = aliases[n]:match("[^%.]+$")
end
end
return table.concat(name_list, ".")
end
common = {
--- valid identifier pattern
identifier_pattern = "[^%%%/%*%+%-%(%)%!%&%|%=%$%§%?%>%<%:%{%}%[%]%,%\"]+",
@ -26,24 +41,27 @@ common = {
return address
end,
--- find a variable/function in a list, going up through the namespace hierarchy
find = function(list, namespace, name)
-- will apply aliases
find = function(aliases, list, namespace, name)
local ns = common.split(namespace)
for i=#ns, 1, -1 do
local fqm = ("%s.%s"):format(table.concat(ns, ".", 1, i), name)
local current_namespace = table.concat(ns, ".", 1, i)
local fqm = ("%s.%s"):format(current_namespace, replace_aliases(aliases, current_namespace, name))
if list[fqm] then
return list[fqm], fqm
end
end
-- root namespace
name = replace_aliases(aliases, "", name)
if list[name] then
return list[name], name
end
return nil, ("can't find %q in namespace %s"):format(name, namespace)
end,
--- transform an identifier into a clean version (trim & alias)
--- transform an identifier into a clean version (trim each part)
format_identifier = function(identifier, state)
local r = identifier:gsub("[^%.]+", function(str)
str = common.trim(str)
return state.aliases[str] or str
return common.trim(str)
end)
return r
end,

View file

@ -92,7 +92,7 @@ local function expression(s, state, namespace, currentPriority, operatingOn)
local name, r = s:match("^("..identifier_pattern..")(.-)$")
name = format_identifier(name, state)
-- variables
local var, vfqm = find(state.variables, namespace, name)
local var, vfqm = find(state.aliases, state.variables, namespace, name)
if var then
return expression(r, state, namespace, currentPriority, {
type = "variable",
@ -103,7 +103,7 @@ local function expression(s, state, namespace, currentPriority, operatingOn)
-- suffix call: detect if prefix is valid variable, suffix call is handled in the binop section below
local sname, suffix = name:match("^(.*)(%."..identifier_pattern..")$")
if sname then
local svar, svfqm = find(state.variables, namespace, sname)
local svar, svfqm = find(state.aliases, state.variables, namespace, sname)
if svar then
return expression(suffix..r, state, namespace, currentPriority, {
type = "variable",
@ -113,7 +113,7 @@ local function expression(s, state, namespace, currentPriority, operatingOn)
end
end
-- functions
local funcs, ffqm = find(state.functions, namespace, name)
local funcs, ffqm = find(state.aliases, state.functions, namespace, name)
if funcs then
local args, explicit_call
if r:match("^%b()") then
@ -162,7 +162,7 @@ local function expression(s, state, namespace, currentPriority, operatingOn)
if op == "." and sright:match("^"..identifier_pattern) then
local name, r = sright:match("^("..identifier_pattern..")(.-)$")
name = format_identifier(name, state)
local funcs, ffqm = find(state.functions, namespace, name)
local funcs, ffqm = find(state.aliases, state.functions, namespace, name)
if funcs then
local args, explicit_call
if r:match("^%b()") then

View file

@ -29,9 +29,25 @@ local function parse_line(line, state, namespace)
l, name = l:match("^(.-)%s*§(.-)$")
local identifier, rem = name:match("^("..identifier_pattern..")(.-)$")
if not identifier then return nil, ("no valid identifier in paragraph decorator %q; at %s"):format(identifier, line.source) end
if rem:match("[^%s]") then return nil, ("expected end-of-line after identifier in paragraph decorator, but got %q; at %s"):format(rem, line.source) end
-- format identifier
local fqm = ("%s%s"):format(namespace, format_identifier(identifier, state))
-- get alias
if rem:match("^%:") then
local content = rem:sub(2)
local alias, rem2 = content:match("^("..identifier_pattern..")(.-)$")
if not alias then return nil, ("expected an identifier in alias in paragraph decorator, but got %q; at %s"):format(content, line.source) end
if rem2:match("[^%s]") then return nil, ("expected end-of-line after identifier in alias in paragraph decorator, but got %q; at %s"):format(rem2, line.source) end
-- format alias
local aliasfqm = ("%s%s"):format(namespace, format_identifier(alias, state))
-- define alias
if state.aliases[aliasfqm] ~= nil and state.aliases[aliasfqm] ~= fqm then
return nil, ("trying to define alias %q for variable %q, but already exist and refer to different variable %q; at %s"):format(aliasfqm, fqm, state.aliases[aliasfqm], line.source)
end
state.aliases[aliasfqm] = fqm
elseif rem:match("[^%s]") then
return nil, ("expected end-of-line after identifier in paragraph decorator, but got %q; at %s"):format(rem, line.source)
end
-- define paragraph
namespace = fqm.."."
r.paragraph = true
r.parent_function = true
@ -50,6 +66,15 @@ local function parse_line(line, state, namespace)
value = 0
}
end
-- define alias for 👁️
local seen_alias = state.builtin_aliases["👁️"]
if seen_alias then
local alias = ("%s.%s"):format(fqm, seen_alias)
if state.aliases[alias] ~= nil and state.aliases[alias] then
return nil, ("trying to define alias %q for variable %q, but already exist and refer to different variable %q; at %s"):format(alias, fqm..".👁️", state.aliases[alias], line.source)
end
state.aliases[alias] = fqm..".👁️"
end
else
table.insert(state.functions[fqm], {
arity = 0,
@ -89,6 +114,20 @@ local function parse_line(line, state, namespace)
if not identifier then return nil, ("no valid identifier in paragraph/function definition line %q; at %s"):format(lc, line.source) end
-- format identifier
local fqm = ("%s%s"):format(namespace, format_identifier(identifier, state))
-- get alias
if rem:match("^%:") then
local content = rem:sub(2)
local alias
alias, rem = content:match("^("..identifier_pattern..")(.-)$")
if not alias then return nil, ("expected an identifier in alias in paragraph/function definition line, but got %q; at %s"):format(content, line.source) end
-- format alias
local aliasfqm = ("%s%s"):format(namespace, format_identifier(alias, state))
-- define alias
if state.aliases[aliasfqm] ~= nil and state.aliases[aliasfqm] ~= fqm then
return nil, ("trying to define alias %q for function/paragraph %q, but already exist and refer to %q; at %s"):format(aliasfqm, fqm, state.aliases[aliasfqm], line.source)
end
state.aliases[aliasfqm] = fqm
end
-- get params
r.params = {}
if r.type == "function" and rem:match("^%b()$") then
@ -113,12 +152,6 @@ local function parse_line(line, state, namespace)
-- don't keep function node in block AST
if r.type == "function" then
r.remove_from_block_ast = true
if not state.variables[fqm..".🏁"] then
state.variables[fqm..".🏁"] = {
type = "string",
value = ""
}
end
end
-- define function and variables
r.namespace = fqm.."."
@ -130,14 +163,44 @@ local function parse_line(line, state, namespace)
vararg = vararg,
value = r
}
-- new function (no overloading yet)
if not state.functions[fqm] then
state.functions[fqm] = { r.variant }
-- define 👁️ variable
if not state.variables[fqm..".👁️"] then
state.variables[fqm..".👁️"] = {
type = "number",
value = 0
}
end
-- define alias for 👁️
local seen_alias = state.builtin_aliases["👁️"]
if seen_alias then
local alias = ("%s.%s"):format(fqm, seen_alias)
if state.aliases[alias] ~= nil and state.aliases[alias] then
return nil, ("trying to define alias %q for variable %q, but already exist and refer to different variable %q; at %s"):format(alias, fqm..".👁️", state.aliases[alias], line.source)
end
state.aliases[alias] = fqm..".👁️"
end
if r.type == "function" then
-- define 🏁 variable
if not state.variables[fqm..".🏁"] then
state.variables[fqm..".🏁"] = {
type = "string",
value = ""
}
end
-- define alias for 🏁
local checkpoint_alias = state.builtin_aliases["🏁"]
if checkpoint_alias then
local alias = ("%s.%s"):format(fqm, checkpoint_alias)
if state.aliases[alias] ~= nil and state.aliases[alias] then
return nil, ("trying to define alias %q for variable %q, but already exist and refer to different variable %q; at %s"):format(alias, fqm..".🏁", state.aliases[alias], line.source)
end
state.aliases[alias] = fqm..".🏁"
end
end
-- overloading
else
-- check for arity conflict
for _, variant in ipairs(state.functions[fqm]) do
@ -181,9 +244,25 @@ local function parse_line(line, state, namespace)
-- get identifier
local identifier, rem2 = rem:match("^("..identifier_pattern..")(.-)$")
if not identifier then return nil, ("no valid identifier after expression in definition line %q; at %s"):format(rem, line.source) end
if rem2:match("[^%s]") then return nil, ("expected end-of-line after identifier in definition line, but got %q; at %s"):format(rem2, line.source) end
-- format identifier & define
-- format identifier
local fqm = ("%s%s"):format(namespace, format_identifier(identifier, state))
-- get alias
if rem2:match("^%:") then
local content = rem2:sub(2)
local alias, rem3 = content:match("^("..identifier_pattern..")(.-)$")
if not alias then return nil, ("expected an identifier in alias in definition line, but got %q; at %s"):format(content, line.source) end
if rem3:match("[^%s]") then return nil, ("expected end-of-line after identifier in alias in definition line, but got %q; at %s"):format(rem3, line.source) end
-- format alias
local aliasfqm = ("%s%s"):format(namespace, format_identifier(alias, state))
-- define alias
if state.aliases[aliasfqm] ~= nil and state.aliases[aliasfqm] ~= fqm then
return nil, ("trying to define alias %s for variable %s, but already exist and refer to different variable %s; at %s"):format(aliasfqm, fqm, state.aliases[aliasfqm], line.source)
end
state.aliases[aliasfqm] = fqm
elseif rem2:match("[^%s]") then
return nil, ("expected end-of-line after identifier in definition line, but got %q; at %s"):format(rem2, line.source)
end
-- define identifier
if state.functions[fqm] then return nil, ("trying to define variable %s, but a function with the same name exists; at %s"):format(fqm, line.source) end
if not state.variables[fqm] or state.variables[fqm].type == "undefined argument" then
local v, e = eval(state, exp)

View file

@ -96,10 +96,7 @@ else
local namespace = filebase:match("([^/]*)$")
math.randomseed(0)
local vm = anselme()
vm:loadalias {
seen = "👁️",
checkpoint = "🏁"
}
vm:setaliases("seen", "checkpoint")
vm:loadfunction {
-- custom event test
["wait"] = {