Packages
boxic_feel
0.1.0
A native Elixir parser and evaluator for the Friendly Enough Expression Language defined by DMN.
Current section
Files
Jump to
Current section
Files
lib/boxic/feel/parser.ex
defmodule Boxic.FEEL.Parser do
@moduledoc false
alias Boxic.FEEL.Error
def parse_expression([:if | _] = tokens), do: parse_if(tokens)
def parse_expression([:for | _] = tokens), do: parse_for(tokens)
def parse_expression([:some | _] = tokens), do: parse_quantifier(tokens)
def parse_expression([:every | _] = tokens), do: parse_quantifier(tokens)
def parse_expression([:function | _] = tokens), do: parse_function(tokens)
def parse_expression(tokens), do: parse_or(tokens)
defp parse_if([:if | rest]) do
with {:ok, condition, [:then | rest2]} <- parse_expression(rest),
{:ok, then_branch, [:else | rest3]} <- parse_expression(rest2),
{:ok, else_branch, rest4} <- parse_expression(rest3) do
{:ok, {:if, condition, then_branch, else_branch}, rest4}
else
_ -> {:error, error(:invalid_syntax, "invalid if-expression")}
end
end
defp parse_if(_tokens), do: {:error, error(:invalid_syntax, "invalid if-expression")}
defp parse_for([:for, {:identifier, variable}, :in | rest]) do
with {:ok, source, rest2} <- parse_for_source(rest),
{:ok, bindings, [:return | rest3]} <-
parse_for_bindings([{variable, source}], rest2),
{:ok, body, rest4} <- parse_expression(rest3) do
{:ok, {:for, bindings, body}, rest4}
else
_ -> {:error, error(:invalid_syntax, "invalid for-expression")}
end
end
defp parse_for(_tokens), do: {:error, error(:invalid_syntax, "invalid for-expression")}
defp parse_for_source(tokens) do
with {:ok, source, rest} <- parse_expression(tokens) do
case rest do
[:range_dots | rest2] ->
with {:ok, finish, rest3} <- parse_expression(rest2) do
{:ok, {:sequence, source, finish}, rest3}
end
_ ->
{:ok, source, rest}
end
end
end
defp parse_for_bindings(bindings, [:comma, {:identifier, variable}, :in | rest]) do
with {:ok, source, rest2} <- parse_for_source(rest) do
parse_for_bindings(bindings ++ [{variable, source}], rest2)
end
end
defp parse_for_bindings(bindings, rest), do: {:ok, bindings, rest}
defp parse_quantifier([kind, {:identifier, var}, :in | rest]) when kind in [:some, :every] do
with {:ok, source, [:satisfies | rest2]} <- parse_expression(rest),
{:ok, predicate, rest3} <- parse_expression(rest2) do
{:ok, {:quantifier, kind, var, source, predicate}, rest3}
else
_ -> {:error, error(:invalid_syntax, "invalid quantifier expression")}
end
end
defp parse_quantifier(_tokens),
do: {:error, error(:invalid_syntax, "invalid quantifier expression")}
defp parse_function([:function, :lparen | rest]) do
with {:ok, params, [:rparen | rest2]} <- parse_param_list(rest),
{:ok, body, rest3} <- parse_expression(rest2) do
{:ok, {:function, params, body}, rest3}
else
_ -> {:error, error(:invalid_syntax, "invalid function expression")}
end
end
defp parse_function(_tokens),
do: {:error, error(:invalid_syntax, "invalid function expression")}
defp parse_param_list([:rparen | _] = rest), do: {:ok, [], rest}
defp parse_param_list([{:identifier, param} | rest]) do
{type, rest} = take_parameter_type(rest)
parse_param_list_tail([{param, type}], rest)
end
defp parse_param_list(_tokens), do: {:error, error(:invalid_syntax, "invalid parameter list")}
defp parse_param_list_tail(params, [:comma, {:identifier, param} | rest]) do
{type, rest} = take_parameter_type(rest)
parse_param_list_tail(params ++ [{param, type}], rest)
end
defp parse_param_list_tail(params, rest), do: {:ok, params, rest}
defp take_parameter_type([:colon, {:identifier, type} | rest]), do: {type, rest}
defp take_parameter_type(rest), do: {nil, rest}
defp parse_or(tokens) do
with {:ok, left, rest} <- parse_and(tokens) do
parse_or_tail(left, rest)
end
end
defp parse_or_tail(left, [:or | rest]) do
with {:ok, right, rest2} <- parse_and(rest) do
parse_or_tail({:binary, :or, left, right}, rest2)
end
end
defp parse_or_tail(left, rest), do: {:ok, left, rest}
defp parse_and(tokens) do
with {:ok, left, rest} <- parse_equality(tokens) do
parse_and_tail(left, rest)
end
end
defp parse_and_tail(left, [:and | rest]) do
with {:ok, right, rest2} <- parse_equality(rest) do
parse_and_tail({:binary, :and, left, right}, rest2)
end
end
defp parse_and_tail(left, rest), do: {:ok, left, rest}
defp parse_equality(tokens) do
with {:ok, left, rest} <- parse_in_expression(tokens) do
parse_equality_tail(left, rest)
end
end
defp parse_equality_tail(left, [:eq | rest]) do
with {:ok, right, rest2} <- parse_in_expression(rest) do
parse_equality_tail({:binary, :eq, left, right}, rest2)
end
end
defp parse_equality_tail(left, [:neq | rest]) do
with {:ok, right, rest2} <- parse_in_expression(rest) do
parse_equality_tail({:binary, :neq, left, right}, rest2)
end
end
defp parse_equality_tail(left, rest), do: {:ok, left, rest}
defp parse_in_expression(tokens) do
with {:ok, left, rest} <- parse_comparison(tokens) do
case rest do
[:in | rest2] ->
with {:ok, tests, rest3} <- parse_in_tests(rest2) do
{:ok, {:in, left, tests}, rest3}
end
_ ->
{:ok, left, rest}
end
end
end
defp parse_in_tests([:lparen | _] = tokens) do
case parse_postfix(tokens) do
{:ok, expression, rest} -> {:ok, expression, rest}
{:error, _error} -> parse_unary_test_list(tl(tokens), [])
end
end
defp parse_in_tests(tokens), do: parse_unary_test(tokens)
defp parse_unary_test([op | rest]) when op in [:eq, :neq, :lt, :lte, :gt, :gte] do
with {:ok, operand, rest2} <- parse_addition(rest) do
{:ok, {:unary_test, op, operand}, rest2}
end
end
defp parse_unary_test(tokens), do: parse_comparison(tokens)
defp parse_unary_test_list([:rparen | rest], tests) when tests != [],
do: {:ok, {:unary_tests, Enum.reverse(tests)}, rest}
defp parse_unary_test_list(tokens, tests) do
with {:ok, test, rest} <- parse_unary_test(tokens) do
case rest do
[:comma | rest2] -> parse_unary_test_list(rest2, [test | tests])
[:rparen | rest2] -> {:ok, {:unary_tests, Enum.reverse([test | tests])}, rest2}
_ -> {:error, error(:invalid_syntax, "invalid unary-test list")}
end
end
end
defp parse_comparison(tokens) do
with {:ok, left, rest} <- parse_addition(tokens) do
parse_comparison_tail(left, rest)
end
end
defp parse_comparison_tail(left, [op | rest]) when op in [:gt, :lt, :gte, :lte] do
with {:ok, right, rest2} <- parse_addition(rest) do
parse_comparison_tail({:binary, op, left, right}, rest2)
end
end
defp parse_comparison_tail(left, [:between | rest]) do
with {:ok, lower, [:and | rest2]} <- parse_addition(rest),
{:ok, upper, rest3} <- parse_addition(rest2) do
{:ok, {:between, left, lower, upper}, rest3}
else
_ -> {:error, error(:invalid_syntax, "invalid between expression")}
end
end
defp parse_comparison_tail(left, [:instance, :of | rest]) when rest != [] do
{:ok, {:instance_of, left, type_tokens_to_string(rest)}, []}
end
defp parse_comparison_tail(left, rest), do: {:ok, left, rest}
defp type_tokens_to_string(tokens) do
tokens
|> Enum.map(fn
{:identifier, value} -> value
:lt -> "<"
:gt -> ">"
:comma -> ","
:colon -> ":"
:minus -> "-"
:and -> "and"
token -> Atom.to_string(token)
end)
|> Enum.join(" ")
|> String.replace(~r/\s*<\s*/, "<")
|> String.replace(~r/\s*>\s*/, ">")
|> String.replace(~r/\s*,\s*/, ",")
|> String.replace(~r/\s*:\s*/, ":")
|> String.replace(~r/\s*-\s*>/, "->")
end
defp parse_addition(tokens) do
with {:ok, left, rest} <- parse_multiplication(tokens) do
parse_addition_tail(left, rest)
end
end
defp parse_addition_tail(left, [:plus | rest]) do
with {:ok, right, rest2} <- parse_multiplication(rest) do
parse_addition_tail({:binary, :plus, left, right}, rest2)
end
end
defp parse_addition_tail(left, [:minus | rest]) do
with {:ok, right, rest2} <- parse_multiplication(rest) do
parse_addition_tail({:binary, :minus, left, right}, rest2)
end
end
defp parse_addition_tail(left, rest), do: {:ok, left, rest}
defp parse_multiplication(tokens) do
with {:ok, left, rest} <- parse_power(tokens) do
parse_multiplication_tail(left, rest)
end
end
defp parse_multiplication_tail(left, [:mul | rest]) do
with {:ok, right, rest2} <- parse_power(rest) do
parse_multiplication_tail({:binary, :mul, left, right}, rest2)
end
end
defp parse_multiplication_tail(left, [:div | rest]) do
with {:ok, right, rest2} <- parse_power(rest) do
parse_multiplication_tail({:binary, :div, left, right}, rest2)
end
end
defp parse_multiplication_tail(left, rest), do: {:ok, left, rest}
defp parse_unary([:not | rest]) do
with {:ok, expr, rest2} <- parse_unary(rest) do
{:ok, {:unary, :not, expr}, rest2}
end
end
defp parse_unary([:minus | rest]) do
with {:ok, expr, rest2} <- parse_unary(rest) do
{:ok, {:unary, :negate, expr}, rest2}
end
end
defp parse_unary(tokens), do: parse_postfix(tokens)
defp parse_power(tokens) do
with {:ok, base, rest} <- parse_unary(tokens) do
parse_power_tail(base, rest)
end
end
defp parse_power_tail(base, [:pow | rest]) do
with {:ok, exponent, rest2} <- parse_unary(rest) do
parse_power_tail({:binary, :pow, base, exponent}, rest2)
end
end
defp parse_power_tail(base, rest), do: {:ok, base, rest}
defp parse_postfix(tokens) do
with {:ok, base, rest} <- parse_primary(tokens) do
parse_postfix_tail(base, rest)
end
end
defp parse_postfix_tail(base, [:dot, {:identifier, key} | rest]) do
parse_postfix_tail({:path, base, key}, rest)
end
defp parse_postfix_tail(base, [:lbracket]), do: {:ok, base, [:lbracket]}
defp parse_postfix_tail(base, [:lbracket, operator | _] = rest)
when operator in [:eq, :neq, :gt, :gte, :lt, :lte],
do: {:ok, base, rest}
defp parse_postfix_tail(base, [:lbracket | rest]) do
with {:ok, predicate, [:rbracket | rest2]} <- parse_expression(rest) do
parse_postfix_tail({:filter, base, predicate}, rest2)
else
_ -> {:error, error(:invalid_syntax, "invalid bracket filter")}
end
end
defp parse_postfix_tail(base, [:lparen | rest]) do
with {:ok, args, [:rparen | rest2]} <- parse_call_args(rest) do
parse_postfix_tail({:call, base, args}, rest2)
else
_ -> {:error, error(:invalid_syntax, "invalid call expression")}
end
end
defp parse_postfix_tail(base, rest), do: {:ok, base, rest}
defp parse_call_args([:rparen | _] = rest), do: {:ok, [], rest}
defp parse_call_args([{:identifier, name}, :colon | rest]) do
with {:ok, value, rest2} <- parse_expression(rest) do
parse_call_args_tail([{:named_arg, name, value}], rest2)
end
end
defp parse_call_args(tokens) do
with {:ok, first, rest} <- parse_expression(tokens) do
parse_call_args_tail([first], rest)
end
end
defp parse_call_args_tail(args, [:comma | rest]) do
with {:ok, arg, rest2} <- parse_call_arg(rest) do
parse_call_args_tail(args ++ [arg], rest2)
end
end
defp parse_call_args_tail(args, rest), do: {:ok, args, rest}
defp parse_call_arg([{:identifier, name}, :colon | rest]) do
with {:ok, value, rest2} <- parse_expression(rest) do
{:ok, {:named_arg, name, value}, rest2}
end
end
defp parse_call_arg(tokens), do: parse_expression(tokens)
defp parse_primary([{:number, value} | rest]), do: {:ok, {:literal, value}, rest}
defp parse_primary([{:string, value} | rest]), do: {:ok, {:literal, value}, rest}
defp parse_primary([{:boolean, value} | rest]), do: {:ok, {:literal, value}, rest}
defp parse_primary([{:null, nil} | rest]), do: {:ok, {:literal, nil}, rest}
defp parse_primary([{:literal, value} | rest]), do: {:ok, {:literal, value}, rest}
defp parse_primary([{:identifier, name} | rest]), do: {:ok, {:identifier, name}, rest}
defp parse_primary([:lbrace | rest]), do: parse_context_literal(rest)
defp parse_primary([:lbracket | rest]), do: parse_list_or_range(rest)
defp parse_primary([:rbracket | rest]), do: parse_alternative_range(rest)
defp parse_primary([:lparen, op | rest]) when op in [:eq, :neq, :lt, :lte, :gt, :gte] do
with {:ok, operand, [:rparen | rest2]} <- parse_addition(rest) do
{:ok, {:unary_test, op, operand}, rest2}
else
_ -> {:error, error(:invalid_syntax, "invalid unary-test value")}
end
end
defp parse_primary([:lparen | rest]), do: parse_group_or_range(rest)
defp parse_primary(_tokens), do: {:error, error(:invalid_syntax, "expected expression")}
defp parse_context_literal([:rbrace | rest]), do: {:ok, {:context, []}, rest}
defp parse_context_literal(tokens) do
with {:ok, key, [:colon | rest]} <- parse_context_key(tokens),
{:ok, expr, rest2} <- parse_expression(rest),
{:ok, entries, rest3} <- parse_context_tail([{key, expr}], rest2) do
{:ok, {:context, entries}, rest3}
else
_ -> {:error, error(:invalid_syntax, "invalid context literal")}
end
end
defp parse_context_tail(entries, [:comma | rest]) do
with {:ok, key, [:colon | rest2]} <- parse_context_key(rest),
{:ok, expr, rest3} <- parse_expression(rest2) do
parse_context_tail(entries ++ [{key, expr}], rest3)
end
end
defp parse_context_tail(entries, [:rbrace | rest]), do: {:ok, entries, rest}
defp parse_context_tail(_entries, _rest),
do: {:error, error(:invalid_syntax, "invalid context literal")}
defp parse_context_key(tokens) do
{key_tokens, rest} = Enum.split_while(tokens, &(&1 != :colon))
case {context_key(key_tokens), rest} do
{{:ok, key}, [_colon | remaining]} -> {:ok, key, [:colon | remaining]}
_ -> {:error, error(:invalid_syntax, "invalid context key")}
end
end
defp context_key([{:string, key}]), do: {:ok, key}
defp context_key([]), do: :error
defp context_key(tokens) do
tokens
|> Enum.reduce_while({:ok, "", nil}, fn token, {:ok, acc, previous} ->
case context_key_token(token) do
{:word, value} ->
separator = if previous == :word, do: " ", else: ""
{:cont, {:ok, acc <> separator <> value, :word}}
{:operator, value} ->
{:cont, {:ok, acc <> value, :operator}}
:error ->
{:halt, :error}
end
end)
|> case do
{:ok, key, _previous} -> {:ok, key}
:error -> :error
end
end
defp context_key_token({:identifier, value}), do: {:word, value}
defp context_key_token(:plus), do: {:operator, "+"}
defp context_key_token(:minus), do: {:operator, "-"}
defp context_key_token(:mul), do: {:operator, "*"}
defp context_key_token(:div), do: {:operator, "/"}
defp context_key_token(_token), do: :error
defp parse_list_or_range([:rbracket | rest]), do: {:ok, {:list, []}, rest}
defp parse_list_or_range(tokens) do
with {:ok, first, rest} <- parse_expression(tokens) do
case rest do
[:range_dots | rest2] -> parse_range_rest(true, first, rest2)
_ -> parse_list_tail([first], rest)
end
end
end
defp parse_alternative_range(tokens) do
with {:ok, first, [:range_dots | rest]} <- parse_expression(tokens) do
parse_range_rest(false, first, rest)
else
_ -> {:error, error(:invalid_syntax, "invalid alternative range literal")}
end
end
defp parse_group_or_range(tokens) do
with {:ok, expr, rest} <- parse_expression(tokens) do
case rest do
[:range_dots | rest2] -> parse_range_rest(false, expr, rest2)
[:rparen | rest2] -> {:ok, expr, rest2}
_ -> {:error, error(:invalid_syntax, "missing closing parenthesis")}
end
end
end
defp parse_range_rest(start_inclusive, start_ast, [token, :lbracket | rest]) do
case token do
{:number, value} ->
{:ok, {:range, start_inclusive, false, start_ast, {:literal, value}}, rest}
{:string, value} ->
{:ok, {:range, start_inclusive, false, start_ast, {:literal, value}}, rest}
{:identifier, value} ->
{:ok, {:range, start_inclusive, false, start_ast, {:identifier, value}}, rest}
_ ->
{:error, error(:invalid_syntax, "invalid range end delimiter")}
end
end
defp parse_range_rest(start_inclusive, start_ast, tokens) do
with {:ok, finish_ast, rest} <- parse_addition(tokens) do
case rest do
[:rbracket | rest2] ->
{:ok, {:range, start_inclusive, true, start_ast, finish_ast}, rest2}
[:rparen | rest2] ->
{:ok, {:range, start_inclusive, false, start_ast, finish_ast}, rest2}
[:lbracket | rest2] ->
{:ok, {:range, start_inclusive, false, start_ast, finish_ast}, rest2}
_ ->
{:error, error(:invalid_syntax, "invalid range end delimiter")}
end
end
end
defp parse_list_tail(items, [:comma | rest]) do
with {:ok, next_item, rest2} <- parse_expression(rest) do
parse_list_tail(items ++ [next_item], rest2)
end
end
defp parse_list_tail(items, [:rbracket | rest]), do: {:ok, {:list, items}, rest}
defp parse_list_tail(_items, _rest),
do: {:error, error(:invalid_syntax, "invalid list literal")}
defp error(code, message), do: %Error{code: code, message: message}
end