From e9606cdee0c3f307ae312a8232c435fe636a575e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Reuh=20Fildadut?= Date: Tue, 27 Sep 2022 17:05:06 +0900 Subject: [PATCH] Add text buffer syntax --- LANGUAGE.md | 10 +- anselme.lua | 6 +- interpreter/common.lua | 95 ++++++----- interpreter/expression.lua | 34 +++- parser/common.lua | 2 +- parser/expression.lua | 13 +- parser/postparser.lua | 10 ++ parser/preparser.lua | 1 + stdlib/types.lua | 228 +++++++++++++++++---------- test/tests/text buffer with tags.ans | 8 + test/tests/text buffer with tags.lua | 41 +++++ test/tests/text buffer.ans | 8 + test/tests/text buffer.lua | 34 ++++ 13 files changed, 345 insertions(+), 145 deletions(-) create mode 100644 test/tests/text buffer with tags.ans create mode 100644 test/tests/text buffer with tags.lua create mode 100644 test/tests/text buffer.ans create mode 100644 test/tests/text buffer.lua diff --git a/LANGUAGE.md b/LANGUAGE.md index 6408936..4bd0730 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -635,6 +635,8 @@ Default types are: * `object`: an object/record. Mutable. Can be created by calling a class function. +* `event buffer`: an [event buffer](#event-buffer). Can be created using `%[text]`, where text is interpreted the same way as a text line (so you can perform interpolation, subtexts, add tags, etc.). Note that this only allows for text and flush events; if the text emit choices and custom events an error will be raised. + Every type is immutable, except `list`, `map` and `object`. How conversions are handled from Anselme to Lua: @@ -645,11 +647,17 @@ How conversions are handled from Anselme to Lua: * `string` -> `string` +* `pair` -> `table`, with a single key-value pair. + +* `annotated` -> the annotated value (the annotation is stripped) + * `list` -> `table` (purely sequential table). * `map` -> `table` (will map each key to a key in the Lua table). -* `pair` -> `table`, with a single key-value pair. +* `object` -> `table` (containing each object and class property and its value) + +* `event buffer` -> `table` (a list of events where each event is a list of two elements (event type and its eventual data), like `{ { "text", text = { { tags = {...}, text = "hello" } } }, { "flush" }, ... }`) How conservions are handled from Lua to Anselme: diff --git a/anselme.lua b/anselme.lua index 14da56b..671f7dc 100644 --- a/anselme.lua +++ b/anselme.lua @@ -250,7 +250,7 @@ local interpreter_methods = { if re then coroutine.yield("error", re) end if rf then r = rf end end - return to_lua(r) + return to_lua(r, self.state) end, --- Evaluate an expression (string) or block, optionally in a specific namespace (string, will use root namespace if not specified). -- The expression can't yield events. @@ -292,7 +292,7 @@ local interpreter_methods = { elseif event ~= "return" then return nil, ("evaluated expression generated an %q event; at %s"):format(event, self.state.interpreter.running_line.source) else - return to_lua(data) + return to_lua(data, self.state) end end, } @@ -730,7 +730,7 @@ local vm_mt = { if not interpreter then return interpreter, err end local r, e = interpreter:eval(expr, namespace) if e then return r, e end - assert(interpreter:step() == "return") -- trigger merge / end-of-script things + assert(interpreter:step() == "return", "evaluated expression can not emit events") -- trigger merge / end-of-script things return r end } diff --git a/interpreter/common.lua b/interpreter/common.lua index 70a12ea..63c8fc7 100644 --- a/interpreter/common.lua +++ b/interpreter/common.lua @@ -5,49 +5,6 @@ local common local identifier_pattern local copy ---- copy some text & process it to be suited to be sent to Lua in an event -local function post_process_text(state, text) - local r = {} - -- copy into r & convert tags to lua - for _, t in ipairs(text) do - local tags = common.to_lua(t.tags) - if state.interpreter.base_lua_tags then - for k, v in pairs(state.interpreter.base_lua_tags) do - if tags[k] == nil then tags[k] = v end - end - end - table.insert(r, { - text = t.text, - tags = tags - }) - end - -- remove trailing spaces - if state.feature_flags["strip trailing spaces"] then - local final = r[#r] - if final then - final.text = final.text:match("^(.-) *$") - if final.text == "" then - table.remove(r) - end - end - end - -- remove duplicate spaces - if state.feature_flags["strip duplicate spaces"] then - for i=1, #r-1 do - local a, b = r[i], r[i+1] - local na = #a.text:match(" *$") - local nb = #b.text:match("^ *") - if na > 0 and nb > 0 then -- remove duplicated spaces from second element first - b.text = b.text:match("^ *(.-)$") - end - if na > 1 then - a.text = a.text:match("^(.- ) *$") - end - end - end - return r -end - local function random_identifier() local r = "" for _=1, 16 do -- that's live 10^31 possibilities, ought to be enough for anyone @@ -383,9 +340,9 @@ common = { --- convert anselme value to lua -- lua value: if success (may be nil!) -- nil, err: if error - to_lua = function(val) + to_lua = function(val, state) if atypes[val.type] and atypes[val.type].to_lua then - return atypes[val.type].to_lua(val.value) + return atypes[val.type].to_lua(val.value, state) else return nil, ("no Lua exporter for type %q"):format(val.type) end @@ -598,14 +555,14 @@ common = { local choices -- copy & process text buffer if type == "text" then - buffer = post_process_text(state, event.value) + buffer = common.post_process_text(state, event.value) -- copy & process choice buffer elseif type == "choice" then -- copy & process choice text content into buffer, and needed private state into choices for each choice buffer = {} choices = {} for _, c in ipairs(event.value) do - table.insert(buffer, post_process_text(state, c)) + table.insert(buffer, common.post_process_text(state, c)) table.insert(choices, c._state) end -- discard empty choices @@ -647,7 +604,49 @@ common = { end return true end - } + }, + --- copy some text & process it to be suited to be sent to Lua in an event + post_process_text = function(state, text) + local r = {} + -- copy into r & convert tags to lua + for _, t in ipairs(text) do + local tags = common.to_lua(t.tags, state) + if state.interpreter.base_lua_tags then + for k, v in pairs(state.interpreter.base_lua_tags) do + if tags[k] == nil then tags[k] = v end + end + end + table.insert(r, { + text = t.text, + tags = tags + }) + end + -- remove trailing spaces + if state.feature_flags["strip trailing spaces"] then + local final = r[#r] + if final then + final.text = final.text:match("^(.-) *$") + if final.text == "" then + table.remove(r) + end + end + end + -- remove duplicate spaces + if state.feature_flags["strip duplicate spaces"] then + for i=1, #r-1 do + local a, b = r[i], r[i+1] + local na = #a.text:match(" *$") + local nb = #b.text:match("^ *") + if na > 0 and nb > 0 then -- remove duplicated spaces from second element first + b.text = b.text:match("^ *(.-)$") + end + if na > 1 then + a.text = a.text:match("^(.- ) *$") + end + end + end + return r + end } package.loaded[...] = common diff --git a/interpreter/expression.lua b/interpreter/expression.lua index e56d2dd..3a8b2fe 100644 --- a/interpreter/expression.lua +++ b/interpreter/expression.lua @@ -29,11 +29,33 @@ local function eval(state, exp) type = "string", value = t } + -- text buffer + elseif exp.type == "text buffer" then + -- eval text expression + local v, e = eval(state, exp.text) + if not v then return v, e end + local l = v.type == "list" and v.value or { v } + -- write resulting buffers (plural if loop in text expression) into a single result buffer + local buffer = {} + for _, item in ipairs(l) do + if item.type == "event buffer" then + for _, event in ipairs(item.value) do + if event.type ~= "text" and event.type ~= "flush" then + return nil, ("event %q can't be captured in a text buffer"):format(event.type) + end + table.insert(buffer, event) + end + end + end + return { + type = "event buffer", + value = buffer + } -- parentheses elseif exp.type == "parentheses" then return eval(state, exp.expression) -- list defined in brackets - elseif exp.type == "list_brackets" then + elseif exp.type == "list brackets" then if exp.expression then local v, e = eval(state, exp.expression) if not v then return nil, e end @@ -53,9 +75,9 @@ local function eval(state, exp) } end -- map defined in brackets - elseif exp.type == "map_brackets" then + elseif exp.type == "map brackets" then -- get constructing list - local list, e = eval(state, { type = "list_brackets", expression = exp.expression }) + local list, e = eval(state, { type = "list brackets", expression = exp.expression }) if not list then return nil, e end -- make map local map = {} @@ -166,7 +188,7 @@ local function eval(state, exp) } -- tag elseif exp.type == "#" then - local right, righte = eval(state, { type = "map_brackets", expression = exp.right }) + local right, righte = eval(state, { type = "map brackets", expression = exp.right }) if not right then return nil, righte end tags:push(state, right) local left, lefte = eval(state, exp.left) @@ -202,7 +224,7 @@ local function eval(state, exp) end -- function elseif exp.type == "function call" then - -- eval args: map_brackets + -- eval args: map brackets local args = {} local last_contiguous_positional = 0 if exp.argument then @@ -444,7 +466,7 @@ local function eval(state, exp) elseif lua_fn.mode == nil then local l_lua = {} for _, v in ipairs(final_args) do - local lv, e = to_lua(v) + local lv, e = to_lua(v, state) if e then return nil, e end table.insert(l_lua, lv) end diff --git a/parser/common.lua b/parser/common.lua index 788dfc0..4c2aff5 100644 --- a/parser/common.lua +++ b/parser/common.lua @@ -291,7 +291,7 @@ common = { implicit_call = implicit_call, -- was call implicitely (no ! or parentheses)? variants = variants, -- list of potential variants argument = { -- wrap everything in a list literal to simplify later things (otherwise may be nil, single value, list constructor) - type = "map_brackets", + type = "map brackets", expression = arg } } diff --git a/parser/expression.lua b/parser/expression.lua index d80f42d..9991cf8 100644 --- a/parser/expression.lua +++ b/parser/expression.lua @@ -105,6 +105,15 @@ local function expression(s, state, namespace, current_priority, operating_on) local l, e = parse_text(d, state, namespace, "string") -- parse interpolated expressions if not l then return l, e end return expression(r, state, namespace, current_priority, l) + -- text buffer + elseif s:match("^%%%[") then + local text = s:match("^%%(.*)$") + local v, r = parse_text(text, state, namespace, "text", "#~", true) + if not v then return nil, r end + return expression(r, state, namespace, current_priority, { + type = "text buffer", + text = v + }) -- paranthesis elseif s:match("^%b()") then local content, r = s:match("^(%b())(.*)$") @@ -134,7 +143,7 @@ local function expression(s, state, namespace, current_priority, operating_on) if r_paren:match("[^%s]") then return nil, ("unexpected %q at end of list parenthesis expression"):format(r_paren) end end return expression(r, state, namespace, current_priority, { - type = "list_brackets", + type = "list brackets", expression = exp }) -- map parenthesis @@ -149,7 +158,7 @@ local function expression(s, state, namespace, current_priority, operating_on) if r_paren:match("[^%s]") then return nil, ("unexpected %q at end of map parenthesis expression"):format(r_paren) end end return expression(r, state, namespace, current_priority, { - type = "map_brackets", + type = "map brackets", expression = exp }) -- identifier diff --git a/parser/postparser.lua b/parser/postparser.lua index 52dd5a9..8c2f55e 100644 --- a/parser/postparser.lua +++ b/parser/postparser.lua @@ -53,6 +53,16 @@ local function parse(state) end end end + -- get list of properties + -- (unlike scoped, does not includes subnamespaces) + if line.properties then + line.properties = {} + for name in pairs(state.variables) do + if name:sub(1, #namespace) == namespace and not name:sub(#namespace+1):match("%.") then + table.insert(line.properties, name) + end + end + end end -- expressions if line.expression then diff --git a/parser/preparser.lua b/parser/preparser.lua index 6b29bf6..00c968f 100644 --- a/parser/preparser.lua +++ b/parser/preparser.lua @@ -97,6 +97,7 @@ local function parse_line(line, state, namespace, parent_function) elseif lr:match("^%%") then r.subtype = "class" r.resume_boundary = true + r.properties = true allow_params = false allow_assign = false elseif lr:match("^%!") then diff --git a/stdlib/types.lua b/stdlib/types.lua index a9a3b6b..2600eb6 100644 --- a/stdlib/types.lua +++ b/stdlib/types.lua @@ -1,4 +1,4 @@ -local format, to_lua, from_lua, events, anselme, escape, hash, mark_constant, update_hashes +local format, to_lua, from_lua, events, anselme, escape, hash, mark_constant, update_hashes, get_variable, find_function_variant_from_fqm, post_process_text local types = {} types.lua = { @@ -114,6 +114,58 @@ types.anselme = { end, mark_constant = function() end, }, + pair = { + format = function(val) + local k, ke = format(val[1]) + if not k then return k, ke end + local v, ve = format(val[2]) + if not v then return v, ve end + return ("%s=%s"):format(k, v) + end, + to_lua = function(val, state) + local k, ke = to_lua(val[1], state) + if ke then return nil, ke end + local v, ve = to_lua(val[2], state) + if ve then return nil, ve end + return { [k] = v } + end, + hash = function(val) + local k, ke = hash(val[1]) + if not k then return k, ke end + local v, ve = hash(val[2]) + if not v then return v, ve end + return ("p(%s=%s)"):format(k, v) + end, + mark_constant = function(v) + mark_constant(v.value[1]) + mark_constant(v.value[2]) + end, + }, + annotated = { + format = function(val) + local k, ke = format(val[1]) + if not k then return k, ke end + local v, ve = format(val[2]) + if not v then return v, ve end + return ("%s::%s"):format(k, v) + end, + to_lua = function(val, state) + local k, ke = to_lua(val[1], state) + if ke then return nil, ke end + return k + end, + hash = function(val) + local k, ke = hash(val[1]) + if not k then return k, ke end + local v, ve = hash(val[2]) + if not v then return v, ve end + return ("a(%s::%s)"):format(k, v) + end, + mark_constant = function(v) + mark_constant(v.value[1]) + mark_constant(v.value[2]) + end, + }, list = { mutable = true, format = function(val) @@ -125,11 +177,11 @@ types.anselme = { end return ("[%s]"):format(table.concat(l, ", ")) end, - to_lua = function(val) + to_lua = function(val, state) local l = {} for _, v in ipairs(val) do - local s, e = to_lua(v) - if not s and e then return s, e end + local s, e = to_lua(v, state) + if e then return nil, e end table.insert(l, s) end return l @@ -164,13 +216,13 @@ types.anselme = { table.sort(l) return ("{%s}"):format(table.concat(l, ", ")) end, - to_lua = function(val) + to_lua = function(val, state) local l = {} for _, v in pairs(val) do - local kl, ke = to_lua(v[1]) - if not kl and ke then return kl, ke end - local xl, xe = to_lua(v[2]) - if not xl and xe then return xl, xe end + local kl, ke = to_lua(v[1], state) + if ke then return nil, ke end + local xl, xe = to_lua(v[2], state) + if xe then return nil, xe end l[kl] = xl end return l @@ -196,56 +248,56 @@ types.anselme = { update_hashes(v) end, }, - pair = { + object = { + mutable = true, format = function(val) - local k, ke = format(val[1]) - if not k then return k, ke end - local v, ve = format(val[2]) - if not v then return v, ve end - return ("%s=%s"):format(k, v) + local attributes = {} + for name, v in pairs(val.attributes) do + table.insert(attributes, ("%s=%s"):format(name:gsub("^"..escape(val.class)..".", ""), format(v))) + end + if #attributes > 0 then + table.sort(attributes) + return ("%%%s(%s)"):format(val.class, table.concat(attributes, ", ")) + else + return ("%%%s"):format(val.class) + end end, - to_lua = function(val) - local k, ke = to_lua(val[1]) - if not k and ke then return k, ke end - local v, ve = to_lua(val[2]) - if not v and ve then return v, ve end - return { [k] = v } + to_lua = function(val, state) + local r = {} + local namespacePattern = "^"..escape(val.class).."%." + -- set object properties + for name, v in pairs(val.attributes) do + local var, err = to_lua(v, state) + if err then return nil, err end + r[name:gsub(namespacePattern, "")] = var + end + -- set class properties + local class, err = find_function_variant_from_fqm(val.class, state, nil) + if not class then return nil, err end + assert(#class == 1 and class[1].subtype == "class") + class = class[1] + for _, prop in ipairs(class.properties) do + if not val.attributes[prop] then + local var + var, err = get_variable(state, prop) + if not var then return nil, err end + var, err = to_lua(var, state) + if err then return nil, err end + r[prop:gsub(namespacePattern, "")] = var + end + end + return r end, hash = function(val) - local k, ke = hash(val[1]) - if not k then return k, ke end - local v, ve = hash(val[2]) - if not v then return v, ve end - return ("p(%s=%s)"):format(k, v) + local attributes = {} + for name, v in pairs(val.attributes) do + table.insert(attributes, ("%s=%s"):format(name:gsub("^"..escape(val.class)..".", ""), format(v))) + end + table.sort(attributes) + return ("%%(%s;%s)"):format(val.class, table.concat(attributes, ",")) end, mark_constant = function(v) - mark_constant(v.value[1]) - mark_constant(v.value[2]) - end, - }, - annotated = { - format = function(val) - local k, ke = format(val[1]) - if not k then return k, ke end - local v, ve = format(val[2]) - if not v then return v, ve end - return ("%s::%s"):format(k, v) - end, - to_lua = function(val) - local k, ke = to_lua(val[1]) - if not k and ke then return k, ke end - return k - end, - hash = function(val) - local k, ke = hash(val[1]) - if not k then return k, ke end - local v, ve = hash(val[2]) - if not v then return v, ve end - return ("a(%s::%s)"):format(k, v) - end, - mark_constant = function(v) - mark_constant(v.value[1]) - mark_constant(v.value[2]) + v.constant = true end, }, ["function reference"] = { @@ -273,47 +325,55 @@ types.anselme = { end, mark_constant = function() end, }, - object = { - mutable = true, - format = function(val) - local attributes = {} - for name, v in pairs(val.attributes) do - table.insert(attributes, ("%s=%s"):format(name:gsub("^"..escape(val.class)..".", ""), format(v))) - end - if #attributes > 0 then - table.sort(attributes) - return ("%%%s(%s)"):format(val.class, table.concat(attributes, ", ")) - else - return ("%%%s"):format(val.class) - end - end, - to_lua = nil, - hash = function(val) - local attributes = {} - for name, v in pairs(val.attributes) do - table.insert(attributes, ("%s=%s"):format(name:gsub("^"..escape(val.class)..".", ""), format(v))) - end - table.sort(attributes) - return ("%%(%s;%s)"):format(val.class, table.concat(attributes, ",")) - end, - mark_constant = function(v) - v.constant = true - end, - }, - -- internal types + -- event buffer: can only be used outside of Anselme internal for text & flush events (through text buffers) ["event buffer"] = { format = function(val) -- triggered from subtexts local v, e = events:write_buffer(anselme.running.state, val) if not v then return v, e end return "" - end + end, + to_lua = function(val, state) + local r = {} + for _, event in ipairs(val) do + if event.type == "text" then + table.insert(r, { "text", post_process_text(state, event.value) }) + elseif event.type == "flush" then + table.insert(r, { "flush" }) + else + return nil, ("event %q in event buffer can't be converted to a Lua value"):format(event.type) + end + end + return r + end, + hash = function(val) + local l = {} + for _, event in ipairs(val) do + if event.type == "text" then + local text = {} + for _, t in ipairs(event.value) do + local str = ("s(%s)"):format(t.text) + local tags, e = hash(t.tags) + if not tags then return nil, e end + table.insert(text, ("%s#%s"):format(str, tags)) + end + table.insert(l, ("text(%s)"):format(table.concat(text, ","))) + elseif event.type == "flush" then + table.insert(l, "flush") + else + return nil, ("event %q in event buffer cannot be hashed"):format(event.type) + end + end + return ("eb(%s)"):format(table.concat(l, ",")) + end, + mark_constant = function() end, }, } package.loaded[...] = types local common = require((...):gsub("stdlib%.types$", "interpreter.common")) -format, to_lua, from_lua, events, hash, mark_constant, update_hashes = common.format, common.to_lua, common.from_lua, common.events, common.hash, common.mark_constant, common.update_hashes +format, to_lua, from_lua, events, hash, mark_constant, update_hashes, get_variable, post_process_text = common.format, common.to_lua, common.from_lua, common.events, common.hash, common.mark_constant, common.update_hashes, common.get_variable, common.post_process_text anselme = require((...):gsub("stdlib%.types$", "anselme")) -escape = require((...):gsub("stdlib%.types$", "parser.common")).escape +local pcommon = require((...):gsub("stdlib%.types$", "parser.common")) +escape, find_function_variant_from_fqm = pcommon.escape, pcommon.find_function_variant_from_fqm return types diff --git a/test/tests/text buffer with tags.ans b/test/tests/text buffer with tags.ans new file mode 100644 index 0000000..1e69662 --- /dev/null +++ b/test/tests/text buffer with tags.ans @@ -0,0 +1,8 @@ +:$ f + lol # 1 + + d + +:a = %[a {f} [t # 2] b] + +@a diff --git a/test/tests/text buffer with tags.lua b/test/tests/text buffer with tags.lua new file mode 100644 index 0000000..717e9e5 --- /dev/null +++ b/test/tests/text buffer with tags.lua @@ -0,0 +1,41 @@ +local _={} +_[21]={} +_[20]={2} +_[19]={} +_[18]={1} +_[17]={} +_[16]={text="b",tags=_[21]} +_[15]={text="t ",tags=_[20]} +_[14]={text="d",tags=_[19]} +_[13]={text="lol",tags=_[18]} +_[12]={text="a",tags=_[17]} +_[11]={_[15],_[16]} +_[10]={_[14]} +_[9]={_[13]} +_[8]={_[12]} +_[7]={"text",_[11]} +_[6]={"text",_[10]} +_[5]={"flush"} +_[4]={"text",_[9]} +_[3]={"text",_[8]} +_[2]={_[3],_[4],_[5],_[6],_[7]} +_[1]={"return",_[2]} +return {_[1]} +--[[ +{ "return", { { "text", { { + tags = {}, + text = "a" + } } }, { "text", { { + tags = { 1 }, + text = "lol" + } } }, { "flush" }, { "text", { { + tags = {}, + text = "d" + } } }, { "text", { { + tags = { 2 }, + text = "t " + }, { + tags = {}, + text = "b" + } } } } } +]]-- \ No newline at end of file diff --git a/test/tests/text buffer.ans b/test/tests/text buffer.ans new file mode 100644 index 0000000..e27078c --- /dev/null +++ b/test/tests/text buffer.ans @@ -0,0 +1,8 @@ +:$ f + lol + + d + +:a = %[a {f} b] + +@a diff --git a/test/tests/text buffer.lua b/test/tests/text buffer.lua new file mode 100644 index 0000000..7cd1090 --- /dev/null +++ b/test/tests/text buffer.lua @@ -0,0 +1,34 @@ +local _={} +_[17]={} +_[16]={} +_[15]={} +_[14]={} +_[13]={tags=_[17],text=" b"} +_[12]={tags=_[16],text="d"} +_[11]={tags=_[15],text="lol"} +_[10]={tags=_[14],text="a"} +_[9]={_[12],_[13]} +_[8]={_[11]} +_[7]={_[10]} +_[6]={"text",_[9]} +_[5]={"flush"} +_[4]={"text",_[8]} +_[3]={"text",_[7]} +_[2]={_[3],_[4],_[5],_[6]} +_[1]={"return",_[2]} +return {_[1]} +--[[ +{ "return", { { "text", { { + tags = {}, + text = "a" + } } }, { "text", { { + tags = {}, + text = "lol" + } } }, { "flush" }, { "text", { { + tags = {}, + text = "d" + }, { + tags = {}, + text = " b" + } } } } } +]]-- \ No newline at end of file