ExPanda - full macro expansion for Elixir AST introspection

I’ve been heavily working with macros for more than 10 years already and I always used kinda ad-hoc code to expand/debug/test those when in trouble.

So yeah, it’s better to get a reputation of a slowpoke rather than to spend another 10 years fighting the ghosts.

Welcome ExPanda library that expands macros all the turtles down (short of defmodule and def/defp/defmacro/defmacrop structural forms.)

It’s time to take a look at what use GenServer does actually become in your code:

iex|🌢|1 ▶ "defmodule M, do: use GenServer"
          |> ExPanda.expand_to_string()
          |> elem(1)
          |> IO.puts() 

defmodule M do
  require GenServer
  opts = []
  @behaviour GenServer
  case :erlang.not(Module.has_attribute?(M, :doc)) do
    false ->
      nil

    true ->
      Module.__put_attribute__(
        M,
        :doc,
        {0,
         "Returns a specification to start this module under a supervisor.\n\nSee `Supervisor`.\n"},
        nil,
        []
      )
  end

  def child_spec(init_arg) do
    default = %{id: M, start: {M, :start_link, [init_arg]}}
    Supervisor.child_spec(default, unquote(Macro.escape(opts)))
  end

  defoverridable child_spec: 1
  @before_compile GenServer
  @doc false
  def handle_call(msg, _from, state) do
    proc =
      case Process.info(:erlang.self(), :registered_name) do
        {_, []} -> :erlang.self()
        {_, name} -> name
      end

    case :erlang.phash2(1, 1) do
      0 ->
        :erlang.error(
          RuntimeError.exception(
            <<"attempted to call GenServer ", String.Chars.to_string(inspect(proc))::binary,
              " but no handle_call/3 clause was provided">>
          ),
          :none,
          error_info: %{module: Exception}
        )

      1 ->
        {:stop, {:bad_call, msg}, state}
    end
  end

  @doc false
  def handle_info(msg, state) do
    proc =
      case Process.info(:erlang.self(), :registered_name) do
        {_, []} -> :erlang.self()
        {_, name} -> name
      end

    :logger.error(
      %{label: {GenServer, :no_handle_info}, report: %{module: M, message: msg, name: proc}},
      %{
        domain: [:otp, :elixir],
        error_logger: %{tag: :error_msg},
        report_cb: &:erlang./(GenServer.format_report(), 1)
      }
    )

    {:noreply, state}
  end

  @doc false
  def handle_cast(msg, state) do
    proc =
      case Process.info(:erlang.self(), :registered_name) do
        {_, []} -> :erlang.self()
        {_, name} -> name
      end

    case :erlang.phash2(1, 1) do
      0 ->
        :erlang.error(
          RuntimeError.exception(
            <<"attempted to cast GenServer ", String.Chars.to_string(inspect(proc))::binary,
              " but no handle_cast/2 clause was provided">>
          ),
          :none,
          error_info: %{module: Exception}
        )

      1 ->
        {:stop, {:bad_cast, msg}, state}
    end
  end

  @doc false
  def terminate(_reason, _state) do
    :ok
  end

  @doc false
  def code_change(_old, state, _extra) do
    {:ok, state}
  end

  defoverridable code_change: 3, terminate: 2, handle_info: 2, handle_cast: 2, handle_call: 3
end

Enjoy!

16 Likes

Nice. I’ve been patiently waiting years for someone to integrate this sort of thing into the LSP for me.

1 Like

10/10 name.

5 Likes

The version 1.0 is to roll out with a motto “chewing bamboo shoots to the roots.”

1 Like

I have plans to ping ExpertLSP team members after I have it tested thoroughly with my own Ragex.

3 Likes

Wow, thanks for sharing. It could be pretty helpful