defmodule Expression.Parser do
@moduledoc """
Expression.Parser is responsible for accepting a string
containing an expression and returning the abstract syntax
tree (AST) representing the expression.
The AST generated by this module can be evaluated by
Expression.Eval
# Example
iex(1)> Expression.Parser.parse("hello @world")
{:ok, [text: "hello ", expression: [atom: "world"]], "", %{}, {1, 0}, 12}
"""
import NimbleParsec
import Expression.BooleanHelpers
import Expression.DateHelpers
import Expression.LiteralHelpers
import Expression.OperatorHelpers
# literal = 1, 2.1, "three", 'four', true, false, ISO dates
defparsec(
:literal,
choice([
datetime(),
decimal(),
int(),
boolean(),
single_quoted_string(),
double_quoted_string()
])
|> unwrap_and_tag(:literal)
)
# atom = atom
atom =
ascii_string([?a..?z, ?A..?Z, ?0..?9], min: 1)
|> ascii_string([?a..?z, ?A..?Z, ?0..?9, ?_, ?-], min: 0)
|> map({String, :downcase, []})
|> reduce({Enum, :join, []})
range =
int()
|> ignore(string(".."))
|> concat(int())
|> optional(
ignore(string("//"))
|> concat(int())
)
|> tag(:range)
whitespace = choice([string(" "), string("\n"), string("\r")])
ignore_surrounding_whitespace = fn p ->
ignore(repeat(whitespace))
|> concat(p)
|> ignore(repeat(whitespace))
end
# argument separator = ", "
argument_separator =
string(",")
|> ignore_surrounding_whitespace.()
lambda_capture =
ignore(string("&"))
|> concat(int())
|> unwrap_and_tag(:capture)
# arguments = expression "," expression
defparsec(
:arguments,
parsec(:aexpr)
|> optional(ignore(argument_separator) |> parsec(:arguments))
)
primitives =
choice([
lambda_capture,
range,
parsec(:lambda),
parsec(:function),
parsec(:literal),
parsec(:variable),
parsec(:list)
])
defparsec(
:aexpr_factor,
choice([
primitives,
ignore(string("(")) |> parsec(:aexpr) |> ignore(string(")"))
])
|> ignore_surrounding_whitespace.()
)
defparsec(
:key,
ignore(ascii_char([91]))
|> replace(:key)
|> parsec(:aexpr)
|> ignore(ascii_char([93]))
|> label("[..]")
)
defparsec(
:attribute,
ascii_char([?.])
|> replace(:attribute)
|> label(".")
)
attribute_or_key =
repeat(
choice([
parsec(:attribute) |> parsec(:aexpr_factor),
parsec(:key)
])
)
# The difference between this one and the one above
# is that this one does not allow for spaces or arithmatic
# which makes it suitable for use in `@foo` type expressions
# because otherwise `info@support.com for` (note the space)
# is parsed as being part of the expression.
#
# That would be wrong since spaces are only allowed in
# expressions starting with brackets like `@( ... )`
attribute_or_key_with_primitives_only =
repeat(
choice([
parsec(:attribute) |> concat(primitives),
parsec(:key)
])
)
defparsec(
:aexpr_exponent,
parsec(:aexpr_factor)
|> optional(attribute_or_key)
|> repeat(
exponent()
|> parsec(:aexpr_factor)
|> optional(attribute_or_key)
)
|> reduce(:fold_infixl)
)
defparsec(
:aexpr_term,
parsec(:aexpr_exponent)
|> repeat(
choice([times(), divide()])
|> ignore_surrounding_whitespace.()
|> parsec(:aexpr_exponent)
)
|> reduce(:fold_infixl)
)
defparsec(
:aexpr,
parsec(:aexpr_term)
|> repeat(
choice([
plus(),
minus(),
concatenate(),
gte(),
lte(),
neq(),
gt(),
lt(),
eq()
])
|> parsec(:aexpr_term)
|> ignore_surrounding_whitespace.()
)
|> reduce(:fold_infixl)
)
def fold_infixl(acc) do
acc
|> Enum.reverse()
|> Enum.chunk_every(2)
|> List.foldr([], fn
[l], [] -> l
[r, op], l -> {op, [l, r]}
end)
end
defparsec(
:list,
ignore(string("["))
|> optional(parsec(:arguments) |> tag(:args))
|> ignore(string("]"))
|> tag(:list)
)
# function = "(" arguments ")"
defparsec(
:function,
atom
|> unwrap_and_tag(:name)
|> ignore(string("("))
|> optional(parsec(:arguments) |> tag(:args))
|> ignore(string(")"))
|> tag(:function)
)
defparsec(
:lambda,
ignore(string("&"))
|> optional(parsec(:arguments) |> tag(:args))
|> tag(:lambda)
)
# variable = atom
defparsec(
:variable,
atom
|> unwrap_and_tag(:atom)
)
expression_block =
ignore(string("@"))
|> lookahead_not(string("@"))
|> ignore(string("("))
|> parsec(:aexpr)
|> ignore(string(")"))
|> tag(:expression)
expression =
ignore(string("@"))
|> lookahead_not(string("@"))
|> repeat(
choice([
parsec(:list),
parsec(:function),
parsec(:variable)
])
|> optional(attribute_or_key_with_primitives_only)
)
|> reduce(:fold_infixl)
|> tag(:expression)
escaped_at =
ignore(string("@"))
|> string("@")
|> unwrap_and_tag(:text)
text =
empty()
|> lookahead_not(string("@"))
|> utf8_string([], 1)
|> times(min: 1)
|> reduce({Enum, :join, []})
|> unwrap_and_tag(:text)
defparsec(:parse, repeat(choice([expression_block, expression, escaped_at, text])))
end