lib/expression/eval.ex

defmodule Expression.Eval do
  @moduledoc """
  Take an AST and evaluate it.
  """
  import Expression.Ast, only: [fold_infixl: 1]

  def evaluate(ast, context, mod) do
    {:ok, evaluate!(ast, context, mod)}
  rescue
    error in RuntimeError -> {:error, error.message}
  end

  def evaluate!(ast, context, mod) do
    context = Expression.Context.new(context)

    resp =
      ast
      |> Enum.reduce([], fn
        {:escaped_at, ["@@"]}, acc ->
          ["@" | acc]

        {:substitution, ast}, acc ->
          [eval!(fold_infixl(ast), context, mod) | acc]

        {:text, text}, acc ->
          [text | acc]
      end)

    case resp do
      [value] ->
        value

      values ->
        values
        |> Enum.map(&to_string/1)
        |> Enum.reverse()
        |> Enum.join()
    end
  end

  def eval!(ast, _ctx, _mod) when is_number(ast), do: ast
  def eval!(ast, _ctx, _mod) when is_binary(ast), do: ast
  def eval!(ast, _ctx, _mod) when is_boolean(ast), do: ast
  def eval!({:variable, k}, ctx, _mod), do: get_var!(ctx, k)
  def eval!({:literal, value}, _ctx, _mod), do: value
  def eval!({:substitution, ast}, ctx, mod), do: eval!(fold_infixl(ast), ctx, mod)
  def eval!({:block, ast}, ctx, mod), do: eval!(fold_infixl(ast), ctx, mod)
  #  function calls without arguments
  def eval!({:function, [name]}, ctx, mod),
    do: eval!({:function, [name, {:arguments, []}]}, ctx, mod)

  def eval!({:function, [name, ast]}, ctx, mod) do
    case mod.handle(name, eval!(ast, ctx, mod), ctx) do
      {:ok, value} -> value
      {:error, reason} -> "ERROR: #{inspect(reason)}"
    end
  end

  def eval!({:arguments, ast}, ctx, mod), do: Enum.map(ast, &eval!(&1, ctx, mod))
  def eval!({:+, [a, b]}, ctx, mod), do: eval!(a, ctx, mod, :num) + eval!(b, ctx, mod, :num)
  def eval!({:-, [a, b]}, ctx, mod), do: eval!(a, ctx, mod, :num) - eval!(b, ctx, mod, :num)
  def eval!({:*, [a, b]}, ctx, mod), do: eval!(a, ctx, mod, :num) * eval!(b, ctx, mod, :num)
  def eval!({:/, [a, b]}, ctx, mod), do: eval!(a, ctx, mod, :num) / eval!(b, ctx, mod, :num)
  def eval!({:>, [a, b]}, ctx, mod), do: eval!(a, ctx, mod, :num) > eval!(b, ctx, mod, :num)
  def eval!({:>=, [a, b]}, ctx, mod), do: eval!(a, ctx, mod, :num) >= eval!(b, ctx, mod, :num)
  def eval!({:<, [a, b]}, ctx, mod), do: eval!(a, ctx, mod, :num) < eval!(b, ctx, mod, :num)
  def eval!({:<=, [a, b]}, ctx, mod), do: eval!(a, ctx, mod, :num) <= eval!(b, ctx, mod, :num)
  def eval!({:==, [a, b]}, ctx, mod), do: eval!(a, ctx, mod) == eval!(b, ctx, mod)
  def eval!({:!=, [a, b]}, ctx, mod), do: eval!(a, ctx, mod) != eval!(b, ctx, mod)
  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(&eval!(&1, ctx, mod)) |> Enum.join("")

  defp eval!(ast, ctx, mod, type), do: ast |> eval!(ctx, mod) |> guard_type!(type)

  defp get_var!(ctx, k), do: get_in(ctx, k) |> guard_nil!(k)

  defp guard_nil!(nil, k), do: raise("variable #{k} undefined or null")
  defp guard_nil!(v, _), do: v

  defp guard_type!(v, :num) when is_number(v), do: v
  defp guard_type!(v, :num), do: raise("expression is not a number: `#{inspect(v)}`")
end