diff --git a/README.md b/README.md index 1ba1b7c..9c557f0 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/anselme.lua b/anselme.lua index 28a9fd8..9709554 100644 --- a/anselme.lua +++ b/anselme.lua @@ -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 = { -- [":="] = { diff --git a/parser/common.lua b/parser/common.lua index 427aa84..2dd5286 100644 --- a/parser/common.lua +++ b/parser/common.lua @@ -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, diff --git a/parser/expression.lua b/parser/expression.lua index efe43e6..67151e7 100644 --- a/parser/expression.lua +++ b/parser/expression.lua @@ -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 diff --git a/parser/preparser.lua b/parser/preparser.lua index 63495e6..9e5b661 100644 --- a/parser/preparser.lua +++ b/parser/preparser.lua @@ -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) diff --git a/test/run.lua b/test/run.lua index 99cf8b6..2c7f68f 100644 --- a/test/run.lua +++ b/test/run.lua @@ -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"] = {