lib/rexbug_copy/translator.ex

defmodule Rexbug.Translator do
  @moduledoc """
  Utility module for translating Elixir syntax to the one expected by
  `:redbug`.

  You probably don't need to use it directly.
  """

  @valid_guard_functions [
    :is_atom,
    :is_binary,
    :is_bitstring,
    :is_boolean,
    :is_float,
    :is_function,
    :is_integer,
    :is_list,
    :is_map,
    :is_nil,
    :is_number,
    :is_pid,
    :is_port,
    :is_reference,
    :is_tuple,
    :abs,
    :bit_size,
    :byte_size,
    :hd,
    :length,
    :map_size,
    :round,
    :tl,
    :trunc,
    :tuple_size,

    # erlang guard
    :size
  ]

  @infix_guards_mapping %{
    # comparison
    :== => :==,
    :!= => :"/=",
    :=== => :"=:=",
    :!== => :"=/=",
    :> => :>,
    :>= => :>=,
    :< => :<,
    :<= => :"=<"
  }

  @valid_infix_guards Map.keys(@infix_guards_mapping)

  @infix_guard_combinators_mapping %{
    :and => :andalso,
    :or => :orelse
  }

  @valid_infix_guard_combinators Map.keys(@infix_guard_combinators_mapping)

  # ===========================================================================
  # Public functions
  # ===========================================================================

  # ---------------------------------------------------------------------------
  # Translating trace pattern
  # ---------------------------------------------------------------------------

  @spec translate(Rexbug.trace_pattern()) ::
          {:ok, charlist | atom} | {:ok, [charlist | atom]} | {:error, term}
  @doc """
  Translates the Elixir trace pattern(s) (understood by Rexbug) to the
  Erlang trace pattern charlist(s) understood by `:redbug`.

  The translated version is not necessarily the cleanest possible, but should
  be correct and functionally equivalent.

  ## Example
      iex> import Rexbug.Translator
      iex> translate(":cowboy.start_clear/3")
      {:ok, '\\'cowboy\\':\\'start_clear\\'/3'}
      iex> translate("MyModule.do_sth(_, [pretty: true])")
      {:ok, '\\'Elixir.MyModule\\':\\'do_sth\\'(_, [{\\'pretty\\', true}])'}
  """

  def translate(s) when s in [:send, "send"], do: {:ok, :send}
  def translate(r) when r in [:receive, "receive"], do: {:ok, :receive}

  def translate(patterns) when is_list(patterns) do
    patterns
    |> Enum.map(&translate/1)
    |> collapse_errors()
  end

  def translate(trace_pattern) when is_binary(trace_pattern) do
    with {mfag, actions} = split_to_mfag_and_actions!(trace_pattern),
         {:ok, quoted} <- Code.string_to_quoted(mfag),
         {:ok, {mfa, guards}} = split_quoted_into_mfa_and_guards(quoted),
         {:ok, {mfa, arity}} = split_mfa_into_mfa_and_arity(mfa),
         {:ok, {module, function, args}} = split_mfa_into_module_function_and_args(mfa),
         :ok <- validate_mfaa(module, function, args, arity),
         {:ok, translated_module} <- translate_module(module),
         {:ok, translated_function} <- translate_function(function),
         {:ok, translated_args} <- translate_args(args),
         {:ok, translated_arity} <- translate_arity(arity),
         {:ok, translated_guards} <- translate_guards(guards),
         translated_actions = translate_actions!(actions) do
      translated =
        case translated_arity do
          :any ->
            # no args, no arity
            "#{translated_module}#{translated_function}#{translated_actions}"

          arity when is_integer(arity) ->
            # no args, arity present
            "#{translated_module}#{translated_function}/#{arity}#{translated_actions}"

          nil ->
            # args present, no arity
            "#{translated_module}#{translated_function}#{translated_args}#{translated_guards}#{
              translated_actions
            }"
        end

      {:ok, String.to_charlist(translated)}
    end
  end

  def translate(_), do: {:error, :invalid_trace_pattern_type}

  @doc false
  def split_to_mfag_and_actions!(trace_pattern) do
    {mfag, actions} =
      case String.split(trace_pattern, " ::", parts: 2) do
        [mfag, actions] -> {mfag, actions}
        [mfag] -> {mfag, ""}
      end

    {String.trim(mfag), String.trim(actions)}
  end

  @spec translate_guards(term) :: {:ok, String.t()} | {:error, term}
  defp translate_guards(nil), do: {:ok, ""}

  defp translate_guards(els) do
    _translate_guards(els)
    |> map_success(fn guards -> " when #{guards}" end)
  end

  # ---------------------------------------------------------------------------
  # Translating options
  # ---------------------------------------------------------------------------

  @spec translate_options(Keyword.t()) :: {:ok, Keyword.t()} | {:error, term}
  @doc """
  Translates the options to be passed to `Rexbug.start/2` to the format expected by
  `:redbug`

  Relevant values passed as strings will be converted to charlists.
  """

  def translate_options(options) when is_list(options) do
    options
    |> Enum.map(&translate_option/1)
    |> collapse_errors()
  end

  def translate_options(_), do: {:error, :invalid_options}

  # ===========================================================================
  # Private functions
  # ===========================================================================

  @binary_to_charlist_options [:file, :print_file]

  defp translate_option({file_option, filename})
       when file_option in @binary_to_charlist_options and is_binary(filename) do
    {:ok, {file_option, String.to_charlist(filename)}}
  end

  defp translate_option({k, v}) do
    {:ok, {k, v}}
  end

  defp translate_option(_), do: {:error, :invalid_options}

  @spec collapse_errors([{:ok, term} | {:error, term}]) :: {:ok, [term]} | {:error, term}
  defp collapse_errors(tuples) do
    # we could probably play around with some monads for this
    first_error = Enum.find(tuples, :no_error_to_collapse, fn res -> !match?({:ok, _}, res) end)

    case first_error do
      :no_error_to_collapse ->
        results = Enum.map(tuples, fn {:ok, res} -> res end)
        {:ok, results}

      err ->
        err
    end
  end

  defp split_quoted_into_mfa_and_guards({:when, _line, [mfa, guards]}) do
    {:ok, {mfa, guards}}
  end

  defp split_quoted_into_mfa_and_guards(els) do
    {:ok, {els, nil}}
  end

  defp split_mfa_into_mfa_and_arity({:/, _line, [mfa, arity]}) do
    {:ok, {mfa, arity}}
  end

  defp split_mfa_into_mfa_and_arity(els) do
    {:ok, {els, nil}}
  end

  defp split_mfa_into_module_function_and_args({{:., _l1, [module, function]}, _l2, args}) do
    {:ok, {module, function, args}}
  end

  defp split_mfa_into_module_function_and_args(els) do
    {:ok, {els, nil, nil}}
  end

  # handling fringe cases that shouldn't happen
  defp validate_mfaa(module, function, args, arity)

  defp validate_mfaa(nil, _, _, _), do: {:error, :missing_module}

  defp validate_mfaa(_, nil, args, _) when not (args in [nil, []]),
    do: {:error, :missing_function}

  defp validate_mfaa(_, nil, _, arity) when arity != nil, do: {:error, :missing_function}

  defp validate_mfaa(_, _, args, arity)
       when not (args in [nil, []]) and arity != nil do
    {:error, :both_args_and_arity_provided}
  end

  defp validate_mfaa(_, _, _, _), do: :ok

  defp translate_module({:__aliases__, _line, elixir_module}) when is_list(elixir_module) do
    joined =
      [:"Elixir" | elixir_module]
      |> Enum.map(&Atom.to_string/1)
      |> Enum.join(".")

    {:ok, "'#{joined}'"}
  end

  defp translate_module(erlang_mod) when is_atom(erlang_mod) do
    {:ok, "\'#{Atom.to_string(erlang_mod)}\'"}
  end

  defp translate_module(module), do: {:error, {:invalid_module, module}}

  defp translate_function(nil) do
    {:ok, ""}
  end

  defp translate_function(f) when is_atom(f) do
    {:ok, ":'#{Atom.to_string(f)}'"}
  end

  defp translate_function(els) do
    {:error, {:invalid_function, els}}
  end

  defp translate_args(nil), do: {:ok, ""}

  defp translate_args(args) when is_list(args) do
    args
    |> Enum.map(&translate_arg/1)
    |> collapse_errors()
    |> map_success(&Enum.join(&1, ", "))
    |> map_success(fn res -> "(#{res})" end)
  end

  defp translate_args(els) do
    {:error, {:invalid_args, els}}
  end

  defp translate_arg(nil), do: {:ok, "nil"}

  defp translate_arg(boolean) when is_boolean(boolean) do
    {:ok, "#{boolean}"}
  end

  defp translate_arg(arg) when is_atom(arg) do
    {:ok, "'#{Atom.to_string(arg)}'"}
  end

  defp translate_arg(string) when is_binary(string) do
    # TODO: more strict ASCII checking here
    if String.printable?(string) && byte_size(string) == String.length(string) do
      {:ok, "<<\"#{string}\">>"}
    else
      translate_arg({:<<>>, [line: 1], [string]})
    end
  end

  defp translate_arg({:<<>>, _line, contents}) when is_list(contents) do
    contents
    |> Enum.map(&translate_binary_element/1)
    |> collapse_errors()
    |> map_success(&Enum.join(&1, ", "))
    |> map_success(fn res -> "<<#{res}>>" end)
  end

  # defp translate_arg(bs) when is_bitstring(bs) do
  #   :error
  # end

  defp translate_arg(ls) when is_list(ls) do
    ls
    |> Enum.map(&translate_arg/1)
    |> collapse_errors()
    |> map_success(fn elements -> "[#{Enum.join(elements, ", ")}]" end)
  end

  defp translate_arg({:-, _line, [num]}) when is_integer(num) do
    with {:ok, translated_num} = translate_arg(num),
      do: {:ok, "-#{translated_num}"}
  end

  defp translate_arg(num) when is_integer(num) do
    {:ok, "#{num}"}
  end

  defp translate_arg(f) when is_float(f) do
    {:error, {:bad_type, :float}}
  end

  defp translate_arg({:%{}, _line, kvs}) when is_list(kvs) do
    {ks, vs} = Enum.unzip(kvs)

    if Enum.any?(ks, &is_variable/1) do
      {:error, :variable_in_map_key}
    else
      key_args =
        ks
        |> Enum.map(&translate_arg/1)
        |> collapse_errors()

      value_args =
        vs
        |> Enum.map(&translate_arg/1)
        |> collapse_errors()

      [key_args, value_args]
      |> collapse_errors
      |> map_success(fn [keys, values] ->
        middle =
          keys
          |> Enum.zip(values)
          |> Enum.map(fn {k, v} -> "#{k} => #{v}" end)
          |> Enum.join(", ")

        "\#{#{middle}}"
      end)
    end
  end

  # there's a catch here:
  # iex(12)> Code.string_to_quoted!("{1,2,3}")
  # {:{}, [line: 1], [1, 2, 3]}
  # iex(13)> Code.string_to_quoted!("{1,2}")
  # {1, 2}
  defp translate_arg({:{}, _line, tuple_elements}) do
    tuple_elements
    |> Enum.map(&translate_arg/1)
    |> collapse_errors()
    |> map_success(fn elements -> "{#{Enum.join(elements, ", ")}}" end)
  end

  # the literally represented 2-tuples
  defp translate_arg({x, y}), do: translate_arg({:{}, [line: 1], [x, y]})

  # other atoms are just variable names
  defp translate_arg({var, _line, nil}) when is_atom(var) do
    var
    |> Atom.to_string()
    |> String.capitalize()
    |> wrap_in_ok()
  end

  defp translate_arg(arg) do
    {:error, {:invalid_arg, arg}}
  end

  defp is_variable({var, _line, nil}) when is_atom(var), do: true
  defp is_variable(_), do: false

  defp translate_binary_element(i) when is_integer(i) do
    {:ok, "#{i}"}
  end

  defp translate_binary_element(s) when is_binary(s) do
    res =
      s
      |> :binary.bin_to_list()
      |> Enum.join(", ")

    {:ok, res}
  end

  defp translate_binary_element(els), do: {:error, {:invalid_binary_element, els}}

  defp translate_arity({var, [line: 1], nil}) when is_atom(var) do
    {:ok, :any}
  end

  defp translate_arity(i) when is_integer(i) do
    {:ok, i}
  end

  defp translate_arity(none) when none in [nil, ""] do
    {:ok, nil}
  end

  defp translate_arity(els) do
    {:error, {:invalid_arity, els}}
  end

  defp translate_actions!(empty) when empty in [nil, ""] do
    ""
  end

  defp translate_actions!(actions) when is_binary(actions) do
    " -> #{actions}"
  end

  # ---------------------------------------------------------------------------
  # Guards
  # ---------------------------------------------------------------------------

  @spec _translate_guards(term) :: {:ok, String.t()} | {:error, term}
  defp _translate_guards({:not, _line, [arg]}) do
    _translate_guards(arg)
    |> map_success(fn guard -> "not #{guard}" end)
  end

  defp _translate_guards({combinator, _line, [a, b]})
       when combinator in @valid_infix_guard_combinators do
    erlang_combinator =
      @infix_guard_combinators_mapping[combinator]
      |> Atom.to_string()

    with {:ok, a_guards} <- _translate_guards(a),
         {:ok, b_guards} <- _translate_guards(b),
         do: {:ok, "(#{a_guards} #{erlang_combinator} #{b_guards})"}
  end

  defp _translate_guards(els), do: translate_guard(els)

  @spec translate_guard(term) :: {:ok, String.t()} | {:error, term}
  defp translate_guard({guard_fun, _line, args})
       when guard_fun in @valid_guard_functions do
    with translated_fun = Atom.to_string(guard_fun),
         {:ok, translated_args} <- translate_args(args),
         do: {:ok, "#{translated_fun}#{translated_args}"}
  end

  defp translate_guard({infix_guard_fun, _line, [a, b]})
       when infix_guard_fun in @valid_infix_guards do
    translated_infix_function =
      @infix_guards_mapping[infix_guard_fun]
      |> Atom.to_string()

    with {:ok, a_guard} <- translate_guard(a),
         {:ok, b_guard} <- translate_guard(b),
         do: {:ok, "#{a_guard} #{translated_infix_function} #{b_guard}"}
  end

  defp translate_guard(els) do
    translate_arg(els)
  end

  # ---------------------------------------------------------------------------
  # Helper functions
  # ---------------------------------------------------------------------------

  defp map_success({:ok, var}, fun) do
    {:ok, fun.(var)}
  end

  defp map_success(els, _), do: els

  defp wrap_in_ok(x), do: {:ok, x}
end