lib/rexbug_copy/printing.ex

defmodule Rexbug.Printing do
  @moduledoc """
  Provides the print handler and helper functions for writing custom
  print handlers for `Rexbug`. You shouldn't need to use it directly
  unless you want to implement a print handler yourself.

  See `Rexbug.start/2` `print_fun` option for details.
  """

  alias Rexbug.Printing
  import Rexbug.Printing.Utils

  # ===========================================================================
  # Helper Structs
  # ===========================================================================

  defmodule MFA do
    @type t :: %__MODULE__{}

    defstruct [
      :m,
      :f,
      # either args or arity
      :a
    ]

    def from_erl({m, f, a}) do
      %__MODULE__{m: m, f: f, a: a}
    end

    def from_erl(a) when is_atom(a) or is_list(a) do
      a
    end

    def represent(a, _opts) when is_atom(a) or is_list(a) do
      "(#{inspect(a)})"
    end

    def represent(%__MODULE__{m: m, f: f, a: a}, _opts) do
      mrep =
        case Atom.to_string(m) do
          "Elixir." <> rest -> rest
          erlang_module -> ":#{erlang_module}"
        end

      arep =
        if is_list(a) do
          middle =
            a
            |> Enum.map(&printing_inspect/1)
            |> Enum.join(", ")

          "(#{middle})"
        else
          "/#{a}"
        end

      "#{mrep}.#{f}#{arep}"
    end
  end

  defmodule Timestamp do
    @type t :: %__MODULE__{}
    defstruct ~w(hours minutes seconds us)a

    def from_erl({h, m, s, us}) do
      %__MODULE__{hours: h, minutes: m, seconds: s, us: us}
    end

    def represent(%__MODULE__{hours: h, minutes: m, seconds: s, us: us}, opts) do
      if Keyword.get(opts, :print_msec, false) do
        "#{format_int(h)}:#{format_int(m)}:#{format_int(s)}.#{format_int(div(us, 1000), 3)}"
      else
        "#{format_int(h)}:#{format_int(m)}:#{format_int(s)}"
      end
    end

    defp format_int(i, length \\ 2) do
      i
      |> Integer.to_string()
      |> String.pad_leading(length, "0")
    end
  end

  # ---------------------------------------------------------------------------
  # Received message types
  # ---------------------------------------------------------------------------

  defmodule Call do
    @type t :: %__MODULE__{}
    defstruct ~w(mfa dump from_pid from_mfa time)a

    def represent(%__MODULE__{} = struct, opts) do
      ts = Timestamp.represent(struct.time, opts)
      pid = printing_inspect(struct.from_pid)
      from_mfa = MFA.represent(struct.from_mfa, opts)
      mfa = MFA.represent(struct.mfa, opts)
      maybe_stack = represent_stack(struct.dump, opts)

      "# #{ts} #{pid} #{from_mfa}\n# #{mfa}#{maybe_stack}"
    end

    defp represent_stack(nil, _opts), do: ""
    defp represent_stack("", _opts), do: ""

    defp represent_stack(dump, _opts) do
      dump
      |> Printing.extract_stack()
      |> Enum.map(fn fun_rep -> "\n#   #{fun_rep}" end)
      |> Enum.join("")
    end
  end

  defmodule Return do
    @type t :: %__MODULE__{}
    defstruct ~w(mfa return_value from_pid from_mfa time)a

    def represent(%__MODULE__{} = struct, opts) do
      ts = Timestamp.represent(struct.time, opts)
      pid = printing_inspect(struct.from_pid)
      from_mfa = MFA.represent(struct.from_mfa, opts)
      mfa = MFA.represent(struct.mfa, opts)
      retn = printing_inspect(struct.return_value)

      "# #{ts} #{pid} #{from_mfa}\n# #{mfa} -> #{retn}"
    end
  end

  defmodule Send do
    @type t :: %__MODULE__{}
    defstruct ~w(msg to_pid to_mfa from_pid from_mfa time)a

    def represent(%__MODULE__{} = struct, opts) do
      ts = Timestamp.represent(struct.time, opts)
      to_pid = printing_inspect(struct.to_pid)
      to_mfa = MFA.represent(struct.to_mfa, opts)
      from_pid = printing_inspect(struct.from_pid)
      from_mfa = MFA.represent(struct.from_mfa, opts)
      msg = printing_inspect(struct.msg)

      "# #{ts} #{from_pid} #{from_mfa}\n# #{to_pid} #{to_mfa} <<< #{msg}"
    end
  end

  defmodule Receive do
    @type t :: %__MODULE__{}
    defstruct ~w(msg to_pid to_mfa time)a

    def represent(%__MODULE__{} = struct, opts) do
      ts = Timestamp.represent(struct.time, opts)
      to_pid = printing_inspect(struct.to_pid)
      to_mfa = MFA.represent(struct.to_mfa, opts)
      msg = printing_inspect(struct.msg)

      "# #{ts} #{to_pid} #{to_mfa}\n# <<< #{msg}"
    end
  end

  # ===========================================================================
  # Public Functions
  # ===========================================================================

  @doc """
  The default value for the `Rexbug.start/2` `print_fun` option. Prints out
  the tracing messages generated by `:redbug` in a nice Elixir format.
  """
  @spec print(tuple()) :: :ok
  def print(message) do
    # this is not a 2 arg function just so that no-one passes it as the
    # print_fun to :redbug, the second argument is not an accumulator
    print_with_opts(message, [])
  end

  @doc false
  @spec print_with_opts(tuple(), Keyword.t()) :: :ok
  def print_with_opts(message, opts) do
    IO.puts("\n" <> format(message, opts))
  end

  @doc false
  def format(message, opts \\ []) do
    message
    |> from_erl()
    |> represent(opts)
  end

  @doc """
  Translates the `:redbug` tuples representing the tracing messages to
  Elixir structs.

  You can use it to implement your own custom `print_fun`.
  """
  @spec from_erl(tuple()) ::
          Printing.Call.t()
          | Printing.Return.t()
          | Printing.Send.t()
          | Printing.Receive.t()
          | term()
  def from_erl({:call, {mfa, dump}, from_pid_mfa, time}) do
    {from_pid, from_mfa} = get_pid_mfa(from_pid_mfa)

    %Call{
      mfa: MFA.from_erl(mfa),
      dump: dump,
      from_pid: from_pid,
      from_mfa: MFA.from_erl(from_mfa),
      time: Timestamp.from_erl(time)
    }
  end

  def from_erl({:retn, {mfa, retn}, from_pid_mfa, time}) do
    {from_pid, from_mfa} = get_pid_mfa(from_pid_mfa)

    %Return{
      mfa: MFA.from_erl(mfa),
      return_value: retn,
      from_pid: from_pid,
      from_mfa: MFA.from_erl(from_mfa),
      time: Timestamp.from_erl(time)
    }
  end

  def from_erl({:send, {msg, to_pid_mfa}, from_pid_mfa, time}) do
    {to_pid, to_mfa} = get_pid_mfa(to_pid_mfa)
    {from_pid, from_mfa} = get_pid_mfa(from_pid_mfa)

    %Send{
      msg: msg,
      to_pid: to_pid,
      to_mfa: MFA.from_erl(to_mfa),
      from_pid: from_pid,
      from_mfa: MFA.from_erl(from_mfa),
      time: Timestamp.from_erl(time)
    }
  end

  def from_erl({:recv, msg, to_pid_mfa, time}) do
    {to_pid, to_mfa} = get_pid_mfa(to_pid_mfa)

    %Receive{
      msg: msg,
      to_pid: to_pid,
      to_mfa: MFA.from_erl(to_mfa),
      time: Timestamp.from_erl(time)
    }
  end

  # fallthrough so that you can use it indiscriminately
  def from_erl(message) do
    message
  end

  defp get_pid_mfa({pid, mfa}) do
    {pid, mfa}
  end

  defp get_pid_mfa(other) when is_atom(other) or is_list(other) do
    {nil, other}
  end

  @doc false
  def represent(%mod{} = struct, opts) when mod in [Call, Return, Send, Receive] do
    mod.represent(struct, opts)
  end

  @doc """
  Extracts the call stack from `t:Rexbug.Printing.Call.t/0` `dump` field.

  **NOTE**: The `dump` field will contain call stack information only
  if you specify `" :: stack"` in `Rexbug.start/2`'s trace pattern,
  it will be nil or empty otherwise.
  """
  @spec extract_stack(String.t()) :: [String.t()]
  def extract_stack(dump) do
    String.split(dump, "\n")
    |> Enum.filter(&Regex.match?(~r/Return addr 0x|CP: 0x/, &1))
    |> Enum.flat_map(&extract_function/1)
  end

  # ===========================================================================
  # Internal Functions
  # ===========================================================================

  defp extract_function(line) do
    case Regex.run(~r"^.+\((.+):(.+)/(\d+).+\)$", line, capture: :all_but_first) do
      [m, f, arity] ->
        m = translate_module_from_dump(m)
        f = strip_single_quotes(f)
        ["#{m}.#{f}/#{arity}"]

      nil ->
        []
    end
  end

  defp strip_single_quotes(str) do
    String.trim(str, "'")
  end

  defp translate_module_from_dump(module) do
    case strip_single_quotes(module) do
      "Elixir." <> rest ->
        rest

      erlang_module ->
        ":#{erlang_module}"
    end
  end
end