Packages
expression
2.3.2
2.49.1
2.49.0
2.48.0
2.47.5
2.47.4
2.47.3
2.47.2
2.47.1
2.47.0
2.46.0
2.45.1
2.45.0
2.44.1
2.44.0
2.43.0
2.42.1
2.41.4
2.41.3
2.41.2
2.41.0
2.40.0
2.38.0
2.37.0
2.36.2
2.36.1
2.36.0
2.35.0
2.34.0
2.33.1
2.33.0
2.32.1
2.31.6
2.31.3
2.31.0
2.30.0
2.28.0
2.27.3
2.27.2
2.27.1
2.27.0
2.26.1
2.26.0
2.25.0
2.24.2
2.24.1
2.24.0
2.23.9
2.23.8
2.23.7
2.23.6
2.23.4
2.23.3
2.23.2
2.23.1
2.23.0
2.22.2
2.22.0
2.21.0
2.20.0
2.19.0
2.18.0
2.17.0
2.16.0
2.14.0
2.13.0
2.12.0
2.11.0
2.10.1
2.10.0
2.9.0
2.8.0
2.6.0
2.5.8
2.5.7
2.5.6
2.5.5
2.5.4
2.5.3
2.5.2
2.5.1
2.5.0
2.4.1
2.4.0
2.3.2
2.3.1
2.3.0
2.2.1
2.2.0
2.1.1
2.1.0
2.0.5
2.0.4
2.0.3
2.0.2
2.0.1
2.0.0
1.1.1
1.1.0
1.0.0
0.7.2
0.7.1
0.7.0
0.6.0
0.5.0
0.4.3
0.4.2
0.4.1
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.1
0.1.0
A Excel like expression parser, compatible with FLOIP Expression language.
Current section
Files
Jump to
Current section
Files
lib/expression/eval.ex
defmodule Expression.Eval do
@moduledoc """
Expression.Eval is responsible for taking an abstract syntax
tree (AST) as generated by Expression.Parser and evaluating it.
At a high level, an AST consists of a Keyword list with two top-level
keys, either `:text` or `:expression`.
`Expression.eval!/3` will return the output for each entry in the Keyword
list. `:text` entries are returned as regular strings. `:expression` entries
are returned as typed values.
The returned value is a list containing each.
# Example
iex(1)> Expression.Eval.eval!([text: "hello"], %{})
["hello"]
iex(2)> Expression.Eval.eval!([text: "hello", expression: [literal: 1]], %{})
["hello", 1]
iex(3)> Expression.Eval.eval!([
...(3)> text: "hello",
...(3)> expression: [literal: 1],
...(3)> text: "ok",
...(3)> expression: [literal: true]
...(3)> ], %{})
["hello", 1, "ok", true]
"""
def eval!(ast, context, mod \\ Expression.Callbacks)
def eval!({:expression, [ast]}, context, mod) do
eval!(ast, context, mod)
end
def eval!({:atom, atom}, {:not_found, history}, _mod),
do: {:not_found, history ++ [atom]}
def eval!({:atom, atom}, context, _mod) do
Map.get(context, atom, {:not_found, [atom]})
end
def eval!({:attribute, [{:attribute, ast}, literal: literal]}, context, mod) do
# When we receive a key for an attribute, at times this could be a literal.
# The assumption is that all attributes are going to be string based so if we receive
# "@foo.123.bar", `123` will be parsed as a literal but the assumption is that the
# context will look like:
#
# %{"foo" => %{
# "123" => %{ <--- notice the string key here
# "bar" => "the value"
# }
# }}
eval!({:attribute, [{:attribute, ast}, atom: to_string(literal)]}, context, mod)
end
def eval!({:attribute, ast}, context, mod) do
Enum.reduce(ast, context, &eval!(&1, &2, mod))
end
def eval!({:function, opts}, context, mod) do
name = opts[:name] || raise "Functions need a name"
arguments = opts[:args] || []
case mod.handle(name, arguments, context) do
{:ok, value} -> value
{:error, reason} -> "ERROR: #{inspect(reason)}"
end
end
def eval!({:lambda, [{:args, ast}]}, context, mod) do
fn arguments ->
lambda_context = Map.put(context, "__captures", arguments)
eval!(ast, lambda_context, mod)
end
end
def eval!({:capture, index}, context, _mod) do
Enum.at(Map.get(context, "__captures"), index - 1)
end
def eval!({:range, [first, last]}, _context, _mod),
do: Range.new(first, last)
def eval!({:range, [first, last, step]}, _context, _mod),
do: Range.new(first, last, step)
def eval!({:list, [{:args, ast}]}, context, mod) do
ast
|> Enum.reduce([], &[eval!(&1, context, mod) | &2])
|> Enum.reverse()
|> Enum.map(¬_founds_as_nil/1)
end
def eval!({:key, [subject_ast, key_ast]}, context, mod) do
subject = eval!(subject_ast, context, mod)
key = eval!(key_ast, context, mod)
case key do
index when is_number(index) -> get_in(subject, [Access.at(index)])
range when is_struct(range, Range) -> Enum.slice(subject, range)
binary when is_binary(binary) -> Map.get(subject, binary)
end
end
def eval!({:literal, literal}, _context, _mod), do: literal
def eval!({:text, text}, _context, _mod), do: text
@numeric_kernel_operators [:+, :-, :*, :/, :>, :>=, :<, :<=]
@kernel_operators @numeric_kernel_operators ++ [:==, :!=]
def eval!({operator, [a, b]}, ctx, mod)
when operator in @kernel_operators do
a = eval!(a, ctx, mod)
b = eval!(b, ctx, mod)
op(operator, a, b)
end
def eval!({:^, [a, b]}, ctx, mod), do: :math.pow(eval!(a, ctx, mod), eval!(b, ctx, mod))
def eval!({:&, [a, b]}, ctx, mod), do: [a, b] |> Enum.map_join("", &eval!(&1, ctx, mod))
def eval!(ast, context, mod) do
result =
ast
|> Enum.reduce([], fn ast, acc -> [eval!(ast, context, mod) | acc] end)
|> Enum.reverse()
case result do
[result] -> result
chunks -> chunks
end
end
# when acting on integer or Elixir literal numeric types
def op(operator, a, b)
when operator in @numeric_kernel_operators and
(is_number(a) or is_float(a)) and
(is_number(b) or is_float(b)) do
[a, b] = Enum.map([a, b], &guard_type!(&1, :num))
apply(Kernel, operator, [a, b])
end
# when acting on Decimal types
def op(operator, a, b)
when operator in @kernel_operators and
is_struct(a, Decimal) and
is_struct(b, Decimal) do
[a, b] = Enum.map([a, b], &guard_type!(&1, :num))
decimal_op(operator, a, b)
end
# when acting on any other supported type but still expected to be numeric
def op(operator, a, b) when operator in @numeric_kernel_operators do
apply(Kernel, operator, [guard_type!(a, :num), guard_type!(b, :num)])
end
# just leave it to the Kernel to figure out at this stage
def op(operator, a, b) when operator in @kernel_operators do
apply(Kernel, operator, [a, b])
end
def decimal_op(:+, a, b), do: Decimal.add(a, b)
def decimal_op(:*, a, b), do: Decimal.mult(a, b)
def decimal_op(:/, a, b), do: Decimal.div(a, b)
def decimal_op(:-, a, b), do: Decimal.sub(a, b)
def decimal_op(:>, a, b), do: Decimal.gt?(a, b)
def decimal_op(:>=, a, b), do: Decimal.compare(a, b) in [:gt, :eq]
def decimal_op(:<, a, b), do: Decimal.lt?(a, b)
def decimal_op(:<=, a, b), do: Decimal.compare(a, b) in [:lt, :eq]
def decimal_op(:==, a, b), do: Decimal.eq?(a, b)
def decimal_op(:!=, a, b), do: not Decimal.eq?(a, b)
def decimal_op(operator, _a, _b),
do: raise("Invalid operator #{inspect(operator)} for decimal values.")
def not_founds_as_nil({:not_found, _}), do: nil
def not_founds_as_nil(other), do: other
defp guard_type!(v, :num) when is_number(v) or is_struct(v, Decimal), do: v
defp guard_type!({:not_found, attributes}, :num),
do: raise("attribute is not found: `#{Enum.join(attributes, ".")}`")
defp guard_type!(v, :num), do: raise("expression is not a number: `#{inspect(v)}`")
end