sokol-luajit/editor/twig/aspect/ast.lua

384 lines
11 KiB
Lua

local insert = table.insert
local concat = table.concat
local setmetatable = setmetatable
local ipairs = ipairs
local require = require
local type = type
local utils = require("aspect.utils")
local cast = utils.cast_lua
local var_dump = utils.var_dump
--- Intermediate branch element
--- @class aspect.ast.node
--- @field op aspect.ast.op
--- @field c aspect.ast.node|aspect.ast.leaf|nil condition for ternary operator
--- @field l aspect.ast.node|aspect.ast.leaf left node or leaf
--- @field r aspect.ast.node|aspect.ast.leaf right node or leaf
--- @field p aspect.ast.node|nil parent node
--- @field pos number token position
local _node = {}
--- Leaf - node with value. Branch finite element
--- @class aspect.ast.leaf
--- @field type string type of leaf (numeric, string, variable, expr, table)
--- @field value string value of the leaf
--- @field pos number token position
local _leaf = {}
--- AST builder
--- @class aspect.ast
--- @field current aspect.ast.node|aspect.ast.leaf
--- @field nodes number
local ast = {}
--- @type table<aspect.ast.op>
local ops_unary
local ops
local mt = {__index = ast}
function ast.new()
if not ops then
ops_unary = {}
ops = {}
for _, op in ipairs(require("aspect.ast.ops")) do
if type(op.token) == "table" then
for _, token in ipairs(op.token) do
if op.type == "unary" then
ops_unary[token] = op
else
ops[token] = op
end
end
else
if op.type == "unary" then
ops_unary[op.token] = op
else
ops[op.token] = op
end
end
end
end
return setmetatable({
current = nil,
nodes = 0
}, mt)
end
--- @param compiler aspect.compiler
--- @param tok aspect.tokenizer
function ast:parse(compiler, tok)
do
local info, unary_op, leaf = {}, ops_unary[tok:get_token()]
if unary_op then
tok:next()
end
if tok:is("(") then
leaf = {
value = "(" .. compiler:parse_value(tok, info) .. ")",
type = info.type,
bracket = true,
raw = info.raw
}
else
leaf = {
value = compiler:parse_value(tok, info),
type = info.type,
bracket = false,
raw = info.raw
}
end
if unary_op then
self.current = {
op = unary_op,
l = nil,
r = leaf
}
self.nodes = 2
else
self.current = leaf
self.nodes = 1
end
end
while tok:is_valid() do
local token, cond, cond_type = tok:get_token(), nil, nil
local op, op_unary, info = ops[token], nil, {}
if op then -- binary or ternary operator
if op.parse then
cond, cond_type = op.parse(compiler, tok)
if cond_type then
cond = {
value = cond,
type = cond_type
}
end
else
tok:next()
end
if ops_unary[tok:get_token()] then -- has unary operator ('-' or 'not')
op_unary = ops_unary[tok:get_token()]
tok:next()
end
local leaf
if tok:is("(") then
leaf = {
value = "(" .. compiler:parse_value(tok, info) .. ")",
type = info.type,
bracket = true,
raw = info.raw
}
else
leaf = {
value = compiler:parse_value(tok, info),
type = info.type,
bracket = false,
raw = info.raw
}
end
self.nodes = self.nodes + 1
if self.current.value then -- first element of the tree
self:insert(op, leaf, cond)
elseif self.current.op.order <= op.order then
while self.current.p and self.current.op.order < op.order do
self:up()
end
self:insert(op, leaf, cond)
else -- self.current.op.order > op.order
while self.current.r.op and self.current.op.order > op.order do -- just in case
self:down()
end
self:fork(op, leaf, cond)
end
if op_unary then
self:fork(op_unary, nil)
end
elseif ops_unary[token] then -- some unary operators are specific (e.g. 'is')
op_unary = ops_unary[token]
if op_unary.parse then
cond, cond_type = op_unary.parse(compiler, tok)
if cond then
cond = {
value = cond,
type = cond_type or "expr"
}
end
end
if self.current.value then -- first element of the tree
self:fork(op_unary, nil, cond)
elseif self.current.op.order <= op_unary.order then
while self.current.p and self.current.op.order < op_unary.order do
self:up()
end
self:fork(op_unary, nil, cond)
else -- self.current.op.order > op_unary.order
while self.current.r.op and self.current.op.order > op_unary.order do -- just in case
self:down()
end
self:fork(op_unary, nil, cond)
end
self.nodes = self.nodes + 1
else
break
end
end
return self
end
--- Insert new node in the current branch
--- @param op aspect.ast.op
--- @param r aspect.ast.leaf
function ast:insert(op, r, cond)
--print("INSERT " .. op.token)
local parent = self.current.p
self.current = {
p = parent,
op = op,
l = self.current,
r = r,
c = cond
}
if cond and cond.op then
cond.p = self.current
end
if parent then
parent.r = self.current
end
if self.current.l.op then
self.current.l.p = self.current
end
end
--- Insert new node and create new branch
--- @param op aspect.ast.op
--- @param r aspect.ast.leaf|nil if nil - unary operator, just create new branch with current leaf
function ast:fork(op, r, cond)
local current_r = self.current.r
if op.type == "binary" and r then
self.current.r = {
p = self.current,
op = op,
l = current_r,
r = r,
c = cond
}
if self.current.r.l.op then
self.current.r.l.p = self.current.r
end
self.current = self.current.r
elseif op.type == "unary" and not r then
if cond then -- unary with cond - test operator
self.current = {
p = self.current.p,
op = op,
l = nil,
r = self.current,
c = cond
}
if self.current.r.op then
self.current.r.p = self.current
end
else
self.current.r = {
p = self.current,
op = op,
l = nil,
r = current_r,
c = cond
}
if current_r.op then
current_r.p = self.current.r
end
end
else
assert(false, "right is nil but operator is not unary")
end
end
--- Move pointer to up of tree (by nodes)
function ast:up()
if self.current.p then
self.current = self.current.p
end
end
--- Move pointer to down of tree (by nodes)
function ast:down()
if self.current.r and not self.current.value then
self.current = self.current.r
end
end
--- @return aspect.ast.node|aspect.ast.leaf
function ast:get_root()
local node = self.current
while node.p do
node = node.p
end
return node
end
--- @param node aspect.ast.node|aspect.ast.leaf
--- @param indent string
local function dump_visit(node, indent)
local out = {}
if node.op then -- if aspect.ast.node
insert(out, "\n" .. indent .. "OP " .. node.op.token .. " " .. (node.op.delimiter or "") .. " (order " .. node.op.order .. " )")
if node.c then
insert(out, indent .. "c: " .. dump_visit(node.c, indent .. " "))
end
if node.l then
insert(out, indent .. "l: " .. dump_visit(node.l, indent .. " "))
end
if node.r then
insert(out, indent .. "r: " .. dump_visit(node.r, indent .. " "))
end
elseif type(node.value) == "table" then
insert(out, node.type .. "(" .. utils.dump(node.value) .. ")")
else
insert(out, node.type .. "(" .. node.value .. ")")
end
return concat(out, "\n")
end
--- @return string
function ast:dump()
return dump_visit(self:get_root(), "")
end
--- @param node aspect.ast.node
--- @return aspect.ast.leaf
local function pack_node(node)
if node.op then -- if aspect.ast.node
local left, right, cond = node.l, node.r, node.c
if left then
if left.op then
left = pack_node(left)
end
left = cast(left.value, left.type, node.op.l)
end
if right then
if right.op then
right = pack_node(right)
end
right = cast(right.value, right.type, node.op.r)
end
if cond then
if cond.op then
cond = pack_node(cond)
end
cond = cast(cond.value, cond.type, node.op.c)
end
local v = node.op.pack(left, right, cond)
if node.op.brackets then
v = "(" .. v .. ")"
end
return {
value = v,
type = node.op.out,
brackets = node.op.brackets,
}
else -- is leaf
return node
end
end
--- @param node aspect.ast.node
--- @param callback function
local function visit(node, callback)
if node.op then -- if aspect.ast.node
local left, right, cond = node.l, node.r, node.c
if left and left.op then
left = visit(left, callback)
end
if right and right.op then
right = visit(right, callback)
end
if cond and cond.op then
cond = visit(cond, callback)
end
return {
value = callback(node.op, left, right, cond),
type = node.op.out,
}
else -- is leaf
return node
end
end
--- @param callback nil|fun(left:any, right:any, cond:any)
function ast:pack(callback)
if callback then
return visit(self:get_root(), callback)
elseif self.nodes == 1 then --
return self.current
else
return pack_node(self:get_root())
end
end
return ast