1
0
Fork 0
mirror of https://github.com/Reuh/anselme.git synced 2025-10-28 00:59:31 +00:00

Anselme v2.0.0-alpha rewrite

Woke up and felt like changing a couple things. It's actually been worked on for a while, little at a time...

The goal was to make the language and implementation much simpler. Well I don't know if it really ended up being simpler but it sure is more robust.

Main changes:
* proper first class functions and closures supports! proper scoping rules! no more namespace shenanigans!
* everything is an expression, no more statements! make the implementation both simpler and more complex, but it's much more consistent now! the syntax has massively changed as a result though.
* much more organized and easy to modify codebase: one file for each AST node, no more random fields or behavior set by some random node exceptionally, everything should now follow the same API defined in ast.abstract.Node

Every foundational feature should be implemented right now. The vast majority of things that were possible in v2 are possible now; some things aren't, but that's usually because v2 is a bit more sane.
The main missing things before a proper release are tests and documentation. There's a few other things that might be implemented later, see the ideas.md file.
This commit is contained in:
Étienne Fildadut 2023-12-17 17:15:16 +01:00
parent 2ff494d108
commit fe351b5ca4
484 changed files with 7099 additions and 18084 deletions

38
parser/Source.lua Normal file
View file

@ -0,0 +1,38 @@
local class = require("class")
local Source
Source = class {
name = "?",
line = -1,
position = -1,
init = function(self, name, line, position)
self.name = name
self.line = line
self.position = position
end,
increment = function(self, n, ...)
self.position = self.position + n
end,
count = function(self, capture, ...)
self:increment(utf8.len(capture))
return capture, ...
end,
consume = function(self, capture, ...)
self:increment(utf8.len(capture))
return ...
end,
clone = function(self)
return Source:new(self.name, self.line, self.position)
end,
set = function(self, other)
self.name, self.line, self.position = other.name, other.line, other.position
end,
__tostring = function(self)
return ("%s:%s:%s"):format(self.name, self.line, self.position)
end
}
return Source

65
parser/code_to_tree.lua Normal file
View file

@ -0,0 +1,65 @@
--- transform raw code string into a nested tree of lines
local Source = require("parser.Source")
local function indented_to_tree(indented)
local tree = {}
local current_parent = tree
local current_level = 0
local last_line_empty = nil
for _, l in ipairs(indented) do
-- indentation of empty line is determined using the next line
-- (consecutive empty lines are merged into one)
if l.content == "" then
last_line_empty = l
else
-- raise indentation
if l.level > current_level then
if #current_parent == 0 then -- can't add children to nil
error(("invalid indentation; at %s"):format(l.source))
end
current_parent = current_parent[#current_parent]
current_level = l.level
-- lower indentation
elseif l.level < current_level then
current_parent = tree
current_level = 0
while current_level < l.level do -- find correct level starting back from the root
current_parent = current_parent[#current_parent]
current_level = current_parent[1].level
end
if current_level ~= l.level then
error(("invalid indentation; at %s"):format(l.source))
end
end
-- add line
if last_line_empty then
last_line_empty.level = current_level
table.insert(current_parent, last_line_empty)
last_line_empty = nil
end
table.insert(current_parent, l)
end
end
return tree
end
local function code_to_indented(code, source_name)
local indented = {}
local i = 1
for line in (code.."\n"):gmatch("(.-)\n") do
local indent, rem = line:match("^(%s*)(.-)$")
local indent_len = utf8.len(indent)
table.insert(indented, { level = indent_len, content = rem, source = Source:new(source_name, i, 1+indent_len) })
i = i + 1
end
return indented
end
return function(code, source_name)
return indented_to_tree(code_to_indented(code, source_name or "?"))
end

View file

@ -1,342 +0,0 @@
local expression
local escapeCache = {}
local common
--- rewrite name to use defined aliases (under namespace only)
-- namespace should not contain aliases
-- returns the final fqm
local replace_aliases = function(aliases, namespace, name)
local name_list = common.split(name)
local prefix = namespace
for i=1, #name_list, 1 do -- search alias for each part of the fqm
local n = ("%s%s%s"):format(prefix, prefix == "" and "" or ".", name_list[i])
if aliases[n] then
prefix = aliases[n]
else
prefix = n
end
end
return prefix
end
local disallowed_set = ("~`^+-=<>/[]*{}|\\_!?,;:()\"@&$#%"):gsub("[^%w]", "%%%1")
common = {
--- valid identifier pattern
identifier_pattern = "%s*[^0-9%s"..disallowed_set.."][^"..disallowed_set.."]*",
-- names allowed for a function that aren't valid identifiers, mainly for overloading operators
special_functions_names = {
-- operators not included here and why:
-- * assignment operators (:=, +=, -=, //=, /=, *=, %=, ^=): handled with its own syntax (function assignment)
-- * list operator (,): is used when calling every functions, sounds like more trouble than it's worth
-- * |, &, ~? and ~ operators: are lazy and don't behave like regular functions
-- * # operator: need to set tag state _before_ evaluating the left arg
-- prefix unop
"-_", "!_",
"&_",
-- binop
"_;_",
"_=_", "_:_",
"_!=_", "_==_", "_>=_", "_<=_", "_<_", "_>_",
"_+_", "_-_",
"_*_", "_//_", "_/_", "_%_",
"_::_",
"_^_",
"_!_",
"_._",
-- suffix unop
"_;",
"_!",
-- special
"()",
"{}"
},
-- escapement code and their value in strings
-- only includes the "special" escape codes, as the generic \. -> . is handled by default in parse_text
-- I don't think there's a point in supporting form feed, carriage return, and other printer and terminal related codes
string_escapes = {
["\\\\"] = "\\",
["\\n"] = "\n",
["\\t"] = "\t",
},
-- list of possible injections and their associated name in vm.state.inject
injections = {
["function start"] = "function_start", ["function end"] = "function_end", ["function return"] = "function_return",
["scoped function start"] = "scoped_function_start", ["scoped function end"] = "scoped_function_end", ["scoped function return"] = "scoped_function_return",
["checkpoint start"] = "checkpoint_start", ["checkpoint end"] = "checkpoint_end",
["class start"] = "class_start", ["class end"] = "class_end"
},
--- 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 by removing whitespace at the start and end
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
-- will apply aliases
-- returns value, fqm in case of success
-- returns nil, err in case of error
find = function(aliases, list, namespace, name)
if namespace ~= "" then
local ns = common.split(namespace:gsub("%.$", ""))
for i=#ns, 1, -1 do
local current_namespace = table.concat(ns, ".", 1, i)
local fqm = replace_aliases(aliases, current_namespace, name)
if list[fqm] then
return list[fqm], fqm
end
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,
--- same as find, but return a list of every encoutered possibility
-- returns a list of fqm
find_all = function(aliases, list, namespace, name)
local l = {}
if namespace ~= "" then
local ns = common.split(namespace:gsub("%.$", ""))
for i=#ns, 1, -1 do
local current_namespace = table.concat(ns, ".", 1, i)
local fqm = replace_aliases(aliases, current_namespace, name)
if list[fqm] then
table.insert(l, fqm)
end
end
end
-- root namespace
name = replace_aliases(aliases, "", name)
if list[name] then
table.insert(l, name)
end
return l
end,
--- transform an identifier into a clean version (trim each part)
format_identifier = function(identifier)
local r = identifier:gsub("[^%.]+", function(str)
return common.trim(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, 1, list.right)
common.flatten_list(list.left, t)
else
table.insert(t, 1, list)
end
return t
end,
-- parse interpolated expressions in a text
-- type sets the type of the returned expression (text is in text field)
-- allow_subtext (bool) to enable or not [subtext] support
-- if allow_binops is given, if one of the caracters of allow_binops appear unescaped in the text, it will interpreter a binary operator expression
-- * returns an expression with given type (string by default) and as a value a list of strings and expressions (text elements)
-- * if allow_binops is given, also returns remaining string (if the right expression stop before the end of the text)
-- * nil, err: in case of error
parse_text = function(text, state, namespace, type, allow_binops, allow_subtext, in_subtext)
local l = {}
local text_exp = { type = type, text = l }
local delimiters = ""
if allow_binops then
delimiters = allow_binops
end
if allow_subtext then
delimiters = delimiters .. "%["
end
if in_subtext then
delimiters = delimiters .. "%]"
end
while text:match(("[^{%s]+"):format(delimiters)) do
local t, r = text:match(("^([^{%s]*)(.-)$"):format(delimiters))
-- text
if t ~= "" then
-- handle \{ and binop escape: skip to next { until it's not escaped
while t:match("\\$") and r:match(("^[{%s]"):format(delimiters)) do
local t2, r2 = r:match(("^([{%s][^{%s]*)(.-)$"):format(delimiters, delimiters))
t = t .. t2 -- don't need to remove \ as it will be stripped with other escapes codes 3 lines later
r = r2
end
-- replace escape codes
local escaped = t:gsub("\\.", function(escape)
return common.string_escapes[escape] or escape:match("^\\(.)$")
end)
table.insert(l, escaped)
end
-- expr
if r:match("^{") then
local exp, rem = expression(r:gsub("^{", ""), state, namespace, "interpolated expression")
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
-- wrap in format() call
local variant, err = common.find_function(state, namespace, "{}", { type = "parentheses", expression = exp }, true)
if not variant then return variant, err end
-- add to text
table.insert(l, variant)
text = rem:match("^%s*}(.*)$")
-- start subtext
elseif allow_subtext and r:match("^%[") then
local exp, rem = common.parse_text(r:gsub("^%[", ""), state, namespace, "text", "#~", allow_subtext, true)
if not exp then return nil, rem end
if not rem:match("^%]") then return nil, ("expected closing ] at end of subtext before %q"):format(rem) end
-- add to text
table.insert(l, exp)
text = rem:match("^%](.*)$")
-- end subtext
elseif in_subtext and r:match("^%]") then
if allow_binops then
return text_exp, r
else
return text_exp
end
-- binop expression at the end of the text
elseif allow_binops and r:match(("^[%s]"):format(allow_binops)) then
local exp, rem = expression(r, state, namespace, "text binop suffix", nil, text_exp)
if not exp then return nil, rem end
return exp, rem
elseif r == "" then
break
else
error(("unexpected %q at end of text or string"):format(r))
end
end
if allow_binops then
return text_exp, ""
else
return text_exp
end
end,
-- find a list of compatible function variants from a fully qualified name
-- this functions does not guarantee that the returned variants are fully compatible with the given arguments and only performs a pre-selection without the ones which definitely aren't
-- * list of compatible variants: if success
-- * nil, err: if error
find_function_variant_from_fqm = function(fqm, state, arg)
local err = ("compatible function %q variant not found"):format(fqm)
local func = state.functions[fqm]
local args = arg and common.flatten_list(arg) or {}
local variants = {}
for _, variant in ipairs(func) do
local ok = true
-- arity check
-- note: because named args can't be predicted in advance (pairs need to be evaluated), this arity check isn't enough to guarantee a compatible arity
-- (e.g., if there's 3 required args but only provide 3 optional arg in a call, will pass)
local min, max = variant.arity[1], variant.arity[2]
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
-- done
if ok then
table.insert(variants, variant)
end
end
if #variants > 0 then
return variants
else
return nil, err
end
end,
--- same as find_function_variant_from_fqm, but will search every function from the current namespace and up using find
-- returns directly a function expression in case of success
-- return nil, err otherwise
find_function = function(state, namespace, name, arg, paren_call, implicit_call)
local l = common.find_all(state.aliases, state.functions, namespace, name)
return common.find_function_from_list(state, namespace, name, l, arg, paren_call, implicit_call)
end,
--- same as find_function, but take a list of already found ffqm instead of searching
find_function_from_list = function(state, namespace, name, names, arg, paren_call, implicit_call)
local variants = {}
local err = ("compatible function %q variant not found"):format(name)
local l = common.find_all(state.aliases, state.functions, namespace, name)
for _, ffqm in ipairs(l) do
local found
found, err = common.find_function_variant_from_fqm(ffqm, state, arg)
if found then
for _, v in ipairs(found) do
table.insert(variants, v)
end
end
end
if #variants > 0 then
return {
type = "function call",
called_name = name, -- name of the called function
paren_call = paren_call, -- was call with parantheses?
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",
expression = arg
}
}
else
return nil, err -- returns last error
end
end,
-- returns the function's signature text
signature = function(fn)
if fn.signature then return fn.signature end
local signature
local function make_param_signature(p)
local sig = p.name
if p.vararg then
sig = sig .. "..."
end
if p.alias then
sig = sig .. ":" .. p.alias
end
if p.type_constraint then
sig = sig .. "::" .. p.type_constraint
end
if p.default then
sig = sig .. "=" .. p.default
end
return sig
end
local arg_sig = {}
for j, p in ipairs(fn.params) do
arg_sig[j] = make_param_signature(p)
end
if fn.assignment then
signature = ("%s(%s) := %s"):format(fn.name, table.concat(arg_sig, ", "), make_param_signature(fn.assignment))
else
signature = ("%s(%s)"):format(fn.name, table.concat(arg_sig, ", "))
end
return signature
end,
-- same as signature, format the signature for displaying to the user and add some debug information
pretty_signature = function(fn)
return ("%s (at %s)"):format(common.signature(fn), fn.source)
end,
}
package.loaded[...] = common
expression = require((...):gsub("common$", "expression"))
return common

View file

@ -1,558 +0,0 @@
local identifier_pattern, format_identifier, find, escape, find_function, parse_text, find_all, split, find_function_from_list, preparse
--- binop priority
local binops_prio = {
[1] = { ";" },
[2] = { ":=", "+=", "-=", "//=", "/=", "*=", "%=", "^=" },
[3] = { "," },
[4] = { "~?", "~", "#" },
[5] = { "=", ":" },
[6] = { "|", "&" },
[7] = { "!=", "==", ">=", "<=", "<", ">" },
[8] = { "+", "-" },
[9] = { "*", "//", "/", "%" },
[10] = { "::" },
[11] = {}, -- unary operators
[12] = { "^" },
[13] = { "!" },
[14] = {},
[15] = { "." }
}
local call_priority = 13 -- note: higher priority operators will have to deal with potential functions expressions
local implicit_call_priority = 12.5 -- just below call priority so explicit calls automatically take precedence
local pair_priority = 5
local implicit_multiply_priority = 9.5 -- just above / so 1/2x gives 1/(2x)
-- unop priority
local prefix_unops_prio = {
[1] = {},
[2] = {},
[3] = { "$" },
[4] = {},
[5] = {},
[6] = {},
[7] = {},
[8] = {},
[9] = {},
[10] = {},
[11] = { "-", "!" },
[12] = {},
[13] = {},
[14] = { "&" },
[15] = {}
}
local suffix_unops_prio = {
[1] = { ";" },
[2] = {},
[3] = {},
[4] = {},
[5] = {},
[6] = {},
[7] = {},
[8] = {},
[9] = {},
[10] = {},
[11] = {},
[12] = {},
[13] = { "!" },
[14] = {},
[15] = {}
}
local function get_text_in_litteral(s, start_pos)
local d, r
-- find end of string
start_pos = start_pos or 2
local i = start_pos
while true do
local skip
skip = s:match("^[^%\\\"]-%b{}()", i) -- skip interpolated expressions
if skip then i = skip end
skip = s:match("^[^%\\\"]-\\.()", i) -- skip escape codes (need to skip every escape code in order to correctly parse \\": the " is not escaped)
if skip then i = skip end
if not skip then -- nothing skipped
local end_pos = s:match("^[^%\"]-\"()", i) -- search final double quote
if end_pos then
d, r = s:sub(start_pos, end_pos-2), s:sub(end_pos)
break
else
return nil, ("expected \" to finish string near %q"):format(s:sub(i))
end
end
end
return d, r
end
local function random_identifier_alpha()
local r = ""
for _=1, 18 do -- that's live 10^30 possibilities, ought to be enough for anyone
if math.random(1, 2) == 1 then
r = r .. string.char(math.random(65, 90))
else
r = r .. string.char(math.random(97, 122))
end
end
return r
end
--- parse an expression
-- return expr, remaining if success
-- returns nil, err if error
local function expression(s, state, namespace, source, current_priority, operating_on)
s = s:match("^%s*(.*)$")
current_priority = current_priority or 0
if not operating_on then
-- number
if 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, source, current_priority, {
type = "number",
value = tonumber(d)
})
-- string
elseif s:match("^%\"") then
local d, r = get_text_in_litteral(s)
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, source, 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, source, current_priority, {
type = "text buffer",
text = v
})
-- paranthesis
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, source)
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
else
exp = { type = "nil", value = nil }
end
return expression(r, state, namespace, source, current_priority, {
type = "parentheses",
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, source)
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, source, current_priority, {
type = "list brackets",
expression = exp
})
-- map 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, source)
if not exp then return nil, "invalid expression inside map parentheses: "..r_paren end
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, source, current_priority, {
type = "map brackets",
expression = exp
})
-- identifier
elseif s:match("^"..identifier_pattern) then
local name, r = s:match("^("..identifier_pattern..")(.-)$")
name = format_identifier(name)
-- string:value pair shorthand using =
if r:match("^=[^=]") and pair_priority > current_priority then
local val
val, r = expression(r:match("^=(.*)$"), state, namespace, source, pair_priority)
if not val then return val, r end
local args = {
type = "list",
left = {
type = "string",
text = { name }
},
right = val
}
-- find compatible variant
local variant, err = find_function(state, namespace, "_=_", args, true)
if not variant then return variant, err end
return expression(r, state, namespace, source, current_priority, variant)
end
-- variables
-- if name isn't a valid variable, suffix call: detect if a prefix is valid variable, suffix _._ call is handled in the binop section below
local nl = split(name)
for i=#nl, 1, -1 do
local name_prefix = table.concat(nl, ".", 1, i)
local var, vfqm = find(state.aliases, state.variables, namespace, name_prefix)
if var then
if i < #nl then
r = "."..table.concat(nl, ".", i+1, #nl)..r
end
return expression(r, state, namespace, source, current_priority, {
type = "variable",
name = vfqm
})
end
end
-- functions. This is a temporary expression that will either be transformed into a reference by the &_ operator, or an (implicit) function call otherwise.
for i=#nl, 1, -1 do
local name_prefix = table.concat(nl, ".", 1, i)
local lfnqm = find_all(state.aliases, state.functions, namespace, name_prefix)
if #lfnqm > 0 then
if i < #nl then
r = "."..table.concat(nl, ".", i+1, #nl)..r
end
return expression(r, state, namespace, source, current_priority, {
type = "potential function",
called_name = name,
names = lfnqm
})
end
end
return nil, ("can't find function or variable named %q in namespace %q"):format(name, namespace)
end
-- prefix unops
for prio, oplist in ipairs(prefix_unops_prio) do
for _, op in ipairs(oplist) do
local escaped = escape(op)
if s:match("^"..escaped) then
local sright = s:match("^"..escaped.."(.*)$")
-- function and variable reference
if op == "&" then
local right, r = expression(sright, state, namespace, source, prio)
if not right then return nil, ("invalid expression after unop %q: %s"):format(op, r) end
if right.type == "potential function" then
return expression(r, state, namespace, source, current_priority, {
type = "function reference",
names = right.names
})
elseif right.type == "variable" then
return expression(r, state, namespace, source, current_priority, {
type = "variable reference",
name = right.name,
expression = right
})
else
-- find variant
local variant, err = find_function(state, namespace, op.."_", right, true)
if not variant then return variant, err end
return expression(r, state, namespace, source, current_priority, variant)
end
-- anonymous function
elseif op == "$" then
-- get eventual arguments
local params = "()"
if sright:match("^%b()") then
params, sright = sright:match("^(%b())(.*)$")
end
-- define function
local fn_name = ("%s🥸%s"):format(namespace, random_identifier_alpha())
local s, e = preparse(state, (":$%s%s\n\t@%s"):format(fn_name, params, fn_name), "", source)
if not s then return nil, e end
local fn = state.functions[fn_name][1]
-- parse return expression
local right, r = expression(sright, state, fn.namespace, source, prio)
if not right then return nil, ("invalid expression after unop %q: %s"):format(op, r) end
-- put expression in return line
for _, c in ipairs(fn.child) do
if c.type == "return" and c.expression == fn_name then
c.expression = right
end
end
-- return reference to created function
return expression(r, state, namespace, source, current_priority, {
type = "nonpersistent",
expression = {
type = "function reference",
names = { fn_name }
}
})
-- normal prefix unop
else
local right, r = expression(sright, state, namespace, source, prio)
if not right then return nil, ("invalid expression after unop %q: %s"):format(op, r) end
-- find variant
local variant, err = find_function(state, namespace, op.."_", right, true)
if not variant then return variant, err end
return expression(r, state, namespace, source, current_priority, variant)
end
end
end
end
return nil, ("no valid expression before %q"):format(s)
else
-- transform potential function/variable calls into actual calls automatically
-- need to do this before every other operator, since once the code finds the next operator it won't go back to check if this applied and assume it
-- didn't skip anything since it didn't see any other operator before, even if it's actually higher priority...
-- the problems of an implicit operator I guess
if implicit_call_priority > current_priority then
-- implicit call of a function. Unlike for variables, can't be cancelled since there's not any other value this could return, we don't
-- have first class functions here...
if operating_on.type == "potential function" then
local args, paren_call, implicit_call
local r = s
if r:match("^%b()") then
paren_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, source)
if not args then return args, err end
if err:match("[^%s]") then return nil, ("unexpected %q at end of argument list"):format(err) end
end
else -- implicit call; will be changed if there happens to be a ! after in the suffix operator code
implicit_call = true
end
-- find compatible variant
local variant, err = find_function_from_list(state, namespace, operating_on.called_name, operating_on.names, args, paren_call, implicit_call)
if not variant then return variant, err end
return expression(r, state, namespace, source, current_priority, variant)
-- implicit call on variable reference. Might be canceled afterwards due to finding a higher priority operator.
elseif operating_on.type == "variable" or (operating_on.type == "function call" and operating_on.called_name == "_._") then
local implicit_call_variant, err = find_function(state, namespace, "_!", { type = "value passthrough" }, false, true)
if not implicit_call_variant then return implicit_call_variant, err end
return expression(s, state, namespace, source, current_priority, {
type = "implicit call if reference",
variant = implicit_call_variant,
expression = operating_on
})
end
end
-- binop
for prio, oplist in ipairs(binops_prio) do
if prio > current_priority then
-- cancel implicit call operator if we are handling a binop of higher priority
-- see comment a bit above on why the priority handling is stupid for implicit operators
local operating_on = operating_on
if prio > implicit_call_priority and operating_on.type == "implicit call if reference" then
operating_on = operating_on.expression
end
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)
local args, paren_call
if r:match("^%b()") then
paren_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, source)
if not args then return args, err end
if err:match("[^%s]") then return nil, ("unexpected %q at end of argument map"):format(err) end
end
end
-- add first argument
if not args then
args = operating_on
else
if args.type == "list" then -- insert as first element
local first_list = args
while first_list.left.type == "list" do
first_list = first_list.left
end
first_list.left = {
type = "list",
left = operating_on,
right = first_list.left
}
else
args = {
type = "list",
left = operating_on,
right = args
}
end
end
-- find compatible variant
local variant, err = find_function(state, namespace, name, args, paren_call)
if not variant then return variant, err end
return expression(r, state, namespace, source, current_priority, variant)
-- namespace
elseif op == "." and sright:match("^"..identifier_pattern) then
local name, r = sright:match("^("..identifier_pattern..")(.-)$")
name = format_identifier(name)
-- find variant
local args = {
type = "list",
left = operating_on,
right = { type = "string", text = { name } }
}
local variant, err = find_function(state, namespace, "_._", args, true)
if not variant then return variant, err end
return expression(r, state, namespace, source, current_priority, variant)
-- other binops
else
local right, r = expression(sright, state, namespace, source, prio)
if right then
-- list constructor (can't do this through a function call since we need to build a list for its arguments)
if op == "," then
return expression(r, state, namespace, source, current_priority, {
type = "list",
left = operating_on,
right = right
})
-- special binops
elseif op == ":=" or op == "+=" or op == "-=" or op == "//=" or op == "/=" or op == "*=" or op == "%=" or op == "^=" then
-- cancel implicit call on right variable
if operating_on.type == "implicit call if reference" then
operating_on = operating_on.expression
end
-- rewrite assignment + arithmetic operators into a normal assignment
if op ~= ":=" then
local args = {
type = "list",
left = operating_on,
right = right
}
local variant, err = find_function(state, namespace, "_"..op:match("^(.*)%=$").."_", args, true)
if not variant then return variant, err end
right = variant
end
-- assign to a function
if operating_on.type == "function call" then
-- remove non-assignment functions
for i=#operating_on.variants, 1, -1 do
if not operating_on.variants[i].assignment then
table.remove(operating_on.variants, i)
end
end
if #operating_on.variants == 0 then
return nil, ("trying to perform assignment on function %s with no compatible assignment variant"):format(operating_on.called_name)
end
-- rewrite function to perform assignment
operating_on.assignment = right
return expression(r, state, namespace, source, current_priority, operating_on)
elseif operating_on.type ~= "variable" then
return nil, ("trying to perform assignment on a %s expression"):format(operating_on.type)
end
-- assign to a variable
return expression(r, state, namespace, source, current_priority, {
type = ":=",
left = operating_on,
right = right
})
elseif op == "&" or op == "|" or op == "~?" or op == "~" or op == "#" then
return expression(r, state, namespace, source, current_priority, {
type = op,
left = operating_on,
right = right
})
-- normal binop
else
-- find variant
local args = {
type = "list",
left = operating_on,
right = right
}
local variant, err = find_function(state, namespace, "_"..op.."_", args, true)
if not variant then return variant, err end
return expression(r, state, namespace, source, current_priority, variant)
end
end
end
end
end
end
end
-- suffix unop
for prio, oplist in ipairs(suffix_unops_prio) do
if prio > current_priority then
-- cancel implit call operator if we are handling an operator of higher priority
-- see comment a bit above on why the priority handling is stupid for implicit operators
local operating_on = operating_on
if prio > implicit_call_priority and operating_on.type == "implicit call if reference" then
operating_on = operating_on.expression
end
for _, op in ipairs(oplist) do
local escaped = escape(op)
if s:match("^"..escaped) then
local r = s:match("^"..escaped.."(.*)$")
-- remove ! after a previously-assumed implicit function call
if op == "!" and operating_on.type == "function call" and operating_on.implicit_call then
operating_on.implicit_call = false
return expression(r, state, namespace, source, current_priority, operating_on)
-- normal suffix unop
else
local variant, err = find_function(state, namespace, "_"..op, operating_on, true)
if not variant then return variant, err end
return expression(r, state, namespace, source, current_priority, variant)
end
end
end
end
end
-- index / call
if call_priority > current_priority and s:match("^%b()") then
if operating_on.type == "implicit call if reference" then
operating_on = operating_on.expression -- replaced with current call
end
local args = operating_on
local content, r = s:match("^(%b())(.*)$")
content = content:gsub("^%(", ""):gsub("%)$", "")
-- get arguments
if content:match("[^%s]") then
local right, r_paren = expression(content, state, namespace, source)
if not right then return right, r_paren end
if r_paren:match("[^%s]") then return nil, ("unexpected %q at end of index/call expression"):format(r_paren) end
args = { type = "list", left = args, right = right }
end
local variant, err = find_function(state, namespace, "()", args, true)
if not variant then return variant, err end
return expression(r, state, namespace, source, current_priority, variant)
end
-- implicit multiplication
if implicit_multiply_priority > current_priority then
if s:match("^"..identifier_pattern) then
local right, r = expression(s, state, namespace, source, implicit_multiply_priority)
if right then
local args = {
type = "list",
left = operating_on,
right = right
}
local variant, err = find_function(state, namespace, "_*_", args, true)
if not variant then return variant, err end
return expression(r, state, namespace, source, current_priority, variant)
end
end
end
-- nothing to operate
return operating_on, s
end
end
package.loaded[...] = expression
local common = require((...):gsub("expression$", "common"))
identifier_pattern, format_identifier, find, escape, find_function, parse_text, find_all, split, find_function_from_list = common.identifier_pattern, common.format_identifier, common.find, common.escape, common.find_function, common.parse_text, common.find_all, common.split, common.find_function_from_list
preparse = require((...):gsub("expression$", "preparser"))
return expression

View file

@ -0,0 +1,57 @@
local primary = require("parser.expression.primary.primary")
local comment
comment = primary {
match = function(self, str)
return str:match("^%(%(")
end,
parse = function(self, source, str, limit_pattern)
local rem = source:consume(str:match("^(%(%()(.*)$"))
local content_list = {}
while not rem:match("^%)%)") do
local content
content, rem = rem:match("^([^%(%)]*)(.-)$")
-- cut the text prematurely at limit_pattern if relevant
if limit_pattern and content:match(limit_pattern) then
local pos = content:match("()"..limit_pattern) -- limit_pattern can contain $, so can't directly extract with captures
content, rem = source:count(content:sub(1, pos-1)), ("))%s%s"):format(content:sub(pos), rem)
source:increment(-2)
else
source:count(content)
end
table.insert(content_list, content)
-- nested comment
if rem:match("^%(%(") then
local subcomment
subcomment, rem = comment:parse(source, rem, limit_pattern)
table.insert(content_list, "((")
for _, c in ipairs(subcomment) do table.insert(content_list, c) end
table.insert(content_list, "))")
-- no end token after the comment
elseif not rem:match("^%)%)") then
-- single ) or (, keep on commentin'
if rem:match("^[%)%(]") then
local s
s, rem = source:count(rem:match("^([%)%(])(.-)$"))
table.insert(content_list, s)
-- anything other than end-of-line
elseif rem:match("[^%s]") then
error(("unexpected %q at end of comment"):format(rem), 0)
-- consumed everything until end-of-line, close your eyes and imagine the text has been closed
else
rem = rem .. "))"
end
end
end
rem = source:consume(rem:match("^(%)%))(.*)$"))
return table.concat(content_list, ""), rem
end
}
return comment

View file

@ -0,0 +1,40 @@
local primary = require("parser.expression.primary.primary")
local identifier = require("parser.expression.primary.identifier")
local expression_to_ast = require("parser.expression.to_ast")
local ast = require("ast")
local FunctionParameter = ast.FunctionParameter
local operator_priority = require("common").operator_priority
local assignment_priority = operator_priority["_=_"]
local type_check_priority = operator_priority["_::_"]
return primary {
match = function(self, str)
return identifier:match(str)
end,
parse = function(self, source, str, limit_pattern, no_default_value)
local source_param = source:clone()
-- name
local ident, rem = identifier:parse(source, str)
-- type check
local type_check
if rem:match("^%s*::") then
local scheck = source:consume(rem:match("^(%s*::%s*)(.*)$"))
type_check, rem = expression_to_ast(source, scheck, limit_pattern, type_check_priority)
end
-- default value
local default
if not no_default_value then
if rem:match("^%s*=") then
local sdefault = source:consume(rem:match("^(%s*=%s*)(.*)$"))
default, rem = expression_to_ast(source, sdefault, limit_pattern, assignment_priority)
end
end
return FunctionParameter:new(ident, default, type_check):set_source(source_param), rem
end
}

View file

@ -0,0 +1,7 @@
local function_parameter = require("parser.expression.contextual.function_parameter")
return function_parameter {
parse = function(self, source, str, limit_pattern)
return function_parameter:parse(source, str, limit_pattern, true)
end
}

View file

@ -0,0 +1,49 @@
local primary = require("parser.expression.primary.primary")
local function_parameter = require("parser.expression.contextual.function_parameter")
local function_parameter_no_default = require("parser.expression.contextual.function_parameter_no_default")
local ast = require("ast")
local ParameterTuple = ast.ParameterTuple
return primary {
match = function(self, str)
return str:match("^%(")
end,
parse = function(self, source, str, limit_pattern)
local source_start = source:clone()
local parameters = ParameterTuple:new()
local rem = source:consume(str:match("^(%()(.*)$"))
-- i would LOVE to reuse the regular list parsing code for this, but unfortunately the list parsing code
-- itself depends on this and expect this to be available quite early, and it's ANNOYING
while not rem:match("^%s*%)") do
-- parameter
local func_param
func_param, rem = function_parameter:expect(source, rem, limit_pattern)
-- next! comma separator
if not rem:match("^%s*%)") then
if not rem:match("^%s*,") then
error(("unexpected %q at end of argument list"):format(rem), 0)
end
rem = source:consume(rem:match("^(%s*,)(.*)$"))
end
-- add
parameters:insert(func_param)
end
rem = rem:match("^%s*%)(.*)$")
-- assigment param
if rem:match("^%s*=") then
rem = source:consume(rem:match("^(%s*=%s*)(.*)$"))
local func_param
func_param, rem = function_parameter_no_default:expect(source, rem, limit_pattern)
parameters:insert_assignment(func_param)
end
return parameters:set_source(source_start), rem
end
}

View file

@ -0,0 +1,16 @@
local primary = require("parser.expression.primary.primary")
local ast = require("ast")
local Identifier, Call, ArgumentTuple = ast.Identifier, ast.Call, ast.ArgumentTuple
return primary {
match = function(self, str)
return str:match("^_")
end,
parse = function(self, source, str)
local source_start = source:clone()
local rem = source:consume(str:match("^(_)(.-)$"))
return Call:new(Identifier:new("_"), ArgumentTuple:new()):set_source(source_start), rem
end
}

View file

@ -0,0 +1,205 @@
local primary = require("parser.expression.primary.primary")
local function_parameter_no_default = require("parser.expression.contextual.function_parameter_no_default")
local parameter_tuple = require("parser.expression.contextual.parameter_tuple")
local identifier = require("parser.expression.primary.identifier")
local expression_to_ast = require("parser.expression.to_ast")
local escape = require("common").escape
local ast = require("ast")
local Symbol, Definition, Function, ParameterTuple = ast.Symbol, ast.Definition, ast.Function, ast.ParameterTuple
local regular_operators = require("common").regular_operators
local prefixes = regular_operators.prefixes
local suffixes = regular_operators.suffixes
local infixes = regular_operators.infixes
local operator_priority = require("common").operator_priority
-- same as function_parameter_no_default, but allow wrapping in (evenetual) parentheses
-- in order to solve some priotity issues (_._ has higher priority than _::_, leading to not being possible to overload it with type filtering without parentheses)
local function_parameter_maybe_parenthesis = function_parameter_no_default {
match = function(self, str)
if str:match("^%(") then
return function_parameter_no_default:match(str:match("^%((.*)$"))
else
return function_parameter_no_default:match(str)
end
end,
parse = function(self, source, str, limit_pattern)
if str:match("^%(") then
str = source:consume(str:match("^(%()(.*)$"))
local exp, rem = function_parameter_no_default:parse(source, str, limit_pattern)
if not rem:match("^%s*%)") then error(("unexpected %q at end of parenthesis"):format(rem), 0) end
rem = source:consume(rem:match("^(%s*%))(.-)$"))
return exp, rem
else
return function_parameter_no_default:parse(source, str, limit_pattern)
end
end
}
-- signature type 1: unary prefix
-- :$-parameter exp
-- returns symbol, parameter_tuple, rem if success
-- return nil otherwise
local function search_prefix_signature(modifiers, source, str, limit_pattern)
for _, pfx in ipairs(prefixes) do
local prefix = pfx[1]
local prefix_pattern = "%s*"..escape(prefix).."%s*"
if str:match("^"..prefix_pattern) then
-- operator name
local rem = source:consume(str:match("^("..prefix_pattern..")(.*)$"))
local symbol = Symbol:new(prefix.."_", modifiers):set_source(source:clone():increment(-1))
-- parameters
local parameter
parameter, rem = function_parameter_maybe_parenthesis:expect(source, rem, limit_pattern)
local parameters = ParameterTuple:new()
parameters:insert(parameter)
return symbol, parameters, rem
end
end
end
-- signature type 2: binary infix
-- should be checked before suffix signature
-- :$parameterA + parameterB exp
-- returns symbol, parameter_tuple, rem if success
-- return nil otherwise
local function search_infix_signature(modifiers, source, str, limit_pattern)
if function_parameter_maybe_parenthesis:match(str) then
local src = source:clone() -- operate on clone source since search success is not yet guaranteed
local parameter_a, rem = function_parameter_maybe_parenthesis:parse(src, str, limit_pattern)
local parameters = ParameterTuple:new()
parameters:insert(parameter_a)
for _, ifx in ipairs(infixes) do
local infix = ifx[1]
local infix_pattern = "%s*"..escape(infix).."%s*"
if rem:match("^"..infix_pattern) then
-- operator name
rem = src:consume(rem:match("^("..infix_pattern..")(.*)$"))
local symbol = Symbol:new("_"..infix.."_", modifiers):set_source(src:clone():increment(-1))
-- parameters
if function_parameter_maybe_parenthesis:match(rem) then
local parameter_b
parameter_b, rem = function_parameter_maybe_parenthesis:parse(src, rem, limit_pattern)
parameters:insert(parameter_b)
source:set(src)
return symbol, parameters, rem
else
return
end
end
end
end
end
-- signature type 3: unary suffix
-- :$parameter! exp
-- returns symbol, parameter_tuple, rem if success
-- return nil otherwise
local function search_suffix_signature(modifiers, source, str, limit_pattern)
if function_parameter_maybe_parenthesis:match(str) then
local src = source:clone() -- operate on clone source since search success is not yet guaranteed
local parameter_a, rem = function_parameter_maybe_parenthesis:parse(src, str, limit_pattern)
local parameters = ParameterTuple:new()
parameters:insert(parameter_a)
for _, sfx in ipairs(suffixes) do
local suffix = sfx[1]
local suffix_pattern = "%s*"..escape(suffix).."%s*"
if rem:match("^"..suffix_pattern) then
-- operator name
rem = src:count(rem:match("^("..suffix_pattern..")(.*)$"))
local symbol = Symbol:new("_"..suffix, modifiers):set_source(src:clone():increment(-1))
source:set(src)
return symbol, parameters, rem
end
end
end
end
-- signature type 4: regular function
-- :$identifier(parameter_tuple, ...) exp
-- returns symbol, parameter_tuple, rem if success
-- return nil otherwise
local function search_function_signature(modifiers, source, str, limit_pattern)
if identifier:match(str) then
local name_source = source:clone()
local name, rem = identifier:parse(source, str, limit_pattern)
-- name
local symbol = name:to_symbol(modifiers):set_source(name_source)
-- parse eventual parameters
local parameters
if parameter_tuple:match(rem) then
parameters, rem = parameter_tuple:parse(source, rem)
else
parameters = ParameterTuple:new()
end
return symbol, parameters, rem
end
end
return primary {
match = function(self, str)
return str:match("^%::?[&@]?%$")
end,
parse = function(self, source, str, limit_pattern)
local source_start = source:clone()
local mod_const, mod_exported, rem = source:consume(str:match("^(%:(:?)([&@]?)%$)(.-)$"))
-- get modifiers
local constant, exported, persistent
if mod_const == ":" then constant = true end
if mod_exported == "@" then exported = true
elseif mod_exported == "&" then persistent = true end
local modifiers = { constant = constant, exported = exported, persistent = persistent }
-- search for a valid signature
local symbol, parameters
local s, p, r = search_prefix_signature(modifiers, source, rem, limit_pattern)
if s then symbol, parameters, rem = s, p, r
else
s, p, r = search_infix_signature(modifiers, source, rem, limit_pattern)
if s then symbol, parameters, rem = s, p, r
else
s, p, r = search_suffix_signature(modifiers, source, rem, limit_pattern)
if s then symbol, parameters, rem = s, p, r
else
s, p, r = search_function_signature(modifiers, source, rem, limit_pattern)
if s then symbol, parameters, rem = s, p, r end
end
end
end
-- done
if symbol then
-- parse expression
local right
s, right, rem = pcall(expression_to_ast, source, rem, limit_pattern, operator_priority["$_"])
if not s then error(("invalid expression after unop %q: %s"):format(self.operator, right), 0) end
-- return function
local fn = Function:new(parameters, right):set_source(source_start)
return Definition:new(symbol, fn):set_source(source_start), rem
end
end
}

View file

@ -0,0 +1,40 @@
local primary = require("parser.expression.primary.primary")
local Identifier = require("ast.Identifier")
local disallowed_set = (".~`^+-=<>/[]*{}|\\_!?,;:()\"@&$#%"):gsub("[^%w]", "%%%1")
local identifier_pattern = "%s*[^0-9%s'"..disallowed_set.."][^"..disallowed_set.."]*"
local common = require("common")
local trim, escape = common.trim, common.escape
-- for operator identifiers
local regular_operators = require("common").regular_operators
local operators = {}
for _, prefix in ipairs(regular_operators.prefixes) do table.insert(operators, prefix[1].."_") end
for _, infix in ipairs(regular_operators.infixes) do table.insert(operators, "_"..infix[1].."_") end
for _, suffix in ipairs(regular_operators.suffixes) do table.insert(operators, "_"..suffix[1]) end
-- all valid identifier patterns
local identifier_patterns = { identifier_pattern }
for _, operator in ipairs(operators) do table.insert(identifier_patterns, "%s*"..escape(operator)) end
return primary {
match = function(self, str)
for _, pat in ipairs(identifier_patterns) do
if str:match("^"..pat) then return true end
end
return false
end,
parse = function(self, source, str)
for _, pat in ipairs(identifier_patterns) do
if str:match("^"..pat) then
local start_source = source:clone()
local name, rem = source:count(str:match("^("..pat..")(.-)$"))
name = trim(name)
return Identifier:new(name):set_source(start_source), rem
end
end
end
}

View file

@ -0,0 +1,42 @@
--- try to parse a primary expression
local function r(name)
return require("parser.expression.primary."..name), nil
end
local primaries = {
r("number"),
r("string"),
r("text"),
r("parenthesis"),
r("function_definition"),
r("symbol"),
r("identifier"),
r("block_identifier"),
r("tuple"),
r("struct"),
-- prefixes
-- 1
r("prefix.semicolon"),
r("prefix.function"),
-- 2
r("prefix.return"),
-- 3.5
r("prefix.else"),
-- 11
r("prefix.negation"),
r("prefix.not"),
r("prefix.mutable"),
}
return {
-- returns exp, rem if expression found
-- returns nil if no expression found
search = function(self, source, str, limit_pattern)
for _, primary in ipairs(primaries) do
local exp, rem = primary:search(source, str, limit_pattern)
if exp then return exp, rem end
end
end
}

View file

@ -0,0 +1,19 @@
local primary = require("parser.expression.primary.primary")
local Number = require("ast.Number")
return primary {
match = function(self, str)
return str:match("^%d*%.%d+") or str:match("^%d+")
end,
parse = function(self, source, str)
local start_source = source:clone()
local d, r = str:match("^(%d*%.%d+)(.*)$")
if not d then
d, r = source:count(str:match("^(%d+)(.*)$"))
else
source:count(d)
end
return Number:new(tonumber(d)):set_source(start_source), r
end
}

View file

@ -0,0 +1,31 @@
-- either parentheses or nil ()
local primary = require("parser.expression.primary.primary")
local ast = require("ast")
local Nil = ast.Nil
local expression_to_ast = require("parser.expression.to_ast")
return primary {
match = function(self, str)
return str:match("^%(")
end,
parse = function(self, source, str)
local start_source = source:clone()
local rem = source:consume(str:match("^(%()(.*)$"))
local exp
if rem:match("^%s*%)") then
exp = Nil:new()
else
local s
s, exp, rem = pcall(expression_to_ast, source, rem, "%)")
if not s then error("invalid expression inside parentheses: "..exp, 0) end
if not rem:match("^%s*%)") then error(("unexpected %q at end of parenthesis"):format(rem), 0) end
end
rem = source:consume(rem:match("^(%s*%))(.*)$"))
return exp:set_source(start_source), rem
end
}

View file

@ -0,0 +1,8 @@
local prefix_quote_right = require("parser.expression.primary.prefix.prefix_quote_right")
local operator_priority = require("common").operator_priority
return prefix_quote_right {
operator = "~",
identifier = "~_",
priority = operator_priority["~_"]
}

View file

@ -0,0 +1,35 @@
local prefix = require("parser.expression.primary.prefix.prefix")
local parameter_tuple = require("parser.expression.contextual.parameter_tuple")
local escape = require("common").escape
local expression_to_ast = require("parser.expression.to_ast")
local ast = require("ast")
local Function, ParameterTuple = ast.Function, ast.ParameterTuple
local operator_priority = require("common").operator_priority
return prefix {
operator = "$",
priority = operator_priority["$_"],
parse = function(self, source, str, limit_pattern)
local source_start = source:clone()
local escaped = escape(self.operator)
local rem = source:consume(str:match("^("..escaped..")(.*)$"))
-- parse eventual parameters
local parameters
if parameter_tuple:match(rem) then
parameters, rem = parameter_tuple:parse(source, rem)
else
parameters = ParameterTuple:new()
end
-- parse expression
local s, right
s, right, rem = pcall(expression_to_ast, source, rem, limit_pattern, self.priority)
if not s then error(("invalid expression after unop %q: %s"):format(self.operator, right), 0) end
return Function:new(parameters, right):set_source(source_start), rem
end
}

View file

@ -0,0 +1,9 @@
local prefix = require("parser.expression.primary.prefix.prefix")
local operator_priority = require("common").operator_priority
return prefix {
operator = "*",
identifier = "*_",
priority = operator_priority["*_"]
}

View file

@ -0,0 +1,9 @@
local prefix = require("parser.expression.primary.prefix.prefix")
local operator_priority = require("common").operator_priority
return prefix {
operator = "-",
identifier = "-_",
priority = operator_priority["-_"]
}

View file

@ -0,0 +1,9 @@
local prefix = require("parser.expression.primary.prefix.prefix")
local operator_priority = require("common").operator_priority
return prefix {
operator = "!",
identifier = "!_",
priority = operator_priority["!_"]
}

View file

@ -0,0 +1,34 @@
-- unary prefix operators, for example: the - in -5
local primary = require("parser.expression.primary.primary")
local escape = require("common").escape
local expression_to_ast = require("parser.expression.to_ast")
local ast = require("ast")
local Call, Identifier, ArgumentTuple = ast.Call, ast.Identifier, ast.ArgumentTuple
return primary {
operator = nil,
identifier = nil,
priority = nil,
match = function(self, str)
local escaped = escape(self.operator)
return str:match("^"..escaped)
end,
parse = function(self, source, str, limit_pattern)
local source_start = source:clone()
local escaped = escape(self.operator)
local sright = source:consume(str:match("^("..escaped..")(.*)$"))
local s, right, rem = pcall(expression_to_ast, source, sright, limit_pattern, self.priority)
if not s then error(("invalid expression after prefix operator %q: %s"):format(self.operator, right), 0) end
return self:build_ast(right):set_source(source_start), rem
end,
build_ast = function(self, right)
return Call:new(Identifier:new(self.identifier), ArgumentTuple:new(right))
end
}

View file

@ -0,0 +1,11 @@
local prefix = require("parser.expression.primary.prefix.prefix")
local ast = require("ast")
local Call, Identifier, ArgumentTuple, Quote = ast.Call, ast.Identifier, ast.ArgumentTuple, ast.Quote
return prefix {
build_ast = function(self, right)
right = Quote:new(right)
return Call:new(Identifier:new(self.identifier), ArgumentTuple:new(right))
end
}

View file

@ -0,0 +1,15 @@
local prefix = require("parser.expression.primary.prefix.prefix")
local ast = require("ast")
local Return = ast.Return
local operator_priority = require("common").operator_priority
return prefix {
operator = "@",
priority = operator_priority["@_"],
build_ast = function(self, right)
return Return:new(right)
end
}

View file

@ -0,0 +1,13 @@
local prefix = require("parser.expression.primary.prefix.prefix")
local operator_priority = require("common").operator_priority
return prefix {
operator = ";",
identifier = ";_",
priority = operator_priority[";_"],
build_ast = function(self, right)
return right
end
}

View file

@ -0,0 +1,33 @@
local class = require("class")
return class {
new = false, -- static class
-- returns exp, rem if expression found
-- returns nil if no expression found
search = function(self, source, str, limit_pattern)
if not self:match(str) then
return nil
end
return self:parse(source, str, limit_pattern)
end,
-- return bool
-- (not needed if you redefined :search)
match = function(self, str)
return false
end,
-- return AST, rem
-- (not needed if you redefined :search)
parse = function(self, source, str, limit_pattern)
error("unimplemented")
end,
-- class helpers --
-- return AST, rem
expect = function(self, source, str, limit_pattern)
local exp, rem = self:search(source, str, limit_pattern)
if not exp then error(("expected %s but got %s"):format(self.type, str)) end
return exp, rem
end
}

View file

@ -0,0 +1,78 @@
-- note: this is reused in primary.text, hence all the configurable fields
local primary = require("parser.expression.primary.primary")
local StringInterpolation = require("ast.StringInterpolation")
local ast = require("ast")
local String = ast.String
local expression_to_ast = require("parser.expression.to_ast")
local escape = require("common").escape
local escape_code = {
["n"] = "\n",
["t"] = "\t",
-- everything else is identity by default
}
return primary {
type = "string", -- interpolation type - used for errors
start_pattern = "\"", -- pattern that start the string interpolation
stop_char = "\"", -- character that stops the string interpolation - must be a single character!
allow_implicit_stop = false, -- set to true to allow the string to be closed implicitely when reaching the end of the expression or limit_pattern
interpolation = StringInterpolation,
match = function(self, str)
return str:match("^"..self.start_pattern)
end,
parse = function(self, source, str, limit_pattern)
local interpolation = self.interpolation:new()
local stop_pattern = escape(self.stop_char)
local start_source = source:clone()
local rem = source:consume(str:match("^("..self.start_pattern..")(.-)$"))
while not rem:match("^"..stop_pattern) do
local text_source = source:clone()
local text
text, rem = rem:match("^([^%{%\\"..stop_pattern.."]*)(.-)$") -- get all text until something potentially happens
-- cut the text prematurely at limit_pattern if relevant
if self.allow_implicit_stop and limit_pattern and text:match(limit_pattern) then
local pos = text:match("()"..limit_pattern) -- limit_pattern can contain $, so can't directly extract with captures
text, rem = source:count(text:sub(1, pos-1)), ("%s%s%s"):format(self.stop_char, text:sub(pos), rem)
source:increment(-1)
else
source:count(text)
end
interpolation:insert(String:new(text):set_source(text_source))
if rem:match("^%{") then
local ok, exp
ok, exp, rem = pcall(expression_to_ast, source, source:consume(rem:match("^(%{)(.*)$")), "%}")
if not ok then error("invalid expression inside interpolation: "..exp, 0) end
if not rem:match("^%s*%}") then error(("unexpected %q at end of interpolation"):format(rem), 0) end
rem = source:consume(rem:match("^(%s*%})(.*)$"))
interpolation:insert(exp)
elseif rem:match("^\\") then
text, rem = source:consume(rem:match("^(\\(.))(.*)$"))
interpolation:insert(String:new(escape_code[text] or text))
elseif not rem:match("^"..stop_pattern) then
if not self.allow_implicit_stop or rem:match("[^%s]") then
error(("unexpected %q at end of "..self.type):format(rem), 0)
-- consumed everything until end-of-line, implicit stop allowed, close your eyes and imagine the text has been closed
else
rem = rem .. self.stop_char
end
end
end
rem = source:consume(rem:match("^("..stop_pattern..")(.*)$"))
return interpolation:set_source(start_source), rem
end
}

View file

@ -0,0 +1,17 @@
local primary = require("parser.expression.primary.primary")
local tuple = require("parser.expression.primary.tuple")
local ast = require("ast")
local Struct = ast.Struct
return primary {
match = function(self, str)
return str:match("^%{")
end,
parse = function(self, source, str)
local l, rem = tuple:parse_tuple(source, str, "{", '}')
return Struct:from_tuple(l), rem
end
}

View file

@ -0,0 +1,40 @@
local primary = require("parser.expression.primary.primary")
local type_check = require("parser.expression.secondary.infix.type_check")
local identifier = require("parser.expression.primary.identifier")
local ast = require("ast")
local Nil = ast.Nil
return primary {
match = function(self, str)
if str:match("^%::?[&@]?") then
return identifier:match(str:match("^%::?[&@]?(.-)$"))
end
return false
end,
parse = function(self, source, str)
local mod_const, mod_export, rem = source:consume(str:match("^(%:(:?)([&@]?))(.-)$"))
local constant, persistent, type_check_exp, exported
-- get modifier
if mod_const == ":" then constant = true end
if mod_export == "&" then persistent = true
elseif mod_export == "@" then exported = true end
-- name
local ident
ident, rem = identifier:parse(source, rem)
-- type check
local nil_val = Nil:new()
if type_check:match(rem, 0, nil_val) then
local exp
exp, rem = type_check:parse(source, rem, nil, 0, nil_val)
type_check_exp = exp.arguments.list[2]
end
return ident:to_symbol{ constant = constant, persistent = persistent, exported = exported, type_check = type_check_exp }:set_source(source), rem
end
}

View file

@ -0,0 +1,24 @@
local string = require("parser.expression.primary.string")
local ast = require("ast")
local TextInterpolation = ast.TextInterpolation
return string {
type = "text",
start_pattern = "|%s?",
stop_char = "|",
allow_implicit_stop = true,
interpolation = TextInterpolation,
parse = function(self, source, str, limit_pattern)
local interpolation, rem = string.parse(self, source, str, limit_pattern)
-- restore | when chaining with a choice operator
if rem:match("^>") then
rem = "|" .. rem
source:increment(-1)
end
return interpolation, rem
end
}

View file

@ -0,0 +1,41 @@
local primary = require("parser.expression.primary.primary")
local ast = require("ast")
local Tuple = ast.Tuple
local expression_to_ast = require("parser.expression.to_ast")
local escape = require("common").escape
return primary {
match = function(self, str)
return str:match("^%[")
end,
parse = function(self, source, str)
return self:parse_tuple(source, str, "[", "]")
end,
parse_tuple = function(self, source, str, start_char, end_char)
local start_source = source:clone()
local rem = source:consume(str:match("^("..escape(start_char)..")(.*)$"))
local end_match = escape(end_char)
local l
if not rem:match("^%s*"..end_match) then
local s
s, l, rem = pcall(expression_to_ast, source, rem, end_match)
if not s then error("invalid expression in list: "..l, 0) end
end
if not Tuple:is(l) or l.explicit then l = Tuple:new(l) end -- single or no element
if not rem:match("^%s*"..end_match) then
error(("unexpected %q at end of list"):format(rem), 0)
end
rem = source:consume(rem:match("^(%s*"..end_match..")(.*)$"))
l.explicit = true
return l:set_source(start_source), rem
end,
}

View file

@ -0,0 +1,9 @@
local infix = require("parser.expression.secondary.infix.infix")
local operator_priority = require("common").operator_priority
return infix {
operator = "+",
identifier = "_+_",
priority = operator_priority["_+_"]
}

View file

@ -0,0 +1,9 @@
local infix_quote_right = require("parser.expression.secondary.infix.infix_quote_right")
local operator_priority = require("common").operator_priority
return infix_quote_right {
operator = "&",
identifier = "_&_",
priority = operator_priority["_&_"]
}

View file

@ -0,0 +1,23 @@
local infix = require("parser.expression.secondary.infix.infix")
local escape = require("common").escape
local operator_priority = require("common").operator_priority
local ast = require("ast")
local Identifier, Assignment = ast.Identifier, ast.Assignment
return infix {
operator = "=",
identifier = "_=_",
priority = operator_priority["_=_"],
-- return bool
match = function(self, str, current_priority, primary)
local escaped = escape(self.operator)
return self.priority > current_priority and str:match("^"..escaped) and Identifier:is(primary)
end,
build_ast = function(self, left, right)
return Assignment:new(left, right)
end
}

View file

@ -0,0 +1,24 @@
local infix = require("parser.expression.secondary.infix.infix")
local escape = require("common").escape
local operator_priority = require("common").operator_priority
local ast = require("ast")
local Call = ast.Call
return infix {
operator = "=",
identifier = "_=_",
priority = operator_priority["_=_"],
-- return bool
match = function(self, str, current_priority, primary)
local escaped = escape(self.operator)
return self.priority > current_priority and str:match("^"..escaped) and Call:is(primary)
end,
build_ast = function(self, left, right)
left.arguments:set_assignment(right)
return Call:new(left.func, left.arguments) -- recreate Call since we modified left.arguments
end,
}

View file

@ -0,0 +1,35 @@
local ast = require("ast")
local Call, Identifier, ArgumentTuple = ast.Call, ast.Identifier, ast.ArgumentTuple
local assignment = require("parser.expression.secondary.infix.assignment")
local assignment_call = require("parser.expression.secondary.infix.assignment_call")
local infixes = require("common").regular_operators.infixes
local generated = {}
for _, infix in ipairs(infixes) do
local operator = infix[1].."="
local identifier = "_=_"
local infix_identifier = "_"..infix[1].."_"
table.insert(generated, assignment {
operator = operator,
identifier = identifier,
build_ast = function(self, left, right)
right = Call:new(Identifier:new(infix_identifier), ArgumentTuple:new(left, right))
return assignment.build_ast(self, left, right)
end
})
table.insert(generated, assignment_call {
operator = operator,
identifier = identifier,
build_ast = function(self, left, right)
right = Call:new(Identifier:new(infix_identifier), ArgumentTuple:new(left, right))
return assignment_call.build_ast(self, left, right)
end
})
end
return generated

View file

@ -0,0 +1,28 @@
local infix = require("parser.expression.secondary.infix.infix")
local escape = require("common").escape
local identifier = require("parser.expression.primary.identifier")
local operator_priority = require("common").operator_priority
local ast = require("ast")
local Call, ArgumentTuple = ast.Call, ast.ArgumentTuple
return infix {
operator = "!",
identifier = "_!_",
priority = operator_priority["_!_"],
match = function(self, str, current_priority, primary)
local escaped = escape(self.operator)
return self.priority > current_priority and str:match("^"..escaped) and identifier:match(str:match("^"..escaped.."%s*(.-)$"))
end,
build_ast = function(self, left, right)
if Call:is(right) then
right.arguments:insert_positional(1, left)
return right
else
return Call:new(right, ArgumentTuple:new(left))
end
end
}

View file

@ -0,0 +1,17 @@
local infix = require("parser.expression.secondary.infix.infix")
local operator_priority = require("common").operator_priority
local ast = require("ast")
local Call, Identifier, ArgumentTuple, ResumeParentFunction, ParameterTuple, Function = ast.Call, ast.Identifier, ast.ArgumentTuple, ast.ResumeParentFunction, ast.ParameterTuple, ast.Function
return infix {
operator = "|>",
identifier = "_|>_",
priority = operator_priority["_|>_"],
build_ast = function(self, left, right)
right = Function:new(ParameterTuple:new(), ResumeParentFunction:new(right))
return Call:new(Identifier:new(self.identifier), ArgumentTuple:new(left, right))
end
}

View file

@ -0,0 +1,22 @@
local infix = require("parser.expression.secondary.infix.infix")
local escape = require("common").escape
local operator_priority = require("common").operator_priority
local ast = require("ast")
local Definition, Symbol = ast.Definition, ast.Symbol
return infix {
operator = "=",
identifier = "_=_",
priority = operator_priority["_=_"],
match = function(self, str, current_priority, primary)
local escaped = escape(self.operator)
return self.priority > current_priority and str:match("^"..escaped) and Symbol:is(primary)
end,
build_ast = function(self, left, right)
return Definition:new(left, right)
end
}

View file

@ -0,0 +1,9 @@
local infix = require("parser.expression.secondary.infix.infix")
local operator_priority = require("common").operator_priority
return infix {
operator = "!=",
identifier = "_!=_",
priority = operator_priority["_!=_"]
}

View file

@ -0,0 +1,9 @@
local infix = require("parser.expression.secondary.infix.infix")
local operator_priority = require("common").operator_priority
return infix {
operator = "/",
identifier = "_/_",
priority = operator_priority["_/_"]
}

View file

@ -0,0 +1,9 @@
local infix = require("parser.expression.secondary.infix.infix")
local operator_priority = require("common").operator_priority
return infix {
operator = "==",
identifier = "_==_",
priority = operator_priority["_==_"]
}

View file

@ -0,0 +1,9 @@
local infix = require("parser.expression.secondary.infix.infix")
local operator_priority = require("common").operator_priority
return infix {
operator = "^",
identifier = "_^_",
priority = operator_priority["_^_"]
}

View file

@ -0,0 +1,9 @@
local infix = require("parser.expression.secondary.infix.infix")
local operator_priority = require("common").operator_priority
return infix {
operator = ">",
identifier = "_>_",
priority = operator_priority["_>_"]
}

View file

@ -0,0 +1,9 @@
local infix = require("parser.expression.secondary.infix.infix")
local operator_priority = require("common").operator_priority
return infix {
operator = ">=",
identifier = "_>=_",
priority = operator_priority["_>=_"]
}

View file

@ -0,0 +1,9 @@
local infix_quote_right = require("parser.expression.secondary.infix.infix_quote_right")
local operator_priority = require("common").operator_priority
return infix_quote_right {
operator = "~",
identifier = "_~_",
priority = operator_priority["_~_"]
}

View file

@ -0,0 +1,23 @@
local infix = require("parser.expression.secondary.infix.infix")
local identifier = require("parser.expression.primary.identifier")
local operator_priority = require("common").operator_priority
local ast = require("ast")
local Call, Identifier, ArgumentTuple = ast.Call, ast.Identifier, ast.ArgumentTuple
return infix {
operator = "*",
identifier = "_*_",
priority = operator_priority["_*_"]+.5, -- just above / so 1/2x gives 1/(2x)
match = function(self, str, current_priority, primary)
return self.priority > current_priority and identifier:match(str)
end,
parse = function(self, source, str, limit_pattern, current_priority, primary)
local start_source = source:clone()
local right, rem = identifier:parse(source, str, limit_pattern)
return Call:new(Identifier:new(self.identifier), ArgumentTuple:new(primary, right)):set_source(start_source), rem
end
}

View file

@ -0,0 +1,19 @@
local infix = require("parser.expression.secondary.infix.infix")
local operator_priority = require("common").operator_priority
local ast = require("ast")
local Call, Identifier, ArgumentTuple = ast.Call, ast.Identifier, ast.ArgumentTuple
return infix {
operator = ".",
identifier = "_._",
priority = operator_priority["_._"],
build_ast = function(self, left, right)
if Identifier:is(right) then
right = right:to_string()
end
return Call:new(Identifier:new(self.identifier), ArgumentTuple:new(left, right))
end
}

View file

@ -0,0 +1,34 @@
local secondary = require("parser.expression.secondary.secondary")
local escape = require("common").escape
local expression_to_ast = require("parser.expression.to_ast")
local ast = require("ast")
local Call, Identifier, ArgumentTuple = ast.Call, ast.Identifier, ast.ArgumentTuple
return secondary {
operator = nil,
identifier = nil,
priority = nil,
-- return bool
match = function(self, str, current_priority, primary)
local escaped = escape(self.operator)
return self.priority > current_priority and str:match("^"..escaped)
end,
-- return AST, rem
parse = function(self, source, str, limit_pattern, current_priority, primary)
local start_source = source:clone()
local escaped = escape(self.operator)
local sright = source:consume(str:match("^("..escaped..")(.*)$"))
local s, right, rem = pcall(expression_to_ast, source, sright, limit_pattern, self.priority)
if not s then error(("invalid expression after binary operator %q: %s"):format(self.operator, right), 0) end
return self:build_ast(primary, right):set_source(start_source), rem
end,
build_ast = function(self, left, right)
return Call:new(Identifier:new(self.identifier), ArgumentTuple:new(left, right))
end
}

View file

@ -0,0 +1,32 @@
-- same as infix, but skip if no valid expression after the operator instead of erroring
-- useful for operators that are both valid as infix and as suffix
local infix = require("parser.expression.secondary.infix.infix")
local escape = require("common").escape
local expression_to_ast = require("parser.expression.to_ast")
return infix {
-- returns exp, rem if expression found
-- returns nil if no expression found
search = function(self, source, str, limit_pattern, current_priority, operating_on_primary)
if not self:match(str, current_priority, operating_on_primary) then
return nil
end
return self:maybe_parse(source, str, limit_pattern, current_priority, operating_on_primary)
end,
parse = function() error("no guaranteed parse for this operator") end,
-- return AST, rem
-- return nil
maybe_parse = function(self, source, str, limit_pattern, current_priority, primary)
local start_source = source:clone()
local escaped = escape(self.operator)
local sright = source:consume(str:match("^("..escaped..")(.*)$"))
local s, right, rem = pcall(expression_to_ast, source, sright, limit_pattern, self.priority)
if not s then return nil end
return self:build_ast(primary, right):set_source(start_source), rem
end,
}

View file

@ -0,0 +1,12 @@
local infix = require("parser.expression.secondary.infix.infix")
local ast = require("ast")
local Call, Identifier, ArgumentTuple, Quote = ast.Call, ast.Identifier, ast.ArgumentTuple, ast.Quote
return infix {
build_ast = function(self, left, right)
left = Quote:new(left)
right = Quote:new(right)
return Call:new(Identifier:new(self.identifier), ArgumentTuple:new(left, right))
end
}

View file

@ -0,0 +1,11 @@
local infix = require("parser.expression.secondary.infix.infix")
local ast = require("ast")
local Call, Identifier, ArgumentTuple, Quote = ast.Call, ast.Identifier, ast.ArgumentTuple, ast.Quote
return infix {
build_ast = function(self, left, right)
right = Quote:new(right)
return Call:new(Identifier:new(self.identifier), ArgumentTuple:new(left, right))
end
}

View file

@ -0,0 +1,9 @@
local infix = require("parser.expression.secondary.infix.infix")
local operator_priority = require("common").operator_priority
return infix {
operator = "//",
identifier = "_//_",
priority = operator_priority["_//_"]
}

View file

@ -0,0 +1,9 @@
local infix = require("parser.expression.secondary.infix.infix")
local operator_priority = require("common").operator_priority
return infix {
operator = "<",
identifier = "_<_",
priority = operator_priority["_<_"]
}

View file

@ -0,0 +1,9 @@
local infix = require("parser.expression.secondary.infix.infix")
local operator_priority = require("common").operator_priority
return infix {
operator = "<=",
identifier = "_<=_",
priority = operator_priority["_<=_"]
}

View file

@ -0,0 +1,9 @@
local infix = require("parser.expression.secondary.infix.infix")
local operator_priority = require("common").operator_priority
return infix {
operator = "%",
identifier = "_%_",
priority = operator_priority["_%_"]
}

View file

@ -0,0 +1,9 @@
local infix = require("parser.expression.secondary.infix.infix")
local operator_priority = require("common").operator_priority
return infix {
operator = "*",
identifier = "_*_",
priority = operator_priority["_*_"]
}

View file

@ -0,0 +1,9 @@
local infix_quote_right = require("parser.expression.secondary.infix.infix_quote_right")
local operator_priority = require("common").operator_priority
return infix_quote_right {
operator = "|",
identifier = "_|_",
priority = operator_priority["_|_"]
}

View file

@ -0,0 +1,9 @@
local infix = require("parser.expression.secondary.infix.infix")
local operator_priority = require("common").operator_priority
return infix {
operator = ":",
identifier = "_:_",
priority = operator_priority["_:_"]
}

View file

@ -0,0 +1,9 @@
local infix_or_suffix = require("parser.expression.secondary.infix.infix_or_suffix")
local operator_priority = require("common").operator_priority
return infix_or_suffix {
operator = ";",
identifier = "_;_",
priority = operator_priority["_;_"]
}

View file

@ -0,0 +1,9 @@
local infix = require("parser.expression.secondary.infix.infix")
local operator_priority = require("common").operator_priority
return infix {
operator = "-",
identifier = "_-_",
priority = operator_priority["_-_"]
}

View file

@ -0,0 +1,9 @@
local infix_quote_right = require("parser.expression.secondary.infix.infix_quote_right")
local operator_priority = require("common").operator_priority
return infix_quote_right {
operator = "#",
identifier = "_#_",
priority = operator_priority["_#_"]
}

View file

@ -0,0 +1,36 @@
local infix = require("parser.expression.secondary.infix.infix")
local escape = require("common").escape
local expression_to_ast = require("parser.expression.to_ast")
local operator_priority = require("common").operator_priority
local ast = require("ast")
local Tuple = ast.Tuple
return infix {
operator = ",",
identifier = "_,_",
priority = operator_priority["_,_"],
-- reminder: this :parse method is also called from primary.list as an helper to build list bracket litterals
parse = function(self, source, str, limit_pattern, current_priority, primary)
local start_source = source:clone()
local l = Tuple:new()
l:insert(primary)
local escaped = escape(self.operator)
local rem = str
while rem:match("^%s*"..escaped) do
rem = source:consume(rem:match("^(%s*"..escaped..")(.*)$"))
local s, right
s, right, rem = pcall(expression_to_ast, source, rem, limit_pattern, self.priority)
if not s then error(("invalid expression after binop %q: %s"):format(self.operator, right), 0) end
l:insert(right)
end
l.explicit = false
return l:set_source(start_source), rem
end
}

View file

@ -0,0 +1,9 @@
local infix = require("parser.expression.secondary.infix.infix")
local operator_priority = require("common").operator_priority
return infix {
operator = "::",
identifier = "_::_",
priority = operator_priority["_::_"]
}

View file

@ -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["_~?_"]
}

View file

@ -0,0 +1,78 @@
--- try to parse a secondary expression
local function r(name)
return require("parser.expression.secondary."..name), nil
end
local secondaries = {
-- binary infix operators
-- 1
r("infix.semicolon"),
-- 2
r("infix.tuple"),
r("infix.tag"),
-- 4
r("infix.while"),
r("infix.if"),
-- 6
r("infix.choice"),
r("infix.and"),
r("infix.or"),
-- 7
r("infix.equal"),
r("infix.different"),
r("infix.greater_equal"),
r("infix.lower_equal"),
r("infix.greater"),
r("infix.lower"),
-- 8
r("infix.addition"),
r("infix.substraction"),
-- 9
r("infix.multiplication"),
r("infix.integer_division"),
r("infix.division"),
r("infix.modulo"),
-- 9.5
r("infix.implicit_multiplication"),
-- 10
r("infix.exponent"),
-- 11
r("infix.type_check"),
-- 12
r("infix.call"),
-- 14
r("infix.index"),
-- 3
r("infix.assignment"), -- deported after equal
r("infix.assignment_call"),
r("infix.definition"),
-- 5
r("infix.pair"), -- deported after type_check
-- unary suffix operators
-- 1
r("suffix.semicolon"),
-- 12
r("suffix.exclamation_call"),
-- 13
r("suffix.call"),
}
-- add generated assignement+infix operator combos, before the rest
local assignment_operators = r("infix.assignment_with_infix")
for i, op in ipairs(assignment_operators) do
table.insert(secondaries, i, op)
end
return {
-- returns exp, rem if expression found
-- returns nil if no expression found
-- returns nil, err if error
search = function(self, source, str, limit_pattern, current_priority, primary)
for _, secondary in ipairs(secondaries) do
local exp, rem = secondary:search(source, str, limit_pattern, current_priority, primary)
if exp then return exp, rem end
end
end
}

View file

@ -0,0 +1,34 @@
local class = require("class")
return class {
new = false, -- static class
-- returns exp, rem if expression found
-- returns nil if no expression found
search = function(self, source, str, limit_pattern, current_priority, operating_on_primary)
if not self:match(str, current_priority, operating_on_primary) then
return nil
end
return self:parse(source, str, limit_pattern, current_priority, operating_on_primary)
end,
-- return bool
-- (not needed if you redefined :search)
match = function(self, str, current_priority, operating_on_primary)
return false
end,
-- return AST, rem
-- (not needed if you redefined :search)
-- assumes that :match was checked before, and can not return nil (may error though)
parse = function(self, source, str, limit_pattern, current_priority, operating_on_primary)
error("unimplemented")
end,
-- class helpers --
-- return AST, rem
expect = function(self, source, str, limit_pattern, current_priority, operating_on_primary)
local exp, rem = self:search(source, str, limit_pattern, current_priority, operating_on_primary)
if not exp then error(("expected %s but got %s"):format(self.type, str)) end
return exp, rem
end
}

View file

@ -0,0 +1,40 @@
-- index/call
local secondary = require("parser.expression.secondary.secondary")
local parenthesis = require("parser.expression.primary.parenthesis")
local operator_priority = require("common").operator_priority
local ast = require("ast")
local Call, ArgumentTuple, Tuple, Assignment, Nil = ast.Call, ast.ArgumentTuple, ast.Tuple, ast.Assignment, ast.Nil
return secondary {
priority = operator_priority["_()"],
match = function(self, str, current_priority, primary)
return self.priority > current_priority and parenthesis:match(str)
end,
parse = function(self, source, str, limit_pattern, current_priority, primary)
local start_source = source:clone()
local args = ArgumentTuple:new()
local exp, rem = parenthesis:parse(source, str, limit_pattern)
if Nil:is(exp) then
exp = Tuple:new()
elseif not Tuple:is(exp) or exp.explicit then -- single argument
exp = Tuple:new(exp)
end
for i, v in ipairs(exp.list) do
if Assignment:is(v) then
args:set_named(v.identifier, v.expression)
else
args:set_positional(i, v)
end
end
return Call:new(primary, args):set_source(start_source), rem
end
}

View file

@ -0,0 +1,15 @@
local suffix = require("parser.expression.secondary.suffix.suffix")
local operator_priority = require("common").operator_priority
local ast = require("ast")
local Call, ArgumentTuple = ast.Call, ast.ArgumentTuple
return suffix {
operator = "!",
priority = operator_priority["_!"],
build_ast = function(self, left)
return Call:new(left, ArgumentTuple:new())
end
}

View file

@ -0,0 +1,9 @@
local suffix = require("parser.expression.secondary.suffix.suffix")
local operator_priority = require("common").operator_priority
return suffix {
operator = ";",
identifier = "_;",
priority = operator_priority["_;"]
}

View file

@ -0,0 +1,31 @@
-- unary suffix operators, for example the ! in func!
local secondary = require("parser.expression.secondary.secondary")
local escape = require("common").escape
local ast = require("ast")
local Call, Identifier, ArgumentTuple = ast.Call, ast.Identifier, ast.ArgumentTuple
return secondary {
operator = nil,
identifier = nil,
priority = nil,
match = function(self, str, current_priority, primary)
local escaped = escape(self.operator)
return self.priority > current_priority and str:match("^"..escaped)
end,
parse = function(self, source, str, limit_pattern, current_priority, primary)
local start_source = source:clone()
local escaped = escape(self.operator)
local rem = source:consume(str:match("^("..escaped..")(.*)$"))
return self:build_ast(primary):set_source(start_source), rem
end,
build_ast = function(self, left)
return Call:new(Identifier:new(self.identifier), ArgumentTuple:new(left))
end
}

View file

@ -0,0 +1,50 @@
--- transform an expression string into raw AST
local primary, secondary
local comment = require("parser.expression.comment")
-- parse an expression, starting from a secondary element operating on operating_on_primary
-- returns expr, remaining
local function from_secondary(source, s, limit_pattern, current_priority, operating_on_primary)
s = source:consume(s:match("^(%s*)(.*)$"))
current_priority = current_priority or 0
-- if there is a comment, restart the parsing after the comment ends
local c, c_rem = comment:search(source, s, limit_pattern)
if c then return from_secondary(source, c_rem, limit_pattern, current_priority, operating_on_primary) end
-- secondary elements
local exp, rem = secondary:search(source, s, limit_pattern, current_priority, operating_on_primary)
if exp then return from_secondary(source, rem, limit_pattern, current_priority, exp) end
-- nothing to apply on primary
return operating_on_primary, s
end
--- parse an expression
-- current_priority: only elements of strictly higher priority will be parser
-- limit_pattern: set to a string pattern that will trigger the end of elements that would otherwise consume everything until end-of-line (pattern is not consumed)
-- fallback_exp: if no primary expression can be found, will return this instead. Used to avoid raising an error where an empty or comment-only expression is allowed.
-- return expr, remaining
local function expression_to_ast(source, s, limit_pattern, current_priority, fallback_exp)
s = source:consume(s:match("^(%s*)(.*)$"))
current_priority = current_priority or 0
-- if there is a comment, restart the parsing after the comment ends
local c, c_rem = comment:search(source, s, limit_pattern)
if c then return expression_to_ast(source, c_rem, limit_pattern, current_priority, fallback_exp) end
-- primary elements
local exp, rem = primary:search(source, s, limit_pattern)
if exp then return from_secondary(source, rem, limit_pattern, current_priority, exp) end
-- no valid primary expression
if fallback_exp then return fallback_exp, s end
error(("no valid expression before %q"):format(s), 0)
end
package.loaded[...] = expression_to_ast
primary = require("parser.expression.primary")
secondary = require("parser.expression.secondary")
-- return expr, remaining
return function(source, s, limit_pattern, current_priority, operating_on_primary, fallback_exp)
if operating_on_primary then return from_secondary(source, s, limit_pattern, current_priority, operating_on_primary)
else return expression_to_ast(source, s, limit_pattern, current_priority, fallback_exp) end
end

13
parser/init.lua Normal file
View file

@ -0,0 +1,13 @@
local code_to_tree = require("parser.code_to_tree")
local tree_to_ast = require("parser.tree_to_ast")
-- parse code (string) with the associated source (Source)
-- the returned AST tree is stateless and can be stored/evaluated/etc as you please
return function(code, source)
local tree = code_to_tree(code, source)
local block = tree_to_ast(tree)
block:prepare()
return block
end

View file

@ -1,110 +0,0 @@
local expression
local parse_text
-- * true: if success
-- * nil, error: in case of error
local function parse(state)
-- expression parsing
for i=#state.queued_lines, 1, -1 do
local l = state.queued_lines[i]
local line, namespace = l.line, l.namespace
-- default arguments and type constraints
if line.type == "function" then
for _, param in ipairs(line.params) do
-- get type constraints
if param.type_constraint then
local type_exp, rem = expression(param.type_constraint, state, namespace, line.source)
if not type_exp then return nil, ("in type constraint, %s; at %s"):format(rem, line.source) end
if rem:match("[^%s]") then
return nil, ("unexpected characters after parameter %q: %q; at %s"):format(param.full_name, rem, line.source)
end
state.variable_metadata[param.full_name].constraint = { pending = type_exp }
end
-- get default value
if param.default then
local default_exp, rem = expression(param.default, state, namespace, line.source)
if not default_exp then return nil, ("in default value, %s; at %s"):format(rem, line.source) end
if rem:match("[^%s]") then
return nil, ("unexpected characters after parameter %q: %q; at %s"):format(param.full_name, rem, line.source)
end
param.default = default_exp
-- extract type constraint from default value
if default_exp.type == "function call" and default_exp.called_name == "_::_" then
state.variable_metadata[param.full_name].constraint = { pending = default_exp.argument.expression.right }
end
end
end
-- assignment argument
if line.assignment and line.assignment.type_constraint then
local type_exp, rem = expression(line.assignment.type_constraint, state, namespace, line.source)
if not type_exp then return nil, ("in type constraint, %s; at %s"):format(rem, line.source) end
if rem:match("[^%s]") then
return nil, ("unexpected characters after parameter %q: %q; at %s"):format(line.assignment.full_name, rem, line.source)
end
state.variable_metadata[line.assignment.full_name].constraint = { pending = type_exp }
end
-- get list of scoped variables
-- (note includes every variables in the namespace of subnamespace, so subfunctions are scoped alongside this function)
if line.scoped then
line.scoped = {}
for name in pairs(state.variables) do
if name:sub(1, #namespace) == namespace then
if state.variable_metadata[name].persistent then return nil, ("variable %q can not be persistent as it is in a scoped function"):format(name) end
table.insert(line.scoped, name)
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 and type(line.expression) == "string" then
local exp, rem = expression(line.expression, state, namespace, line.source)
if not exp then return nil, ("%s; at %s"):format(rem, line.source) end
if rem:match("[^%s]") then return nil, ("expected end of expression before %q; at %s"):format(rem, line.source) end
line.expression = exp
-- variable pending definition: expression will be evaluated when variable is needed
if line.type == "definition" then
state.variables[line.name].value.expression = line.expression
-- parse constraints
if line.constraint then
local type_exp, rem2 = expression(line.constraint, state, namespace, line.source)
if not type_exp then return nil, ("in type constraint, %s; at %s"):format(rem2, line.source) end
if rem2:match("[^%s]") then
return nil, ("unexpected characters after variable %q: %q; at %s"):format(line.name, rem2, line.source)
end
state.variable_metadata[line.name].constraint = { pending = type_exp }
end
end
end
-- text (text & choice lines)
if line.text then
local txt, err = parse_text(line.text, state, namespace, "text", "#~", true)
if not txt then return nil, ("%s; at %s"):format(err, line.source) end
if err:match("[^%s]") then return nil, ("expected end of expression in end-of-text expression before %q"):format(err) end
line.text = txt
end
table.remove(state.queued_lines, i)
end
if #state.queued_lines > 0 then -- lines were added during post-parsing, process these
return parse(state)
else
return true
end
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

View file

@ -1,569 +0,0 @@
local format_identifier, identifier_pattern, escape, special_functions_names, pretty_signature, signature, copy, injections
local parse_indented
--- try to define an alias using rem, the text that follows the identifier
-- returns true, new_rem, alias_name in case of success
-- returns true, rem in case of no alias and no error
-- returns nil, err in case of alias and error
local function maybe_alias(rem, fqm, namespace, line, state)
local alias
if rem:match("^%:[^%:%=]") then
local param_content = rem:sub(2)
alias, rem = param_content:match("^("..identifier_pattern..")(.-)$")
if not alias then return nil, ("expected an identifier in alias, but got %q; at %s"):format(param_content, line.source) end
alias = format_identifier(alias)
-- format alias
local aliasfqm = ("%s%s"):format(namespace, alias)
-- define alias
if state.aliases[aliasfqm] ~= nil and state.aliases[aliasfqm] ~= fqm then
return nil, ("trying to define alias %q for %q, but already exist and refer to %q; at %s"):format(aliasfqm, fqm, state.aliases[aliasfqm], line.source)
end
state.aliases[aliasfqm] = fqm
end
return true, rem, alias
end
--- inject lines defined for the injection that match parent_function type and inject_type in inject_in starting from index inject_at
local function inject(state, parent_function, inject_type, inject_in, inject_at)
inject_at = inject_at or #inject_in+1
local prefix
if parent_function.subtype == "checkpoint" then
prefix = "checkpoint"
elseif parent_function.subtype == "class" then
prefix = "class"
elseif parent_function.scoped then
prefix = "scoped_function"
else
prefix = "function"
end
local ninject = ("%s_%s"):format(prefix, inject_type)
if state.inject[ninject] then
for i, ll in ipairs(state.inject[ninject]) do
table.insert(inject_in, inject_at+i-1, copy(ll))
end
end
end
--- parse a single line into AST
-- * ast: if success
-- * nil, error: in case of error
local function parse_line(line, state, namespace, parent_resumable, in_scoped)
local l = line.content
local r = {
source = line.source
}
-- else-condition, condition & while
if l:match("^~[~%?]?") then
if l:match("^~~") then
r.type = "else-condition"
elseif l:match("^~%?") then
r.type = "while"
else
r.type = "condition"
end
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.child = true
r.text = l:match("^>%s*(.-)$")
-- definition
elseif l:match("^:") then
local lr = l:match("^:(.*)$")
-- immediately run variable
local run_immediately = false
if lr:match("^~") then
lr = lr:match("^~(.*)$")
run_immediately = true
end
-- function & checkpoint
if lr:match("^%$") or lr:match("^%!") or lr:match("^%%") then -- § is a 2-bytes caracter, DO NOT USE LUA PATTERN OPERATORS as they operate on single bytes
r.type = "function"
r.child = true
-- subtype options
local allow_params = true
local allow_assign = true
local keep_in_ast = false
if lr:match("^%$") then
r.subtype = "function"
r.resumable = true
elseif lr:match("^%%") then
r.subtype = "class"
r.resumable = true
r.properties = true
allow_params = false
allow_assign = false
elseif lr:match("^%!") then
r.subtype = "checkpoint"
allow_params = false
allow_assign = false
keep_in_ast = true
if not parent_resumable then return nil, ("checkpoint definition line is not in a function; at %s"):format(line.source) end
r.parent_resumable = parent_resumable -- store parent resumable function and run checkpoint when line is read
else
error("unknown function line type")
end
-- don't keep function node in block AST
if not keep_in_ast then
r.remove_from_block_ast = true
end
-- lua function
if r.subtype == "function" and state.global_state.link_next_function_definition_to_lua_function then
r.lua_function = state.global_state.link_next_function_definition_to_lua_function
state.global_state.link_next_function_definition_to_lua_function = nil
end
-- get identifier
local lc = lr:match("^[%$%%%!](.-)$")
local identifier, rem = lc:match("^("..identifier_pattern..")(.-)$")
if not identifier then
for _, name in ipairs(special_functions_names) do
identifier, rem = lc:match("^(%s*"..escape(name).."%s*)(.-)$")
if identifier then break end
end
end
if not identifier then
return nil, ("no valid identifier in function definition line %q; at %s"):format(lc, line.source)
end
-- format identifier
local fqm = ("%s%s"):format(namespace, format_identifier(identifier))
local func_namespace = fqm .. "."
-- get alias
local ok_alias
ok_alias, rem = maybe_alias(rem, fqm, namespace, line, state)
if not ok_alias then return ok_alias, rem end
-- anything else are argument, isolate function it its own namespace
-- (to not mix its args and variables with the main variant)
if rem:match("[^%s]") then
func_namespace = ("%s(%s)."):format(fqm, tostring(r))
r.private_namespace = true
end
-- define function
if state.variables[fqm] then return nil, ("trying to define %s %s, but a variable with the same name exists; at %s"):format(r.type, fqm, line.source) end
r.namespace = func_namespace
r.name = fqm
-- get params
r.params = {}
if allow_params and rem:match("^%b()") then
r.scoped = true
local content
content, rem = rem:match("^(%b())%s*(.*)$")
content = content:gsub("^%(", ""):gsub("%)$", "")
for param in content:gmatch("[^%,]+") do
-- get identifier
local param_identifier, param_rem = param:match("^("..identifier_pattern..")(.-)$")
if not param_identifier then return nil, ("no valid identifier in function parameter %q; at %s"):format(param, line.source) end
param_identifier = format_identifier(param_identifier)
-- format identifier
local param_fqm = ("%s%s"):format(func_namespace, param_identifier)
-- get alias
local ok_param_alias, param_alias
ok_param_alias, param_rem, param_alias = maybe_alias(param_rem, param_fqm, func_namespace, line, state)
if not ok_param_alias then return ok_param_alias, param_rem end
-- get potential type constraints and default value
local type_constraint, default
if param_rem:match("^::") then
type_constraint = param_rem:match("^::(.*)$")
elseif param_rem:match("^=") then
default = param_rem:match("^=(.*)$")
elseif param_rem:match("[^%s]") then
return nil, ("unexpected characters after parameter %q: %q; at %s"):format(param_fqm, param_rem, line.source)
end
-- add parameter
table.insert(r.params, { name = param_identifier, alias = param_alias, full_name = param_fqm, type_constraint = type_constraint, default = default, vararg = nil })
end
end
-- get assignment param
if allow_assign and rem:match("^%:%=") then
local param = rem:match("^%:%=(.*)$")
-- get identifier
local param_identifier, param_rem = param:match("^("..identifier_pattern..")(.-)$")
if not param_identifier then return nil, ("no valid identifier in function parameter %q; at %s"):format(param, line.source) end
param_identifier = format_identifier(param_identifier)
-- format identifier
local param_fqm = ("%s%s"):format(func_namespace, param_identifier)
-- get alias
local ok_param_alias, param_alias
ok_param_alias, param_rem, param_alias = maybe_alias(param_rem, param_fqm, func_namespace, line, state)
if not ok_param_alias then return ok_param_alias, param_rem end
-- get potential type constraint
local type_constraint
if param_rem:match("^::") then
type_constraint = param_rem:match("^::(.*)$")
elseif param_rem:match("[^%s]") then
return nil, ("unexpected characters after parameter %q: %q; at %s"):format(param_fqm, param_rem, line.source)
end
-- add parameter
r.assignment = { name = param_identifier, alias = param_alias, full_name = param_fqm, type_constraint = type_constraint, default = nil, vararg = nil }
elseif rem:match("[^%s]") then
return nil, ("expected end-of-line at end of function definition line, but got %q; at %s"):format(rem, line.source)
end
-- calculate arity
local minarity, maxarity = #r.params, #r.params
for _, param in ipairs(r.params) do -- params with default values
if param.default then
minarity = minarity - 1
end
end
-- varargs
if maxarity > 0 and r.params[maxarity].full_name:match("%.%.%.$") then
r.params[maxarity].name = r.params[maxarity].name:match("^(.*)%.%.%.$")
r.params[maxarity].full_name = r.params[maxarity].full_name:match("^(.*)%.%.%.$")
r.params[maxarity].vararg = true
minarity = minarity - 1
maxarity = math.huge
end
r.arity = { minarity, maxarity }
r.signature = signature(r)
r.pretty_signature = pretty_signature(r)
-- check for signature conflict with functions with the same fqm
if state.functions[fqm] then
for _, variant in ipairs(state.functions[fqm]) do
if r.signature == variant.signature then
return nil, ("trying to define %s %s, but another function with same signature %s exists; at %s"):format(r.type, r.pretty_signature, variant.pretty_signature, line.source)
end
end
end
-- define variables
if not line.children then line.children = {} end
local scoped = in_scoped or r.scoped
-- define 👁️ variable
local seen_alias = state.global_state.builtin_aliases["👁️"]
table.insert(line.children, 1, { content = (":%s👁%s=0"):format(scoped and "" or "@", seen_alias and ":"..seen_alias or ""), source = line.source })
-- define 🔖 variable
if r.resumable then
local checkpoint_alias = state.global_state.builtin_aliases["🔖"]
table.insert(line.children, 1, { content = (":%s🔖%s=()"):format(scoped and "" or "@", checkpoint_alias and ":"..checkpoint_alias or ""), source = line.source })
end
-- define 🏁 variable
if r.subtype == "checkpoint" then
local reached_alias = state.global_state.builtin_aliases["🏁"]
table.insert(line.children, 1, { content = (":%s🏁%s=0"):format(scoped and "" or "@", reached_alias and ":"..reached_alias or ""), source = line.source })
end
-- custom code injection
inject(state, r, "start", line.children, 2)
inject(state, r, "end", line.children)
-- update 👁️ variable
table.insert(line.children, { content = "~👁️+=1", source = line.source })
-- define args
for _, param in ipairs(r.params) do
if not state.variables[param.full_name] then
state.variables[param.full_name] = {
type = "undefined argument",
value = nil
}
state.variable_metadata[param.full_name] = {}
else
return nil, ("trying to define parameter %q, but a variable with the same name exists; at %s"):format(param.full_name, line.source)
end
end
if r.assignment then
if not state.variables[r.assignment.full_name] then
state.variables[r.assignment.full_name] = {
type = "undefined argument",
value = nil
}
state.variable_metadata[r.assignment.full_name] = {}
else
return nil, ("trying to define parameter %q, but a variable with the same name exists; at %s"):format(r.assignment.full_name, line.source)
end
end
-- define new function, no other variant yet
if not state.functions[fqm] then
state.functions[fqm] = { r }
-- overloading
else
table.insert(state.functions[fqm], r)
end
-- variable and constants
else
r.type = "definition"
r.remove_from_block_ast = true
local rem = lr
-- check if constant
if rem:match("^:") then
rem = rem:match("^:(.*)$")
r.constant = true
elseif rem:match("^@") then
rem = rem:match("^@(.*)$")
r.persistent = true
end
-- get identifier
local identifier
identifier, rem = rem:match("^("..identifier_pattern..")(.-)$")
if not identifier then return nil, ("no valid identifier at start of definition line %q; at %s"):format(l, line.source) end
-- format identifier
local fqm = ("%s%s"):format(namespace, format_identifier(identifier))
-- get alias
local ok_alias
ok_alias, rem = maybe_alias(rem, fqm, namespace, line, state)
if not ok_alias then return ok_alias, rem end
-- type constraint
if rem:match("^::(.-)=") then
r.constraint, rem = rem:match("^::%s*(.-)%s*(=.*)$")
end
-- get expression
local exp = rem:match("^=(.*)$")
if not exp then return nil, ("expected \"= expression\" after %q in definition line; at %s"):format(rem, line.source) end
-- define identifier
if state.functions[fqm] then return nil, ("trying to define variable %q, but a function with the same name exists; at %s"):format(fqm, line.source) end
if state.variables[fqm] then
if state.variables[fqm].type == "pending definition" then
return nil, ("trying to define variable %q but it is already defined at %s; at %s"):format(fqm, state.variables[fqm].value.source, line.source)
else
return nil, ("trying to define variable %q but it is already defined; at %s"):format(fqm, line.source)
end
end
r.name = fqm
r.expression = exp
state.variables[fqm] = { type = "pending definition", value = { expression = nil, source = r.source } }
state.variable_metadata[fqm] = {}
if r.constant then state.variable_metadata[fqm].constant = true end
if r.persistent then state.variable_metadata[fqm].persistent = true end
end
-- add expression line after to perform the immediate execution
if run_immediately then
line.line_after = { content = "~ "..r.name, source = line.source }
end
-- tag
elseif l:match("^%#") then
r.type = "tag"
r.child = true
local expr = l:match("^%#(.*)$")
r.expression = ("{%s}"):format(expr)
-- return
elseif l:match("^%@") then
if not parent_resumable then return nil, ("return line is not in a function; at %s"):format(line.source) end
r.type = "return"
r.child = true
local expr = l:match("^%@(.*)$")
if expr:match("[^%s]") then
r.expression = expr
else
r.expression = "()"
end
-- custom code injection
if not line.children then line.children = {} end
inject(state, parent_resumable, "return", line.children)
-- update 👁️ variable
table.insert(line.children, { content = "~👁️+=1", source = line.source })
-- text
elseif l:match("[^%s]") then
r.type = "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.source) end
return r
end
--- parse an indented into final AST
-- * block: in case of success
-- * nil, err: in case of error
local function parse_block(indented, state, namespace, parent_resumable, in_scoped)
local block = { type = "block" }
for i, l in ipairs(indented) do
-- parsable line
local ast, err = parse_line(l, state, namespace, parent_resumable, in_scoped)
if err then return nil, err end
-- add to block AST
if not ast.remove_from_block_ast then
ast.parent_block = block
-- add ast node
ast.parent_position = #block+1
table.insert(block, ast)
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 block
if l.children then
if not ast.child then
return nil, ("line %s (%s) can't have children"):format(ast.source, ast.type)
else
local r, e = parse_block(l.children, state, ast.namespace or namespace, (ast.type == "function" and ast.resumable) and ast or parent_resumable, (ast.type == "function" and ast.scoped) or in_scoped)
if not r then return r, e end
r.parent_line = ast
ast.child = r
end
end
-- insert line after
if l.line_after then
table.insert(indented, i+1, l.line_after)
end
end
return block
end
-- returns new_indented
local function transform_indented(indented)
local i = 1
while i <= #indented do
local l = indented[i]
-- comment
if l.content:match("^%(") then
table.remove(indented, i)
else
i = i + 1
-- indented block
if l.children then
transform_indented(l.children)
end
end
end
return indented
end
--- returns the nested list of lines {content="", line=1, children={lines...} or nil}, parsing indentation
-- multiple empty lines are merged
-- * list, last line, insert_empty_line: in case of success
-- * nil, err: in case of error
local function parse_indent(lines, source, 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 = "", source = ("%s:%s"):format(source, insert_empty_line) })
insert_empty_line = false
end
table.insert(indented, { content = line, source = ("%s:%s"):format(source, i) })
elseif #indent > indentLevel then
if #indented == 0 then
return nil, ("unexpected indentation; at %s:%s"):format(source, i)
else
local t
t, i, insert_empty_line = parse_indent(lines, source, i, #indent, insert_empty_line)
if not t then return nil, i end
indented[#indented].children = t
end
else
return indented, i-1, insert_empty_line
end
elseif not insert_empty_line then
insert_empty_line = i
end
i = i + 1
end
return indented, i-1, insert_empty_line
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
--- make indented from intial string
-- * list: in case of success
-- * nil, err: in case of error
parse_indented = function(s, fnname, source)
source = source or fnname
-- parse lines
local lines = parse_lines(s)
local indented, e = parse_indent(lines, source)
if not indented then return nil, e end
-- wrap in named function if neccessary
if fnname ~= nil and fnname ~= "" then
if not fnname:match("^"..identifier_pattern.."$") then
return nil, ("invalid function name %q"):format(fnname)
end
indented = {
{ content = ":$ "..fnname, source = ("%s:%s"):format(source, 0), children = indented },
}
end
-- transform ast
indented = transform_indented(indented)
return indented
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)
-- * block: in case of success
-- * nil, err: in case of error
local function parse(state, s, name, source)
-- get indented
local indented, e = parse_indented(s, name, source)
if not indented then return nil, e end
-- build state proxy
local state_proxy = {
inject = {},
aliases = setmetatable({}, { __index = state.aliases }),
variable_metadata = setmetatable({}, { __index = state.variable_metadata }),
variables = setmetatable({}, { __index = state.aliases }),
functions = setmetatable({}, {
__index = function(self, key)
if state.functions[key] then
local t = {} -- need to copy to allow ipairs over variants
for k, v in ipairs(state.functions[key]) do
t[k] = v
end
self[key] = t
return t
end
return nil
end
}),
queued_lines = {},
global_state = state
}
-- parse injects
for tinject, ninject in pairs(injections) do
if state.inject[ninject] then
local inject_indented, err = parse_indented(state.inject[ninject], nil, "injected "..tinject)
if not inject_indented then return nil, err end
state_proxy.inject[ninject] = inject_indented
end
end
-- parse
local root, err = parse_block(indented, state_proxy, "")
if not root then return nil, err end
-- merge back state proxy into global state
for k,v in pairs(state_proxy.aliases) do
state.aliases[k] = v
end
for k,v in pairs(state_proxy.variable_metadata) do
state.variable_metadata[k] = v
end
for k,v in pairs(state_proxy.variables) do
state.variables[k] = v
end
for k,v in pairs(state_proxy.functions) do
if not state.functions[k] then
state.functions[k] = v
else
for i,w in ipairs(v) do
state.functions[k][i] = w
end
end
end
for _,l in ipairs(state_proxy.queued_lines) do
table.insert(state.queued_lines, l)
end
-- return block
return root
end
package.loaded[...] = parse
local common = require((...):gsub("preparser$", "common"))
format_identifier, identifier_pattern, escape, special_functions_names, pretty_signature, signature, injections = common.format_identifier, common.identifier_pattern, common.escape, common.special_functions_names, common.pretty_signature, common.signature, common.injections
copy = require((...):gsub("parser%.preparser$", "common")).copy
return parse

52
parser/tree_to_ast.lua Normal file
View file

@ -0,0 +1,52 @@
--- transform a tree of lines into raw AST
local tree_to_block
local ast = require("ast")
local Block, Flush, AttachBlock
local expression_to_ast = require("parser.expression.to_ast")
-- wrapper for expression_to_ast to check that there is no crap remaining after the expression has been parsed
-- return AST
local function expect_end(exp, rem)
if rem:match("[^%s]") then
error(("expected end of expression before %q"):format(rem))
end
return exp
end
local function expect_end_block(exp, rem)
if rem:match("[^%s]") and not rem:match("^ ?_$") then
error(("expected end of expression before %q"):format(rem))
end
return exp
end
-- return AST
local function line_to_expression(content, tree)
if #tree > 0 then
local child_block = tree_to_block(tree)
return AttachBlock:new(expect_end_block(expression_to_ast(tree.source:clone(), content.." _", " _$")), child_block):set_source(tree.source)
else
return expect_end(expression_to_ast(tree.source:clone(), content, nil, nil, nil, Flush:new())):set_source(tree.source)
end
end
-- return AST (Block)
tree_to_block = function(tree)
local block = Block:new()
for _, l in ipairs(tree) do
local s, expression = pcall(line_to_expression, l.content, l)
if not s then error(("%s; at %s"):format(expression, l.source), 0) end
block:add(expression)
end
return block
end
package.loaded[...] = tree_to_block
Block, Flush, AttachBlock = ast.Block, ast.Flush, ast.AttachBlock
return tree_to_block