From c4636343b498be8aa1819518b686e468e1154b73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Reuh=20Fildadut?= Date: Sat, 23 Dec 2023 21:09:12 +0100 Subject: [PATCH] Translation system first draft --- ast/AttachBlock.lua | 16 +++- ast/ResumeParentFunction.lua | 6 +- ast/Struct.lua | 12 ++- ast/Table.lua | 10 +-- ast/Translatable.lua | 49 ++++++++++++ ast/abstract/Node.lua | 12 +++ common/init.lua | 3 +- parser/expression/primary/init.lua | 1 + .../primary/prefix/translatable.lua | 16 ++++ parser/expression/primary/text.lua | 5 +- .../expression/secondary/infix/translate.lua | 9 +++ parser/expression/secondary/init.lua | 1 + state/State.lua | 2 + state/translation_manager.lua | 78 +++++++++++++++++++ stdlib/text.lua | 13 +++- 15 files changed, 215 insertions(+), 18 deletions(-) create mode 100644 ast/Translatable.lua create mode 100644 parser/expression/primary/prefix/translatable.lua create mode 100644 parser/expression/secondary/infix/translate.lua create mode 100644 state/translation_manager.lua diff --git a/ast/AttachBlock.lua b/ast/AttachBlock.lua index e8285cb..3f926b8 100644 --- a/ast/AttachBlock.lua +++ b/ast/AttachBlock.lua @@ -3,7 +3,8 @@ local Identifier, Quote local attached_block_identifier, attached_block_symbol -local AttachBlock = ast.abstract.Node { +local AttachBlock +AttachBlock = ast.abstract.Node { type = "attach block", expression = nil, @@ -38,7 +39,18 @@ local AttachBlock = ast.abstract.Node { state.scope:define(attached_block_symbol, Quote:new(self.block)) self.expression:prepare(state) state.scope:pop() - end + end, + + -- class method: if the block identifier is defined in the current scope, wrap node in an AttachBlock so the block is still defined in this node + -- used to preserve the defined _ block without the need to build a full closure + -- used e.g. for -> translation, as we want to preserve _ while still executing the translation in the Translatable scope and not restore a different scope from a closure + -- (operates on un-evaluated nodes!) + preserve = function(self, state, node) + if state.scope:defined_in_current(attached_block_symbol) then + return AttachBlock:new(node, state.scope:get(attached_block_identifier).expression) -- unwrap Quote as that will be rewrap on eval + end + return node + end, } package.loaded[...] = AttachBlock diff --git a/ast/ResumeParentFunction.lua b/ast/ResumeParentFunction.lua index 1ec5fb9..acae124 100644 --- a/ast/ResumeParentFunction.lua +++ b/ast/ResumeParentFunction.lua @@ -26,11 +26,11 @@ local ResumeParentFunction = ast.abstract.Node { end, _eval = function(self, state) - if resumable_manager:resuming(state, self) then + if self:resuming(state) then self.expression:eval(state) - return resumable_manager:get_data(state, self):call(state, ArgumentTuple:new()) + return self:get_data(state):call(state, ArgumentTuple:new()) else - resumable_manager:set_data(state, self, resumable_manager:capture(state, 1)) + self:set_data(state, resumable_manager:capture(state, 1)) return self.expression:eval(state) end end diff --git a/ast/Struct.lua b/ast/Struct.lua index 3083497..8e8bce7 100644 --- a/ast/Struct.lua +++ b/ast/Struct.lua @@ -112,7 +112,17 @@ Struct = ast.abstract.Runtime { has = function(self, key) local hash = key:hash() return not not self.table[hash] - end + end, + iter = function(self) + local t, h = self.table, nil + return function() + local e + h, e = next(t, h) + if h == nil then return nil + else return e[1], e[2] + end + end + end, } package.loaded[...] = Struct diff --git a/ast/Table.lua b/ast/Table.lua index 99ded8e..89b3ecd 100644 --- a/ast/Table.lua +++ b/ast/Table.lua @@ -58,14 +58,8 @@ Table = ast.abstract.Runtime { return s:has(key) end, iter = function(self, state) - local t, h = self.branched:get(state).table, nil - return function() - local e - h, e = next(t, h) - if h == nil then return nil - else return e[1], e[2] - end - end + local s = self.branched:get(state) + return s:iter() end, to_struct = function(self, state) diff --git a/ast/Translatable.lua b/ast/Translatable.lua new file mode 100644 index 0000000..7116936 --- /dev/null +++ b/ast/Translatable.lua @@ -0,0 +1,49 @@ +local ast = require("ast") +local TextInterpolation, String + +local operator_priority = require("common").operator_priority + +local translation_manager + +local Translatable = ast.abstract.Node { + type = "translatable", + format_priority = operator_priority["%_"], + + expression = nil, + + init = function(self, expression) + self.expression = expression + self.context = ast.Struct:new() + self.context:set(String:new("source"), String:new(self.expression.source)) + if TextInterpolation:is(self.expression) then + self.format_priority = expression.format_priority + end + end, + + _format = function(self, ...) + if TextInterpolation:is(self.expression) then -- wrapped in translatable by default + return self.expression:format(...) + else + return "%"..self.expression:format_right(...) + end + end, + + traverse = function(self, fn, ...) + fn(self.expression, ...) + end, + + _eval = function(self, state) + return translation_manager:eval(state, self.context, self) + end, + + list_translatable = function(self, t) + table.insert(t, self) + end +} + +package.loaded[...] = Translatable +TextInterpolation, String = ast.TextInterpolation, ast.String + +translation_manager = require("state.translation_manager") + +return Translatable diff --git a/ast/abstract/Node.lua b/ast/abstract/Node.lua index 171fba5..0df4919 100644 --- a/ast/abstract/Node.lua +++ b/ast/abstract/Node.lua @@ -44,6 +44,9 @@ traverse = { end, hash = function(self, t) table.insert(t, self:hash()) + end, + list_translatable = function(self, t) + self:list_translatable(t) end } @@ -119,6 +122,15 @@ Node = class { self:traverse(traverse.prepare, state) end, + -- generate a list of translatable nodes that appear in this node + -- should only be called on non-runtime nodes + -- if a node is translatable, redefine this to add it to the table - note that it shouldn't call :traverse or :list_translatable on its children, as nested translations should not be needed + list_translatable = function(self, t) + t = t or {} + self:traverse(traverse.list_translatable, t) + return t + end, + -- same as eval, but make the evaluated expression as a resume boundary -- i.e. if a checkpoint is defined somewhere in this eval, it will start back from this node eval when resuming eval_resumable = function(self, state) diff --git a/common/init.lua b/common/init.lua index 24812c5..9802c35 100644 --- a/common/init.lua +++ b/common/init.lua @@ -28,13 +28,14 @@ local common = { { "!", 11 }, { "-", 11 }, { "*", 11 }, + { "%", 11 }, }, suffixes = { { ";", 1 }, { "!", 12 } }, infixes = { - { ";", 1 }, + { ";", 1 }, { "->", 1 }, { "#", 2 }, { "~", 4 }, { "~?", 4 }, { "|>", 5 }, { "&", 5 }, { "|", 5 }, diff --git a/parser/expression/primary/init.lua b/parser/expression/primary/init.lua index 7655a75..370bb69 100644 --- a/parser/expression/primary/init.lua +++ b/parser/expression/primary/init.lua @@ -28,6 +28,7 @@ local primaries = { r("prefix.negation"), r("prefix.not"), r("prefix.mutable"), + r("prefix.translatable"), } return { diff --git a/parser/expression/primary/prefix/translatable.lua b/parser/expression/primary/prefix/translatable.lua new file mode 100644 index 0000000..7c96a70 --- /dev/null +++ b/parser/expression/primary/prefix/translatable.lua @@ -0,0 +1,16 @@ +local prefix = require("parser.expression.primary.prefix.prefix") + +local ast = require("ast") +local Translatable = ast.Translatable + +local operator_priority = require("common").operator_priority + +return prefix { + operator = "%", + identifier = "%_", + priority = operator_priority["%_"], + + build_ast = function(self, right) + return Translatable:new(right) + end +} diff --git a/parser/expression/primary/text.lua b/parser/expression/primary/text.lua index a1c2d41..b9e6195 100644 --- a/parser/expression/primary/text.lua +++ b/parser/expression/primary/text.lua @@ -1,7 +1,7 @@ local string = require("parser.expression.primary.string") local ast = require("ast") -local TextInterpolation = ast.TextInterpolation +local TextInterpolation, Translatable = ast.TextInterpolation, ast.Translatable return string { type = "text", @@ -11,6 +11,7 @@ return string { interpolation = TextInterpolation, parse = function(self, source, str, limit_pattern) + local start_source = source:clone() local interpolation, rem = string.parse(self, source, str, limit_pattern) -- restore | when chaining with a choice operator @@ -19,6 +20,6 @@ return string { source:increment(-1) end - return interpolation, rem + return Translatable:new(interpolation):set_source(start_source), rem end } diff --git a/parser/expression/secondary/infix/translate.lua b/parser/expression/secondary/infix/translate.lua new file mode 100644 index 0000000..ea6a8a7 --- /dev/null +++ b/parser/expression/secondary/infix/translate.lua @@ -0,0 +1,9 @@ +local infix_quote_both = require("parser.expression.secondary.infix.infix_quote_both") + +local operator_priority = require("common").operator_priority + +return infix_quote_both { + operator = "->", + identifier = "_->_", + priority = operator_priority["_->_"] +} diff --git a/parser/expression/secondary/init.lua b/parser/expression/secondary/init.lua index f43cfbd..784765b 100644 --- a/parser/expression/secondary/init.lua +++ b/parser/expression/secondary/init.lua @@ -8,6 +8,7 @@ local secondaries = { -- binary infix operators -- 1 r("infix.semicolon"), + r("infix.translate"), -- 2 r("infix.tuple"), r("infix.tag"), diff --git a/state/State.lua b/state/State.lua index 609a686..e9846cf 100644 --- a/state/State.lua +++ b/state/State.lua @@ -6,6 +6,7 @@ local ScopeStack = require("state.ScopeStack") local tag_manager = require("state.tag_manager") local event_manager = require("state.event_manager") local resumable_manager = require("state.resumable_manager") +local translation_manager = require("state.translation_manager") local uuid = require("common").uuid local parser = require("parser") local binser = require("lib.binser") @@ -32,6 +33,7 @@ State = class { event_manager:setup(self) tag_manager:setup(self) resumable_manager:setup(self) + translation_manager:setup(self) end end, diff --git a/state/translation_manager.lua b/state/translation_manager.lua new file mode 100644 index 0000000..a1e8b57 --- /dev/null +++ b/state/translation_manager.lua @@ -0,0 +1,78 @@ +local class = require("class") + +local ast = require("ast") +local Table, Identifier + +local translations_identifier, translations_symbol + +local translation_manager = class { + init = false, + + setup = function(self, state) + state.scope:define(translations_symbol, Table:new(state)) + end, + + -- context is the context Struct - when translating, the translation will only be used for nodes that at least match this context + -- original is the original node (non-evaluated) + -- translated is the translated node (non-evaluated) + set = function(self, state, context, original, translated) + local translations = state.scope:get(translations_identifier) + if not translations:has(state, original) then + translations:set(state, original, Table:new(state)) + end + local tr = translations:get(state, original) + return tr:set(state, context, translated) + end, + + -- context is the context Struct of the calling translation + -- original is the original node to translate (non-evaluated) + -- returns the (evaluated) translated node, or the original node if no translation defined + eval = function(self, state, context, original) + local translations = state.scope:get(translations_identifier) + if translations:has(state, original) then + local tr = translations:get(state, original) + + -- find most specific translation + local translated, specificity = nil, -1 + for match_context, match_translated in tr:iter(state) do + local matched, match_specificity = true, 0 + for key, val in match_context:iter() do + if context:has(key) and val:hash() == context:get(key):hash() then + match_specificity = match_specificity + 1 + else + matched = false + break + end + end + if matched then + if match_specificity > specificity then + translated, specificity = match_translated, match_specificity + elseif match_specificity == specificity then + print("a a dà é payé") + end + end + end + + -- found, evaluate translated + if translated then + -- eval in a scope where all active translations, as translating the translation would be stupid + state.scope:push_partial(translations_identifier) + state.scope:define(translations_symbol, Table:new(state)) + local r = translated:eval(state) + state.scope:pop() + return r + end + end + + -- no matching translation + return original.expression:eval(state) + end, +} + +package.loaded[...] = translation_manager +Table, Identifier = ast.Table, ast.Identifier + +translations_identifier = Identifier:new("_translations") -- Table of { Translatable = Table{ Struct context = translated node, ... }, ... } +translations_symbol = translations_identifier:to_symbol() + +return translation_manager diff --git a/stdlib/text.lua b/stdlib/text.lua index b5a4552..061b30d 100644 --- a/stdlib/text.lua +++ b/stdlib/text.lua @@ -1,7 +1,9 @@ local ast = require("ast") -local Nil, Choice = ast.Nil, ast.Choice +local Nil, Choice, AttachBlock = ast.Nil, ast.Choice, ast.AttachBlock local event_manager = require("state.event_manager") +local translation_manager = require("state.translation_manager") +local tag_manager = require("state.tag_manager") return { -- text @@ -21,4 +23,13 @@ return { return Nil:new() end }, + + -- translation + { + "_->_", "(original::is(\"quote\"), translated::is(\"quote\"))", + function(state, original, translated) + translation_manager:set(state, tag_manager:get(state), original.expression, AttachBlock:preserve(state, translated.expression)) + return Nil:new() + end + } }