Logger: Format OTP 17.4 terminate reasons

From 17.4 OTP behaviours include the stacktrace in the logged reason
when exit/1 is called. If an elixir exception is raised, or the reason
can be normalized to an elixir exception (not ErlangError), the
exception is formatted as an exception:
** (Exception) message
Otherwise the reason is formatted using Exception.format_exit/1:
** (stop) :reason

defmodule Logger.Translator do
  @moduledoc """
  Default translation for Erlang log messages.

  Logger allows developers to rewrite log messages provided by
  Erlang applications into a format more compatible with Elixir
  log messages by providing a translator.

  A translator is simply a tuple containing a module and a function
  that can be added and removed via the `Logger.add_translator/1` and
  `Logger.remove_translator/1` functions and is invoked for every Erlang
  message above the minimum log level with four arguments:

    * `min_level` - the current Logger level
    * `level` - the level of the message being translator
    * `kind` - if the message is a report or a format
    * `data` - the data to format. If it is a report, it is a tuple
      with `{report_type, report_data}`, if it is a format, it is a
      tuple with `{format_message, format_args}`

  The function must return:

    * `{:ok, iodata}` - if the message was translated with its translation
    * `:skip` - if the message is not meant to be translated nor logged
    * `:none` - if there is no translation, which triggers the next translator

  See the function `translate/4` in this module for an example implementation
  and the default messages translated by Logger.
  """

  def translate(min_level, :error, :format, message) do
    case message do
      {'** Generic server ' ++ _, [name, last, state, reason]} ->
        msg = "GenServer #{inspect name} terminating\n"
        if min_level == :debug do
          msg = msg <> "Last message: #{inspect last}\n"
                    <> "State: #{inspect state}\n"
        end
        {:ok, [msg | format_stop(reason)]}

      {'** gen_event handler ' ++ _, [name, manager, last, state, reason]} ->
        msg = "GenEvent handler #{inspect name} installed in #{inspect manager} terminating\n"
        if min_level == :debug do
          msg = msg <> "Last message: #{inspect last}\n"
                    <> "State: #{inspect state}\n"
        end
        {:ok, [msg | format_stop(reason)]}

      {'** Task ' ++ _, [name, starter, function, args, reason]} ->
        msg = "Task #{inspect name} started from #{inspect starter} terminating\n" <>
              "Function: #{inspect function}\n" <>
              "    Args: #{inspect args}\n"
        {:ok, [msg | format_stop(reason)]}

      _ ->
        :none
    end
  end

  def translate(_min_level, :info, :report,
                {:std_info, [application: app, exited: reason, type: _type]}) do
    {:ok, "Application #{app} exited: #{Application.format_error(reason)}"}
  end

  def translate(min_level, :error, :report, {:supervisor_report, data}) do
    translate_supervisor(min_level, data)
  end

  def translate(min_level, :error, :report, {:crash_report, data}) do
    translate_crash(min_level, data)
  end

  def translate(min_level, :info, :report, {:progress, data}) do
    translate_progress(min_level, data)
  end

  def translate(_min_level, _level, _kind, _message) do
    :none
  end

  defp translate_supervisor(min_level,
                           [supervisor: sup, errorContext: context,
                             reason: reason,
                             offender: [{:pid, pid}, {:name, name} | offender]])
                           when is_pid(pid) and context !== :shutdown do
    {:ok, ["Child ", inspect(name), " of Supervisor ",
            sup_name(sup), ?\s, sup_context(context), ?\n,
            "Pid: ", inspect(pid), ?\n,
            child_info(min_level, offender), ?\n,
            "** (exit) " | offender_reason(reason, context)]}
  end

  defp translate_supervisor(min_level,
                           [supervisor: sup, errorContext: context,
                             reason: reason,
                             offender: [{:pid, _pid},
                                        {:name, name} | offender]]) do
    {:ok, ["Child ", inspect(name), " of Supervisor ",
            sup_name(sup), ?\s, sup_context(context), ?\n,
            child_info(min_level, offender), ?\n,
            "** (exit) " | offender_reason(reason, context)]}
  end

  defp translate_supervisor(min_level,
                           [supervisor: sup, errorContext: context,
                             reason: reason,
                             offender: [{:pid, pid} | offender]]) do
    {:ok, ["Child of Supervisor ",
            sup_name(sup), ?\s, sup_context(context), ?\n,
            "Pid: ", inspect(pid), ?\n,
            child_info(min_level, offender), ?\n,
            "** (exit) " | offender_reason(reason, context)]}
  end

  defp translate_supervisor(min_level,
                           [supervisor: sup, errorContext: context,
                             reason: reason,
                             offender: [{:nb_children, n},
                                        {:name, name} | offender]]) do
    {:ok, ["Children ", inspect(name), " of Supervisor ",
            sup_name(sup), ?\s, sup_context(context), ?\n,
            "Number: ", inspect(n), ?\n,
            child_info(min_level, offender), ?\n,
            "** (exit) " | offender_reason(reason, context)]}
  end

  defp translate_supervisor(_min_level, _other), do: :none

  defp translate_progress(_min_level,
                          [application: app, started_at: node_name]) do
    {:ok, ["Application ", to_string(app), " started at " | inspect(node_name)]}
  end

  defp translate_progress(min_level,
                          [supervisor: sup,
                            started: [{:pid, pid}, {:name, name} | started]]) do
    {:ok, ["Child ", inspect(name), " of Supervisor ",
            sup_name(sup), " started\n",
            "Pid: ", inspect(pid), ?\n |
            child_info(min_level, started)]}
  end

  defp translate_progress(min_level,
                          [supervisor: sup,
                            started: [{:pid, pid} | started]]) do
    {:ok, ["Child of Supervisor ", sup_name(sup), " started\n",
            "Pid: ", inspect(pid), ?\n |
            child_info(min_level, started)]}
  end

  defp translate_progress(_min_level, _other), do: :none

  defp sup_name({:local, name}), do: inspect(name)
  defp sup_name({:global, name}), do: inspect(name)
  defp sup_name({:via, _mod, name}), do: inspect(name)
  defp sup_name({pid, mod}), do: [inspect(pid), " (", inspect(mod), ?)]

  defp sup_context(:start_error), do: "failed to start"
  defp sup_context(:child_terminated), do: "terminated"
  defp sup_context(:shutdown), do: "caused shutdown"
  defp sup_context(:shutdown_error), do: "shutdown abnormally"

  defp child_info(min_level, [{:mfargs, {mod, fun, args}} | debug]) do
    ["Start Call: ", Exception.format_mfa(mod, fun, args) |
      child_debug(min_level, debug)]
  end

  defp child_info(min_level, [{:mfa, {mod, fun, args}} | debug]) do
    ["Start Call: ", Exception.format_mfa(mod, fun, args) |
      child_debug(min_level, debug)]
  end

  defp child_info(min_level, [{:mod, mod} | debug]) do
    ["Start Module: ", inspect(mod) |
      child_debug(min_level, debug)]
  end

  defp child_debug(:debug,
                   [restart_type: restart, shutdown: shutdown, child_type: type]) do
    [?\n,
      "Restart: ", inspect(restart), ?\n,
      "Shutdown: ", inspect(shutdown), ?\n,
      "Type: ", inspect(type)]
  end

  defp child_debug(_min_level, _child) do
    []
  end

  # If start call raises reason will be of form {:EXIT, reason}
  defp offender_reason({:EXIT, reason}, :start_error) do
    Exception.format_exit(reason)
  end

  defp offender_reason(reason, _context) do
    Exception.format_exit(reason)
  end

  defp translate_crash(min_level,
                       [[{:initial_call, _} = initial_call,
                         {:pid, pid},
                         {:registered_name, name},
                         {:error_info, {kind, exception, stack}} | crashed],
                        linked]) do
    {:ok, ["Process ", crash_name(pid, name) , " terminating\n",
            crash_info(min_level, [initial_call | crashed]),
            crash_linked(min_level, linked) |
            Exception.format(kind, exception, stack)]}
  end

  defp translate_crash(min_level,
                       [[{:pid, pid},
                         {:registered_name, name},
                         {:error_info, {kind, exception, stack}} | crashed],
                        linked]) do
    {:ok, ["Process ", crash_name(pid, name) , " terminating\n",
            crash_info(min_level, crashed),
            crash_linked(min_level, linked) |
            Exception.format(kind, exception, stack)]}
  end

  defp crash_name(pid, []), do: inspect(pid)
  defp crash_name(pid, name), do: [inspect(name), " (", inspect(pid), ?)]

  defp crash_info(min_level, info, prefix \\ [])

  defp crash_info(min_level,
                  [{:initial_call, {mod, fun, args}} | info], prefix) do
    [prefix, "Initial Call: ", crash_call(mod, fun, args), ?\n |
      crash_info(min_level, info, prefix)]
  end

  defp crash_info(min_level,
                  [{:current_function, {mod, fun, args}} | info], prefix) do
    [prefix, "Current Call: ", crash_call(mod, fun, args), ?\n |
      crash_info(min_level, info, prefix)]
  end

  defp crash_info(min_level, [{:current_function, []} | info], prefix) do
    crash_info(min_level, info, prefix)
  end

  defp crash_info(min_level,
                  [{:ancestors, ancestors} | debug], prefix) do
    [prefix, "Ancestors: ", inspect(ancestors), ?\n |
      crash_debug(min_level, debug, prefix)]
  end

  defp crash_call(mod, fun, arity) when is_integer(arity) do
    Exception.format_mfa(mod, fun, arity)
  end

  defp crash_call(mod, fun, args) do
    Exception.format_mfa(mod, fun, length(args))
  end

  defp crash_debug(:debug,
                   [messages: msgs, links: links, dictionary: dict,
                     trap_exit: trap, status: status, heap_size: heap_size,
                     stack_size: stack_size, reductions: reductions], prefix) do
    [prefix, "Messages: ", inspect(msgs), ?\n,
      prefix, "Links: ", inspect(links), ?\n,
      prefix, "Dictionary: ", inspect(dict), ?\n,
      prefix, "Trapping Exits: ", inspect(trap), ?\n,
      prefix, "Status: ", inspect(status), ?\n,
      prefix, "Heap Size: ", inspect(heap_size), ?\n,
      prefix, "Stack Size: ", inspect(stack_size), ?\n,
      prefix, "Reductions: ", inspect(reductions), ?\n]
  end

  defp crash_debug(_min_level, _info, _prefix) do
    []
  end

  defp crash_linked(_min_level, []), do: []

  defp crash_linked(min_level, neighbours) do
    Enum.reduce(neighbours, "Neighbours:\n", fn({:neighbour, info}, acc) ->
      [acc | crash_neighbour(min_level, info)]
    end)
  end

  defp crash_neighbour(min_level,
                       [{:pid, pid}, {:registered_name, []} | info]) do
    prefix = "    "
    [prefix, inspect(pid), ?\n |
      crash_info(min_level, info, [prefix | prefix])]
  end

  defp crash_neighbour(min_level,
                       [{:pid, pid}, {:registered_name, name} | info]) do
    prefix = "    "
    [prefix, inspect(name), " (", inspect(pid), ")\n" |
      crash_info(min_level, info, [prefix | prefix])]
  end

  defp format_stop({maybe_exception, [_ | _ ] = maybe_stacktrace} = reason) do
    try do
      for maybe_entry <- maybe_stacktrace do
        [<<"\n    ">> | Exception.format_stacktrace_entry(maybe_entry)]
      end
    catch
      :error, _ ->
        format_stop_banner(reason)
    else
      formatted_stacktrace ->
        [format_stop_banner(maybe_exception, maybe_stacktrace) |
          formatted_stacktrace]
    end
  end

  defp format_stop(reason) do
    format_stop_banner(reason)
  end

  defp format_stop_banner(reason) do
    ["** (stop) " | Exception.format_exit(reason)]
  end

  # OTP process rewrite the :undef error to these reasons when logging
  @gen_undef [:"module could not be loaded", :"function not exported"]

  defp format_stop_banner(undef, [{mod, fun, args, _info} | _ ]  = stacktrace)
  when undef in @gen_undef and is_atom(mod) and is_atom(fun) do
    cond do
      is_list(args) ->
        format_undef(mod, fun, length(args), undef, stacktrace)
      is_integer(args) ->
        format_undef(mod, fun, args, undef, stacktrace)
      true ->
        format_stop_banner(undef)
    end
  end

  defp format_stop_banner(reason, stacktrace) do
    if Exception.exception?(reason) do
        Exception.format_banner(:error, reason, stacktrace)
    else
      case Exception.normalize(:error, reason, stacktrace) do
        %ErlangError{} ->
          format_stop_banner(reason)
        exception ->
          Exception.format_banner(:error, exception, stacktrace)
      end
    end
  end

  defp format_undef(mod, fun, arity, undef, stacktrace) do
    opts = [module: mod, function: fun, arity: arity, reason: undef]
    exception = UndefinedFunctionError.exception(opts)
    Exception.format_banner(:error, exception, stacktrace)
  end
end

defmodule Logger.Translator do
  @moduledoc """
  Default translation for Erlang log messages.

  Logger allows developers to rewrite log messages provided by
  Erlang applications into a format more compatible with Elixir
  log messages by providing a translator.

  A translator is simply a tuple containing a module and a function
  that can be added and removed via the `Logger.add_translator/1` and
  `Logger.remove_translator/1` functions and is invoked for every Erlang
  message above the minimum log level with four arguments:

    * `min_level` - the current Logger level
    * `level` - the level of the message being translator
    * `kind` - if the message is a report or a format
    * `data` - the data to format. If it is a report, it is a tuple
      with `{report_type, report_data}`, if it is a format, it is a
      tuple with `{format_message, format_args}`

  The function must return:

    * `{:ok, iodata}` - if the message was translated with its translation
    * `:skip` - if the message is not meant to be translated nor logged
    * `:none` - if there is no translation, which triggers the next translator

  See the function `translate/4` in this module for an example implementation
  and the default messages translated by Logger.
  """

  def translate(min_level, :error, :format, message) do
    case message do
      {'** Generic server ' ++ _, [name, last, state, reason]} ->
        msg = "GenServer #{inspect name} terminating\n"
        if min_level == :debug do
          msg = msg <> "Last message: #{inspect last}\n"
                    <> "State: #{inspect state}\n"
        end
        {:ok, msg <> "** (exit) " <> format_otp_exit(reason)}

      {'** gen_event handler ' ++ _, [name, manager, last, state, reason]} ->
        msg = "GenEvent handler #{inspect name} installed in #{inspect manager} terminating\n"
        if min_level == :debug do
          msg = msg <> "Last message: #{inspect last}\n"
                    <> "State: #{inspect state}\n"
        end
        {:ok, msg <> "** (exit) " <> format_otp_exit(reason)}

      {'** Task ' ++ _, [name, starter, function, args, reason]} ->
        msg = "Task #{inspect name} started from #{inspect starter} terminating\n" <>
              "Function: #{inspect function}\n" <>
              "    Args: #{inspect args}\n" <>
              "** (exit) " <> format_otp_exit(reason)
        {:ok, msg}

      _ ->
        :none
    end
  end

  def translate(_min_level, :info, :report,
                {:std_info, [application: app, exited: reason, type: _type]}) do
    {:ok, "Application #{app} exited: #{Application.format_error(reason)}"}
  end

  def translate(min_level, :error, :report, {:supervisor_report, data}) do
    translate_supervisor(min_level, data)
  end

  def translate(min_level, :error, :report, {:crash_report, data}) do
    translate_crash(min_level, data)
  end

  def translate(min_level, :info, :report, {:progress, data}) do
    translate_progress(min_level, data)
  end

  def translate(_min_level, _level, _kind, _message) do
    :none
  end

  defp translate_supervisor(min_level,
                           [supervisor: sup, errorContext: context,
                             reason: reason,
                             offender: [{:pid, pid}, {:name, name} | offender]])
                           when is_pid(pid) and context !== :shutdown do
    {:ok, ["Child ", inspect(name), " of Supervisor ",
            sup_name(sup), ?\s, sup_context(context), ?\n,
            "Pid: ", inspect(pid), ?\n,
            child_info(min_level, offender), ?\n,
            "** (exit) " | offender_reason(reason, context)]}
  end

  defp translate_supervisor(min_level,
                           [supervisor: sup, errorContext: context,
                             reason: reason,
                             offender: [{:pid, _pid},
                                        {:name, name} | offender]]) do
    {:ok, ["Child ", inspect(name), " of Supervisor ",
            sup_name(sup), ?\s, sup_context(context), ?\n,
            child_info(min_level, offender), ?\n,
            "** (exit) " | offender_reason(reason, context)]}
  end

  defp translate_supervisor(min_level,
                           [supervisor: sup, errorContext: context,
                             reason: reason,
                             offender: [{:pid, pid} | offender]]) do
    {:ok, ["Child of Supervisor ",
            sup_name(sup), ?\s, sup_context(context), ?\n,
            "Pid: ", inspect(pid), ?\n,
            child_info(min_level, offender), ?\n,
            "** (exit) " | offender_reason(reason, context)]}
  end

  defp translate_supervisor(min_level,
                           [supervisor: sup, errorContext: context,
                             reason: reason,
                             offender: [{:nb_children, n},
                                        {:name, name} | offender]]) do
    {:ok, ["Children ", inspect(name), " of Supervisor ",
            sup_name(sup), ?\s, sup_context(context), ?\n,
            "Number: ", inspect(n), ?\n,
            child_info(min_level, offender), ?\n,
            "** (exit) " | offender_reason(reason, context)]}
  end

  defp translate_supervisor(_min_level, _other), do: :none

  defp translate_progress(_min_level,
                          [application: app, started_at: node_name]) do
    {:ok, ["Application ", to_string(app), " started at " | inspect(node_name)]}
  end

  defp translate_progress(min_level,
                          [supervisor: sup,
                            started: [{:pid, pid}, {:name, name} | started]]) do
    {:ok, ["Child ", inspect(name), " of Supervisor ",
            sup_name(sup), " started\n",
            "Pid: ", inspect(pid), ?\n |
            child_info(min_level, started)]}
  end

  defp translate_progress(min_level,
                          [supervisor: sup,
                            started: [{:pid, pid} | started]]) do
    {:ok, ["Child of Supervisor ", sup_name(sup), " started\n",
            "Pid: ", inspect(pid), ?\n |
            child_info(min_level, started)]}
  end

  defp translate_progress(_min_level, _other), do: :none

  defp sup_name({:local, name}), do: inspect(name)
  defp sup_name({:global, name}), do: inspect(name)
  defp sup_name({:via, _mod, name}), do: inspect(name)
  defp sup_name({pid, mod}), do: [inspect(pid), " (", inspect(mod), ?)]

  defp sup_context(:start_error), do: "failed to start"
  defp sup_context(:child_terminated), do: "terminated"
  defp sup_context(:shutdown), do: "caused shutdown"
  defp sup_context(:shutdown_error), do: "shutdown abnormally"

  defp child_info(min_level, [{:mfargs, {mod, fun, args}} | debug]) do
    ["Start Call: ", Exception.format_mfa(mod, fun, args) |
      child_debug(min_level, debug)]
  end

  defp child_info(min_level, [{:mfa, {mod, fun, args}} | debug]) do
    ["Start Call: ", Exception.format_mfa(mod, fun, args) |
      child_debug(min_level, debug)]
  end

  defp child_info(min_level, [{:mod, mod} | debug]) do
    ["Start Module: ", inspect(mod) |
      child_debug(min_level, debug)]
  end

  defp child_debug(:debug,
                   [restart_type: restart, shutdown: shutdown, child_type: type]) do
    [?\n,
      "Restart: ", inspect(restart), ?\n,
      "Shutdown: ", inspect(shutdown), ?\n,
      "Type: ", inspect(type)]
  end

  defp child_debug(_min_level, _child) do
    []
  end

  # If start call raises reason will be of form {:EXIT, reason}
  defp offender_reason({:EXIT, reason}, :start_error) do
    Exception.format_exit(reason)
  end

  defp offender_reason(reason, _context) do
    Exception.format_exit(reason)
  end

  defp translate_crash(min_level,
                       [[{:initial_call, _} = initial_call,
                         {:pid, pid},
                         {:registered_name, name},
                         {:error_info, {kind, exception, stack}} | crashed],
                        linked]) do
    {:ok, ["Process ", crash_name(pid, name) , " terminating\n",
            crash_info(min_level, [initial_call | crashed]),
            crash_linked(min_level, linked) |
            Exception.format(kind, exception, stack)]}
  end

  defp translate_crash(min_level,
                       [[{:pid, pid},
                         {:registered_name, name},
                         {:error_info, {kind, exception, stack}} | crashed],
                        linked]) do
    {:ok, ["Process ", crash_name(pid, name) , " terminating\n",
            crash_info(min_level, crashed),
            crash_linked(min_level, linked) |
            Exception.format(kind, exception, stack)]}
  end

  defp crash_name(pid, []), do: inspect(pid)
  defp crash_name(pid, name), do: [inspect(name), " (", inspect(pid), ?)]

  defp crash_info(min_level, info, prefix \\ [])

  defp crash_info(min_level,
                  [{:initial_call, {mod, fun, args}} | info], prefix) do
    [prefix, "Initial Call: ", crash_call(mod, fun, args), ?\n |
      crash_info(min_level, info, prefix)]
  end

  defp crash_info(min_level,
                  [{:current_function, {mod, fun, args}} | info], prefix) do
    [prefix, "Current Call: ", crash_call(mod, fun, args), ?\n |
      crash_info(min_level, info, prefix)]
  end

  defp crash_info(min_level, [{:current_function, []} | info], prefix) do
    crash_info(min_level, info, prefix)
  end

  defp crash_info(min_level,
                  [{:ancestors, ancestors} | debug], prefix) do
    [prefix, "Ancestors: ", inspect(ancestors), ?\n |
      crash_debug(min_level, debug, prefix)]
  end

  defp crash_call(mod, fun, arity) when is_integer(arity) do
    Exception.format_mfa(mod, fun, arity)
  end

  defp crash_call(mod, fun, args) do
    Exception.format_mfa(mod, fun, length(args))
  end

  defp crash_debug(:debug,
                   [messages: msgs, links: links, dictionary: dict,
                     trap_exit: trap, status: status, heap_size: heap_size,
                     stack_size: stack_size, reductions: reductions], prefix) do
    [prefix, "Messages: ", inspect(msgs), ?\n,
      prefix, "Links: ", inspect(links), ?\n,
      prefix, "Dictionary: ", inspect(dict), ?\n,
      prefix, "Trapping Exits: ", inspect(trap), ?\n,
      prefix, "Status: ", inspect(status), ?\n,
      prefix, "Heap Size: ", inspect(heap_size), ?\n,
      prefix, "Stack Size: ", inspect(stack_size), ?\n,
      prefix, "Reductions: ", inspect(reductions), ?\n]
  end

  defp crash_debug(_min_level, _info, _prefix) do
    []
  end

  defp crash_linked(_min_level, []), do: []

  defp crash_linked(min_level, neighbours) do
    Enum.reduce(neighbours, "Neighbours:\n", fn({:neighbour, info}, acc) ->
      [acc | crash_neighbour(min_level, info)]
    end)
  end

  defp crash_neighbour(min_level,
                       [{:pid, pid}, {:registered_name, []} | info]) do
    prefix = "    "
    [prefix, inspect(pid), ?\n |
      crash_info(min_level, info, [prefix | prefix])]
  end

  defp crash_neighbour(min_level,
                       [{:pid, pid}, {:registered_name, name} | info]) do
    prefix = "    "
    [prefix, inspect(name), " (", inspect(pid), ")\n" |
      crash_info(min_level, info, [prefix | prefix])]
  end

  # OTP process rewrite the :undef error to these reasons when logging
  @gen_undef [:"module could not be loaded", :"function not exported"]

  defp format_otp_exit({undef, [{mod, fun, args, _info} | _ ]  = stacktrace} = reason)
  when undef in @gen_undef and is_atom(mod) and is_atom(fun) do
    cond do
      is_list(args) ->
        format_undef(mod, fun, length(args), undef, stacktrace)
      is_integer(args) ->
        format_undef(mod, fun, args, undef, stacktrace)
      true ->
        Exception.format_exit(reason)
    end
  end

  defp format_otp_exit(reason) do
    Exception.format_exit(reason)
  end

  defp format_undef(mod, fun, arity, undef, stacktrace) do
    opts = [module: mod, function: fun, arity: arity, reason: undef]
    exception = UndefinedFunctionError.exception(opts)
    Exception.format_exit({exception, stacktrace})
  end
end


Add `impl true` to PageLive from `mix phx.new --live` (#3756)

defmodule <%= web_namespace %>.PageLive do
  use <%= web_namespace %>, :live_view

  @impl true
  def mount(_params, _session, socket) do
    {:ok, assign(socket, query: "", results: %{})}
  end

  @impl true
  def handle_event("suggest", %{"q" => query}, socket) do
    {:noreply, assign(socket, results: search(query), query: query)}
  end

  @impl true
  def handle_event("search", %{"q" => query}, socket) do
    case search(query) do
      %{^query => vsn} ->
        {:noreply, redirect(socket, external: "https://hexdocs.pm/#{query}/#{vsn}")}

      _ ->
        {:noreply,
         socket
         |> put_flash(:error, "No dependencies found matching \"#{query}\"")
         |> assign(results: %{}, query: query)}
    end
  end

  defp search(query) do
    if not <%= web_namespace %>.Endpoint.config(:code_reloader) do
      raise "action disabled when not in development"
    end

    for {app, desc, vsn} <- Application.started_applications(),
        app = to_string(app),
        String.starts_with?(app, query) and not List.starts_with?(desc, ~c"ERTS"),
        into: %{},
        do: {app, vsn}
  end
end

defmodule <%= web_namespace %>.PageLive do
  use <%= web_namespace %>, :live_view

  def mount(_params, _session, socket) do
    {:ok, assign(socket, query: "", results: %{})}
  end

  def handle_event("suggest", %{"q" => query}, socket) do
    {:noreply, assign(socket, results: search(query), query: query)}
  end

  def handle_event("search", %{"q" => query}, socket) do
    case search(query) do
      %{^query => vsn} ->
        {:noreply, redirect(socket, external: "https://hexdocs.pm/#{query}/#{vsn}")}

      _ ->
        {:noreply,
         socket
         |> put_flash(:error, "No dependencies found matching \"#{query}\"")
         |> assign(results: %{}, query: query)}
    end
  end

  defp search(query) do
    if not <%= web_namespace %>.Endpoint.config(:code_reloader) do
      raise "action disabled when not in development"
    end

    for {app, desc, vsn} <- Application.started_applications(),
        app = to_string(app),
        String.starts_with?(app, query) and not List.starts_with?(desc, ~c"ERTS"),
        into: %{},
        do: {app, vsn}
  end
end


Run the code formatter on Mix.Tasks.App.TreeTest (#6839)

Code.require_file("../../test_helper.exs", __DIR__)

defmodule Mix.Tasks.App.TreeTest do
  use MixTest.Case

  defmodule AppDepsSample do
    def project do
      [app: :test, version: "0.1.0", start_permanent: true]
    end

    def application do
      [applications: [:logger, :app_deps_sample]]
    end
  end

  @tag apps: [:test, :app_deps_sample, :app_deps2_sample, :app_deps3_sample, :app_deps4_sample]
  test "shows the application tree", context do
    Mix.Project.push(AppDepsSample)

    in_tmp context.test, fn ->
      load_apps()
      Mix.Tasks.App.Tree.run(["--format", "pretty"])

      assert_received {:mix_shell, :info, ["test"]}
      assert_received {:mix_shell, :info, ["└── app_deps_sample"]}
      assert_received {:mix_shell, :info, ["    ├── app_deps2_sample"]}
      assert_received {:mix_shell, :info, ["    │   └── app_deps4_sample (included)"]}
      assert_received {:mix_shell, :info, ["    └── app_deps3_sample"]}
    end
  end

  @tag apps: [:foo, :bar]
  test "show the application tree for umbrella apps" do
    in_fixture "umbrella_dep/deps/umbrella", fn ->
      Mix.Project.in_project(:umbrella, ".", fn _ ->
        Mix.Task.run("app.tree", ["--format", "pretty"])
        assert_received {:mix_shell, :info, ["├── elixir"]}
        assert_received {:mix_shell, :info, ["foo"]}
        assert_received {:mix_shell, :info, ["    └── elixir"]}
      end)
    end
  end

  @tag apps: [:test, :app_deps_sample, :app_deps2_sample, :app_deps3_sample, :app_deps4_sample]
  test "shows the given application tree", context do
    Mix.Project.push(AppDepsSample)

    in_tmp context.test, fn ->
      assert_raise Mix.Error, "could not find application app_deps_sample", fn ->
        Mix.Tasks.App.Tree.run(["--format", "pretty", "app_deps_sample"])
      end

      load_apps()
      Mix.Tasks.App.Tree.run(["--format", "pretty", "app_deps_sample"])

      assert_received {:mix_shell, :info, ["app_deps_sample"]}
      assert_received {:mix_shell, :info, ["├── app_deps2_sample"]}
      assert_received {:mix_shell, :info, ["│   └── app_deps4_sample (included)"]}
      assert_received {:mix_shell, :info, ["└── app_deps3_sample"]}
    end
  end

  @tag apps: [:test, :app_deps_sample, :app_deps2_sample, :app_deps3_sample, :app_deps4_sample]
  test "shows the application dependency tree excluding applications", context do
    Mix.Project.push(AppDepsSample)

    in_tmp context.test, fn ->
      load_apps()

      exclude = ["--exclude", "app_deps4_sample", "--exclude", "app_deps3_sample"]
      Mix.Tasks.App.Tree.run(["--format", "pretty" | exclude])

      assert_received {:mix_shell, :info, ["test"]}
      assert_received {:mix_shell, :info, ["└── app_deps_sample"]}
      assert_received {:mix_shell, :info, ["    └── app_deps2_sample"]}
      refute_received {:mix_shell, :info, ["    │   └── app_deps4_sample (included)"]}
      refute_received {:mix_shell, :info, ["    └── app_deps3_sample"]}
    end
  end

  @tag apps: [:test, :app_deps_sample, :app_deps2_sample, :app_deps3_sample, :app_deps4_sample]
  test "shows the application tree in dot form", context do
    Mix.Project.push(AppDepsSample)

    in_tmp context.test, fn ->
      load_apps()
      Mix.Tasks.App.Tree.run(["--format", "dot"])

      assert File.read!("app_tree.dot") == """
             digraph "application tree" {
               "test"
               "test" -> "elixir"
               "test" -> "logger"
               "logger" -> "elixir"
               "test" -> "app_deps_sample"
               "app_deps_sample" -> "app_deps2_sample"
               "app_deps2_sample" -> "app_deps4_sample" [label="(included)"]
               "app_deps_sample" -> "app_deps3_sample"
             }
             """
    end
  end

  def load_apps() do
    :ok = :application.load({:application, :app_deps4_sample, [vsn: '1.0.0', env: []]})
    :ok = :application.load({:application, :app_deps3_sample, [vsn: '1.0.0', env: []]})

    opts = [vsn: '1.0.0', env: [], included_applications: [:app_deps4_sample]]
    :ok = :application.load({:application, :app_deps2_sample, opts})

    opts = [vsn: '1.0.0', env: [], applications: [:app_deps2_sample, :app_deps3_sample]]
    :ok = :application.load({:application, :app_deps_sample, opts})
  end
end

Code.require_file "../../test_helper.exs", __DIR__

defmodule Mix.Tasks.App.TreeTest do
  use MixTest.Case

  defmodule AppDepsSample do
    def project do
      [app: :test, version: "0.1.0", start_permanent: true]
    end

    def application do
      [applications: [:logger, :app_deps_sample]]
    end
  end

  @tag apps: [:test, :app_deps_sample, :app_deps2_sample, :app_deps3_sample, :app_deps4_sample]
  test "shows the application tree", context do
    Mix.Project.push AppDepsSample

    in_tmp context.test, fn ->
      load_apps()
      Mix.Tasks.App.Tree.run(["--format", "pretty"])

      assert_received {:mix_shell, :info, ["test"]}
      assert_received {:mix_shell, :info, ["└── app_deps_sample"]}
      assert_received {:mix_shell, :info, ["    ├── app_deps2_sample"]}
      assert_received {:mix_shell, :info, ["    │   └── app_deps4_sample (included)"]}
      assert_received {:mix_shell, :info, ["    └── app_deps3_sample"]}
    end
  end

  @tag apps: [:foo, :bar]
  test "show the application tree for umbrella apps" do
    in_fixture "umbrella_dep/deps/umbrella", fn ->
      Mix.Project.in_project(:umbrella, ".", fn _ ->
        Mix.Task.run "app.tree", ["--format", "pretty"]
        assert_received {:mix_shell, :info, ["├── elixir"]}
        assert_received {:mix_shell, :info, ["foo"]}
        assert_received {:mix_shell, :info, ["    └── elixir"]}
      end)
    end
  end

  @tag apps: [:test, :app_deps_sample, :app_deps2_sample, :app_deps3_sample, :app_deps4_sample]
  test "shows the given application tree", context do
    Mix.Project.push AppDepsSample

    in_tmp context.test, fn ->
      assert_raise Mix.Error, "could not find application app_deps_sample", fn ->
        Mix.Tasks.App.Tree.run(["--format", "pretty", "app_deps_sample"])
      end

      load_apps()
      Mix.Tasks.App.Tree.run(["--format", "pretty", "app_deps_sample"])

      assert_received {:mix_shell, :info, ["app_deps_sample"]}
      assert_received {:mix_shell, :info, ["├── app_deps2_sample"]}
      assert_received {:mix_shell, :info, ["│   └── app_deps4_sample (included)"]}
      assert_received {:mix_shell, :info, ["└── app_deps3_sample"]}
    end
  end

  @tag apps: [:test, :app_deps_sample, :app_deps2_sample, :app_deps3_sample, :app_deps4_sample]
  test "shows the application dependency tree excluding applications", context do
    Mix.Project.push AppDepsSample

    in_tmp context.test, fn ->
      load_apps()
      Mix.Tasks.App.Tree.run(["--format", "pretty", "--exclude", "app_deps4_sample", "--exclude", "app_deps3_sample"])

      assert_received {:mix_shell, :info, ["test"]}
      assert_received {:mix_shell, :info, ["└── app_deps_sample"]}
      assert_received {:mix_shell, :info, ["    └── app_deps2_sample"]}
      refute_received {:mix_shell, :info, ["    │   └── app_deps4_sample (included)"]}
      refute_received {:mix_shell, :info, ["    └── app_deps3_sample"]}
    end
  end

  @tag apps: [:test, :app_deps_sample, :app_deps2_sample, :app_deps3_sample, :app_deps4_sample]
  test "shows the application tree in dot form", context do
    Mix.Project.push AppDepsSample

    in_tmp context.test, fn ->
      load_apps()
      Mix.Tasks.App.Tree.run(["--format", "dot"])

      assert File.read!("app_tree.dot") == """
        digraph "application tree" {
          "test"
          "test" -> "elixir"
          "test" -> "logger"
          "logger" -> "elixir"
          "test" -> "app_deps_sample"
          "app_deps_sample" -> "app_deps2_sample"
          "app_deps2_sample" -> "app_deps4_sample" [label="(included)"]
          "app_deps_sample" -> "app_deps3_sample"
        }
        """
    end
  end

  def load_apps() do
    :ok = :application.load({:application, :app_deps4_sample, [vsn: '1.0.0', env: []]})
    :ok = :application.load({:application, :app_deps3_sample, [vsn: '1.0.0', env: []]})
    :ok = :application.load({:application, :app_deps2_sample, [vsn: '1.0.0', env: [], included_applications: [:app_deps4_sample]]})
    :ok = :application.load({:application, :app_deps_sample, [vsn: '1.0.0', env: [], applications: [:app_deps2_sample, :app_deps3_sample]]})
  end
end


Remove an old module, which is no longer used anywhere

defmodule OpenAperture.Deployer.DeployServer do
  @moduledoc "Handles Fleet deployments for OpenAperture"
  require    Logger
  use        GenServer

  alias OpenAperture.Router
  alias OpenAperture.Deployer
  alias Deployer.Repo
  alias Deployer.EtcdCluster
  alias Deployer.SystemdUnit
  alias OpenAperture.Messaging
  # alias OpenApertureBuildServer.DB.Models.ProductCluster

  @doc """
  Starts DeployerServer process.

  Returns `{:ok, pid}` or `{:error, reason}` or another standard GenServer
  msg. See its docs for details.
  """
  @spec start_link :: {:ok, pid} | {:error, String.t}
  def start_link do
    Logger.info("Starting #{__MODULE__}")
    GenServer.start_link(__MODULE__, nil)
  end

  @doc  """
  Deploys the Dockerized app to a CoreOS cluster.
  Returns `:ok` or `{:error, reason}`
  """
  @spec deploy(Map) :: :ok | {:error, String.t}
  def deploy(%{deployment_repo: deployment_repo} = options) do
    Logger.info("Beginning Fleet re-deployment...")

    case Repo.download(deployment_repo) do
      # :ok -> redeploy(options)
      :ok -> do_deploy(options)
        # GenServer.call(pid, {:redeploy, options} )
        # handle_call({:redeploy, options}, nil)
        # Workflow.next_step(
        #   xxx, %{
        #     deployment_repo: deployment_repo,
        #     etcd_token: Repo.get_etcd_token(deployment_repo)
        #   }
        # )
      {:error, reason} -> :error
        # TODO replace this with something?
        # Workflow.step_failed(_workflow, reason)
    end
  end

  @doc """
  Implements the deploy request callback.
  Returns {:reply, state}
  """
  # @spec handle_call(pid, term) :: :ok
  # def handle_call({:deploy, options}, state) do
  #   deploy(options)
  #   {:reply, state}
  # end

  @doc """
  Redeploys the Dockerized app to a OpenAperture cluster.
  Returns :ok or {:error, reason}
  """
  @spec redeploy(Map) :: :ok | {:error, String.t}
  def redeploy(options) do
    Logger.info ("Beginning Fleet re-deployment...")
    do_deploy(options)
  end

  @doc """
  Sends the redeploy request to the server.
  Returns {:noreply, new_state}
  """
  @spec handle_call({:redeploy, Map}, term) :: :ok
  def handle_call({:redeploy, options}, _state) do
    {:reply, redeploy(options)}
  end

  defp create_port_list(list, cur_idx, max_cnt) do
    if cur_idx == max_cnt do
      list
    else
      create_port_list(list ++ [0], cur_idx+1, max_cnt)
    end
  end

  defp configure_router(options, cluster, deployed_units) do
    if options[:openaperture_router] do
      case Router.validate_routing_options(options[:openaperture_router]) do
        :ok ->
          case update_openaperture_router(options[:openaperture_router], cluster, deployed_units) do
            :ok -> :ok
            {:error, errors} ->
              {:error, "Router returned the following errors: #{JSON.encode!(errors)}"}
          end
        {:error, errors} ->
          {:error, "Routing options have been misconfigured: #{JSON.encode!(errors)}"}
      end
    else
      :ok
    end
  end

  @doc """
  Redeploys the Dockerized app to a OpenAperture cluster.
  Returns :ok or {:error, reason}
  """
  @spec do_deploy(Map) :: :ok | {:error, String.t}
  defp do_deploy(options) do
    deployment_repo = options.deployment_repo

    if options[:product_cluster_etcd_token] do
      cluster = case EtcdCluster.create(options[:product_cluster_etcd_token]) do
        {:ok, etcd_cluster} -> etcd_cluster
        {:error, reason}    ->
          Logger.error("Failed to create etcd cluster:  #{reason}")
          nil
      end
    else
      #legacy (no product deployment info)
      cluster = Repo.get_etcd_cluster(deployment_repo)
    end

    host_cnt = if cluster, do: EtcdCluster.get_host_count(cluster), else: 0

    Logger.debug("Parsing units...")
    new_units     = Repo.get_units(deployment_repo)
    new_units_cnt = if new_units, do: length(new_units), else: 0

    cond do
      host_cnt == 0 ->
        # Workflow.step_failed(_workflow, "Unable to complete deployment, unable to find hosts from the cluster associated with the deployment repo!")
        Logger.error("Unable to complete deployment, no hosts associated with the repo!")
        {:error, "no hosts associated with the repo"}
      new_units_cnt == 0 ->
        # Workflow.step_failed(_workflow, "Unable to complete deployment, no valid Units were retrieved from the deployment repo!")
        Logger.error("Unable to complete deployemnt, no valid Units were retrieved from the deployment repo")
        {:error, "No valid units were retrieved from the repo"}
      true ->
        requested_instance_cnt = host_cnt

        if options[:min_instance_cnt] && options[:min_instance_cnt] > requested_instance_cnt do
          requested_instance_cnt = options[:min_instance_cnt]
        end

        Logger.debug("Allocating #{requested_instance_cnt} ports on the cluster...");

        if options[:product_cluster] != nil && options[:product_component] != nil do
          Logger.debug("Allocating ports on the cluster...")
          # available_ports = ProductCluster.allocate_ports_for_component(options[:product_cluster], options[:product_component], requested_instance_cnt)

          #find all current port entries so we can remove then after the deploy units finishes
        else
          #legacy (no product deployment info)
          #just create a bogus list of port 0s.  this won't be used by the old .service.eex files
          Logger.debug("Allocating ports bogus 0-ports on the cluster...")
          available_ports = create_port_list([], 0, requested_instance_cnt)
        end

        Logger.debug("Deploying units...")
        # Workflow.publish_success_notification(_workflow, "Preparing to deploy #{new_units_cnt} units onto #{host_cnt} hosts...")
        deployed_units = EtcdCluster.deploy_units(cluster, new_units, available_ports)

        case configure_router(options, cluster, deployed_units) do
          :ok ->
            # Workflow.next_step(_workflow, %{deployed_units: deployed_units, etcd_cluster: cluster})
            Logger.info("Router configured")
          {:error, reason} ->
            # Workflow.step_failed(workflow, reason)
            Logger.error("Unable to configure Router: #{inspect reason}")
            {:error, reason}
        end
    end
  end

  defp update_openaperture_router(openaperture_router, cluster, deployed_units) do
    if (deployed_units && length(deployed_units) > 0) do
      host_map   = %{}
      hosts      = EtcdCluster.get_hosts(cluster)
      etcd_token = EtcdCluster.get_token(cluster)

      if (hosts != nil && length(hosts) > 0) do
        host_map = Enum.reduce hosts, host_map, fn(host, host_map) ->
          Map.put(host_map, host["id"], host)
        end
      end

      Enum.reduce deployed_units, nil, fn(deployed_unit, errors) ->
        dst_port   = SystemdUnit.get_assigned_port(deployed_unit)
        machine_id = SystemdUnit.get_machine_id(deployed_unit)
        host       = host_map[machine_id]

        SystemdUnit.set_etcd_token(deployed_unit, etcd_token)
        SystemdUnit.refresh(deployed_unit)

        if (host != nil) do
          Router.add_route(openaperture_router,host["primaryIP"], dst_port)
        end
      end
    end

    Router.update_changes(openaperture_router)
  end
end


Add a bunch of backticks to some docs in Code.Identifier

[ci skip]

defmodule Code.Identifier do
  @moduledoc false

  @doc """
  Checks if the given identifier is an unary op.

  ## Examples

      iex> Code.Identifier.unary_op(:+)
      {:non_associative, 300}

  """
  @spec unary_op(atom) :: {:non_associative, precedence :: pos_integer} | :error
  def unary_op(op) do
    cond do
      op in [:&] -> {:non_associative, 90}
      op in [:!, :^, :not, :+, :-, :~~~] -> {:non_associative, 300}
      op in [:@] -> {:non_associative, 320}
      true -> :error
    end
  end

  @doc """
  Checks if the given identifier is a binary op.

  ## Examples

      iex> Code.Identifier.binary_op(:+)
      {:left, 210}

  """
  @spec binary_op(atom) :: {:left | :right, precedence :: pos_integer} | :error
  def binary_op(op) do
    cond do
      op in [:<-, :\\] -> {:left, 40}
      op in [:when] -> {:right, 50}
      op in [:::] -> {:right, 60}
      op in [:|] -> {:right, 70}
      op in [:=] -> {:right, 100}
      op in [:||, :|||, :or] -> {:left, 130}
      op in [:&&, :&&&, :and] -> {:left, 140}
      op in [:==, :!=, :=~, :===, :!==] -> {:left, 150}
      op in [:<, :<=, :>=, :>] -> {:left, 160}
      op in [:|>, :<<<, :>>>, :<~, :~>, :<<~, :~>>, :<~>, :<|>] -> {:left, 170}
      op in [:in] -> {:left, 180}
      op in [:^^^] -> {:left, 190}
      op in [:++, :--, :.., :<>] -> {:right, 200}
      op in [:+, :-] -> {:left, 210}
      op in [:*, :/] -> {:left, 220}
      op in [:.] -> {:left, 310}
      true -> :error
    end
  end

  @doc """
  Classifies the given atom into one of the following categories:

    * `:alias` - a valid Elixir alias, like `Foo`, `Foo.Bar` and so on

    * `:callable_local` - an atom that can be used as a local call;
      this category includes identifiers like `:foo`

    * `:callable_operators` - all callable operators, such as `:<>`. Note
      operators such as `:..` are not callable because of ambiguity

    * `:not_callable` - an atom that cannot be used as a function call after the
      `.` operator (for example, `:<<>>` is not callable because `Foo.<<>>` is a
      syntax error); this category includes atoms like `:Foo`, since they are
      valid identifiers but they need quotes to be used in function calls
      (`Foo."Bar"`)

    * `:other` - any other atom (these are usually escaped when inspected, like
      `:"foo and bar"`)

  """
  def classify(atom) when is_atom(atom) do
    charlist = Atom.to_charlist(atom)

    cond do
      atom in [:%, :%{}, :{}, :<<>>, :..., :.., :., :->] ->
        :not_callable

      unary_op(atom) != :error or binary_op(atom) != :error ->
        :callable_operator

      valid_alias?(charlist) ->
        :alias

      true ->
        case :elixir_config.safe_get(:identifier_tokenizer, String.Tokenizer).tokenize(charlist) do
          {kind, _acc, [], _, _, special} ->
            if kind == :identifier and not :lists.member(?@, special) do
              :callable_local
            else
              :not_callable
            end

          _ ->
            :other
        end
    end
  end

  defp valid_alias?('Elixir' ++ rest), do: valid_alias_piece?(rest)
  defp valid_alias?(_other), do: false

  defp valid_alias_piece?([?., char | rest]) when char >= ?A and char <= ?Z,
    do: valid_alias_piece?(trim_leading_while_valid_identifier(rest))

  defp valid_alias_piece?([]), do: true
  defp valid_alias_piece?(_other), do: false

  defp trim_leading_while_valid_identifier([char | rest])
       when char >= ?a and char <= ?z
       when char >= ?A and char <= ?Z
       when char >= ?0 and char <= ?9
       when char == ?_ do
    trim_leading_while_valid_identifier(rest)
  end

  defp trim_leading_while_valid_identifier(other) do
    other
  end

  @doc """
  Inspects the identifier as an atom.
  """
  def inspect_as_atom(atom) when is_nil(atom) or is_boolean(atom) do
    Atom.to_string(atom)
  end

  def inspect_as_atom(atom) when is_atom(atom) do
    binary = Atom.to_string(atom)

    case classify(atom) do
      :alias ->
        case binary do
          binary when binary in ["Elixir", "Elixir.Elixir"] -> binary
          "Elixir.Elixir." <> _rest -> binary
          "Elixir." <> rest -> rest
        end

      type when type in [:callable_local, :callable_operator, :not_callable] ->
        ":" <> binary

      :other ->
        {escaped, _} = escape(binary, ?")
        IO.iodata_to_binary([?:, ?", escaped, ?"])
    end
  end

  @doc """
  Inspects the given identifier as a key.
  """
  def inspect_as_key(atom) when is_atom(atom) do
    binary = Atom.to_string(atom)

    case classify(atom) do
      type when type in [:callable_local, :callable_operator, :not_callable] ->
        IO.iodata_to_binary([binary, ?:])

      _ ->
        {escaped, _} = escape(binary, ?")
        IO.iodata_to_binary([?", escaped, ?", ?:])
    end
  end

  @doc """
  Inspects the given identifier as a function name.
  """
  def inspect_as_function(atom) when is_atom(atom) do
    binary = Atom.to_string(atom)

    case classify(atom) do
      type when type in [:callable_local, :callable_operator] ->
        binary

      type ->
        escaped =
          if type in [:not_callable, :alias] do
            binary
          else
            elem(escape(binary, ?"), 0)
          end

        IO.iodata_to_binary([?", escaped, ?"])
    end
  end

  @doc """
  Extracts the name and arity of the parent from the anonymous function identifier.
  """
  # Example of this format: -NAME/ARITY-fun-COUNT-
  def extract_anonymous_fun_parent(atom) when is_atom(atom) do
    with "-" <> rest <- Atom.to_string(atom),
         [trailing | reversed] = rest |> String.split("/") |> Enum.reverse(),
         [arity, _inner, _count, ""] <- String.split(trailing, "-") do
      {reversed |> Enum.reverse() |> Enum.join("/") |> String.to_atom(), arity}
    else
      _ -> :error
    end
  end

  @doc """
  Escapes the given identifier.
  """
  def escape(other, char, count \\ :infinity, fun \\ &escape_map/1) do
    escape(other, char, count, [], fun)
  end

  defp escape(<<_, _::binary>> = binary, _char, 0, acc, _fun) do
    {acc, binary}
  end

  defp escape(<<char, t::binary>>, char, count, acc, fun) do
    escape(t, char, decrement(count), [acc | [?\\, char]], fun)
  end

  defp escape(<<?#, ?{, t::binary>>, char, count, acc, fun) do
    escape(t, char, decrement(count), [acc | '\\\#{'], fun)
  end

  defp escape(<<h::utf8, t::binary>>, char, count, acc, fun) do
    escaped = if value = fun.(h), do: value, else: escape_char(h)
    escape(t, char, decrement(count), [acc | escaped], fun)
  end

  defp escape(<<a::4, b::4, t::binary>>, char, count, acc, fun) do
    escape(t, char, decrement(count), [acc | ['\\x', to_hex(a), to_hex(b)]], fun)
  end

  defp escape(<<>>, _char, _count, acc, _fun) do
    {acc, <<>>}
  end

  defp escape_char(0), do: '\\0'

  defp escape_char(65279), do: '\\uFEFF'

  defp escape_char(char)
       when char in 0x20..0x7E
       when char in 0xA0..0xD7FF
       when char in 0xE000..0xFFFD
       when char in 0x10000..0x10FFFF do
    <<char::utf8>>
  end

  defp escape_char(char) when char < 0x100 do
    <<a::4, b::4>> = <<char::8>>
    ['\\x', to_hex(a), to_hex(b)]
  end

  defp escape_char(char) when char < 0x10000 do
    <<a::4, b::4, c::4, d::4>> = <<char::16>>
    ['\\x{', to_hex(a), to_hex(b), to_hex(c), to_hex(d), ?}]
  end

  defp escape_char(char) when char < 0x1000000 do
    <<a::4, b::4, c::4, d::4, e::4, f::4>> = <<char::24>>
    ['\\x{', to_hex(a), to_hex(b), to_hex(c), to_hex(d), to_hex(e), to_hex(f), ?}]
  end

  defp escape_map(?\a), do: '\\a'
  defp escape_map(?\b), do: '\\b'
  defp escape_map(?\d), do: '\\d'
  defp escape_map(?\e), do: '\\e'
  defp escape_map(?\f), do: '\\f'
  defp escape_map(?\n), do: '\\n'
  defp escape_map(?\r), do: '\\r'
  defp escape_map(?\t), do: '\\t'
  defp escape_map(?\v), do: '\\v'
  defp escape_map(?\\), do: '\\\\'
  defp escape_map(_), do: false

  @compile {:inline, to_hex: 1, decrement: 1}
  defp to_hex(c) when c in 0..9, do: ?0 + c
  defp to_hex(c) when c in 10..15, do: ?A + c - 10

  defp decrement(:infinity), do: :infinity
  defp decrement(counter), do: counter - 1
end

defmodule Code.Identifier do
  @moduledoc false

  @doc """
  Checks if the given identifier is an unary op.

  ## Examples

      iex> Code.Identifier.unary_op(:+)
      {:non_associative, 300}

  """
  @spec unary_op(atom) :: {:non_associative, precedence :: pos_integer} | :error
  def unary_op(op) do
    cond do
      op in [:&] -> {:non_associative, 90}
      op in [:!, :^, :not, :+, :-, :~~~] -> {:non_associative, 300}
      op in [:@] -> {:non_associative, 320}
      true -> :error
    end
  end

  @doc """
  Checks if the given identifier is a binary op.

  ## Examples

      iex> Code.Identifier.binary_op(:+)
      {:left, 210}

  """
  @spec binary_op(atom) :: {:left | :right, precedence :: pos_integer} | :error
  def binary_op(op) do
    cond do
      op in [:<-, :\\] -> {:left, 40}
      op in [:when] -> {:right, 50}
      op in [:::] -> {:right, 60}
      op in [:|] -> {:right, 70}
      op in [:=] -> {:right, 100}
      op in [:||, :|||, :or] -> {:left, 130}
      op in [:&&, :&&&, :and] -> {:left, 140}
      op in [:==, :!=, :=~, :===, :!==] -> {:left, 150}
      op in [:<, :<=, :>=, :>] -> {:left, 160}
      op in [:|>, :<<<, :>>>, :<~, :~>, :<<~, :~>>, :<~>, :<|>] -> {:left, 170}
      op in [:in] -> {:left, 180}
      op in [:^^^] -> {:left, 190}
      op in [:++, :--, :.., :<>] -> {:right, 200}
      op in [:+, :-] -> {:left, 210}
      op in [:*, :/] -> {:left, 220}
      op in [:.] -> {:left, 310}
      true -> :error
    end
  end

  @doc """
  Classifies the given atom into one of the following categories:

    * :alias - a valid Elixir alias, like Foo, Foo.Bar and so on

    * :callable_local - an atom that can be used as a local call;
      this category includes identifiers like :foo

    * :callable_operators - all callable operators, such as `:<>`. Note
      operators such as `:..` are not callable because of ambiguity

    * :not_callable - an atom that cannot be used as a function call after the
      . operator (for example, :<<>> is not callable because Foo.<<>> is a
      syntax error); this category includes atoms like :Foo, since they are
      valid identifiers but they need quotes to be used in function calls
      (Foo."Bar")

    * :other - any other atom (these are usually escaped when inspected, like
      :"foo and bar")

  """
  def classify(atom) when is_atom(atom) do
    charlist = Atom.to_charlist(atom)

    cond do
      atom in [:%, :%{}, :{}, :<<>>, :..., :.., :., :->] ->
        :not_callable

      unary_op(atom) != :error or binary_op(atom) != :error ->
        :callable_operator

      valid_alias?(charlist) ->
        :alias

      true ->
        case :elixir_config.safe_get(:identifier_tokenizer, String.Tokenizer).tokenize(charlist) do
          {kind, _acc, [], _, _, special} ->
            if kind == :identifier and not :lists.member(?@, special) do
              :callable_local
            else
              :not_callable
            end

          _ ->
            :other
        end
    end
  end

  defp valid_alias?('Elixir' ++ rest), do: valid_alias_piece?(rest)
  defp valid_alias?(_other), do: false

  defp valid_alias_piece?([?., char | rest]) when char >= ?A and char <= ?Z,
    do: valid_alias_piece?(trim_leading_while_valid_identifier(rest))

  defp valid_alias_piece?([]), do: true
  defp valid_alias_piece?(_other), do: false

  defp trim_leading_while_valid_identifier([char | rest])
       when char >= ?a and char <= ?z
       when char >= ?A and char <= ?Z
       when char >= ?0 and char <= ?9
       when char == ?_ do
    trim_leading_while_valid_identifier(rest)
  end

  defp trim_leading_while_valid_identifier(other) do
    other
  end

  @doc """
  Inspects the identifier as an atom.
  """
  def inspect_as_atom(atom) when is_nil(atom) or is_boolean(atom) do
    Atom.to_string(atom)
  end

  def inspect_as_atom(atom) when is_atom(atom) do
    binary = Atom.to_string(atom)

    case classify(atom) do
      :alias ->
        case binary do
          binary when binary in ["Elixir", "Elixir.Elixir"] -> binary
          "Elixir.Elixir." <> _rest -> binary
          "Elixir." <> rest -> rest
        end

      type when type in [:callable_local, :callable_operator, :not_callable] ->
        ":" <> binary

      :other ->
        {escaped, _} = escape(binary, ?")
        IO.iodata_to_binary([?:, ?", escaped, ?"])
    end
  end

  @doc """
  Inspects the given identifier as a key.
  """
  def inspect_as_key(atom) when is_atom(atom) do
    binary = Atom.to_string(atom)

    case classify(atom) do
      type when type in [:callable_local, :callable_operator, :not_callable] ->
        IO.iodata_to_binary([binary, ?:])

      _ ->
        {escaped, _} = escape(binary, ?")
        IO.iodata_to_binary([?", escaped, ?", ?:])
    end
  end

  @doc """
  Inspects the given identifier as a function name.
  """
  def inspect_as_function(atom) when is_atom(atom) do
    binary = Atom.to_string(atom)

    case classify(atom) do
      type when type in [:callable_local, :callable_operator] ->
        binary

      type ->
        escaped =
          if type in [:not_callable, :alias] do
            binary
          else
            elem(escape(binary, ?"), 0)
          end

        IO.iodata_to_binary([?", escaped, ?"])
    end
  end

  @doc """
  Extracts the name and arity of the parent from the anonymous function identifier.
  """
  # Example of this format: -NAME/ARITY-fun-COUNT-
  def extract_anonymous_fun_parent(atom) when is_atom(atom) do
    with "-" <> rest <- Atom.to_string(atom),
         [trailing | reversed] = rest |> String.split("/") |> Enum.reverse(),
         [arity, _inner, _count, ""] <- String.split(trailing, "-") do
      {reversed |> Enum.reverse() |> Enum.join("/") |> String.to_atom(), arity}
    else
      _ -> :error
    end
  end

  @doc """
  Escapes the given identifier.
  """
  def escape(other, char, count \\ :infinity, fun \\ &escape_map/1) do
    escape(other, char, count, [], fun)
  end

  defp escape(<<_, _::binary>> = binary, _char, 0, acc, _fun) do
    {acc, binary}
  end

  defp escape(<<char, t::binary>>, char, count, acc, fun) do
    escape(t, char, decrement(count), [acc | [?\\, char]], fun)
  end

  defp escape(<<?#, ?{, t::binary>>, char, count, acc, fun) do
    escape(t, char, decrement(count), [acc | '\\\#{'], fun)
  end

  defp escape(<<h::utf8, t::binary>>, char, count, acc, fun) do
    escaped = if value = fun.(h), do: value, else: escape_char(h)
    escape(t, char, decrement(count), [acc | escaped], fun)
  end

  defp escape(<<a::4, b::4, t::binary>>, char, count, acc, fun) do
    escape(t, char, decrement(count), [acc | ['\\x', to_hex(a), to_hex(b)]], fun)
  end

  defp escape(<<>>, _char, _count, acc, _fun) do
    {acc, <<>>}
  end

  defp escape_char(0), do: '\\0'

  defp escape_char(65279), do: '\\uFEFF'

  defp escape_char(char)
       when char in 0x20..0x7E
       when char in 0xA0..0xD7FF
       when char in 0xE000..0xFFFD
       when char in 0x10000..0x10FFFF do
    <<char::utf8>>
  end

  defp escape_char(char) when char < 0x100 do
    <<a::4, b::4>> = <<char::8>>
    ['\\x', to_hex(a), to_hex(b)]
  end

  defp escape_char(char) when char < 0x10000 do
    <<a::4, b::4, c::4, d::4>> = <<char::16>>
    ['\\x{', to_hex(a), to_hex(b), to_hex(c), to_hex(d), ?}]
  end

  defp escape_char(char) when char < 0x1000000 do
    <<a::4, b::4, c::4, d::4, e::4, f::4>> = <<char::24>>
    ['\\x{', to_hex(a), to_hex(b), to_hex(c), to_hex(d), to_hex(e), to_hex(f), ?}]
  end

  defp escape_map(?\a), do: '\\a'
  defp escape_map(?\b), do: '\\b'
  defp escape_map(?\d), do: '\\d'
  defp escape_map(?\e), do: '\\e'
  defp escape_map(?\f), do: '\\f'
  defp escape_map(?\n), do: '\\n'
  defp escape_map(?\r), do: '\\r'
  defp escape_map(?\t), do: '\\t'
  defp escape_map(?\v), do: '\\v'
  defp escape_map(?\\), do: '\\\\'
  defp escape_map(_), do: false

  @compile {:inline, to_hex: 1, decrement: 1}
  defp to_hex(c) when c in 0..9, do: ?0 + c
  defp to_hex(c) when c in 10..15, do: ?A + c - 10

  defp decrement(:infinity), do: :infinity
  defp decrement(counter), do: counter - 1
end


Update typespecs in Inspect.Algebra

Update typespecs in Inspect.Algebra

defmodule Inspect.Algebra do
  @moduledoc %S"""
  A set of functions for creating and manipulating algebra
  documents, as described in ["Strictly Pretty" (2000) by Christian Lindig][0].

  An algebra document is represented by an `Inspect.Algebra` node
  or a regular string.

      iex> Inspect.Algebra.empty
      :doc_nil
      iex> "foo"
      "foo"

  With the functions in this module, we can concatenate different
  elements together and render them:

      iex> doc = Inspect.Algebra.concat(Inspect.Algebra.empty, "foo")
      iex> Inspect.Algebra.pretty(doc, 80)
      "foo"

  The functions `nest/2`, `space/2` and `line/2` help you put the
  document together into a rigid structure. However, the document
  algebra gets interesting when using functions like `break/2`, which
  converts the given string into a line break depending on how much space
  there is to print. Let's glue two docs together with a break and then
  render it:

      iex> doc = Inspect.Algebra.glue("a", " ", "b")
      iex> Inspect.Algebra.pretty(doc, 80)
      "a b"

  Notice the break was represented as is, because we haven't reached
  a line limit. Once we do, it is replaced by a newline:

      iex> doc = Inspect.Algebra.glue(String.duplicate("a", 20), " ", "b")
      iex> Inspect.Algebra.pretty(doc, 10)
      "aaaaaaaaaaaaaaaaaaaa\nb"

  Finally, this module also contains Elixir related functions, a bit
  tied to Elixir formatting, namely `surround/3` and `surround_many/5`.

  ## Implementation details

  The original Haskell implementation of the algorithm by [Wadler][1]
  relies on lazy evaluation to unfold document groups on two alternatives:
  `:flat` (breaks as spaces) and `:break` (breaks as newlines).
  Implementing the same logic in a strict language such as Elixir leads
  to an exponential growth of possible documents, unless document groups
  are encoded explictly as `:flat` or `:break`. Those groups are then reduced
  to a simple document, where the layout is already decided, per [Lindig][0].

  This implementation slightly changes the semantic of Lindig's algorithm
  to allow elements that belong to the same group to be printed together
  in the same line, even if they do not fit the line fully. This was achieved
  by changing `:break` to mean a possible break and `:flat` to force a flat
  structure. Then deciding if a break works as a newline is just a matter
  of checking if we have enough space until the next break that is not
  inside a group (which is still flat).

  Custom pretty printers can be implemented using the documents returned
  by this module and by providing their own rendering functions.

    [0]: http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.34.2200
    [1]: http://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf

  """

  @surround_separator ","
  @tail_separator " |"
  @newline "\n"
  @nesting 1
  @break " "

  # Functional interface to `doc` records

  @type t :: :doc_nil | doc_cons_t | doc_nest_t | doc_break_t | doc_group_t | binary
  defrecordp :doc_cons, left: :doc_nil :: t, right: :doc_nil :: t
  defrecordp :doc_nest, indent: 1 :: non_neg_integer, doc: :doc_nil :: t
  defrecordp :doc_break, str: " " :: binary
  defrecordp :doc_group, doc: :doc_nil :: t

  defmacrop is_doc(doc) do
    if __CALLER__.in_guard? do
      do_is_doc(doc)
    else
      var = quote do: doc
      quote do
        unquote(var) = unquote(doc)
        unquote(do_is_doc(var))
      end
    end
  end

  defp do_is_doc(doc) do
    quote do
      is_binary(unquote(doc)) or
      unquote(doc) == :doc_nil or
      (is_tuple(unquote(doc)) and
       elem(unquote(doc), 0) in [:doc_cons, :doc_nest, :doc_break, :doc_group])
    end
  end

  @doc """
  Returns `:doc_nil` which is a document entity used to represent
  nothingness. Takes no arguments.

  ## Examples

      iex> Inspect.Algebra.empty
      :doc_nil

  """
  @spec empty() :: :doc_nil
  def empty, do: :doc_nil

  @doc """
  Concatenates two document entities. Takes two arguments:
  left doc and right doc. Returns a DocCons doc

  ## Examples

      iex> doc = Inspect.Algebra.concat "Tasteless", "Artosis"
      iex> Inspect.Algebra.pretty(doc, 80)
      "TastelessArtosis"

  """
  @spec concat(t, t) :: doc_cons_t
  def concat(x, y) when is_doc(x) and is_doc(y) do
    doc_cons(left: x, right: y)
  end

  @doc """
  Concatenates a list of documents.
  """
  @spec concat([t]) :: doc_cons_t
  def concat(docs) do
    folddoc(docs, &concat(&1, &2))
  end

  @doc """
  Nests document entity `x` positions deep. Nesting will be
  appended to the line breaks.

  ## Examples

      iex> doc = Inspect.Algebra.nest(Inspect.Algebra.concat(Inspect.Algebra.break, "6"), 5)
      iex> Inspect.Algebra.pretty(doc, 80)
      " 6"

  """
  @spec nest(t, non_neg_integer) :: doc_nest_t
  def nest(x, 0) when is_doc(x) do
    x
  end

  def nest(x, i) when is_doc(x) and is_integer(i) do
    doc_nest(indent: i, doc: x)
  end

  @doc %S"""
  Document entity representing a break. This break can
  be rendered as a linebreak or as spaces, depending on the
  `mode` of the chosen layout or the provided separator.

  ## Examples

  Let's glue two docs together with a break and then render it:

      iex> doc = Inspect.Algebra.glue("a", " ", "b")
      iex> Inspect.Algebra.pretty(doc, 80)
      "a b"

  Notice the break was represented as is, because we haven't reached
  a line limit. Once we do, it is replaced by a newline:

      iex> doc = Inspect.Algebra.glue(String.duplicate("a", 20), " ", "b")
      iex> Inspect.Algebra.pretty(doc, 10)
      "aaaaaaaaaaaaaaaaaaaa\nb"

  """
  @spec break(binary) :: doc_break_t
  def break(s) when is_binary(s), do: doc_break(str: s)

  @spec break() :: doc_break_t
  def break(), do: doc_break(str: @break)

  @doc """
  Inserts a break between two docs. See `break/1` for more info.
  """
  @spec glue(t, t) :: doc_cons_t
  def glue(x, y), do: concat(x, concat(break, y))

  @doc """
  Inserts a break, passed as the second argument, between two docs,
  the first and the third arguments.
  """
  @spec glue(t, binary, t) :: doc_cons_t
  def glue(x, g, y) when is_binary(g), do: concat(x, concat(break(g), y))

  @doc %S"""
  Returns a group containing the specified document.

  ## Examples

      iex> doc = Inspect.Algebra.group(
      ...>   Inspect.Algebra.concat(
      ...>     Inspect.Algebra.group(
      ...>       Inspect.Algebra.concat(
      ...>         "Hello,",
      ...>         Inspect.Algebra.concat(
      ...>           Inspect.Algebra.break,
      ...>           "A"
      ...>         )
      ...>       )
      ...>     ),
      ...>     Inspect.Algebra.concat(
      ...>       Inspect.Algebra.break,
      ...>       "B"
      ...>     )
      ...> ))
      iex> Inspect.Algebra.pretty(doc, 80)
      "Hello, A B"
      iex> Inspect.Algebra.pretty(doc, 6)
      "Hello,\nA B"

  """
  @spec group(t) :: doc_group_t
  def group(d) when is_doc(d) do
    doc_group(doc: d)
  end

  @doc """
  Inserts a mandatory single space between two document entities.

  ## Examples

      iex> doc = Inspect.Algebra.space "Hughes", "Wadler"
      iex> Inspect.Algebra.pretty(doc, 80)
      "Hughes Wadler"

  """
  @spec space(t, t) :: doc_cons_t
  def space(x, y), do: concat(x, concat(" ", y))

  @doc %S"""
  Inserts a mandatory linebreak between two document entities.

  ## Examples

      iex> doc = Inspect.Algebra.line "Hughes", "Wadler"
      iex> Inspect.Algebra.pretty(doc, 80)
      "Hughes\nWadler"

  """
  @spec line(t, t) :: doc_cons_t
  def line(x, y), do: concat(x, concat(@newline, y))

  @doc """
  Folds a list of document entities into a document entity
  using a function that is passed as the first argument.

  ## Examples

      iex> doc = ["A", "B"]
      iex> doc = Inspect.Algebra.folddoc(doc, fn(x,y) ->
      ...>   Inspect.Algebra.concat [x, "!", y]
      ...> end)
      iex> Inspect.Algebra.pretty(doc, 80)
      "A!B"

  """
  @spec folddoc([t], ((t, t) -> t)) :: t
  def folddoc([], _), do: empty
  def folddoc([doc], _), do: doc
  def folddoc([d|ds], f), do: f.(d, folddoc(ds, f))

  # Elixir conveniences

  @doc %S"""
  Surrounds a document with characters.

  Puts the document between left and right enclosing and nesting it.
  The document is marked as a group, to show the maximum as possible
  concisely together.

  ## Examples

      iex> doc = Inspect.Algebra.surround "[", Inspect.Algebra.glue("a", "b"), "]"
      iex> Inspect.Algebra.pretty(doc, 3)
      "[a\n b]"

  """
  @spec surround(binary, t, binary) :: t
  def surround(left, doc, right) do
    group concat left, concat(nest(doc, @nesting), right)
  end

  @doc %S"""
  Maps and glues a collection of items together using the given separator
  and surrounds them. A limit can be passed which, once reached, stops
  gluing and outputs "..." instead.

  ## Examples

      iex> doc = Inspect.Algebra.surround_many("[", Enum.to_list(1..5), "]", :infinity, &integer_to_binary(&1))
      iex> Inspect.Algebra.pretty(doc, 5)
      "[1,\n 2,\n 3,\n 4,\n 5]"

      iex> doc = Inspect.Algebra.surround_many("[", Enum.to_list(1..5), "]", 3, &integer_to_binary(&1))
      iex> Inspect.Algebra.pretty(doc, 20)
      "[1, 2, 3, ...]"

      iex> doc = Inspect.Algebra.surround_many("[", Enum.to_list(1..5), "]", 3, &integer_to_binary(&1), "!")
      iex> Inspect.Algebra.pretty(doc, 20)
      "[1! 2! 3! ...]"

  """
  @spec surround_many(binary, [any], binary, integer | :infinity, (term -> t), binary) :: t
  def surround_many(left, docs, right, limit, fun, separator // @surround_separator)

  def surround_many(left, [], right, _, _fun, _) do
    concat(left, right)
  end

  def surround_many(left, docs, right, limit, fun, sep) do
    surround(left, surround_many(docs, limit, fun, sep), right)
  end

  defp surround_many(_, 0, _fun, _sep) do
    "..."
  end

  defp surround_many([h], _limit, fun, _sep) do
    fun.(h)
  end

  defp surround_many([h|t], limit, fun, sep) when is_list(t) do
    glue(
      concat(fun.(h), sep),
      surround_many(t, decrement(limit), fun, sep)
    )
  end

  defp surround_many([h|t], _limit, fun, _sep) do
    glue(
      concat(fun.(h), @tail_separator),
      fun.(t)
    )
  end

  defp decrement(:infinity), do: :infinity
  defp decrement(counter),   do: counter - 1

  @doc """
  The pretty printing function.

  Takes the maximum width and a document to print as its arguments
  and returns the string representation of the best layout for the
  document to fit in the given width.
  """
  @spec pretty(t, non_neg_integer | :infinity) :: binary
  def pretty(d, w) do
    sdoc = format w, 0, [{0, default_mode(w), doc_group(doc: d)}]
    render(sdoc)
  end

  defp default_mode(:infinity), do: :flat
  defp default_mode(_),         do: :break

  # Rendering and internal helpers

  # Record representing the document mode to be rendered: flat or broken
  @typep mode :: :flat | :break

  @doc false
  @spec fits?(integer, [{ integer, mode, t }]) :: boolean
  def fits?(w, _) when w < 0,                               do: false
  def fits?(_, []),                                         do: true
  def fits?(w, [{_, _, :doc_nil} | t]),                     do: fits?(w, t)
  def fits?(w, [{i, m, doc_cons(left: x, right: y)} | t]),  do: fits?(w, [{i, m, x} | [{i, m, y} | t]])
  def fits?(w, [{i, m, doc_nest(indent: j, doc: x)} | t]),  do: fits?(w, [{i + j, m, x} | t])
  def fits?(w, [{i, _, doc_group(doc: x)} | t]),            do: fits?(w, [{i, :flat, x} | t])
  def fits?(w, [{_, _, s} | t]) when is_binary(s),          do: fits?((w - byte_size s), t)
  def fits?(w, [{_, :flat, doc_break(str: s)} | t]),        do: fits?((w - byte_size s), t)
  def fits?(_, [{_, :break, doc_break(str: _)} | _]),       do: true

  @doc false
  @spec format(integer | :infinity, integer, [{ integer, mode, t }]) :: [binary]
  def format(_, _, []),                                        do: []
  def format(w, k, [{_, _, :doc_nil} | t]),                    do: format(w, k, t)
  def format(w, k, [{i, m, doc_cons(left: x, right: y)} | t]), do: format(w, k, [{i, m, x} | [{i, m, y} | t]])
  def format(w, k, [{i, m, doc_nest(indent: j, doc: x)} | t]), do: format(w, k, [{i + j, m, x} | t])
  def format(w, k, [{i, m, doc_group(doc: x)} | t]),           do: format(w, k, [{i, m, x} | t])
  def format(w, k, [{_, _, s} | t]) when is_binary(s),         do: [s | format(w, (k + byte_size s), t)]
  def format(w, k, [{_, :flat, doc_break(str: s)} | t]),       do: [s | format(w, (k + byte_size s), t)]
  def format(w, k, [{i, :break, doc_break(str: s)} | t]) do
    k = k + byte_size(s)

    if w == :infinity or fits?(w - k, t) do
      [s | format(w, k, t)]
    else
      prefix = :binary.copy(" ", i)
      [@newline, prefix | format(w, i, t)]
    end
  end

  @doc false
  @spec render([binary]) :: binary
  def render(sdoc) do
    iolist_to_binary sdoc
  end
end

defmodule Inspect.Algebra do
  @moduledoc %S"""
  A set of functions for creating and manipulating algebra
  documents, as described in ["Strictly Pretty" (2000) by Christian Lindig][0].

  An algebra document is represented by an `Inspect.Algebra` node
  or a regular string.

      iex> Inspect.Algebra.empty
      :doc_nil
      iex> "foo"
      "foo"

  With the functions in this module, we can concatenate different
  elements together and render them:

      iex> doc = Inspect.Algebra.concat(Inspect.Algebra.empty, "foo")
      iex> Inspect.Algebra.pretty(doc, 80)
      "foo"

  The functions `nest/2`, `space/2` and `line/2` help you put the
  document together into a rigid structure. However, the document
  algebra gets interesting when using functions like `break/2`, which
  converts the given string into a line break depending on how much space
  there is to print. Let's glue two docs together with a break and then
  render it:

      iex> doc = Inspect.Algebra.glue("a", " ", "b")
      iex> Inspect.Algebra.pretty(doc, 80)
      "a b"

  Notice the break was represented as is, because we haven't reached
  a line limit. Once we do, it is replaced by a newline:

      iex> doc = Inspect.Algebra.glue(String.duplicate("a", 20), " ", "b")
      iex> Inspect.Algebra.pretty(doc, 10)
      "aaaaaaaaaaaaaaaaaaaa\nb"

  Finally, this module also contains Elixir related functions, a bit
  tied to Elixir formatting, namely `surround/3` and `surround_many/5`.

  ## Implementation details

  The original Haskell implementation of the algorithm by [Wadler][1]
  relies on lazy evaluation to unfold document groups on two alternatives:
  `:flat` (breaks as spaces) and `:break` (breaks as newlines).
  Implementing the same logic in a strict language such as Elixir leads
  to an exponential growth of possible documents, unless document groups
  are encoded explictly as `:flat` or `:break`. Those groups are then reduced
  to a simple document, where the layout is already decided, per [Lindig][0].

  This implementation slightly changes the semantic of Lindig's algorithm
  to allow elements that belong to the same group to be printed together
  in the same line, even if they do not fit the line fully. This was achieved
  by changing `:break` to mean a possible break and `:flat` to force a flat
  structure. Then deciding if a break works as a newline is just a matter
  of checking if we have enough space until the next break that is not
  inside a group (which is still flat).

  Custom pretty printers can be implemented using the documents returned
  by this module and by providing their own rendering functions.

    [0]: http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.34.2200
    [1]: http://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf

  """

  @surround_separator ","
  @tail_separator " |"
  @newline "\n"
  @nesting 1
  @break " "

  # Functional interface to `doc` records

  @type t :: :doc_nil | doc_cons_t | doc_nest_t | doc_break_t | doc_group_t | binary
  defrecordp :doc_cons, left: :doc_nil :: t, right: :doc_nil :: t
  defrecordp :doc_nest, indent: 1 :: non_neg_integer, doc: :doc_nil :: t
  defrecordp :doc_break, str: " " :: binary
  defrecordp :doc_group, doc: :doc_nil :: t

  defmacrop is_doc(doc) do
    if __CALLER__.in_guard? do
      do_is_doc(doc)
    else
      var = quote do: doc
      quote do
        unquote(var) = unquote(doc)
        unquote(do_is_doc(var))
      end
    end
  end

  defp do_is_doc(doc) do
    quote do
      is_binary(unquote(doc)) or
      unquote(doc) == :doc_nil or
      (is_tuple(unquote(doc)) and
       elem(unquote(doc), 0) in [:doc_cons, :doc_nest, :doc_break, :doc_group])
    end
  end

  @doc """
  Returns `:doc_nil` which is a document entity used to represent
  nothingness. Takes no arguments.

  ## Examples

      iex> Inspect.Algebra.empty
      :doc_nil

  """
  @spec empty() :: :doc_nil
  def empty, do: :doc_nil

  @doc """
  Concatenates two document entities. Takes two arguments:
  left doc and right doc. Returns a DocCons doc

  ## Examples

      iex> doc = Inspect.Algebra.concat "Tasteless", "Artosis"
      iex> Inspect.Algebra.pretty(doc, 80)
      "TastelessArtosis"

  """
  @spec concat(t, t) :: doc_cons_t
  def concat(x, y) when is_doc(x) and is_doc(y) do
    doc_cons(left: x, right: y)
  end

  @doc """
  Concatenates a list of documents.
  """
  @spec concat([t]) :: doc_cons_t
  def concat(docs) do
    folddoc(docs, &concat(&1, &2))
  end

  @doc """
  Nests document entity `x` positions deep. Nesting will be
  appended to the line breaks.

  ## Examples

      iex> doc = Inspect.Algebra.nest(Inspect.Algebra.concat(Inspect.Algebra.break, "6"), 5)
      iex> Inspect.Algebra.pretty(doc, 80)
      " 6"

  """
  @spec nest(t, non_neg_integer) :: doc_nest_t
  def nest(x, 0) when is_doc(x) do
    x
  end

  def nest(x, i) when is_doc(x) and is_integer(i) do
    doc_nest(indent: i, doc: x)
  end

  @doc %S"""
  Document entity representing a break. This break can
  be rendered as a linebreak or as spaces, depending on the
  `mode` of the chosen layout or the provided separator.

  ## Examples

  Let's glue two docs together with a break and then render it:

      iex> doc = Inspect.Algebra.glue("a", " ", "b")
      iex> Inspect.Algebra.pretty(doc, 80)
      "a b"

  Notice the break was represented as is, because we haven't reached
  a line limit. Once we do, it is replaced by a newline:

      iex> doc = Inspect.Algebra.glue(String.duplicate("a", 20), " ", "b")
      iex> Inspect.Algebra.pretty(doc, 10)
      "aaaaaaaaaaaaaaaaaaaa\nb"

  """
  @spec break(binary) :: doc_break_t
  def break(s) when is_binary(s), do: doc_break(str: s)

  @spec break() :: doc_break_t
  def break(), do: doc_break(str: @break)

  @doc """
  Inserts a break between two docs. See `break/1` for more info.
  """
  @spec glue(t, t) :: doc_cons_t
  def glue(x, y), do: concat(x, concat(break, y))

  @doc """
  Inserts a break, passed as the second argument, between two docs,
  the first and the third arguments.
  """
  @spec glue(t, binary, t) :: doc_cons_t
  def glue(x, g, y) when is_binary(g), do: concat(x, concat(break(g), y))

  @doc %S"""
  Returns a group containing the specified document.

  ## Examples

      iex> doc = Inspect.Algebra.group(
      ...>   Inspect.Algebra.concat(
      ...>     Inspect.Algebra.group(
      ...>       Inspect.Algebra.concat(
      ...>         "Hello,",
      ...>         Inspect.Algebra.concat(
      ...>           Inspect.Algebra.break,
      ...>           "A"
      ...>         )
      ...>       )
      ...>     ),
      ...>     Inspect.Algebra.concat(
      ...>       Inspect.Algebra.break,
      ...>       "B"
      ...>     )
      ...> ))
      iex> Inspect.Algebra.pretty(doc, 80)
      "Hello, A B"
      iex> Inspect.Algebra.pretty(doc, 6)
      "Hello,\nA B"

  """
  @spec group(t) :: doc_group_t
  def group(d) when is_doc(d) do
    doc_group(doc: d)
  end

  @doc """
  Inserts a mandatory single space between two document entities.

  ## Examples

      iex> doc = Inspect.Algebra.space "Hughes", "Wadler"
      iex> Inspect.Algebra.pretty(doc, 80)
      "Hughes Wadler"

  """
  @spec space(t, t) :: doc_cons_t
  def space(x, y), do: concat(x, concat(" ", y))

  @doc %S"""
  Inserts a mandatory linebreak between two document entities.

  ## Examples

      iex> doc = Inspect.Algebra.line "Hughes", "Wadler"
      iex> Inspect.Algebra.pretty(doc, 80)
      "Hughes\nWadler"

  """
  @spec line(t, t) :: doc_cons_t
  def line(x, y), do: concat(x, concat(@newline, y))

  @doc """
  Folds a list of document entities into a document entity
  using a function that is passed as the first argument.

  ## Examples

      iex> doc = ["A", "B"]
      iex> doc = Inspect.Algebra.folddoc(doc, fn(x,y) ->
      ...>   Inspect.Algebra.concat [x, "!", y]
      ...> end)
      iex> Inspect.Algebra.pretty(doc, 80)
      "A!B"

  """
  @spec folddoc([t], ((t, t) -> t)) :: t
  def folddoc([], _), do: empty
  def folddoc([doc], _), do: doc
  def folddoc([d|ds], f), do: f.(d, folddoc(ds, f))

  # Elixir conveniences

  @doc %S"""
  Surrounds a document with characters.

  Puts the document between left and right enclosing and nesting it.
  The document is marked as a group, to show the maximum as possible
  concisely together.

  ## Examples

      iex> doc = Inspect.Algebra.surround "[", Inspect.Algebra.glue("a", "b"), "]"
      iex> Inspect.Algebra.pretty(doc, 3)
      "[a\n b]"

  """
  @spec surround(binary, t, binary) :: t
  def surround(left, doc, right) do
    group concat left, concat(nest(doc, @nesting), right)
  end

  @doc %S"""
  Maps and glues a collection of items together using the given separator
  and surrounds them. A limit can be passed which, once reached, stops
  gluing and outputs "..." instead.

  ## Examples

      iex> doc = Inspect.Algebra.surround_many("[", Enum.to_list(1..5), "]", :infinity, &integer_to_binary(&1))
      iex> Inspect.Algebra.pretty(doc, 5)
      "[1,\n 2,\n 3,\n 4,\n 5]"

      iex> doc = Inspect.Algebra.surround_many("[", Enum.to_list(1..5), "]", 3, &integer_to_binary(&1))
      iex> Inspect.Algebra.pretty(doc, 20)
      "[1, 2, 3, ...]"

      iex> doc = Inspect.Algebra.surround_many("[", Enum.to_list(1..5), "]", 3, &integer_to_binary(&1), "!")
      iex> Inspect.Algebra.pretty(doc, 20)
      "[1! 2! 3! ...]"

  """
  @spec surround_many(binary, [any], binary, integer | :infinity, (term -> t), binary) :: t
  def surround_many(left, docs, right, limit, fun, separator // @surround_separator)

  def surround_many(left, [], right, _, _fun, _) do
    concat(left, right)
  end

  def surround_many(left, docs, right, limit, fun, sep) do
    surround(left, surround_many(docs, limit, fun, sep), right)
  end

  defp surround_many(_, 0, _fun, _sep) do
    "..."
  end

  defp surround_many([h], _limit, fun, _sep) do
    fun.(h)
  end

  defp surround_many([h|t], limit, fun, sep) when is_list(t) do
    glue(
      concat(fun.(h), sep),
      surround_many(t, decrement(limit), fun, sep)
    )
  end

  defp surround_many([h|t], _limit, fun, _sep) do
    glue(
      concat(fun.(h), @tail_separator),
      fun.(t)
    )
  end

  defp decrement(:infinity), do: :infinity
  defp decrement(counter),   do: counter - 1

  @doc """
  The pretty printing function.

  Takes the maximum width and a document to print as its arguments
  and returns the string representation of the best layout for the
  document to fit in the given width.
  """
  @spec pretty(t, non_neg_integer | :infinity) :: binary
  def pretty(d, w) do
    sdoc = format w, 0, [{0, default_mode(w), doc_group(doc: d)}]
    render(sdoc)
  end

  defp default_mode(:infinity), do: :flat
  defp default_mode(_),         do: :break

  # Rendering and internal helpers

  # Record representing the document mode to be rendered: flat or broken
  @typep mode :: :flat | :break

  @doc false
  @spec fits?(integer, [{ integer, mode, t }]) :: boolean
  def fits?(:infinity, _),                                  do: true # no pretty printing
  def fits?(w, _) when w < 0,                               do: false
  def fits?(_, []),                                         do: true
  def fits?(w, [{_, _, :doc_nil} | t]),                     do: fits?(w, t)
  def fits?(w, [{i, m, doc_cons(left: x, right: y)} | t]),  do: fits?(w, [{i, m, x} | [{i, m, y} | t]])
  def fits?(w, [{i, m, doc_nest(indent: j, doc: x)} | t]),  do: fits?(w, [{i + j, m, x} | t])
  def fits?(w, [{_, _, s} | t]) when is_binary(s),          do: fits?((w - byte_size s), t)
  def fits?(w, [{_, :flat, doc_break(str: s)} | t]),        do: fits?((w - byte_size s), t)
  def fits?(_, [{_, :break, doc_break(str: _)} | _]),       do: true
  def fits?(w, [{i, _, doc_group(doc: x)} | t]),            do: fits?(w, [{i, :flat, x} | t])

  @doc false
  @spec format(integer | :infinity, integer, [{ integer, mode, t }]) :: atom | tuple
  def format(_, _, []),                                        do: []
  def format(w, k, [{_, _, :doc_nil} | t]),                    do: format(w, k, t)
  def format(w, k, [{i, m, doc_cons(left: x, right: y)} | t]), do: format(w, k, [{i, m, x} | [{i, m, y} | t]])
  def format(w, k, [{i, m, doc_nest(indent: j, doc: x)} | t]), do: format(w, k, [{i + j, m, x} | t])
  def format(w, k, [{i, m, doc_group(doc: x)} | t]),           do: format(w, k, [{i, m, x} | t])
  def format(w, k, [{_, _, s} | t]) when is_binary(s),         do: [s | format(w, (k + byte_size s), t)]
  def format(w, k, [{_, :flat, doc_break(str: s)} | t]),       do: [s | format(w, (k + byte_size s), t)]
  def format(w, k, [{i, :break, doc_break(str: s)} | t]) do
    k = k + byte_size(s)

    if w == :infinity or fits?(w - k, t) do
      [s | format(w, k, t)]
    else
      prefix = :binary.copy(" ", i)
      [@newline, prefix | format(w, i, t)]
    end
  end

  @doc false
  @spec render([binary]) :: binary
  def render(sdoc) do
    iolist_to_binary sdoc
  end
end