mirror of
https://github.com/Reuh/anselme.git
synced 2025-10-28 00:59:31 +00:00
Rewrite
This commit is contained in:
parent
7a5a05ff34
commit
b233d7fa1e
138 changed files with 4369 additions and 1611 deletions
153
parser/common.lua
Normal file
153
parser/common.lua
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
local expression
|
||||
|
||||
local escapeCache = {}
|
||||
|
||||
local common
|
||||
common = {
|
||||
--- valid identifier pattern
|
||||
identifier_pattern = "[^%%%/%*%+%-%(%)%!%&%|%=%$%?%>%<%:%{%}%[%]%,]+",
|
||||
--- escape a string to be used as an exact match pattern
|
||||
escape = function(str)
|
||||
if not escapeCache[str] then
|
||||
escapeCache[str] = str:gsub("[^%w]", "%%%1")
|
||||
end
|
||||
return escapeCache[str]
|
||||
end,
|
||||
--- trim a string
|
||||
trim = function(str)
|
||||
return str:match("^%s*(.-)%s*$")
|
||||
end,
|
||||
--- split a string separated by .
|
||||
split = function(str)
|
||||
local address = {}
|
||||
for name in str:gmatch("[^%.]+") do
|
||||
table.insert(address, name)
|
||||
end
|
||||
return address
|
||||
end,
|
||||
--- find a variable/function in a list, going up through the namespace hierarchy
|
||||
find = function(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)
|
||||
if list[fqm] then
|
||||
return list[fqm], fqm
|
||||
end
|
||||
end
|
||||
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
|
||||
format_identifier = function(identifier, state)
|
||||
local r = identifier:gsub("[^%.]+", function(str)
|
||||
str = common.trim(str)
|
||||
return state.aliases[str] or str
|
||||
end)
|
||||
return r
|
||||
end,
|
||||
--- flatten a nested list expression into a list of expressions
|
||||
flatten_list = function(list, t)
|
||||
t = t or {}
|
||||
if list.type == "list" then
|
||||
table.insert(t, list.left)
|
||||
common.flatten_list(list.right, t)
|
||||
else
|
||||
table.insert(t, list)
|
||||
end
|
||||
return t
|
||||
end,
|
||||
-- * list of strings and expressions
|
||||
-- * nil, err: in case of error
|
||||
parse_text = function(text, state, namespace)
|
||||
local l = {}
|
||||
while text:match("[^%{]+") do
|
||||
local t, e = text:match("^([^%{]*)(.-)$")
|
||||
-- text
|
||||
if t ~= "" then table.insert(l, t) end
|
||||
-- expr
|
||||
if e:match("^{") then
|
||||
local exp, rem = expression(e:gsub("^{", ""), state, namespace)
|
||||
if not exp then return nil, rem end
|
||||
if not rem:match("^%s*}") then return nil, ("expected closing } at end of expression before %q"):format(rem) end
|
||||
table.insert(l, exp)
|
||||
text = rem:match("^%s*}(.*)$")
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
return l
|
||||
end,
|
||||
-- find compatible function variant
|
||||
-- * variant: if success
|
||||
-- * nil, err: if error
|
||||
find_function_variant = function(fqm, state, arg, explicit_call)
|
||||
local err = ("function %q variant not found"):format(fqm)
|
||||
local func = state.functions[fqm] or {}
|
||||
local args = arg and common.flatten_list(arg) or {}
|
||||
for _, variant in ipairs(func) do
|
||||
local ok = true
|
||||
local return_type = variant.return_type
|
||||
if variant.arity then
|
||||
local min, max
|
||||
if type(variant.arity) == "table" then
|
||||
min, max = variant.arity[1], variant.arity[2]
|
||||
else
|
||||
min, max = variant.arity, variant.arity
|
||||
end
|
||||
if #args < min or #args > max then
|
||||
if min == max then
|
||||
err = ("function %q expected %s arguments but received %s"):format(fqm, min, #args)
|
||||
else
|
||||
err = ("function %q expected between %s and %s arguments but received %s"):format(fqm, min, max, #args)
|
||||
end
|
||||
ok = false
|
||||
end
|
||||
end
|
||||
if ok and variant.check then
|
||||
local s, e = variant.check(state, args)
|
||||
if not s then
|
||||
err = e or ("function %q variant failed to check arguments"):format(fqm)
|
||||
ok = false
|
||||
end
|
||||
return_type = s == true and return_type or s
|
||||
end
|
||||
if ok and variant.types then
|
||||
for j, t in pairs(variant.types) do
|
||||
if args[j] and args[j].return_type and args[j].return_type ~= t then
|
||||
err = ("function %q expected a %s as argument %s but received a %s"):format(fqm, t, j, args[j].return_type)
|
||||
ok = false
|
||||
end
|
||||
end
|
||||
end
|
||||
if ok then
|
||||
if variant.rewrite then
|
||||
local r, e = variant.rewrite(fqm, state, arg, explicit_call)
|
||||
if not r then
|
||||
err = e
|
||||
ok = false
|
||||
end
|
||||
if ok then
|
||||
return r
|
||||
end
|
||||
else
|
||||
return {
|
||||
type = "function",
|
||||
return_type = return_type,
|
||||
name = fqm,
|
||||
explicit_call = explicit_call,
|
||||
variant = variant,
|
||||
argument = arg
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil, err
|
||||
end
|
||||
}
|
||||
|
||||
package.loaded[...] = common
|
||||
expression = require((...):gsub("common$", "expression"))
|
||||
|
||||
return common
|
||||
247
parser/expression.lua
Normal file
247
parser/expression.lua
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
local identifier_pattern, format_identifier, find, escape, find_function_variant, parse_text
|
||||
|
||||
--- binop priority
|
||||
local binops_prio = {
|
||||
[1] = { ";" },
|
||||
[2] = { ":=", "+=", "-=", "//=", "/=", "*=", "%=", "^=" },
|
||||
[3] = { "," },
|
||||
[4] = { "|", "&" },
|
||||
[5] = { "!=", "=", ">=", "<=", "<", ">" },
|
||||
[6] = { "+", "-" },
|
||||
[7] = { "*", "//", "/", "%" },
|
||||
[8] = {}, -- unary operators
|
||||
[9] = { "^", ":" },
|
||||
[10] = { "." }
|
||||
}
|
||||
-- unop priority
|
||||
local unops_prio = {
|
||||
[1] = {},
|
||||
[2] = {},
|
||||
[3] = {},
|
||||
[4] = {},
|
||||
[5] = {},
|
||||
[6] = {},
|
||||
[7] = {},
|
||||
[8] = { "-", "!" },
|
||||
[9] = {}
|
||||
}
|
||||
|
||||
--- parse an expression
|
||||
-- return expr, remaining if success
|
||||
-- returns nil, err if error
|
||||
local function expression(s, state, namespace, currentPriority, operatingOn)
|
||||
s = s:match("^%s*(.*)$")
|
||||
currentPriority = currentPriority or 0
|
||||
if not operatingOn then
|
||||
-- number
|
||||
if s:match("^%d+%.%d*") or s:match("^%d*%.%d+") or s:match("^%d+") then
|
||||
local d, r = s:match("^(%d*%.%d*)(.*)$")
|
||||
if not d then
|
||||
d, r = s:match("^(%d+)(.*)$")
|
||||
end
|
||||
return expression(r, state, namespace, currentPriority, {
|
||||
type = "number",
|
||||
return_type = "number",
|
||||
value = tonumber(d)
|
||||
})
|
||||
-- string
|
||||
elseif s:match("^%\"[^\"]*%\"") then
|
||||
local d, r = s:match("^%\"([^\"]*)%\"(.*)$")
|
||||
while d:match("\\$") and not d:match("\\\\$") do
|
||||
local nd, nr = r:match("([^\"]*)%\"(.*)$")
|
||||
if not nd then return nil, ("unfinished string near %q"):format(r) end
|
||||
d, r = d:sub(1, -2) .. "\"" .. nd, nr
|
||||
end
|
||||
local l, e = parse_text(tostring(d), state, namespace)
|
||||
if not l then return l, e end
|
||||
return expression(r, state, namespace, currentPriority, {
|
||||
type = "string",
|
||||
return_type = "string",
|
||||
value = l
|
||||
})
|
||||
-- paranthesis
|
||||
elseif s:match("^%b()") then
|
||||
local content, r = s:match("^(%b())(.*)$")
|
||||
content = content:gsub("^%(", ""):gsub("%)$", "")
|
||||
local exp, r_paren = expression(content, state, namespace)
|
||||
if not exp then return nil, "invalid expression inside parentheses: "..r_paren end
|
||||
if r_paren:match("[^%s]") then return nil, ("unexpected %q at end of parenthesis expression"):format(r_paren) end
|
||||
return expression(r, state, namespace, currentPriority, {
|
||||
type = "parentheses",
|
||||
return_type = exp.return_type,
|
||||
expression = exp
|
||||
})
|
||||
-- list parenthesis
|
||||
elseif s:match("^%b[]") then
|
||||
local content, r = s:match("^(%b[])(.*)$")
|
||||
content = content:gsub("^%[", ""):gsub("%]$", "")
|
||||
local exp
|
||||
if content:match("[^%s]") then
|
||||
local r_paren
|
||||
exp, r_paren = expression(content, state, namespace)
|
||||
if not exp then return nil, "invalid expression inside list parentheses: "..r_paren end
|
||||
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, currentPriority, {
|
||||
type = "list_parentheses",
|
||||
return_type = "list",
|
||||
expression = exp
|
||||
})
|
||||
-- identifier
|
||||
elseif s:match("^"..identifier_pattern) then
|
||||
local name, r = s:match("^("..identifier_pattern..")(.-)$")
|
||||
name = format_identifier(name, state)
|
||||
-- functions
|
||||
local funcs, ffqm = find(state.functions, namespace, name)
|
||||
if funcs then
|
||||
local args, explicit_call
|
||||
if r:match("^%b()") then
|
||||
explicit_call = true
|
||||
local content, rem = r:match("^(%b())(.*)$")
|
||||
content = content:gsub("^%(", ""):gsub("%)$", "")
|
||||
r = rem
|
||||
-- get arguments
|
||||
if content:match("[^%s]") then
|
||||
local err
|
||||
args, err = expression(content, state, namespace)
|
||||
if not args then return args, err end
|
||||
end
|
||||
end
|
||||
-- find compatible variant
|
||||
local variant, err = find_function_variant(ffqm, state, args, explicit_call)
|
||||
if not variant then return variant, err end
|
||||
return expression(r, state, namespace, currentPriority, variant)
|
||||
end
|
||||
-- variables
|
||||
local var, vfqm = find(state.variables, namespace, name)
|
||||
if var then
|
||||
return expression(r, state, namespace, currentPriority, {
|
||||
type = "variable",
|
||||
return_type = var.type ~= "undefined argument" and var.type or nil,
|
||||
name = vfqm
|
||||
})
|
||||
end
|
||||
-- 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)
|
||||
if svar then
|
||||
return expression(suffix..r, state, namespace, currentPriority, {
|
||||
type = "variable",
|
||||
return_type = svar.type ~= "undefined argument" and svar.type or nil,
|
||||
name = svfqm
|
||||
})
|
||||
end
|
||||
end
|
||||
return nil, ("unknown identifier %q"):format(name)
|
||||
end
|
||||
-- unops
|
||||
for prio, oplist in ipairs(unops_prio) do
|
||||
for _, op in ipairs(oplist) do
|
||||
local escaped = escape(op)
|
||||
if s:match("^"..escaped) then
|
||||
local right, r = expression(s:match("^"..escaped.."(.*)$"), state, namespace, prio)
|
||||
if not right then return nil, ("invalid expression after unop %q: %s"):format(op, r) end
|
||||
-- find variant
|
||||
local variant, err = find_function_variant(op, state, right, true)
|
||||
if not variant then return variant, err end
|
||||
return expression(r, state, namespace, currentPriority, variant)
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil, ("no valid expression before %q"):format(s)
|
||||
else
|
||||
-- binop
|
||||
for prio, oplist in ipairs(binops_prio) do
|
||||
if prio >= currentPriority then
|
||||
for _, op in ipairs(oplist) do
|
||||
local escaped = escape(op)
|
||||
if s:match("^"..escaped) then
|
||||
local sright = s:match("^"..escaped.."(.*)$")
|
||||
-- suffix call
|
||||
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)
|
||||
if funcs then
|
||||
local args, explicit_call
|
||||
if r:match("^%b()") then
|
||||
explicit_call = true
|
||||
local content, rem = r:match("^(%b())(.*)$")
|
||||
content = content:gsub("^%(", ""):gsub("%)$", "")
|
||||
r = rem
|
||||
-- get arguments
|
||||
if content:match("[^%s]") then
|
||||
local err
|
||||
args, err = expression(content, state, namespace)
|
||||
if not args then return args, err end
|
||||
end
|
||||
end
|
||||
-- add first argument
|
||||
if not args then
|
||||
args = operatingOn
|
||||
else
|
||||
args = {
|
||||
type = "list",
|
||||
return_type = "list",
|
||||
left = operatingOn,
|
||||
right = args
|
||||
}
|
||||
end
|
||||
-- find compatible variant
|
||||
local variant, err = find_function_variant(ffqm, state, args, explicit_call)
|
||||
if not variant then return variant, err end
|
||||
return expression(r, state, namespace, currentPriority, variant)
|
||||
end
|
||||
else
|
||||
local right, r = expression(sright, state, namespace, prio)
|
||||
if not right then return nil, ("invalid expression after binop %q: %s"):format(op, r) end
|
||||
-- list constructor
|
||||
if op == "," then
|
||||
return expression(r, state, namespace, currentPriority, {
|
||||
type = "list",
|
||||
return_type = "list",
|
||||
left = operatingOn,
|
||||
right = right
|
||||
})
|
||||
-- normal binop
|
||||
else
|
||||
-- find variant
|
||||
local args = {
|
||||
type = "list",
|
||||
return_type = "list",
|
||||
left = operatingOn,
|
||||
-- wrap in parentheses to avoid appending to argument list if right is a list
|
||||
right = { type = "parentheses", return_type = right.return_type, expression = right }
|
||||
}
|
||||
local variant, err = find_function_variant(op, state, args, true)
|
||||
if not variant then return variant, err end
|
||||
return expression(r, state, namespace, currentPriority, variant)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-- index
|
||||
if s:match("^%b()") then
|
||||
local content, r = s:match("^(%b())(.*)$")
|
||||
-- get arguments (parentheses are kept)
|
||||
local right, r_paren = expression(content, state, namespace)
|
||||
if not right then return right, r_paren end
|
||||
if r_paren:match("[^%s]") then return nil, ("unexpected %q at end of index expression"):format(r_paren) end
|
||||
local args = { type = "list", left = operatingOn, right = right }
|
||||
local variant, err = find_function_variant("(", state, args, true)
|
||||
if not variant then return variant, err end
|
||||
return expression(r, state, namespace, currentPriority, variant)
|
||||
end
|
||||
-- nothing to operate
|
||||
return operatingOn, s
|
||||
end
|
||||
end
|
||||
|
||||
package.loaded[...] = expression
|
||||
local common = require((...):gsub("expression$", "common"))
|
||||
identifier_pattern, format_identifier, find, escape, find_function_variant, parse_text = common.identifier_pattern, common.format_identifier, common.find, common.escape, common.find_function_variant, common.parse_text
|
||||
|
||||
return expression
|
||||
70
parser/postparser.lua
Normal file
70
parser/postparser.lua
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
local expression
|
||||
local parse_text
|
||||
|
||||
-- * true: if success
|
||||
-- * nil, error: in case of error
|
||||
local function parse(state)
|
||||
for _, l in ipairs(state.queued_lines) do
|
||||
local line, namespace = l.line, l.namespace
|
||||
-- decorators
|
||||
if line.condition then
|
||||
if line.condition:match("[^%s]") then
|
||||
local exp, rem = expression(line.condition, state, namespace)
|
||||
if not exp then return nil, ("%s; at line %s"):format(rem, line.line) end
|
||||
if rem:match("[^%s]") then return nil, ("expected end of expression before %q in condition decorator; at line %s"):format(rem, line.line) end
|
||||
line.condition = exp
|
||||
else
|
||||
line.condition = nil
|
||||
end
|
||||
end
|
||||
if line.tag then
|
||||
if line.tag:match("[^%s]") then
|
||||
local exp, rem = expression(line.tag, state, namespace)
|
||||
if not exp then return nil, ("%s; at line %s"):format(rem, line.line) end
|
||||
if rem:match("[^%s]") then return nil, ("expected end of expression before %q in condition decorator; at line %s"):format(rem, line.line) end
|
||||
line.tag = exp
|
||||
else
|
||||
line.tag = nil
|
||||
end
|
||||
end
|
||||
-- expressions
|
||||
if line.expression then
|
||||
if line.expression:match("[^%s]") then
|
||||
local exp, rem = expression(line.expression, state, namespace)
|
||||
if not exp then return nil, ("%s; at line %s"):format(rem, line.line) end
|
||||
if rem:match("[^%s]") then return nil, ("expected end of expression before %q; at line %s"):format(rem, line.line) end
|
||||
line.expression = exp
|
||||
else
|
||||
line.expression = nil
|
||||
end
|
||||
-- function return type information
|
||||
if line.type == "return" then
|
||||
local variant = line.parent_function.variant
|
||||
local return_type = line.expression.return_type
|
||||
if return_type then
|
||||
if not variant.return_type then
|
||||
variant.return_type = return_type
|
||||
elseif variant.return_type ~= return_type then
|
||||
return nil, ("trying to return a %s in a function that returns a %s; at line %s"):format(return_type, variant.return_type, line.line)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-- text
|
||||
if line.text then
|
||||
local txt, err = parse_text(line.text, state, namespace)
|
||||
if err then return nil, ("%s; at line %s"):format(err, line.line) end
|
||||
line.text = txt
|
||||
end
|
||||
end
|
||||
state.queued_lines = {}
|
||||
return true
|
||||
end
|
||||
|
||||
package.loaded[...] = parse
|
||||
expression = require((...):gsub("postparser$", "expression"))
|
||||
local common = require((...):gsub("postparser$", "common"))
|
||||
parse_text = common.parse_text
|
||||
|
||||
--- postparse shit: parse expressions and do variable existence and type checking
|
||||
return parse
|
||||
332
parser/preparser.lua
Normal file
332
parser/preparser.lua
Normal file
|
|
@ -0,0 +1,332 @@
|
|||
local expression
|
||||
local format_identifier, identifier_pattern
|
||||
local eval
|
||||
|
||||
-- * ast: if success
|
||||
-- * nil, error: in case of error
|
||||
local function parse_line(line, state, namespace)
|
||||
local l = line.content
|
||||
local r = {
|
||||
line = line.line
|
||||
}
|
||||
-- comment
|
||||
if l:match("^%(") then
|
||||
r.type = "comment"
|
||||
r.remove_from_block_ast = true
|
||||
return r
|
||||
end
|
||||
-- decorators
|
||||
while l:match("^..+[~#]") or l:match("^..+§") do
|
||||
-- condition
|
||||
if l:match("^..+%~.-$") then
|
||||
local expr
|
||||
l, expr = l:match("^(.-)%s*%~(.-)$")
|
||||
r.condition = expr
|
||||
-- paragraph
|
||||
elseif l:match("^..+§.-$") then
|
||||
local name
|
||||
l, name = l:match("^(.-)%s*§(.-)$")
|
||||
local fqm = ("%s%s"):format(namespace, format_identifier(name, state))
|
||||
namespace = fqm.."."
|
||||
r.paragraph = true
|
||||
r.parent_function = true
|
||||
r.namespace = fqm.."."
|
||||
r.name = fqm
|
||||
if not state.functions[fqm] then
|
||||
state.functions[fqm] = {
|
||||
{
|
||||
arity = 0,
|
||||
value = r
|
||||
}
|
||||
}
|
||||
if not state.variables[fqm..".👁️"] then
|
||||
state.variables[fqm..".👁️"] = {
|
||||
type = "number",
|
||||
value = 0
|
||||
}
|
||||
end
|
||||
else
|
||||
table.insert(state.functions[fqm], {
|
||||
arity = 0,
|
||||
value = r
|
||||
})
|
||||
end
|
||||
-- tag
|
||||
elseif l:match("^..+%#.-$") then
|
||||
local expr
|
||||
l, expr = l:match("^(.-)%s*%#(.-)$")
|
||||
r.tag = expr
|
||||
end
|
||||
end
|
||||
-- else-condition & condition
|
||||
if l:match("^~~?") then
|
||||
r.type = l:match("^~~") and "else-condition" or "condition"
|
||||
r.child = true
|
||||
local expr = l:match("^~~?(.*)$")
|
||||
if expr:match("[^%s]") then
|
||||
r.expression = expr
|
||||
else
|
||||
r.expression = "1"
|
||||
end
|
||||
-- choice
|
||||
elseif l:match("^>") then
|
||||
r.type = "choice"
|
||||
r.push_event = "choice"
|
||||
r.child = true
|
||||
r.text = l:match("^>%s*(.-)$")
|
||||
-- function & paragraph
|
||||
elseif l:match("^%$") or l:match("^§") then -- § is a 2-bytes caracter, DO NOT USE LUA PATTERN OPERATORS as they operate on single bytes
|
||||
r.type = l:match("^%$") and "function" or "paragraph"
|
||||
r.child = true
|
||||
local fqm = ("%s%s"):format(namespace, format_identifier(l:match("^%$(.*)$") or l:match("^§(.*)$"), state))
|
||||
-- get params
|
||||
r.params = {}
|
||||
if r.type == "function" and fqm:match("%b()$") then
|
||||
local content
|
||||
fqm, content = fqm:match("^(.-)(%b())$")
|
||||
content = content:gsub("^%(", ""):gsub("%)$", "")
|
||||
for param in content:gmatch("[^%,]+") do
|
||||
table.insert(r.params, format_identifier(("%s.%s"):format(fqm, param), state))
|
||||
end
|
||||
end
|
||||
local arity, vararg = #r.params, nil
|
||||
if arity > 0 and r.params[arity]:match("%.%.%.$") then -- varargs
|
||||
r.params[arity] = r.params[arity]:match("^(.*)%.%.%.$")
|
||||
vararg = arity
|
||||
arity = { arity-1, math.huge }
|
||||
end
|
||||
-- store parent function and run paragraph when line is read
|
||||
if r.type == "paragraph" then
|
||||
r.paragraph = true
|
||||
r.parent_function = true
|
||||
end
|
||||
-- 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.."."
|
||||
r.name = fqm
|
||||
if state.variables[fqm] then return nil, ("trying to define %s %s, but a variable with the same name exists; at line %s"):format(r.type, fqm, line.line) end
|
||||
r.variant = {
|
||||
arity = arity,
|
||||
types = {},
|
||||
vararg = vararg,
|
||||
value = r
|
||||
}
|
||||
if not state.functions[fqm] then
|
||||
state.functions[fqm] = { r.variant }
|
||||
if not state.variables[fqm..".👁️"] then
|
||||
state.variables[fqm..".👁️"] = {
|
||||
type = "number",
|
||||
value = 0
|
||||
}
|
||||
end
|
||||
else
|
||||
-- check for arity conflict
|
||||
for _, variant in ipairs(state.functions[fqm]) do
|
||||
local vmin, vmax = 0, math.huge
|
||||
if type(variant.arity) == "table" then
|
||||
vmin, vmax = variant.arity[1], variant.arity[2]
|
||||
elseif variant.arity then
|
||||
vmin, vmax = variant.arity, variant.arity
|
||||
end
|
||||
local min, max = 0, math.huge
|
||||
if type(r.variant.arity) == "table" then
|
||||
min, max = r.variant.arity[1], r.variant.arity[2]
|
||||
elseif r.variant.arity then
|
||||
min, max = variant.arity, r.variant.arity
|
||||
end
|
||||
if min == vmin and max == vmax then
|
||||
return nil, ("trying to define %s %s with arity [%s;%s], but another function with the arity exist; at line %s"):format(r.type, fqm, min, max, line.line)
|
||||
end
|
||||
end
|
||||
-- add
|
||||
table.insert(state.functions[fqm], r.variant)
|
||||
end
|
||||
-- set type check information
|
||||
for i, param in ipairs(r.params) do
|
||||
if not state.variables[param] then
|
||||
state.variables[param] = {
|
||||
type = "undefined argument",
|
||||
value = { r.variant, i }
|
||||
}
|
||||
elseif state.variables[param].type ~= "undefined argument" then
|
||||
r.variant.types[i] = state.variables[param].type
|
||||
end
|
||||
end
|
||||
-- definition
|
||||
elseif l:match("^:") then
|
||||
r.type = "definition"
|
||||
r.remove_from_block_ast = true
|
||||
local exp, rem = expression(l:match("^:(.*)$"), state, namespace) -- expression parsing is done directly to get type information
|
||||
if not exp then return nil, ("%s; at line %s"):format(rem, line.line) end
|
||||
local fqm = ("%s%s"):format(namespace, format_identifier(rem, state))
|
||||
if state.functions[fqm] then return nil, ("trying to define variable %s, but a function with the same name exists; at line %s"):format(fqm, line.line) end
|
||||
if not state.variables[fqm] or state.variables[fqm].type == "undefined argument" then
|
||||
local v, e = eval(state, exp)
|
||||
if not v then return v, e end
|
||||
-- update function typecheck information
|
||||
if state.variables[fqm] and state.variables[fqm].type == "undefined argument" then
|
||||
local und = state.variables[fqm].value
|
||||
und[1].types[und[2]] = v.type
|
||||
end
|
||||
state.variables[fqm] = v
|
||||
elseif state.variables[fqm].type ~= exp.type then
|
||||
return nil, ("trying to define variable %s of type %s but a it is already defined with type %s; at line %s"):format(fqm, exp.type, state.variables[fqm].type, line.line)
|
||||
end
|
||||
-- tag
|
||||
elseif l:match("^%#") then
|
||||
r.type = "tag"
|
||||
r.child = true
|
||||
r.expression = l:match("^%#(.*)$")
|
||||
-- return
|
||||
elseif l:match("^%@") then
|
||||
r.type = "return"
|
||||
r.parent_function = true
|
||||
r.expression = l:match("^%@(.*)$")
|
||||
-- text
|
||||
elseif l:match("[^%s]") then
|
||||
r.type = "text"
|
||||
r.push_event = "text"
|
||||
r.text = l
|
||||
-- flush events
|
||||
else
|
||||
r.type = "flush_events"
|
||||
end
|
||||
if not r.type then return nil, ("unknown line %s type"):format(line.line) end
|
||||
return r
|
||||
end
|
||||
|
||||
-- * block: in case of success
|
||||
-- * nil, err: in case of error
|
||||
local function parse_block(indented, state, namespace, parent_function, last_event)
|
||||
local block = { type = "block" }
|
||||
local lastLine -- last line AST
|
||||
for i, l in ipairs(indented) do
|
||||
-- parsable line
|
||||
if l.content then
|
||||
local ast, err = parse_line(l, state, namespace)
|
||||
if err then return nil, err end
|
||||
lastLine = ast
|
||||
-- store parent function
|
||||
if ast.parent_function then ast.parent_function = parent_function end
|
||||
-- add to block AST
|
||||
if not ast.remove_from_block_ast then
|
||||
ast.parent_block = block
|
||||
-- insert flush on event type change
|
||||
if ast.type == "flush" then last_event = nil end
|
||||
if ast.push_event then
|
||||
if last_event and ast.push_event ~= last_event then
|
||||
table.insert(block, { line = l.line, type = "flush_events" })
|
||||
end
|
||||
last_event = ast.push_event
|
||||
end
|
||||
-- add ast node
|
||||
ast.parent_position = #block+1
|
||||
if ast.replace_with then
|
||||
if indented[i+1].content then
|
||||
table.insert(indented, i+1, { content = ast.replace_with, line = l.line })
|
||||
else
|
||||
table.insert(indented, i+2, { content = ast.replace_with, line = l.line }) -- if line has children
|
||||
end
|
||||
else
|
||||
table.insert(block, ast)
|
||||
end
|
||||
end
|
||||
-- add child
|
||||
if ast.child then ast.child = { type = "block", parent_line = ast } end
|
||||
-- queue in expression evalution
|
||||
table.insert(state.queued_lines, { namespace = ast.namespace or namespace, line = ast })
|
||||
-- indented (ignore block comments)
|
||||
elseif lastLine.type ~= "comment" then
|
||||
if not lastLine.child then
|
||||
return nil, ("line %s (%s) can't have children"):format(lastLine.line, lastLine.type)
|
||||
else
|
||||
local r, e = parse_block(l, state, lastLine.namespace or namespace, lastLine.type == "function" and lastLine or parent_function, last_event)
|
||||
if not r then return r, e end
|
||||
r.parent_line = lastLine
|
||||
lastLine.child = r
|
||||
end
|
||||
end
|
||||
end
|
||||
return block
|
||||
end
|
||||
|
||||
--- returns the nested list of lines {content="", line=1}, grouped by indentation
|
||||
-- multiple empty lines are merged
|
||||
-- * list, last line
|
||||
local function parse_indent(lines, i, indentLevel, insert_empty_line)
|
||||
i = i or 1
|
||||
indentLevel = indentLevel or 0
|
||||
local indented = {}
|
||||
while i <= #lines do
|
||||
if lines[i]:match("[^%s]") then
|
||||
local indent, line = lines[i]:match("^(%s*)(.*)$")
|
||||
if #indent == indentLevel then
|
||||
if insert_empty_line then
|
||||
table.insert(indented, { content = "", line = insert_empty_line })
|
||||
insert_empty_line = false
|
||||
end
|
||||
table.insert(indented, { content = line, line = i })
|
||||
elseif #indent > indentLevel then
|
||||
local t
|
||||
t, i = parse_indent(lines, i, #indent, insert_empty_line)
|
||||
table.insert(indented, t)
|
||||
else
|
||||
return indented, i-1
|
||||
end
|
||||
elseif not insert_empty_line then
|
||||
insert_empty_line = i
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
return indented, i-1
|
||||
end
|
||||
|
||||
--- return the list of raw lines of s
|
||||
local function parse_lines(s)
|
||||
local lines = {}
|
||||
for l in (s.."\n"):gmatch("(.-)\n") do
|
||||
table.insert(lines, l)
|
||||
end
|
||||
return lines
|
||||
end
|
||||
|
||||
--- preparse shit: create AST structure, define variables and functions, but don't parse expression or perform any type checking
|
||||
-- (wait for other files to be parsed before doing this with postparse)
|
||||
-- * state: in case of success
|
||||
-- * nil, err: in case of error
|
||||
local function parse(state, s, name)
|
||||
-- parse lines
|
||||
local lines = parse_lines(s)
|
||||
local indented = parse_indent(lines)
|
||||
-- wrap in named function if neccessary
|
||||
if name ~= "" then
|
||||
if not name:match("^"..identifier_pattern.."$") then
|
||||
return nil, ("invalid function name %q"):format(name)
|
||||
end
|
||||
indented = {
|
||||
{ content = "$ "..name, line = 0 },
|
||||
indented
|
||||
}
|
||||
end
|
||||
-- parse
|
||||
local root, err = parse_block(indented, state, "")
|
||||
if not root then return nil, err end
|
||||
return state
|
||||
end
|
||||
|
||||
package.loaded[...] = parse
|
||||
expression = require((...):gsub("preparser$", "expression"))
|
||||
local common = require((...):gsub("preparser$", "common"))
|
||||
format_identifier, identifier_pattern = common.format_identifier, common.identifier_pattern
|
||||
eval = require((...):gsub("parser%.preparser$", "interpreter.expression"))
|
||||
|
||||
return parse
|
||||
Loading…
Add table
Add a link
Reference in a new issue