lib/patch/macro.ex

defmodule Patch.Macro do
  @doc """
  Utility function that acts like `inspect/1` but prints out the Macro as code.
  """
  @spec debug(ast :: Macro.t()) :: Macro.t()
  def debug(ast) do
    ast
    |> Macro.to_string()
    |> IO.puts()

    ast
  end

  @doc """
  Performs an non-hygienic match.

  If the match succeeds true is returned, otherwise a MatchError is raised.

  Since the match is non-hygienic pins can be used from the user-scope and binds will effect
  user-scope.
  """
  @spec match(pattern :: Macro.t(), expression :: Macro.t()) :: Macro.t()
  defmacro match(pattern, expression) do
    user_pattern = user_variables(pattern)
    pattern_expression = pattern_expression(pattern)
    variables = variables(pattern)

    quote generated: true do
      unquote(pattern_expression) =
        case unquote(expression) do
          unquote(user_pattern) ->
            _ = unquote(variables)
            unquote(expression)

          _ ->
            raise MatchError, term: unquote(expression)
        end

      _ = unquote(variables)
      true
    end
  end

  @doc """
  Performs a match, return true if match matches, false otherwise.
  """
  @spec match?(pattern :: Macro.t(), expression :: Macro.t()) :: Macro.t()
  defmacro match?(pattern, expression) do
    quote generated: true do
      try do
        Patch.Macro.match(unquote(pattern), unquote(expression))
        true
      rescue
        MatchError ->
          false
      end
    end
  end

  ## Private

  defp pattern_expression(pattern) do
    Macro.prewalk(pattern, fn
      {:^, _, [{name, meta, _}]} ->
        {name, meta, nil}

      {:_, _, _} ->
        unique_variable()

      node ->
        node
    end)
  end

  defp unique_variable do
    {:"_ignore#{:erlang.unique_integer([:positive])}", [generated: true], nil}
  end

  defp user_variables(pattern) do
    Macro.prewalk(pattern, fn
      {name, meta, context} when is_atom(name) and is_atom(context) ->
        {name, meta, nil}

      node ->
        node
    end)
  end

  defp variables(pattern) do
    pattern
    |> Macro.prewalk([], fn
      {:_, _, _} = node, acc ->
        {node, acc}

      {:@, anno, _}, acc ->
        # Replace module attribute with wildcard so we don't convert into a variable
        {{:_, anno, nil}, acc}

      {name, meta, context} = node, acc when is_atom(name) and is_atom(context) ->
        ignored? =
          name
          |> Atom.to_string()
          |> String.starts_with?("_")

        if ignored? do
          {node, acc}
        else
          {node, [{name, meta, nil} | acc]}
        end

      node, acc ->
        {node, acc}
    end)
    |> elem(1)
  end
end