lib/ex_pression.ex
defmodule ExPression do
@moduledoc """
Evaluate user input expression.
"""
alias ExPression.Error
alias ExPression.Interpreter
alias ExPression.Parser
@type ast() :: any()
@doc """
Parse expression in string format into AST format.
This can be used for optimizations: to parse expression once and evaluate AST many times.
"""
@spec parse(binary()) :: {:ok, ast()} | {:error, ExPression.Error.t()}
def parse(expression_str) when is_binary(expression_str) do
case Parser.parse(expression_str) do
{:ok, ast} ->
{:ok, ast}
{:error, {:parsing_error, rest}} ->
error = Error.new("SyntaxError", "Syntax Error: couldn't parse '#{rest}'", %{rest: rest})
{:error, error}
end
end
@doc """
Evaluate expression.
## Options
* `:bindings` - map variable names and values.
* `:functions_module` - module with functions that will be accessible from expressions.
## Examples
iex> eval("1 + 0.5")
{:ok, 1.5}
iex> eval("div(x, y)", bindings: %{"x" => 5, "y" => 2}, functions_module: Kernel)
{:ok, 2}
iex> eval(~s/{"1": "en", "2": "fr"}[str(int_code)]/, bindings: %{"int_code" => 1})
{:ok, "en"}
iex> eval("not true or false or 1 == 1")
{:ok, true}
iex> eval("exit(self())")
{:error, %ExPression.Error{name: "UndefinedFunctionError", message: "Function 'self/0' was referenced, but was not defined", data: %{function: :self}}}
"""
@spec eval(binary(), Keyword.t()) :: {:ok, any()} | {:error, ExPression.Error.t()}
def eval(expression_str, opts \\ []) when is_binary(expression_str) do
case parse(expression_str) do
{:ok, ast} -> eval_ast(ast, opts)
error -> error
end
end
@doc """
Evaluate expression given in AST format.
"""
@spec eval_ast(ast(), Keyword.t()) :: {:ok, any()} | {:error, ExPression.Error.t()}
def eval_ast(ast, opts \\ []) do
bindings = Keyword.get(opts, :bindings, %{})
functions_module = Keyword.get(opts, :functions_module)
case Interpreter.eval(ast, bindings, functions_module) do
{:ok, res} ->
{:ok, res}
{:error, error} ->
error = build_eval_error(error)
{:error, error}
end
end
defp build_eval_error({:var_not_bound, var}) do
Error.new("UndefinedVariableError", "Variable '#{var}' was used, but was not defined", %{
var: var
})
end
defp build_eval_error({:fun_not_defined, fun, arity}) do
Error.new(
"UndefinedFunctionError",
"Function '#{fun}/#{arity}' was referenced, but was not defined",
%{function: fun}
)
end
defp build_eval_error({:function_call_exception, fun, args, exception, msg}) do
Error.new(
"FunctionCallException",
"Function '#{fun}' called with args #{inspect(args)} raised exception: #{inspect(exception.__struct__)}",
%{function: fun, args: args, exception: exception, message: msg}
)
end
defp build_eval_error({:bad_op_arg_types, {op, args}}) do
Error.new(
"BadOperationArgumentTypes",
"Opeartion '#{op}' does not support argument types of #{inspect(args)}",
%{
operation: op,
arguments: args
}
)
end
defp build_eval_error({:special_without_module, special, value}) do
Error.new(
"SpecialWithoutModule",
"Special symbol '#{special}' used with value #{value}, but no functions module was provided",
%{
special: special,
value: value
}
)
end
end